bitex_bot 0.6.1 → 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +3 -3
  3. data/Gemfile +3 -1
  4. data/bitex_bot.gemspec +5 -2
  5. data/lib/bitex_bot/database.rb +2 -2
  6. data/lib/bitex_bot/models/api_wrappers/api_wrapper.rb +47 -35
  7. data/lib/bitex_bot/models/api_wrappers/bitex/bitex_api_wrapper.rb +178 -0
  8. data/lib/bitex_bot/models/api_wrappers/bitstamp/bitstamp_api_wrapper.rb +62 -45
  9. data/lib/bitex_bot/models/api_wrappers/itbit/itbit_api_wrapper.rb +52 -28
  10. data/lib/bitex_bot/models/api_wrappers/kraken/kraken_api_wrapper.rb +61 -28
  11. data/lib/bitex_bot/models/api_wrappers/kraken/kraken_order.rb +12 -6
  12. data/lib/bitex_bot/models/buy_closing_flow.rb +3 -2
  13. data/lib/bitex_bot/models/buy_opening_flow.rb +31 -6
  14. data/lib/bitex_bot/models/closing_flow.rb +37 -22
  15. data/lib/bitex_bot/models/open_buy.rb +1 -3
  16. data/lib/bitex_bot/models/open_sell.rb +1 -3
  17. data/lib/bitex_bot/models/opening_flow.rb +42 -28
  18. data/lib/bitex_bot/models/order_book_simulator.rb +14 -13
  19. data/lib/bitex_bot/models/sell_closing_flow.rb +3 -2
  20. data/lib/bitex_bot/models/sell_opening_flow.rb +29 -4
  21. data/lib/bitex_bot/robot.rb +28 -43
  22. data/lib/bitex_bot/settings.rb +2 -0
  23. data/lib/bitex_bot/version.rb +1 -1
  24. data/settings.rb.sample +23 -5
  25. data/spec/bitex_bot/settings_spec.rb +13 -6
  26. data/spec/factories/bitex_ask.rb +14 -0
  27. data/spec/factories/bitex_bid.rb +14 -0
  28. data/spec/factories/bitex_buy.rb +7 -7
  29. data/spec/factories/bitex_sell.rb +7 -7
  30. data/spec/factories/buy_opening_flow.rb +10 -10
  31. data/spec/factories/open_buy.rb +8 -8
  32. data/spec/factories/open_sell.rb +8 -8
  33. data/spec/factories/sell_opening_flow.rb +10 -10
  34. data/spec/fixtures/bitstamp/balance.yml +63 -0
  35. data/spec/fixtures/bitstamp/order_book.yml +60 -0
  36. data/spec/fixtures/bitstamp/orders/all.yml +62 -0
  37. data/spec/fixtures/bitstamp/orders/failure_sell.yml +60 -0
  38. data/spec/fixtures/bitstamp/orders/successful_buy.yml +62 -0
  39. data/spec/fixtures/bitstamp/transactions.yml +244 -0
  40. data/spec/fixtures/bitstamp/user_transactions.yml +223 -0
  41. data/spec/models/api_wrappers/bitex_api_wrapper_spec.rb +147 -0
  42. data/spec/models/api_wrappers/bitstamp_api_wrapper_spec.rb +134 -140
  43. data/spec/models/api_wrappers/itbit_api_wrapper_spec.rb +9 -3
  44. data/spec/models/api_wrappers/kraken_api_wrapper_spec.rb +142 -73
  45. data/spec/models/bitex_api_spec.rb +4 -4
  46. data/spec/models/buy_closing_flow_spec.rb +19 -24
  47. data/spec/models/buy_opening_flow_spec.rb +102 -83
  48. data/spec/models/order_book_simulator_spec.rb +5 -0
  49. data/spec/models/robot_spec.rb +7 -4
  50. data/spec/models/sell_closing_flow_spec.rb +21 -25
  51. data/spec/models/sell_opening_flow_spec.rb +100 -80
  52. data/spec/spec_helper.rb +3 -1
  53. data/spec/support/bitex_stubs.rb +80 -40
  54. data/spec/support/bitstamp/bitstamp_api_wrapper_stubs.rb +2 -2
  55. data/spec/support/bitstamp/bitstamp_stubs.rb +3 -3
  56. data/spec/support/vcr.rb +8 -0
  57. data/spec/support/webmock.rb +8 -0
  58. metadata +77 -10
@@ -1,11 +1,13 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe BitstampApiWrapper do
4
- let(:api_wrapper) { described_class }
5
4
  let(:taker_settings) do
6
5
  BitexBot::SettingsClass.new(
7
6
  bitstamp: {
8
- api_key: 'YOUR_API_KEY', secret: 'YOUR_API_SECRET', client_id: 'YOUR_BITSTAMP_USERNAME'
7
+ api_key: 'BITSTAMP_API_KEY',
8
+ secret: 'BITSTAMP_API_SECRET',
9
+ client_id: 'BITSTAMP_USERNAME',
10
+ order_book: 'btcusd'
9
11
  }
10
12
  )
11
13
  end
@@ -15,186 +17,178 @@ describe BitstampApiWrapper do
15
17
  BitexBot::Robot.setup
16
18
  end
17
19
 
18
- it 'Sends User-Agent header' do
19
- url = 'https://www.bitstamp.net/api/v2/balance/btcusd/'
20
- stub_stuff = stub_request(:post, url).with(headers: { 'User-Agent': BitexBot.user_agent })
20
+ let(:api_wrapper) { BitexBot::Robot.taker }
21
21
 
22
- # we don't care about the response
23
- api_wrapper.balance rescue nil
22
+ describe 'Sends User-Agent header' do
23
+ let(:url) { 'https://www.bitstamp.net/api/v2/balance/btcusd/' }
24
24
 
25
- expect(stub_stuff).to have_been_requested
26
- end
25
+ it do
26
+ stub_stuff = stub_request(:post, url).with(headers: { 'User-Agent': BitexBot.user_agent })
27
27
 
28
- def stub_balance(balance: '0.5', reserved: '1.5', available: '2.0', fee: '0.2')
29
- Bitstamp.stub(:balance) do
30
- {
31
- 'btc_balance' => balance,
32
- 'btc_reserved' => reserved,
33
- 'btc_available' => available,
34
- 'usd_balance' => balance,
35
- 'usd_reserved' => reserved,
36
- 'usd_available' => balance,
37
- 'fee' => fee
38
- }
28
+ # we don't care about the response
29
+ api_wrapper.balance rescue nil
30
+
31
+ expect(stub_stuff).to have_been_requested
39
32
  end
40
33
  end
41
34
 
42
- it '#balance' do
43
- stub_balance
35
+ describe '#currency_pair' do
36
+ it { expect(api_wrapper.currency_pair).to eq({ name: 'btcusd', base: 'btc', quote: 'usd' }) }
37
+ end
38
+
39
+ describe '#base' do
40
+ it { expect(api_wrapper.base).to eq('btc') }
41
+ end
44
42
 
45
- balance = api_wrapper.balance
46
- balance.should be_a(ApiWrapper::BalanceSummary)
47
- balance.crypto.should be_a(ApiWrapper::Balance)
48
- balance.fiat.should be_a(ApiWrapper::Balance)
43
+ describe '#quote' do
44
+ it { expect(api_wrapper.quote).to eq('usd') }
45
+ end
49
46
 
50
- crypto = balance.crypto
51
- crypto.total.should be_a(BigDecimal)
52
- crypto.reserved.should be_a(BigDecimal)
53
- crypto.available.should be_a(BigDecimal)
47
+ describe '#balance', vcr: { cassette_name: 'bitstamp/balance' } do
48
+ subject { api_wrapper.balance }
54
49
 
55
- fiat = balance.fiat
56
- fiat.total.should be_a(BigDecimal)
57
- fiat.reserved.should be_a(BigDecimal)
58
- fiat.available.should be_a(BigDecimal)
50
+ it { is_expected.to be_a(ApiWrapper::BalanceSummary) }
59
51
 
60
- balance.fee.should be_a(BigDecimal)
61
- end
52
+ its(:members) { is_expected.to eq(%i[crypto fiat fee]) }
62
53
 
63
- it '#cancel' do
64
- stub_orders
54
+ shared_examples_for 'currency balance' do |currency_type|
55
+ subject { api_wrapper.balance.send(currency_type) }
65
56
 
66
- expect(api_wrapper.orders.sample).to respond_to(:cancel!)
67
- end
57
+ it { is_expected.to be_a(ApiWrapper::Balance) }
68
58
 
69
- def stub_order_book(count: 3, price: 1.5, amount: 2.5)
70
- Bitstamp.stub(:order_book) do
71
- {
72
- 'timestamp' => Time.now.to_i.to_s,
73
- 'bids' => count.times.map { |i| [(price + i).to_s, (amount + i).to_s] },
74
- 'asks' => count.times.map { |i| [(price + i).to_s, (amount + i).to_s] }
75
- }
59
+ its(:members) { is_expected.to eq(%i[total reserved available]) }
60
+
61
+ its(:total) { is_expected.to be_a(BigDecimal) }
62
+ its(:reserved) { is_expected.to be_a(BigDecimal) }
63
+ its(:available) { is_expected.to be_a(BigDecimal) }
64
+ end
65
+
66
+ it_behaves_like 'currency balance', :crypto
67
+ it_behaves_like 'currency balance', :fiat
68
+
69
+ it 'fee' do
70
+ expect(api_wrapper.balance.fee).to be_a(BigDecimal)
76
71
  end
77
72
  end
78
73
 
79
- it '#order_book' do
80
- stub_order_book
74
+ describe '.order_book', vcr: { cassette_name: 'bitstamp/order_book' } do
75
+ subject { api_wrapper.order_book }
81
76
 
82
- order_book = api_wrapper.order_book
83
- order_book.should be_a(ApiWrapper::OrderBook)
84
- order_book.bids.all? { |bid| bid.should be_a(ApiWrapper::OrderSummary) }
85
- order_book.asks.all? { |ask| ask.should be_a(ApiWrapper::OrderSummary) }
86
- order_book.timestamp.should be_a(Integer)
77
+ it { is_expected.to be_a(ApiWrapper::OrderBook) }
87
78
 
88
- bid = order_book.bids.sample
89
- bid.price.should be_a(BigDecimal)
90
- bid.quantity.should be_a(BigDecimal)
79
+ its(:members) { is_expected.to eq(%i[timestamp bids asks]) }
91
80
 
92
- ask = order_book.asks.sample
93
- ask.price.should be_a(BigDecimal)
94
- ask.quantity.should be_a(BigDecimal)
95
- end
81
+ its(:timestamp) { is_expected.to be_a(Integer) }
82
+ its(:bids) { is_expected.to be_a(Array) }
83
+ its(:asks) { is_expected.to be_a(Array) }
96
84
 
97
- # [<Bitstamp::Order @id=76, @type=0, @price='1.1', @amount='1.0', @datetime='2013-09-26 23:15:04'>]
98
- def stub_orders(count: 1, price: 1.5, amount: 2.5)
99
- Bitstamp.orders.stub(:all) do
100
- count.times.map do |i|
101
- Bitstamp::Order.new(
102
- id: i,
103
- type: (i % 2),
104
- price: (price + 1).to_s,
105
- amount: (amount + i).to_s,
106
- datetime: 1.seconds.ago.strftime('%Y-%m-%d %H:%m:%S')
107
- )
108
- end
85
+ shared_examples_for :orders do |order_type|
86
+ subject { api_wrapper.order_book.send(order_type).sample }
87
+
88
+ it { is_expected.to be_a(ApiWrapper::OrderSummary) }
89
+
90
+ its(:price) { is_expected.to be_a(BigDecimal) }
91
+ its(:quantity) { is_expected.to be_a(BigDecimal) }
109
92
  end
93
+
94
+ it_behaves_like :orders, :bids
95
+ it_behaves_like :orders, :asks
110
96
  end
111
97
 
112
- it '#orders' do
113
- stub_orders
98
+ describe '.send_order' do
99
+ context 'successful buy', vcr: { cassette_name: 'bitstamp/orders/successful_buy' } do
100
+ subject { api_wrapper.send_order(:buy, 1.01, 1) }
114
101
 
115
- api_wrapper.orders.all? { |o| o.should be_a(ApiWrapper::Order) }
102
+ it { is_expected.to be_a(ApiWrapper::Order) }
116
103
 
117
- order = api_wrapper.orders.sample
118
- order.id.should be_a(String)
119
- order.type.should be_a(Symbol)
120
- order.price.should be_a(BigDecimal)
121
- order.amount.should be_a(BigDecimal)
122
- order.timestamp.should be_a(Integer)
104
+ its(:members) { is_expected.to eq(%i[id type price amount timestamp raw]) }
123
105
 
124
- expect(order).to respond_to(:cancel!)
125
- end
106
+ its(:id) { is_expected.to be_a(String) }
107
+ its(:type) { is_expected.to be_a(Symbol) }
108
+ its(:price) { is_expected.to be_a(BigDecimal) }
109
+ its(:amount) { is_expected.to be_a(BigDecimal) }
110
+ its(:timestamp) { is_expected.to be_a(Integer) }
111
+ its(:raw) { is_expected.to be_a(Bitstamp::Order) }
126
112
 
127
- context '#place_order' do
128
- it 'raises OrderNotFound error on bitstamp errors' do
129
- Bitstamp.orders.stub(:buy) do
130
- raise OrderNotFound
131
- end
113
+ context 'raw order' do
114
+ subject { api_wrapper.send_order(:buy, 1.01, 1).raw }
132
115
 
133
- expect { api_wrapper.place_order(:buy, 10, 100) }.to raise_exception(OrderNotFound)
116
+ its(:id) { is_expected.to be_a(Integer) }
117
+ its(:type) { is_expected.to be_a(Integer) }
118
+ its(:price) { is_expected.to be_a(String) }
119
+ its(:amount) { is_expected.to be_a(String) }
120
+ its(:datetime) { is_expected.to be_a(String) }
121
+ end
134
122
  end
135
- end
136
123
 
137
- # [<Bitstamp::Transactions @tid=14, @price='1.9', @amount='1.1', @date='1380648951'>]
138
- def stub_transactions(count: 1, price: 1.5, amount: 2.5)
139
- Bitstamp.stub(:transactions) do
140
- count.times.map do |i|
141
- double(
142
- tid: i,
143
- date: 1.seconds.ago.to_i,
144
- price: (price + i).to_s,
145
- amount: (amount + i).to_s
146
- )
147
- end
124
+ context 'failure sell', vcr: { cassette_name: 'bitstamp/orders/failure_sell' } do
125
+ subject { api_wrapper.send_order(:sell, 1_000, 1) }
126
+
127
+ it { is_expected.to be_nil }
148
128
  end
149
129
  end
150
130
 
151
- it '#transactions' do
152
- stub_transactions
131
+ describe '.transactions', vcr: { cassette_name: 'bitstamp/transactions' } do
132
+ subject { api_wrapper.transactions.sample }
133
+
134
+ it { is_expected.to be_a(ApiWrapper::Transaction) }
153
135
 
154
- api_wrapper.transactions.all? { |o| o.should be_a(ApiWrapper::Transaction) }
136
+ its(:members) { is_expected.to eq(%i[id price amount timestamp raw]) }
155
137
 
156
- transaction = api_wrapper.transactions.sample
157
- transaction.id.should be_a(Integer)
158
- transaction.price.should be_a(BigDecimal)
159
- transaction.amount.should be_a(BigDecimal)
160
- transaction.timestamp.should be_a(Integer)
138
+ its(:id) { is_expected.to be_a(Integer) }
139
+ its(:price) { is_expected.to be_a(BigDecimal) }
140
+ its(:amount) { is_expected.to be_a(BigDecimal) }
141
+ its(:timestamp) { is_expected.to be_a(Integer) }
142
+ its(:raw) { is_expected.to be_a(Bitstamp::Transactions) }
161
143
  end
162
144
 
163
- # [<Bitstamp::UserTransaction @id=76, @order_id=14, @type=1, @usd='0.00', @btc='-3.078', @btc_usd='0.00', @fee='0.00', @datetime='2013-09-26 13:46:59'>]
164
- def stub_user_transactions(count: 1, usd: 1.5, btc: 2.5, btc_usd: 3.5, fee: 0.05)
165
- Bitstamp.user_transactions.stub(:all) do
166
- count.times.map do |i|
167
- double(
168
- id: i,
169
- order_id: i,
170
- type: (i % 2),
171
- usd: (usd + i).to_s,
172
- btc: (btc + i).to_s,
173
- btc_usd: (btc_usd + i).to_s,
174
- fee: fee.to_s,
175
- datetime: 1.seconds.ago.strftime('%Y-%m-%d %H:%m:%S')
176
- )
177
- end
145
+ describe '.user_transaction', vcr: { cassette_name: 'bitstamp/user_transactions' } do
146
+ subject { api_wrapper.user_transactions.sample }
147
+
148
+ it { is_expected.to be_a(ApiWrapper::UserTransaction) }
149
+
150
+ its(:members) { is_expected.to eq(%i[order_id fiat crypto crypto_fiat fee type timestamp]) }
151
+
152
+ # same user transactions haven't order_id
153
+ its(:order_id) do
154
+ is_expected.to be_a(Integer) if subject.order_id.present?
155
+ is_expected.to be_nil unless subject.order_id.present?
178
156
  end
157
+
158
+ its(:fiat) { is_expected.to be_a(BigDecimal) }
159
+ its(:crypto) { is_expected.to be_a(BigDecimal) }
160
+ its(:crypto_fiat) { is_expected.to be_a(BigDecimal) }
161
+ its(:fee) { is_expected.to be_a(BigDecimal) }
162
+ its(:type) { is_expected.to be_a(Integer) }
163
+ its(:timestamp) { is_expected.to be_a(Integer) }
179
164
  end
180
165
 
181
- it '#user_transaction' do
182
- stub_user_transactions
183
- BitstampApiWrapper.user_transactions.all? { |ut| ut.should be_a(ApiWrapper::UserTransaction) }
184
-
185
- user_transaction = BitstampApiWrapper.user_transactions.sample
186
- user_transaction.usd.should be_a(BigDecimal)
187
- user_transaction.btc.should be_a(BigDecimal)
188
- user_transaction.btc_usd.should be_a(BigDecimal)
189
- user_transaction.order_id.should be_a(Integer)
190
- user_transaction.fee.should be_a(BigDecimal)
191
- user_transaction.type.should be_a(Integer)
192
- user_transaction.timestamp.should be_a(Integer)
166
+ describe '.orders', vcr: { cassette_name: 'bitstamp/orders/all' } do
167
+ subject { api_wrapper.orders.sample }
168
+
169
+ it { is_expected.to be_a(ApiWrapper::Order) }
170
+
171
+ its(:id) { is_expected.to be_a(String) }
172
+ its(:type) { is_expected.to be_a(Symbol) }
173
+ its(:price) { is_expected.to be_a(BigDecimal) }
174
+ its(:amount) { is_expected.to be_a(BigDecimal) }
175
+ its(:amount) { is_expected.to be_a(BigDecimal) }
176
+ its(:timestamp) { is_expected.to be_a(Integer) }
177
+ end
178
+
179
+ describe '.find_lost', vcr: { cassette_name: 'bitstamp/orders/all', allow_playback_repeats: true } do
180
+ before(:each) { Timecop.freeze(Time.strptime(order.timestamp.to_s, '%s') - 3.minutes.ago) }
181
+
182
+ let(:order) { api_wrapper.orders.sample }
183
+
184
+ subject { api_wrapper.find_lost(order.type, order.price, order.amount) }
185
+
186
+ it { is_expected.to be_present }
193
187
  end
194
188
 
195
- it '#find_lost' do
196
- stub_orders
189
+ describe '.cancel', vcr: { cassette_name: 'bitstamp/orders/all' } do
190
+ subject { api_wrapper.orders.sample }
197
191
 
198
- api_wrapper.orders.all? { |o| api_wrapper.find_lost(o.type, o.price, o.amount).present? }
192
+ it { is_expected.to respond_to(:cancel!) }
199
193
  end
200
194
  end
@@ -1,11 +1,15 @@
1
1
  require 'spec_helper'
2
2
 
3
3
  describe ItbitApiWrapper do
4
- let(:api_wrapper) { described_class }
5
4
  let(:taker_settings) do
6
5
  BitexBot::SettingsClass.new(
7
6
  itbit: {
8
- client_key: 'client-key', secret: 'secret', user_id: 'user-id', default_wallet_id: 'wallet-000', sandbox: false
7
+ client_key: 'client-key',
8
+ secret: 'secret',
9
+ user_id: 'user-id',
10
+ default_wallet_id: 'wallet-000',
11
+ sandbox: false,
12
+ order_book: 'xbtusd'
9
13
  }
10
14
  )
11
15
  end
@@ -15,8 +19,10 @@ describe ItbitApiWrapper do
15
19
  BitexBot::Robot.setup
16
20
  end
17
21
 
22
+ let(:api_wrapper) { BitexBot::Robot.taker }
23
+
18
24
  it 'Sends User-Agent header' do
19
- url = 'https://api.itbit.com/v1/markets/XBTUSD/order_book'
25
+ url = "https://api.itbit.com/v1/markets/#{api_wrapper.currency_pair[:name].upcase}/order_book"
20
26
  stub_stuff = stub_request(:get, url).with(headers: { 'User-Agent': BitexBot.user_agent })
21
27
 
22
28
  # We don't care about the response