bitex_bot 0.3.7 → 0.4.0
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/.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
|