bitex_bot 0.3.6 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/bitex_bot.gemspec +1 -0
- data/lib/bitex_bot/models/bitfinex_api_wrapper.rb +1 -1
- data/lib/bitex_bot/models/bitstamp_api_wrapper.rb +1 -1
- data/lib/bitex_bot/models/closing_flow.rb +6 -4
- data/lib/bitex_bot/models/itbit_api_wrapper.rb +1 -1
- data/lib/bitex_bot/models/kraken_api_wrapper.rb +188 -0
- data/lib/bitex_bot/robot.rb +2 -0
- data/lib/bitex_bot/version.rb +1 -1
- data/settings.rb.sample +6 -1
- data/spec/bitex_bot/settings_spec.rb +1 -0
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: c1bcc53a7b4d8a6382173bdd864dd306b3a2bdc5
|
4
|
+
data.tar.gz: e4254d0ffe025a95c5eba3429814b2b60405abb7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0b64dae4070f6fb5ce595bfb0d5cac5c88d9d853264b948ae62241e812a1946ed83e9f3af344b4fa8fb7a215d61f9cc8c2b960c388b820d2b880f3dc72cc32b7
|
7
|
+
data.tar.gz: a8ac2e4ace3edaa00b529d6aafd1808d4bb8cfb8139b6588789a80e8455b38505be31afb6a813048905d9f8c32f3738b979c23c45a1ab1340a49454180347ecc
|
data/bitex_bot.gemspec
CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
|
|
27
27
|
spec.add_dependency "bitex", "0.3"
|
28
28
|
spec.add_dependency "itbit", "0.0.6"
|
29
29
|
spec.add_dependency "bitfinex-rb", "0.0.6"
|
30
|
+
spec.add_dependency "kraken_client", "~> 1.2.1"
|
30
31
|
spec.add_dependency "mail"
|
31
32
|
spec.add_dependency "hashie", "~> 3.5.4"
|
32
33
|
|
@@ -95,7 +95,7 @@ class BitfinexApiWrapper
|
|
95
95
|
def self.place_order(type, price, quantity)
|
96
96
|
with_retry "place order #{type} #{price} #{quantity}" do
|
97
97
|
order_data = Bitfinex::Client.new
|
98
|
-
.new_order('btcusd', quantity, 'exchange limit', type.to_s, price)
|
98
|
+
.new_order('btcusd', quantity.round(4), 'exchange limit', type.to_s, price.round(2))
|
99
99
|
BitfinexOrder.new(order_data)
|
100
100
|
end
|
101
101
|
end
|
@@ -35,15 +35,15 @@ module BitexBot
|
|
35
35
|
|
36
36
|
def create_order_and_close_position(quantity, price)
|
37
37
|
order = BitexBot::Robot.taker.place_order(
|
38
|
-
order_method, price
|
38
|
+
order_method, price, quantity)
|
39
39
|
if order.nil? || order.id.nil?
|
40
40
|
Robot.logger.error("Closing: Error on #{order_method} for "\
|
41
41
|
"#{self.class.name} ##{id} #{quantity} BTC @ $#{price}."\
|
42
42
|
"#{order.to_s}")
|
43
43
|
return
|
44
44
|
end
|
45
|
-
Robot.logger.info("Closing: Going to #{order_method} ##{order.id} for"\
|
46
|
-
"#{self.class.name} ##{id} #{
|
45
|
+
Robot.logger.info("Closing: Going to #{order_method} ##{order.id} for "\
|
46
|
+
"#{self.class.name} ##{id} #{order.amount} BTC @ $#{order.price}")
|
47
47
|
close_positions.create!(order_id: order.id)
|
48
48
|
end
|
49
49
|
|
@@ -74,14 +74,16 @@ module BitexBot
|
|
74
74
|
self.btc_profit = get_btc_profit
|
75
75
|
self.usd_profit = get_usd_profit
|
76
76
|
self.done = true
|
77
|
-
Robot.logger.info("Closing: Finished #{self.class.name} ##{id}"\
|
77
|
+
Robot.logger.info("Closing: Finished #{self.class.name} ##{id} "\
|
78
78
|
"earned $#{self.usd_profit} and #{self.btc_profit} BTC. ")
|
79
79
|
save!
|
80
80
|
end
|
81
81
|
elsif latest_close.created_at < self.class.close_time_to_live.seconds.ago
|
82
82
|
Robot.with_cooldown do
|
83
83
|
begin
|
84
|
+
Robot.logger.debug("Finalising #{order.class}##{order.id}")
|
84
85
|
order.cancel!
|
86
|
+
Robot.logger.debug("Finalised #{order.class}##{order.id}")
|
85
87
|
rescue StandardError => e
|
86
88
|
nil # just pass, we'll keep on trying until it's not in orders anymore.
|
87
89
|
end
|
@@ -47,7 +47,7 @@ class ItbitApiWrapper
|
|
47
47
|
|
48
48
|
def self.place_order(type, price, quantity)
|
49
49
|
begin
|
50
|
-
return Itbit::Order.create!(type, :xbtusd, quantity, price, wait: true)
|
50
|
+
return Itbit::Order.create!(type, :xbtusd, quantity.round(4), price.round(2), wait: true)
|
51
51
|
rescue RestClient::RequestTimeout => e
|
52
52
|
# On timeout errors, we still look for the latest active closing order
|
53
53
|
# that may be available. We have a magic threshold of 5 minutes
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'kraken_client'
|
2
|
+
|
3
|
+
class KrakenApiWrapper
|
4
|
+
def self.setup(settings)
|
5
|
+
HTTParty::Basement.headers('User-Agent' => BitexBot.user_agent)
|
6
|
+
@settings = settings.kraken
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.client
|
10
|
+
@client ||= KrakenClient.load(@settings)
|
11
|
+
end
|
12
|
+
|
13
|
+
#{
|
14
|
+
# tid:i,
|
15
|
+
# date: (i+1).seconds.ago.to_i.to_s,
|
16
|
+
# price: price.to_s,
|
17
|
+
# amount: amount.to_s
|
18
|
+
#}
|
19
|
+
def self.transactions
|
20
|
+
client.public.trades('XBTUSD')[:XXBTZUSD].reverse.collect do |t|
|
21
|
+
Hashie::Mash.new({
|
22
|
+
tid: t[2].to_s,
|
23
|
+
price: t[0],
|
24
|
+
amount: t[1],
|
25
|
+
date: t[2]
|
26
|
+
})
|
27
|
+
end
|
28
|
+
rescue NoMethodError => e
|
29
|
+
retry
|
30
|
+
end
|
31
|
+
|
32
|
+
# { 'timestamp' => DateTime.now.to_i.to_s,
|
33
|
+
# 'bids' =>
|
34
|
+
# [['30', '3'], ['25', '2'], ['20', '1.5'], ['15', '4'], ['10', '5']],
|
35
|
+
# 'asks' =>
|
36
|
+
# [['10', '2'], ['15', '3'], ['20', '1.5'], ['25', '3'], ['30', '3']]
|
37
|
+
# }
|
38
|
+
def self.order_book(retries = 20)
|
39
|
+
book = client.public.order_book('XBTUSD')[:XXBTZUSD]
|
40
|
+
{ 'bids' => book[:bids].collect { |b| [ b[0], b[1] ] },
|
41
|
+
'asks' => book[:asks].collect { |a| [ a[0], a[1] ] } }
|
42
|
+
rescue NoMethodError => e
|
43
|
+
retry
|
44
|
+
end
|
45
|
+
|
46
|
+
# {"btc_balance"=> "10.0", "btc_reserved"=> "0", "btc_available"=> "10.0",
|
47
|
+
# "usd_balance"=> "100.0", "usd_reserved"=>"0", "usd_available"=> "100.0",
|
48
|
+
# "fee"=> "0.5000"}
|
49
|
+
def self.balance
|
50
|
+
balances = client.private.balance
|
51
|
+
open_orders = KrakenOrder.open
|
52
|
+
sell_orders = open_orders.select { |o| o.type == :sell }
|
53
|
+
btc_reserved = sell_orders.collect { |o| o.amount - o.executed_amount }.sum
|
54
|
+
buy_orders = open_orders - sell_orders
|
55
|
+
usd_reserved = buy_orders.collect { |o| (o.amount - o.executed_amount) * o.price }.sum
|
56
|
+
{ 'btc_balance' => balances['XXBT'].to_d,
|
57
|
+
'btc_reserved' => btc_reserved,
|
58
|
+
'btc_available' => balances['XXBT'].to_d - btc_reserved,
|
59
|
+
'usd_balance' => balances['ZUSD'].to_d,
|
60
|
+
'usd_reserved' => usd_reserved,
|
61
|
+
'usd_available' => balances['ZUSD'].to_d - usd_reserved,
|
62
|
+
'fee' => client.private.trade_volume(pair: 'XBTUSD')[:fees][:XXBTZUSD][:fee].to_d
|
63
|
+
}
|
64
|
+
rescue KrakenClient::ErrorResponse, Net::ReadTimeout => e
|
65
|
+
retry
|
66
|
+
end
|
67
|
+
|
68
|
+
# ask = double(amount: args[:amount], price: args[:price],
|
69
|
+
# type: 1, id: remote_id, datetime: DateTime.now.to_s)
|
70
|
+
# ask.stub(:cancel!) do
|
71
|
+
def self.orders
|
72
|
+
KrakenOrder.open
|
73
|
+
end
|
74
|
+
|
75
|
+
# We don't need to fetch the list of transactions
|
76
|
+
# for Kraken
|
77
|
+
def self.user_transactions
|
78
|
+
[ ]
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.amount_and_quantity(order_id, transactions)
|
82
|
+
KrakenOrder.amount_and_quantity(order_id, transactions)
|
83
|
+
end
|
84
|
+
|
85
|
+
def self.place_order(type, price, quantity)
|
86
|
+
KrakenOrder.create(type, price, quantity)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
class KrakenOrder
|
91
|
+
attr_accessor :id, :amount, :executed_amount, :price, :avg_price, :type, :datetime
|
92
|
+
def initialize(id, order_data)
|
93
|
+
self.id = id
|
94
|
+
self.amount = order_data['vol'].to_d
|
95
|
+
self.executed_amount = order_data['vol_exec'].to_d
|
96
|
+
self.price = order_data['descr']['price'].to_d
|
97
|
+
self.avg_price = order_data['price'].to_d
|
98
|
+
self.type = order_data['descr']['type'].to_sym
|
99
|
+
self.datetime = order_data['opentm'].to_i
|
100
|
+
end
|
101
|
+
|
102
|
+
def cancel!
|
103
|
+
self.class.client.private.cancel_order(txid: id)
|
104
|
+
rescue KrakenClient::ErrorResponse => e
|
105
|
+
retry if e.message == 'EService:Unavailable'
|
106
|
+
raise
|
107
|
+
end
|
108
|
+
|
109
|
+
def ==(order)
|
110
|
+
if order.is_a?(self.class)
|
111
|
+
id == order.id
|
112
|
+
elsif order.is_a?(Array)
|
113
|
+
[ type, price, amount ] == order
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
def self.client
|
118
|
+
KrakenApiWrapper.client
|
119
|
+
end
|
120
|
+
|
121
|
+
def self.find(id)
|
122
|
+
new(*client.private.query_orders(txid: id).first)
|
123
|
+
rescue KrakenClient::ErrorResponse => e
|
124
|
+
retry
|
125
|
+
end
|
126
|
+
|
127
|
+
def self.amount_and_quantity(order_id, transactions)
|
128
|
+
order = find(order_id)
|
129
|
+
[ order.avg_price * order.executed_amount, order.executed_amount ]
|
130
|
+
end
|
131
|
+
|
132
|
+
def self.open
|
133
|
+
client.private.open_orders['open'].collect { |o| new(*o) }
|
134
|
+
rescue KrakenClient::ErrorResponse => e
|
135
|
+
retry
|
136
|
+
end
|
137
|
+
|
138
|
+
def self.closed(start: 1.hour.ago.to_i)
|
139
|
+
client.private.closed_orders(start: start)[:closed].collect { |o| new(*o) }
|
140
|
+
rescue KrakenClient::ErrorResponse => e
|
141
|
+
retry
|
142
|
+
end
|
143
|
+
|
144
|
+
def self.find_lost(type, price, quantity, last_closed_order)
|
145
|
+
order_descr = [ type, price, quantity ]
|
146
|
+
|
147
|
+
BitexBot::Robot.logger.debug("Looking for #{type} order in open orders...")
|
148
|
+
if order = self.open.detect { |o| o == order_descr }
|
149
|
+
BitexBot::Robot.logger.debug("Found open order with ID #{order.id}")
|
150
|
+
return order
|
151
|
+
end
|
152
|
+
|
153
|
+
BitexBot::Robot.logger.debug("Looking for #{type} order in closed orders...")
|
154
|
+
order = closed(start: last_closed_order).detect { |o| o == order_descr }
|
155
|
+
if order && order.id != last_closed_order
|
156
|
+
BitexBot::Robot.logger.debug("Found closed order with ID #{order.id}")
|
157
|
+
return order
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
def self.create(type, price, quantity)
|
162
|
+
last_closed_order = closed.first.try(:id) || Time.now.to_i
|
163
|
+
price = price.truncate(1)
|
164
|
+
quantity = quantity.truncate(8)
|
165
|
+
order_info = client.private.add_order(pair: 'XBTUSD', type: type, ordertype: 'limit',
|
166
|
+
price: price, volume: quantity)
|
167
|
+
find(order_info['txid'].first)
|
168
|
+
rescue KrakenClient::ErrorResponse => e
|
169
|
+
# Order could not be placed
|
170
|
+
if e.message == 'EService:Unavailable'
|
171
|
+
BitexBot::Robot.logger.debug('Captured EService:Unavailable error when placing order on Kraken. Retrying...')
|
172
|
+
retry
|
173
|
+
elsif e.message.start_with?('EGeneral:Invalid')
|
174
|
+
BitexBot::Robot.logger.debug("Captured #{e.message}: type: #{type}, price: #{price}, quantity: #{quantity}")
|
175
|
+
return
|
176
|
+
end
|
177
|
+
raise unless e.message == 'error'
|
178
|
+
BitexBot::Robot.logger.debug('Captured error when placing order on Kraken')
|
179
|
+
# Order may have gone through and be stuck somewhere in Kraken's
|
180
|
+
# pipeline. We just sleep for a bit and then look for the order.
|
181
|
+
8.times do
|
182
|
+
sleep 15
|
183
|
+
order = find_lost(type, price, quantity, last_closed_order)
|
184
|
+
return order if order
|
185
|
+
end
|
186
|
+
raise
|
187
|
+
end
|
188
|
+
end
|
data/lib/bitex_bot/robot.rb
CHANGED
data/lib/bitex_bot/version.rb
CHANGED
data/settings.rb.sample
CHANGED
@@ -15,7 +15,7 @@ time_to_live 20
|
|
15
15
|
sandbox false
|
16
16
|
|
17
17
|
# Which market to use for taking (we're always makers on bitex)
|
18
|
-
# 'itbit', 'bitstamp' or '
|
18
|
+
# 'itbit', 'bitstamp', 'bitfinex' or 'kraken'
|
19
19
|
taker 'bitstamp'
|
20
20
|
|
21
21
|
# Settings for buying on bitex and selling on bitstamp.
|
@@ -73,6 +73,11 @@ itbit client_key: 'the-client-key',
|
|
73
73
|
bitfinex api_key: 'your_api_key',
|
74
74
|
api_secret: 'your_api_secret'
|
75
75
|
|
76
|
+
# These are passed in to the kraken gem:
|
77
|
+
# see https://github.com/shideneyu/kraken_client for more info.
|
78
|
+
kraken api_key: 'your_api_key',
|
79
|
+
api_secret: 'your_api_secret'
|
80
|
+
|
76
81
|
# Settings for the ActiveRecord Database to use.
|
77
82
|
# sqlite is just fine. Check this link for more options:
|
78
83
|
# http://apidock.com/rails/ActiveRecord/Base/establish_connection/class
|
@@ -10,6 +10,7 @@ describe BitexBot::Settings do
|
|
10
10
|
:buying => {:amount_to_spend_per_order=>10.0, :profit=>0.5},
|
11
11
|
:database => {:adapter=>:sqlite3, :database=>"bitex_bot.db"},
|
12
12
|
:itbit => {:client_key=>"the-client-key", :secret=>"the-secret", :user_id=>"the-user-id", :default_wallet_id=>"wallet-000"},
|
13
|
+
:kraken => {:api_key=>"your_api_key", :api_secret=>"your_api_secret"},
|
13
14
|
:log => {:file=>"bitex_bot.log", :level=>:info},
|
14
15
|
:mailer => {:from=>"robot@example.com", :to=>"you@example.com", :delivery_method=>:smtp, :options=>{:address=>"your_smtp_server_address.com", :port=>587, :authentication=>"plain", :enable_starttls_auto=>true, :user_name=>"your_user_name", :password=>"your_smtp_password"}},
|
15
16
|
:sandbox => false,
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: bitex_bot
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.3.
|
4
|
+
version: 0.3.7
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Nubis
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-
|
12
|
+
date: 2017-11-09 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activerecord
|
@@ -95,6 +95,20 @@ dependencies:
|
|
95
95
|
- - '='
|
96
96
|
- !ruby/object:Gem::Version
|
97
97
|
version: 0.0.6
|
98
|
+
- !ruby/object:Gem::Dependency
|
99
|
+
name: kraken_client
|
100
|
+
requirement: !ruby/object:Gem::Requirement
|
101
|
+
requirements:
|
102
|
+
- - "~>"
|
103
|
+
- !ruby/object:Gem::Version
|
104
|
+
version: 1.2.1
|
105
|
+
type: :runtime
|
106
|
+
prerelease: false
|
107
|
+
version_requirements: !ruby/object:Gem::Requirement
|
108
|
+
requirements:
|
109
|
+
- - "~>"
|
110
|
+
- !ruby/object:Gem::Version
|
111
|
+
version: 1.2.1
|
98
112
|
- !ruby/object:Gem::Dependency
|
99
113
|
name: mail
|
100
114
|
requirement: !ruby/object:Gem::Requirement
|
@@ -293,6 +307,7 @@ files:
|
|
293
307
|
- lib/bitex_bot/models/close_sell.rb
|
294
308
|
- lib/bitex_bot/models/closing_flow.rb
|
295
309
|
- lib/bitex_bot/models/itbit_api_wrapper.rb
|
310
|
+
- lib/bitex_bot/models/kraken_api_wrapper.rb
|
296
311
|
- lib/bitex_bot/models/open_buy.rb
|
297
312
|
- lib/bitex_bot/models/open_sell.rb
|
298
313
|
- lib/bitex_bot/models/opening_flow.rb
|