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.
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