bitex_bot 0.3.7 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.circleci/config.yml +63 -0
- data/.rubocop.yml +33 -0
- data/Gemfile +1 -1
- data/Rakefile +1 -1
- data/bin/bitex_bot +1 -1
- data/bitex_bot.gemspec +34 -34
- data/lib/bitex_bot/database.rb +67 -67
- data/lib/bitex_bot/models/api_wrappers/api_wrapper.rb +142 -0
- data/lib/bitex_bot/models/api_wrappers/bitstamp/bitstamp_api_wrapper.rb +137 -0
- data/lib/bitex_bot/models/api_wrappers/itbit/itbit_api_wrapper.rb +116 -0
- data/lib/bitex_bot/models/api_wrappers/kraken/kraken_api_wrapper.rb +111 -0
- data/lib/bitex_bot/models/api_wrappers/kraken/kraken_order.rb +117 -0
- data/lib/bitex_bot/models/buy_closing_flow.rb +23 -16
- data/lib/bitex_bot/models/buy_opening_flow.rb +48 -54
- data/lib/bitex_bot/models/close_buy.rb +2 -2
- data/lib/bitex_bot/models/closing_flow.rb +98 -79
- data/lib/bitex_bot/models/open_buy.rb +11 -10
- data/lib/bitex_bot/models/open_sell.rb +11 -10
- data/lib/bitex_bot/models/opening_flow.rb +157 -99
- data/lib/bitex_bot/models/order_book_simulator.rb +62 -67
- data/lib/bitex_bot/models/sell_closing_flow.rb +25 -20
- data/lib/bitex_bot/models/sell_opening_flow.rb +47 -54
- data/lib/bitex_bot/models/store.rb +3 -1
- data/lib/bitex_bot/robot.rb +203 -176
- data/lib/bitex_bot/settings.rb +71 -12
- data/lib/bitex_bot/version.rb +1 -1
- data/lib/bitex_bot.rb +40 -16
- data/settings.rb.sample +43 -66
- data/spec/bitex_bot/settings_spec.rb +87 -15
- data/spec/factories/bitex_buy.rb +3 -3
- data/spec/factories/bitex_sell.rb +3 -3
- data/spec/factories/buy_opening_flow.rb +1 -1
- data/spec/factories/open_buy.rb +12 -10
- data/spec/factories/open_sell.rb +12 -10
- data/spec/factories/sell_opening_flow.rb +1 -1
- data/spec/models/api_wrappers/bitstamp_api_wrapper_spec.rb +200 -0
- data/spec/models/api_wrappers/itbit_api_wrapper_spec.rb +176 -0
- data/spec/models/api_wrappers/kraken_api_wrapper_spec.rb +209 -0
- data/spec/models/bitex_api_spec.rb +1 -1
- data/spec/models/buy_closing_flow_spec.rb +140 -71
- data/spec/models/buy_opening_flow_spec.rb +126 -56
- data/spec/models/order_book_simulator_spec.rb +10 -10
- data/spec/models/robot_spec.rb +61 -47
- data/spec/models/sell_closing_flow_spec.rb +130 -62
- data/spec/models/sell_opening_flow_spec.rb +129 -60
- data/spec/spec_helper.rb +19 -16
- data/spec/support/bitex_stubs.rb +13 -14
- data/spec/support/bitstamp/bitstamp_api_wrapper_stubs.rb +35 -0
- data/spec/support/bitstamp/bitstamp_stubs.rb +91 -0
- metadata +60 -42
- data/lib/bitex_bot/models/bitfinex_api_wrapper.rb +0 -118
- data/lib/bitex_bot/models/bitstamp_api_wrapper.rb +0 -82
- data/lib/bitex_bot/models/itbit_api_wrapper.rb +0 -68
- data/lib/bitex_bot/models/kraken_api_wrapper.rb +0 -188
- data/spec/models/bitfinex_api_wrapper_spec.rb +0 -17
- data/spec/models/bitstamp_api_wrapper_spec.rb +0 -15
- data/spec/models/itbit_api_wrapper_spec.rb +0 -15
- data/spec/support/bitstamp_stubs.rb +0 -110
@@ -1,188 +0,0 @@
|
|
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
|
@@ -1,17 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe BitfinexApiWrapper do
|
4
|
-
before(:each) do
|
5
|
-
BitexBot::Robot.stub(taker: 'bitfinex')
|
6
|
-
BitexBot::Robot.stub(taker: BitfinexApiWrapper)
|
7
|
-
BitexBot::Robot.setup
|
8
|
-
end
|
9
|
-
|
10
|
-
it 'Sends User-Agent header' do
|
11
|
-
stub_request(:post, "https://api.bitfinex.com/v1/balances")
|
12
|
-
.with(headers: { 'User-Agent': BitexBot.user_agent })
|
13
|
-
stub_request(:post, "https://api.bitfinex.com/v1/account_infos")
|
14
|
-
.with(headers: { 'User-Agent': BitexBot.user_agent })
|
15
|
-
BitfinexApiWrapper.balance rescue nil # we don't care about the response
|
16
|
-
end
|
17
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe BitstampApiWrapper do
|
4
|
-
before(:each) do
|
5
|
-
BitexBot::Robot.stub(taker: 'bitstamp')
|
6
|
-
BitexBot::Robot.stub(taker: BitstampApiWrapper)
|
7
|
-
BitexBot::Robot.setup
|
8
|
-
end
|
9
|
-
|
10
|
-
it 'Sends User-Agent header' do
|
11
|
-
stub_request(:post, "https://www.bitstamp.net/api/v2/balance/btcusd/")
|
12
|
-
.with(headers: { 'User-Agent': BitexBot.user_agent })
|
13
|
-
BitstampApiWrapper.balance rescue nil # we don't care about the response
|
14
|
-
end
|
15
|
-
end
|
@@ -1,15 +0,0 @@
|
|
1
|
-
require 'spec_helper'
|
2
|
-
|
3
|
-
describe ItbitApiWrapper do
|
4
|
-
before(:each) do
|
5
|
-
BitexBot::Robot.stub(taker: 'itbit')
|
6
|
-
BitexBot::Robot.stub(taker: ItbitApiWrapper)
|
7
|
-
BitexBot::Robot.setup
|
8
|
-
end
|
9
|
-
|
10
|
-
it 'Sends User-Agent header' do
|
11
|
-
stub_request(:get, "https://api.itbit.com/v1/wallets?userId=the-user-id")
|
12
|
-
.with(headers: { 'User-Agent': BitexBot.user_agent })
|
13
|
-
ItbitApiWrapper.balance rescue nil # we don't care about the response
|
14
|
-
end
|
15
|
-
end
|
@@ -1,110 +0,0 @@
|
|
1
|
-
module BitstampStubs
|
2
|
-
def stub_bitstamp_balance(usd = nil, coin = nil, fee = nil)
|
3
|
-
Bitstamp.stub(balance: bitstamp_balance_stub(usd, coin, fee))
|
4
|
-
end
|
5
|
-
|
6
|
-
def bitstamp_balance_stub(usd = nil, coin = nil, fee = nil)
|
7
|
-
{"btc_balance"=> coin || "10.0", "btc_reserved"=> "0", "btc_available"=> coin || "10.0",
|
8
|
-
"usd_balance"=> usd || "100.0", "usd_reserved"=>"0", "usd_available"=> usd || "100.0",
|
9
|
-
"fee"=> fee || "0.5000"}
|
10
|
-
end
|
11
|
-
|
12
|
-
def stub_bitstamp_order_book
|
13
|
-
Bitstamp.stub(order_book: bitstamp_order_book_stub)
|
14
|
-
end
|
15
|
-
|
16
|
-
def bitstamp_order_book_stub
|
17
|
-
{ 'timestamp' => Time.now.to_i.to_s,
|
18
|
-
'bids' =>
|
19
|
-
[['30', '3'], ['25', '2'], ['20', '1.5'], ['15', '4'], ['10', '5']],
|
20
|
-
'asks' =>
|
21
|
-
[['10', '2'], ['15', '3'], ['20', '1.5'], ['25', '3'], ['30', '3']]
|
22
|
-
}
|
23
|
-
end
|
24
|
-
|
25
|
-
def stub_bitstamp_transactions(volume = 0.2)
|
26
|
-
Bitstamp.stub(transactions: bitstamp_transactions_stub(volume))
|
27
|
-
end
|
28
|
-
|
29
|
-
def bitstamp_transactions_stub(price = 30, amount = 1)
|
30
|
-
transactions = 5.times.collect do |i|
|
31
|
-
double(
|
32
|
-
tid:i,
|
33
|
-
date: (i+1).seconds.ago.to_i.to_s,
|
34
|
-
price: price.to_s,
|
35
|
-
amount: amount.to_s
|
36
|
-
)
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def stub_bitstamp_user_transactions
|
41
|
-
Bitstamp.stub(user_transactions: double(all: []))
|
42
|
-
end
|
43
|
-
|
44
|
-
# Takes all active orders and mockes them as executed in a single transaction.
|
45
|
-
# If a ratio is provided then each order is only partially executed and added
|
46
|
-
# as a transaction and the order itself is kept in the order list.
|
47
|
-
def stub_bitstamp_orders_into_transactions(options={})
|
48
|
-
ratio = options[:ratio] || 1
|
49
|
-
orders = Bitstamp.orders.all
|
50
|
-
transactions = orders.collect do |o|
|
51
|
-
usd = o.amount * o.price
|
52
|
-
usd, btc = o.type == 0 ? [-usd, o.amount] : [usd, -o.amount]
|
53
|
-
double(usd: (usd * ratio).to_s, btc: (btc * ratio).to_s,
|
54
|
-
btc_usd: o.price.to_s, order_id: o.id, fee: "0.5", type: 2,
|
55
|
-
datetime: DateTime.now.to_s)
|
56
|
-
end
|
57
|
-
Bitstamp.stub(user_transactions: double(all: transactions))
|
58
|
-
|
59
|
-
if ratio == 1
|
60
|
-
stub_bitstamp_sell
|
61
|
-
stub_bitstamp_buy
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def ensure_bitstamp_orders_stub
|
66
|
-
begin
|
67
|
-
Bitstamp.orders
|
68
|
-
rescue Exception => e
|
69
|
-
Bitstamp.stub(orders: double)
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
def stub_bitstamp_sell(remote_id=nil, orders = [])
|
74
|
-
ensure_bitstamp_orders_stub
|
75
|
-
Bitstamp.orders.stub(all: orders)
|
76
|
-
Bitstamp.orders.stub(:sell) do |args|
|
77
|
-
remote_id = Bitstamp.orders.all.size + 1 if remote_id.nil?
|
78
|
-
ask = double(amount: args[:amount], price: args[:price],
|
79
|
-
type: 1, id: remote_id, datetime: DateTime.now.to_s)
|
80
|
-
ask.stub(:cancel!) do
|
81
|
-
orders = Bitstamp.orders.all.reject do |x|
|
82
|
-
x.id.to_s == ask.id.to_s && x.type == 1
|
83
|
-
end
|
84
|
-
stub_bitstamp_sell(remote_id + 1, orders)
|
85
|
-
end
|
86
|
-
stub_bitstamp_sell(remote_id + 1, Bitstamp.orders.all + [ask])
|
87
|
-
ask
|
88
|
-
end
|
89
|
-
end
|
90
|
-
|
91
|
-
def stub_bitstamp_buy(remote_id=nil, orders = [])
|
92
|
-
ensure_bitstamp_orders_stub
|
93
|
-
Bitstamp.orders.stub(all: orders)
|
94
|
-
Bitstamp.orders.stub(:buy) do |args|
|
95
|
-
remote_id = Bitstamp.orders.all.size + 1 if remote_id.nil?
|
96
|
-
bid = double(amount: args[:amount], price: args[:price],
|
97
|
-
type: 0, id: remote_id, datetime: DateTime.now.to_s)
|
98
|
-
bid.stub(:cancel!) do
|
99
|
-
orders = Bitstamp.orders.all.reject do |x|
|
100
|
-
x.id.to_s == bid.id.to_s && x.type == 0
|
101
|
-
end
|
102
|
-
stub_bitstamp_buy(remote_id + 1, orders)
|
103
|
-
end
|
104
|
-
stub_bitstamp_buy(remote_id + 1, Bitstamp.orders.all + [bid])
|
105
|
-
bid
|
106
|
-
end
|
107
|
-
end
|
108
|
-
end
|
109
|
-
|
110
|
-
RSpec.configuration.include BitstampStubs
|