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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +63 -0
  3. data/.rubocop.yml +33 -0
  4. data/Gemfile +1 -1
  5. data/Rakefile +1 -1
  6. data/bin/bitex_bot +1 -1
  7. data/bitex_bot.gemspec +34 -34
  8. data/lib/bitex_bot/database.rb +67 -67
  9. data/lib/bitex_bot/models/api_wrappers/api_wrapper.rb +142 -0
  10. data/lib/bitex_bot/models/api_wrappers/bitstamp/bitstamp_api_wrapper.rb +137 -0
  11. data/lib/bitex_bot/models/api_wrappers/itbit/itbit_api_wrapper.rb +116 -0
  12. data/lib/bitex_bot/models/api_wrappers/kraken/kraken_api_wrapper.rb +111 -0
  13. data/lib/bitex_bot/models/api_wrappers/kraken/kraken_order.rb +117 -0
  14. data/lib/bitex_bot/models/buy_closing_flow.rb +23 -16
  15. data/lib/bitex_bot/models/buy_opening_flow.rb +48 -54
  16. data/lib/bitex_bot/models/close_buy.rb +2 -2
  17. data/lib/bitex_bot/models/closing_flow.rb +98 -79
  18. data/lib/bitex_bot/models/open_buy.rb +11 -10
  19. data/lib/bitex_bot/models/open_sell.rb +11 -10
  20. data/lib/bitex_bot/models/opening_flow.rb +157 -99
  21. data/lib/bitex_bot/models/order_book_simulator.rb +62 -67
  22. data/lib/bitex_bot/models/sell_closing_flow.rb +25 -20
  23. data/lib/bitex_bot/models/sell_opening_flow.rb +47 -54
  24. data/lib/bitex_bot/models/store.rb +3 -1
  25. data/lib/bitex_bot/robot.rb +203 -176
  26. data/lib/bitex_bot/settings.rb +71 -12
  27. data/lib/bitex_bot/version.rb +1 -1
  28. data/lib/bitex_bot.rb +40 -16
  29. data/settings.rb.sample +43 -66
  30. data/spec/bitex_bot/settings_spec.rb +87 -15
  31. data/spec/factories/bitex_buy.rb +3 -3
  32. data/spec/factories/bitex_sell.rb +3 -3
  33. data/spec/factories/buy_opening_flow.rb +1 -1
  34. data/spec/factories/open_buy.rb +12 -10
  35. data/spec/factories/open_sell.rb +12 -10
  36. data/spec/factories/sell_opening_flow.rb +1 -1
  37. data/spec/models/api_wrappers/bitstamp_api_wrapper_spec.rb +200 -0
  38. data/spec/models/api_wrappers/itbit_api_wrapper_spec.rb +176 -0
  39. data/spec/models/api_wrappers/kraken_api_wrapper_spec.rb +209 -0
  40. data/spec/models/bitex_api_spec.rb +1 -1
  41. data/spec/models/buy_closing_flow_spec.rb +140 -71
  42. data/spec/models/buy_opening_flow_spec.rb +126 -56
  43. data/spec/models/order_book_simulator_spec.rb +10 -10
  44. data/spec/models/robot_spec.rb +61 -47
  45. data/spec/models/sell_closing_flow_spec.rb +130 -62
  46. data/spec/models/sell_opening_flow_spec.rb +129 -60
  47. data/spec/spec_helper.rb +19 -16
  48. data/spec/support/bitex_stubs.rb +13 -14
  49. data/spec/support/bitstamp/bitstamp_api_wrapper_stubs.rb +35 -0
  50. data/spec/support/bitstamp/bitstamp_stubs.rb +91 -0
  51. metadata +60 -42
  52. data/lib/bitex_bot/models/bitfinex_api_wrapper.rb +0 -118
  53. data/lib/bitex_bot/models/bitstamp_api_wrapper.rb +0 -82
  54. data/lib/bitex_bot/models/itbit_api_wrapper.rb +0 -68
  55. data/lib/bitex_bot/models/kraken_api_wrapper.rb +0 -188
  56. data/spec/models/bitfinex_api_wrapper_spec.rb +0 -17
  57. data/spec/models/bitstamp_api_wrapper_spec.rb +0 -15
  58. data/spec/models/itbit_api_wrapper_spec.rb +0 -15
  59. 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