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