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.
- checksums.yaml +4 -4
- data/.rubocop.yml +3 -3
- data/Gemfile +3 -1
- data/bitex_bot.gemspec +5 -2
- data/lib/bitex_bot/database.rb +2 -2
- data/lib/bitex_bot/models/api_wrappers/api_wrapper.rb +47 -35
- data/lib/bitex_bot/models/api_wrappers/bitex/bitex_api_wrapper.rb +178 -0
- data/lib/bitex_bot/models/api_wrappers/bitstamp/bitstamp_api_wrapper.rb +62 -45
- data/lib/bitex_bot/models/api_wrappers/itbit/itbit_api_wrapper.rb +52 -28
- data/lib/bitex_bot/models/api_wrappers/kraken/kraken_api_wrapper.rb +61 -28
- data/lib/bitex_bot/models/api_wrappers/kraken/kraken_order.rb +12 -6
- data/lib/bitex_bot/models/buy_closing_flow.rb +3 -2
- data/lib/bitex_bot/models/buy_opening_flow.rb +31 -6
- data/lib/bitex_bot/models/closing_flow.rb +37 -22
- data/lib/bitex_bot/models/open_buy.rb +1 -3
- data/lib/bitex_bot/models/open_sell.rb +1 -3
- data/lib/bitex_bot/models/opening_flow.rb +42 -28
- data/lib/bitex_bot/models/order_book_simulator.rb +14 -13
- data/lib/bitex_bot/models/sell_closing_flow.rb +3 -2
- data/lib/bitex_bot/models/sell_opening_flow.rb +29 -4
- data/lib/bitex_bot/robot.rb +28 -43
- data/lib/bitex_bot/settings.rb +2 -0
- data/lib/bitex_bot/version.rb +1 -1
- data/settings.rb.sample +23 -5
- data/spec/bitex_bot/settings_spec.rb +13 -6
- data/spec/factories/bitex_ask.rb +14 -0
- data/spec/factories/bitex_bid.rb +14 -0
- data/spec/factories/bitex_buy.rb +7 -7
- data/spec/factories/bitex_sell.rb +7 -7
- data/spec/factories/buy_opening_flow.rb +10 -10
- data/spec/factories/open_buy.rb +8 -8
- data/spec/factories/open_sell.rb +8 -8
- data/spec/factories/sell_opening_flow.rb +10 -10
- data/spec/fixtures/bitstamp/balance.yml +63 -0
- data/spec/fixtures/bitstamp/order_book.yml +60 -0
- data/spec/fixtures/bitstamp/orders/all.yml +62 -0
- data/spec/fixtures/bitstamp/orders/failure_sell.yml +60 -0
- data/spec/fixtures/bitstamp/orders/successful_buy.yml +62 -0
- data/spec/fixtures/bitstamp/transactions.yml +244 -0
- data/spec/fixtures/bitstamp/user_transactions.yml +223 -0
- data/spec/models/api_wrappers/bitex_api_wrapper_spec.rb +147 -0
- data/spec/models/api_wrappers/bitstamp_api_wrapper_spec.rb +134 -140
- data/spec/models/api_wrappers/itbit_api_wrapper_spec.rb +9 -3
- data/spec/models/api_wrappers/kraken_api_wrapper_spec.rb +142 -73
- data/spec/models/bitex_api_spec.rb +4 -4
- data/spec/models/buy_closing_flow_spec.rb +19 -24
- data/spec/models/buy_opening_flow_spec.rb +102 -83
- data/spec/models/order_book_simulator_spec.rb +5 -0
- data/spec/models/robot_spec.rb +7 -4
- data/spec/models/sell_closing_flow_spec.rb +21 -25
- data/spec/models/sell_opening_flow_spec.rb +100 -80
- data/spec/spec_helper.rb +3 -1
- data/spec/support/bitex_stubs.rb +80 -40
- data/spec/support/bitstamp/bitstamp_api_wrapper_stubs.rb +2 -2
- data/spec/support/bitstamp/bitstamp_stubs.rb +3 -3
- data/spec/support/vcr.rb +8 -0
- data/spec/support/webmock.rb +8 -0
- 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
|
-
|
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 =
|
7
|
-
conf.secret =
|
8
|
-
conf.user_id =
|
9
|
-
conf.default_wallet_id =
|
10
|
-
conf.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
|
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
|
34
|
+
def balance
|
23
35
|
balance_summary_parser(wallet[:balances])
|
24
36
|
end
|
25
37
|
|
26
|
-
def
|
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
|
31
|
-
order_book_parser(
|
42
|
+
def order_book
|
43
|
+
order_book_parser(market.orders)
|
32
44
|
end
|
33
45
|
|
34
|
-
def
|
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
|
39
|
-
Itbit::Order.create!(type,
|
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
|
53
|
-
|
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
|
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
|
68
|
-
BalanceSummary.new(balance_parser(balances,
|
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
|
83
|
+
def wallet
|
72
84
|
Itbit::Wallet.all.find { |w| w[:id] == Itbit.default_wallet_id }
|
73
85
|
end
|
74
86
|
|
75
|
-
def
|
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
|
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
|
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
|
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
|
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
|
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
|
7
|
-
|
8
|
-
|
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
|
12
|
-
|
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
|
20
|
+
def amount_and_quantity(order_id)
|
16
21
|
KrakenOrder.amount_and_quantity(order_id)
|
17
22
|
end
|
18
23
|
|
19
|
-
def
|
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
|
30
|
+
def enough_order_size?(quantity, _price)
|
26
31
|
quantity >= MIN_AMOUNT
|
27
32
|
end
|
28
33
|
|
29
|
-
def
|
34
|
+
def find_lost(type, price, quantity)
|
30
35
|
KrakenOrder.find_lost(type, price, quantity)
|
31
36
|
end
|
32
37
|
|
33
|
-
def
|
34
|
-
order_book_parser(client.public.order_book(
|
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
|
44
|
+
def orders
|
40
45
|
KrakenOrder.open.map { |ko| order_parser(ko) }
|
41
46
|
end
|
42
47
|
|
43
|
-
def
|
48
|
+
def send_order(type, price, quantity)
|
44
49
|
KrakenOrder.create!(type, price, quantity)
|
45
50
|
end
|
46
51
|
|
47
|
-
def
|
48
|
-
client.public.trades(
|
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
|
59
|
+
def user_transactions
|
55
60
|
[]
|
56
61
|
end
|
57
62
|
|
58
63
|
# { ZEUR: '1433.0939', XXBT: '0.0000000000', 'XETH': '99.7497224800' }
|
59
|
-
|
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, :
|
63
|
-
balance_parser(balances, :
|
64
|
-
client.private.trade_volume(pair:
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
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
|
-
|
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(*
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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(
|
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
|
-
|
70
|
+
value_to_use_needed / safest_price
|
66
71
|
end
|
67
72
|
|
68
|
-
def self.safest_price(transactions,
|
69
|
-
OrderBookSimulator.run(Settings.time_to_live, transactions,
|
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
|
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
|
-
|
11
|
-
return if open_positions.empty?
|
12
|
+
return unless open_positions.any?
|
12
13
|
|
13
|
-
|
14
|
-
|
15
|
-
|
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
|
-
|
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
|
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!
|
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
|
-
|
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
|
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.
|
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
|
114
|
+
def sync_position(order_id)
|
104
115
|
latest = latest_close
|
105
|
-
latest.amount, latest.quantity = Robot.taker.amount_and_quantity(order_id
|
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
|
-
#
|
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(
|
121
|
-
|
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(
|
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(
|
8
|
+
scope :open, -> { where(closing_flow: nil) }
|
11
9
|
end
|
12
10
|
end
|