bitex_bot 0.3.6 → 0.3.7
Sign up to get free protection for your applications and to get access to all the features.
- 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
|