bitex_bot 0.6.1 → 0.9.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 (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,17 +1,29 @@
1
1
  # Wrapper implementation for Itbit API.
2
2
  # https://api.itbit.com/docs
3
3
  class ItbitApiWrapper < ApiWrapper
4
- def self.setup(settings)
4
+ attr_accessor :client_key, :secret, :user_id, :default_wallet_id, :sandbox
5
+
6
+ def initialize(settings)
7
+ self.client_key = settings.client_key
8
+ self.secret = settings.secret
9
+ self.user_id = settings.user_id
10
+ self.default_wallet_id = settings.default_wallet_id
11
+ self.sandbox = settings.sandbox
12
+ currency_pair(settings.order_book)
13
+ setup
14
+ end
15
+
16
+ def setup
5
17
  Itbit.tap do |conf|
6
- conf.client_key = settings.client_key
7
- conf.secret = settings.secret
8
- conf.user_id = settings.user_id
9
- conf.default_wallet_id = settings.default_wallet_id
10
- conf.sandbox = settings.sandbox
18
+ conf.client_key = client_key
19
+ conf.secret = secret
20
+ conf.user_id = user_id
21
+ conf.default_wallet_id = default_wallet_id
22
+ conf.sandbox = sandbox
11
23
  end
12
24
  end
13
25
 
14
- def self.amount_and_quantity(order_id, _transactions)
26
+ def amount_and_quantity(order_id)
15
27
  order = Itbit::Order.find(order_id)
16
28
  amount = order.volume_weighted_average_price * order.amount_filled
17
29
  quantity = order.amount_filled
@@ -19,24 +31,24 @@ class ItbitApiWrapper < ApiWrapper
19
31
  [amount, quantity]
20
32
  end
21
33
 
22
- def self.balance
34
+ def balance
23
35
  balance_summary_parser(wallet[:balances])
24
36
  end
25
37
 
26
- def self.find_lost(type, price, _quantity)
38
+ def find_lost(type, price, _quantity)
27
39
  orders.find { |o| o.type == type && o.price == price && o.timestamp >= 5.minutes.ago.to_i }
28
40
  end
29
41
 
30
- def self.order_book
31
- order_book_parser(Itbit::XBTUSDMarketData.orders)
42
+ def order_book
43
+ order_book_parser(market.orders)
32
44
  end
33
45
 
34
- def self.orders
35
- Itbit::Order.all(status: :open).map { |o| order_parser(o) }
46
+ def orders
47
+ Itbit::Order.all(instrument: currency_pair, status: :open).map { |o| order_parser(o) }
36
48
  end
37
49
 
38
- def self.place_order(type, price, quantity)
39
- Itbit::Order.create!(type, :xbtusd, quantity.round(4), price.round(2), wait: true)
50
+ def place_order(type, price, quantity)
51
+ Itbit::Order.create!(type, currency_pair, quantity.round(4), price.round(2), wait: true, currency: currency_base)
40
52
  rescue RestClient::RequestTimeout => e
41
53
  # On timeout errors, we still look for the latest active closing order that may be available.
42
54
  # We have a magic threshold of 5 minutes and also use the price to recognize an order as the current one.
@@ -49,12 +61,12 @@ class ItbitApiWrapper < ApiWrapper
49
61
  raise e
50
62
  end
51
63
 
52
- def self.transactions
53
- Itbit::XBTUSDMarketData.trades.map { |t| transaction_parser(t.symbolize_keys) }
64
+ def transactions
65
+ market.trades.map { |t| transaction_parser(t.symbolize_keys) }
54
66
  end
55
67
 
56
68
  # We don't need to fetch the list of transaction for itbit since we wont actually use them later.
57
- def self.user_transactions
69
+ def user_transactions
58
70
  []
59
71
  end
60
72
 
@@ -64,15 +76,15 @@ class ItbitApiWrapper < ApiWrapper
64
76
  # { total_balance: 0.0, currency: :eur, available_balance: 0.0 },
65
77
  # { total_balance: 0.0, currency: :sgd, available_balance: 0.0 }
66
78
  # ]
67
- def self.balance_summary_parser(balances)
68
- BalanceSummary.new(balance_parser(balances, :xbt), balance_parser(balances, :usd), 0.5.to_d)
79
+ def balance_summary_parser(balances)
80
+ BalanceSummary.new(balance_parser(balances, base.to_sym), balance_parser(balances, quote.to_sym), 0.5.to_d)
69
81
  end
70
82
 
71
- def self.wallet
83
+ def wallet
72
84
  Itbit::Wallet.all.find { |w| w[:id] == Itbit.default_wallet_id }
73
85
  end
74
86
 
75
- def self.balance_parser(balances, currency)
87
+ def balance_parser(balances, currency)
76
88
  currency_balance = balances.find { |balance| balance[:currency] == currency }
77
89
  Balance.new(
78
90
  currency_balance[:total_balance].to_d,
@@ -81,7 +93,7 @@ class ItbitApiWrapper < ApiWrapper
81
93
  )
82
94
  end
83
95
 
84
- def self.last_order_by(price)
96
+ def last_order_by(price)
85
97
  Itbit::Order.all.select { |o| o.price == price && (o.created_time - Time.now.to_i).abs < 500 }.first
86
98
  end
87
99
 
@@ -89,11 +101,11 @@ class ItbitApiWrapper < ApiWrapper
89
101
  # bids: [[0.63921e3, 0.195e1], [0.637e3, 0.47e0], [0.63e3, 0.158e1]],
90
102
  # asks: [[0.6424e3, 0.4e0], [0.6433e3, 0.95e0], [0.6443e3, 0.25e0]]
91
103
  # }
92
- def self.order_book_parser(book)
104
+ def order_book_parser(book)
93
105
  OrderBook.new(Time.now.to_i, order_summary_parser(book[:bids]), order_summary_parser(book[:asks]))
94
106
  end
95
107
 
96
- def self.order_summary_parser(orders)
108
+ def order_summary_parser(orders)
97
109
  orders.map { |order| OrderSummary.new(order[0], order[1]) }
98
110
  end
99
111
 
@@ -103,12 +115,24 @@ class ItbitApiWrapper < ApiWrapper
103
115
  # @volume_weighted_average_price=0.0, @amount_filled=0.0, @created_time=1415290187, @status=:open,
104
116
  # @metadata={foo: 'bar'}, @client_order_identifier='o'
105
117
  # >
106
- def self.order_parser(order)
118
+ def order_parser(order)
107
119
  Order.new(order.id, order.side, order.price, order.amount, order.created_time, order)
108
120
  end
109
121
 
110
122
  # { tid: 601855, price: 0.41814e3, amount: 0.19e-1, date: 1460161126 }
111
- def self.transaction_parser(transaction)
112
- Transaction.new(transaction[:tid], transaction[:price], transaction[:amount], transaction[:date])
123
+ def transaction_parser(transaction)
124
+ Transaction.new(transaction[:tid], transaction[:price], transaction[:amount], transaction[:date], transaction)
125
+ end
126
+
127
+ def market
128
+ "Itbit::#{currency_pair[:name].upcase}MarketData".constantize
129
+ end
130
+
131
+ def currency_pair(order_book = '')
132
+ @currency_pair ||= {
133
+ name: order_book,
134
+ base: order_book.slice(0..2),
135
+ quote: order_book.slice(3..6)
136
+ }
113
137
  end
114
138
  end
@@ -1,83 +1,90 @@
1
1
  # Wrapper implementation for Kraken API.
2
2
  # https://www.kraken.com/en-us/help/api
3
3
  class KrakenApiWrapper < ApiWrapper
4
+ attr_accessor :api_key, :api_secret, :client
5
+
4
6
  MIN_AMOUNT = 0.002
5
7
 
6
- def self.setup(settings)
7
- HTTParty::Basement.headers('User-Agent' => BitexBot.user_agent)
8
- @settings = settings
8
+ def initialize(settings)
9
+ self.api_key = settings.api_key
10
+ self.api_secret = settings.api_secret
11
+ setup
9
12
  end
10
13
 
11
- def self.client
12
- @client ||= KrakenClient.load(@settings)
14
+ def setup
15
+ KrakenOrder.api_wrapper = self
16
+ self.client ||= KrakenClient.load(api_key: api_key, api_secret: api_secret)
17
+ HTTParty::Basement.headers('User-Agent' => BitexBot.user_agent)
13
18
  end
14
19
 
15
- def self.amount_and_quantity(order_id, _transactions)
20
+ def amount_and_quantity(order_id)
16
21
  KrakenOrder.amount_and_quantity(order_id)
17
22
  end
18
23
 
19
- def self.balance
24
+ def balance
20
25
  balance_summary_parser(client.private.balance)
21
26
  rescue KrakenClient::ErrorResponse, Net::ReadTimeout
22
27
  retry
23
28
  end
24
29
 
25
- def self.enough_order_size?(quantity, _price)
30
+ def enough_order_size?(quantity, _price)
26
31
  quantity >= MIN_AMOUNT
27
32
  end
28
33
 
29
- def self.find_lost(type, price, quantity)
34
+ def find_lost(type, price, quantity)
30
35
  KrakenOrder.find_lost(type, price, quantity)
31
36
  end
32
37
 
33
- def self.order_book
34
- order_book_parser(client.public.order_book('XBTUSD')[:XXBTZUSD])
38
+ def order_book
39
+ order_book_parser(client.public.order_book(currency_pair[:altname])[currency_pair[:name]])
35
40
  rescue NoMethodError
36
41
  retry
37
42
  end
38
43
 
39
- def self.orders
44
+ def orders
40
45
  KrakenOrder.open.map { |ko| order_parser(ko) }
41
46
  end
42
47
 
43
- def self.send_order(type, price, quantity)
48
+ def send_order(type, price, quantity)
44
49
  KrakenOrder.create!(type, price, quantity)
45
50
  end
46
51
 
47
- def self.transactions
48
- client.public.trades('XBTUSD')[:XXBTZUSD].reverse.map { |t| transaction_parser(t) }
52
+ def transactions
53
+ client.public.trades(currency_pair[:altname])[currency_pair[:name]].reverse.map { |t| transaction_parser(t) }
49
54
  rescue NoMethodError
50
55
  retry
51
56
  end
52
57
 
53
58
  # We don't need to fetch the list of transactions for Kraken
54
- def self.user_transactions
59
+ def user_transactions
55
60
  []
56
61
  end
57
62
 
58
63
  # { ZEUR: '1433.0939', XXBT: '0.0000000000', 'XETH': '99.7497224800' }
59
- def self.balance_summary_parser(balances)
64
+ # rubocop:disable Metrics/AbcSize
65
+ def balance_summary_parser(balances)
60
66
  open_orders = KrakenOrder.open
61
67
  BalanceSummary.new(
62
- balance_parser(balances, :XXBT, btc_reserved(open_orders)),
63
- balance_parser(balances, :ZUSD, usd_reserved(open_orders)),
64
- client.private.trade_volume(pair: 'XBTUSD')[:fees][:XXBTZUSD][:fee].to_d
68
+ balance_parser(balances, currency_pair[:base], btc_reserved(open_orders)),
69
+ balance_parser(balances, currency_pair[:quote], usd_reserved(open_orders)),
70
+ client.private.trade_volume(pair: currency_pair[:altname])[:fees][currency_pair[:name]][:fee].to_d
65
71
  )
66
72
  end
73
+ # rubocop:enable Metrics/AbcSize
67
74
 
68
- def self.balance_parser(balances, currency, reserved)
75
+ def balance_parser(balances, currency, reserved)
69
76
  Balance.new(balances[currency].to_d, reserved, balances[currency].to_d - reserved)
70
77
  end
71
78
 
72
- def self.btc_reserved(open_orders)
79
+ def btc_reserved(open_orders)
73
80
  orders_by(open_orders, :sell).map { |o| (o.amount - o.executed_amount).to_d }.sum
74
81
  end
75
82
 
76
- def self.usd_reserved(open_orders)
83
+ def usd_reserved(open_orders)
77
84
  orders_by(open_orders, :buy).map { |o| (o.amount - o.executed_amount) * o.price.to_d }.sum
78
85
  end
79
86
 
80
- def self.orders_by(open_orders, order_type)
87
+ def orders_by(open_orders, order_type)
81
88
  open_orders.select { |o| o.type == order_type }
82
89
  end
83
90
 
@@ -85,16 +92,16 @@ class KrakenApiWrapper < ApiWrapper
85
92
  # 'asks': [['204.52893', '0.010', 1440291148], ['204.78790', '0.312', 1440291132]],
86
93
  # 'bids': [['204.24000', '0.100', 1440291016], ['204.23010', '0.312', 1440290699]]
87
94
  # }
88
- def self.order_book_parser(book)
95
+ def order_book_parser(book)
89
96
  OrderBook.new(Time.now.to_i, order_summary_parser(book[:bids]), order_summary_parser(book[:asks]))
90
97
  end
91
98
 
92
- def self.order_summary_parser(stock_market)
99
+ def order_summary_parser(stock_market)
93
100
  stock_market.map { |stock| OrderSummary.new(stock[0].to_d, stock[1].to_d) }
94
101
  end
95
102
 
96
103
  # <KrakenOrder: @id='O5TDV2-WDYB2-6OGJRD', @type=:buy, @price='1.01', @amount='1.00000000', @datetime='2013-09-26 23:15:04'>
97
- def self.order_parser(order)
104
+ def order_parser(order)
98
105
  Order.new(order.id.to_s, order.type, order.price, order.amount, order.datetime, order)
99
106
  end
100
107
 
@@ -103,7 +110,33 @@ class KrakenApiWrapper < ApiWrapper
103
110
  # ['202.51626', '0.01440000', 1440277319.1922, 'b', 'l', ''],
104
111
  # ['202.54000', '0.10000000', 1440277322.8993, 'b', 'l', '']
105
112
  # ]
106
- def self.transaction_parser(transaction)
113
+ def transaction_parser(transaction)
107
114
  Transaction.new(transaction[2].to_i, transaction[0].to_d, transaction[1].to_d, transaction[2].to_i)
108
115
  end
116
+
117
+ # {
118
+ # 'XBTUSD' => {
119
+ # 'altname' => 'XBTUSD',
120
+ # 'aclass_base' => 'currency',
121
+ # 'base' => 'XXBT',
122
+ # 'aclass_quote' => 'currency',
123
+ # 'quote' => 'ZUSD',
124
+ # 'lot' => 'unit',
125
+ # 'pair_decimals' => 1,
126
+ # 'lot_decimals' => 8,
127
+ # 'lot_multiplier' => 1,
128
+ # 'leverage_buy' => [2, 3, 4, 5],
129
+ # 'leverage_sell' => [2, 3, 4, 5],
130
+ # 'fees' => [[0, 0.26], .., [250_000, 0.2]],
131
+ # 'fees_maker' => [[0, 0.16], .., [250_000, 0.1]],
132
+ # 'fee_volume_currency' => 'ZUSD',
133
+ # 'margin_call' => 80,
134
+ # 'margin_stop' => 40
135
+ # }
136
+ # }
137
+ def currency_pair
138
+ @currency_pair ||= client.public.asset_pairs.map do |currency_pair, data|
139
+ [data['altname'], data.merge(name: currency_pair).with_indifferent_access]
140
+ end.to_h[BitexBot::Settings.taker_settings.order_book.upcase]
141
+ end
109
142
  end
@@ -2,7 +2,7 @@ require 'kraken_client'
2
2
 
3
3
  # Wrapper for Kraken orders.
4
4
  class KrakenOrder
5
- cattr_accessor :last_closed_order
5
+ cattr_accessor :last_closed_order, :api_wrapper
6
6
  attr_accessor :id, :amount, :executed_amount, :price, :avg_price, :type, :datetime
7
7
 
8
8
  # rubocop:disable Metrics/AbcSize
@@ -24,11 +24,17 @@ class KrakenOrder
24
24
  # rubocop:enable Metrics/AbcSize
25
25
 
26
26
  def self.order_info_by(type, price, quantity)
27
- KrakenApiWrapper.client.private.add_order(pair: 'XBTUSD', type: type, ordertype: 'limit', price: price, volume: quantity)
27
+ api_wrapper.client.private.add_order(
28
+ pair: KrakenApiWrapper.currency_pair[:altname],
29
+ type: type,
30
+ ordertype: 'limit',
31
+ price: price,
32
+ volume: quantity
33
+ )
28
34
  end
29
35
 
30
36
  def self.find(id)
31
- new(*KrakenApiWrapper.client.private.query_orders(txid: id).first)
37
+ new(*api_wrapper.client.private.query_orders(txid: id).first)
32
38
  rescue KrakenClient::ErrorResponse
33
39
  retry
34
40
  end
@@ -42,13 +48,13 @@ class KrakenOrder
42
48
  end
43
49
 
44
50
  def self.open
45
- KrakenApiWrapper.client.private.open_orders['open'].map { |o| new(*o) }
51
+ api_wrapper.client.private.open_orders['open'].map { |o| new(*o) }
46
52
  rescue KrakenClient::ErrorResponse
47
53
  retry
48
54
  end
49
55
 
50
56
  def self.closed(start: 1.hour.ago.to_i)
51
- KrakenApiWrapper.client.private.closed_orders(start: start)[:closed].map { |o| new(*o) }
57
+ api_wrapper.client.private.closed_orders(start: start)[:closed].map { |o| new(*o) }
52
58
  rescue KrakenClient::ErrorResponse
53
59
  retry
54
60
  end
@@ -100,7 +106,7 @@ class KrakenOrder
100
106
  end
101
107
 
102
108
  def cancel!
103
- KrakenApiWrapper.client.private.cancel_order(txid: id)
109
+ api_wrapper.client.private.cancel_order(txid: id)
104
110
  rescue KrakenClient::ErrorResponse => e
105
111
  e.message == 'EService:Unavailable' ? retry : raise
106
112
  end
@@ -10,9 +10,10 @@ module BitexBot
10
10
  OpenBuy
11
11
  end
12
12
 
13
- def fx_rate
13
+ def self.fx_rate
14
14
  Settings.buying_fx_rate
15
15
  end
16
+ def_delegator self, :fx_rate
16
17
 
17
18
  private
18
19
 
@@ -37,7 +38,7 @@ module BitexBot
37
38
  # end: create_or_cancel! hookers
38
39
 
39
40
  # create_order_and_close_position hookers
40
- def order_method
41
+ def order_type
41
42
  :sell
42
43
  end
43
44
  # end: create_order_and_close_position hookers
@@ -28,13 +28,13 @@ module BitexBot
28
28
  # @return [BuyOpeningFlow] The newly created flow.
29
29
  # @raise [CannotCreateFlow] If there's any problem creating this flow, for example when you run out of USD on bitex or out
30
30
  # of BTC on the other exchange.
31
- def self.create_for_market(btc_balance, order_book, transactions, maker_fee, taker_fee, store)
31
+ def self.create_for_market(taker_crypto_balance, taker_bids, taker_transactions, maker_fee, taker_fee, store)
32
32
  super
33
33
  end
34
34
 
35
35
  # sync_open_positions helpers
36
36
  def self.transaction_order_id(transaction)
37
- transaction.bid_id
37
+ transaction.raw.bid_id
38
38
  end
39
39
 
40
40
  def self.open_position_class
@@ -50,23 +50,28 @@ module BitexBot
50
50
 
51
51
  # create_for_market helpers
52
52
  def self.maker_price(crypto_to_resell)
53
- value_to_use / crypto_to_resell * (1 - profit / 100)
53
+ value_to_use * fx_rate / crypto_to_resell * (1 - profit / 100)
54
54
  end
55
55
 
56
56
  def self.order_class
57
57
  Bitex::Bid
58
58
  end
59
+ def_delegator self, :order_class
60
+
61
+ def self.order_type
62
+ :buy
63
+ end
59
64
 
60
65
  def self.profit
61
66
  store.buying_profit || Settings.buying.profit
62
67
  end
63
68
 
64
69
  def self.remote_value_to_use(value_to_use_needed, safest_price)
65
- (value_to_use_needed / fx_rate) / safest_price
70
+ value_to_use_needed / safest_price
66
71
  end
67
72
 
68
- def self.safest_price(transactions, order_book, amount_to_use)
69
- OrderBookSimulator.run(Settings.time_to_live, transactions, order_book, amount_to_use / fx_rate, nil)
73
+ def self.safest_price(transactions, taker_bids, amount_to_use)
74
+ OrderBookSimulator.run(Settings.time_to_live, transactions, taker_bids, amount_to_use, nil, fx_rate)
70
75
  end
71
76
 
72
77
  def self.value_to_use
@@ -77,5 +82,25 @@ module BitexBot
77
82
  def self.fx_rate
78
83
  Settings.buying_fx_rate
79
84
  end
85
+
86
+ def self.value_per_order
87
+ value_to_use * fx_rate
88
+ end
89
+
90
+ def self.maker_specie_to_spend
91
+ Robot.maker.quote.upcase
92
+ end
93
+
94
+ def self.maker_specie_to_obtain
95
+ Robot.maker.base.upcase
96
+ end
97
+
98
+ def self.taker_specie_to_spend
99
+ Robot.taker.base.upcase
100
+ end
101
+
102
+ def self.taker_specie_to_obtain
103
+ Robot.taker.quote.upcase
104
+ end
80
105
  end
81
106
  end
@@ -1,22 +1,28 @@
1
1
  module BitexBot
2
2
  # Close buy/sell positions.
3
3
  class ClosingFlow < ActiveRecord::Base
4
+ extend Forwardable
5
+
4
6
  self.abstract_class = true
5
7
 
6
8
  cattr_reader(:close_time_to_live) { 30 }
7
9
 
8
- # Start a new CloseBuy that closes exising OpenBuy's by selling on another exchange what was just bought on bitex.
10
+ # Start a new CloseBuy that closes existing OpenBuy's by selling on another exchange what was just bought on bitex.
9
11
  def self.close_open_positions
10
- open_positions = open_position_class.open
11
- return if open_positions.empty?
12
+ return unless open_positions.any?
12
13
 
13
- quantity = open_positions.map(&:quantity).sum
14
- amount = open_positions.map(&:amount).sum
15
- price = suggested_amount(open_positions) / quantity
14
+ positions = open_positions
15
+ quantity = positions.sum(&:quantity)
16
+ amount = positions.sum(&:amount) / fx_rate
17
+ price = suggested_amount(positions) / quantity
16
18
 
17
- # Don't even bother trying to close a position that's too small.
18
19
  return unless Robot.taker.enough_order_size?(quantity, price)
19
- create_closing_flow!(price, quantity, amount, open_positions)
20
+
21
+ create_closing_flow!(price, quantity, amount, positions)
22
+ end
23
+
24
+ def self.open_positions
25
+ open_position_class.open
20
26
  end
21
27
 
22
28
  # close_open_positions helpers
@@ -36,9 +42,9 @@ module BitexBot
36
42
  end
37
43
 
38
44
  # TODO: should receive a order_ids and user_transaccions array, then each Wrapper should know how to search for them.
39
- def sync_closed_positions(orders, transactions)
45
+ def sync_closed_positions
40
46
  # Maybe we couldn't create the bitstamp order when this flow was created, so we try again when syncing.
41
- latest_close.nil? ? create_initial_order_and_close_position! : create_or_cancel!(orders, transactions)
47
+ latest_close.nil? ? create_initial_order_and_close_position! : create_or_cancel!
42
48
  end
43
49
 
44
50
  def estimate_fiat_profit
@@ -53,15 +59,14 @@ module BitexBot
53
59
 
54
60
  # sync_closed_positions helpers
55
61
  # rubocop:disable Metrics/AbcSize
56
- # Metrics/AbcSize: Assignment Branch Condition size for create_or_cancel! is too high. [17.23/16]
57
- def create_or_cancel!(orders, transactions)
62
+ def create_or_cancel!
58
63
  order_id = latest_close.order_id.to_s
59
- order = orders.find { |o| o.id.to_s == order_id }
64
+ order = Robot.with_cooldown { Robot.taker.orders.find { |o| o.id.to_s == order_id } }
60
65
 
61
66
  # When order is nil it means the other exchange is done executing it so we can now have a look of all the sales that were
62
67
  # spawned from it.
63
68
  if order.nil?
64
- sync_position(order_id, transactions)
69
+ sync_position(order_id)
65
70
  create_next_position!
66
71
  elsif latest_close.created_at < close_time_to_live.seconds.ago
67
72
  cancel!(order)
@@ -77,9 +82,9 @@ module BitexBot
77
82
  # create_or_cancel! helpers
78
83
  def cancel!(order)
79
84
  Robot.with_cooldown do
80
- Robot.log(:debug, "Finalising #{order.class}##{order.id}")
85
+ Robot.log(:debug, "Finalising #{order.raw.class}##{order.id}")
81
86
  order.cancel!
82
- Robot.log(:debug, "Finalised #{order.class}##{order.id}")
87
+ Robot.log(:debug, "Finalised #{order.raw.class}##{order.id}")
83
88
  end
84
89
  rescue StandardError => error
85
90
  Robot.log(:debug, error)
@@ -90,19 +95,25 @@ module BitexBot
90
95
  # estimate_crypto_profit
91
96
  # amount_positions_balance
92
97
  # next_price_and_quantity
98
+ # rubocop:disable Metrics/AbcSize
93
99
  def create_next_position!
94
100
  next_price, next_quantity = next_price_and_quantity
95
101
  if Robot.taker.enough_order_size?(next_quantity, next_price)
96
102
  create_order_and_close_position(next_quantity, next_price)
97
103
  else
98
104
  update!(crypto_profit: estimate_crypto_profit, fiat_profit: estimate_fiat_profit, fx_rate: fx_rate, done: true)
99
- Robot.logger.info("Closing: Finished #{self.class.name} ##{id} earned $#{fiat_profit} and #{crypto_profit} BTC.")
105
+ Robot.log(
106
+ :info,
107
+ "Closing: Finished #{self.class} ##{id} earned"\
108
+ "#{Robot.maker.quote.upcase} #{fiat_profit} and #{Robot.maker.base.upcase} #{crypto_profit}."
109
+ )
100
110
  end
101
111
  end
112
+ # rubocop:enable Metrics/AbcSize
102
113
 
103
- def sync_position(order_id, transactions)
114
+ def sync_position(order_id)
104
115
  latest = latest_close
105
- latest.amount, latest.quantity = Robot.taker.amount_and_quantity(order_id, transactions)
116
+ latest.amount, latest.quantity = Robot.taker.amount_and_quantity(order_id)
106
117
  latest.save!
107
118
  end
108
119
  # end: create_or_cancel! helpers
@@ -114,11 +125,15 @@ module BitexBot
114
125
  # end: next_price_and_quantity helpers
115
126
 
116
127
  # This use hooks methods, these must be defined in the subclass:
117
- # order_method
128
+ # order_type
118
129
  def create_order_and_close_position(quantity, price)
119
130
  # TODO: investigate how to generate an ID to insert in the fields of goals where possible.
120
- Robot.log(:info, "Closing: Going to place #{order_method} order for #{self.class.name} ##{id} #{quantity} BTC @ $#{price}")
121
- order = Robot.taker.place_order(order_method, price, quantity)
131
+ Robot.log(
132
+ :info,
133
+ "Closing: Going to place #{order_type} order for #{self.class} ##{id}"\
134
+ " #{Robot.taker.base.upcase} #{quantity} @ #{Robot.taker.quote.upcase} #{price}"
135
+ )
136
+ order = Robot.taker.place_order(order_type, price, quantity)
122
137
  close_positions.create!(order_id: order.id)
123
138
  end
124
139
  end
@@ -1,12 +1,10 @@
1
1
  module BitexBot
2
2
  # An OpenBuy represents a Buy transaction on Bitex.
3
3
  # OpenBuys are open buy positions that are closed by one or several CloseBuys.
4
- # TODO: document attributes.
5
- #
6
4
  class OpenBuy < ActiveRecord::Base
7
5
  belongs_to :opening_flow, class_name: 'BuyOpeningFlow', foreign_key: :opening_flow_id
8
6
  belongs_to :closing_flow, class_name: 'BuyClosingFlow', foreign_key: :closing_flow_id
9
7
 
10
- scope :open, -> { where('closing_flow_id IS NULL') }
8
+ scope :open, -> { where(closing_flow: nil) }
11
9
  end
12
10
  end
@@ -1,12 +1,10 @@
1
1
  module BitexBot
2
2
  # An OpenSell represents a Sell transaction on Bitex.
3
3
  # OpenSells are open sell positions that are closed by one SellClosingFlow.
4
- # TODO: document attributes.
5
- #
6
4
  class OpenSell < ActiveRecord::Base
7
5
  belongs_to :opening_flow, class_name: 'SellOpeningFlow', foreign_key: :opening_flow_id
8
6
  belongs_to :closing_flow, class_name: 'SellClosingFlow', foreign_key: :closing_flow_id
9
7
 
10
- scope :open, -> { where('closing_flow_id IS NULL') }
8
+ scope :open, -> { where(closing_flow: nil) }
11
9
  end
12
10
  end