bitex_bot 0.3.7 → 0.4.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/.circleci/config.yml +63 -0
- data/.rubocop.yml +33 -0
- data/Gemfile +1 -1
- data/Rakefile +1 -1
- data/bin/bitex_bot +1 -1
- data/bitex_bot.gemspec +34 -34
- data/lib/bitex_bot/database.rb +67 -67
- data/lib/bitex_bot/models/api_wrappers/api_wrapper.rb +142 -0
- data/lib/bitex_bot/models/api_wrappers/bitstamp/bitstamp_api_wrapper.rb +137 -0
- data/lib/bitex_bot/models/api_wrappers/itbit/itbit_api_wrapper.rb +116 -0
- data/lib/bitex_bot/models/api_wrappers/kraken/kraken_api_wrapper.rb +111 -0
- data/lib/bitex_bot/models/api_wrappers/kraken/kraken_order.rb +117 -0
- data/lib/bitex_bot/models/buy_closing_flow.rb +23 -16
- data/lib/bitex_bot/models/buy_opening_flow.rb +48 -54
- data/lib/bitex_bot/models/close_buy.rb +2 -2
- data/lib/bitex_bot/models/closing_flow.rb +98 -79
- data/lib/bitex_bot/models/open_buy.rb +11 -10
- data/lib/bitex_bot/models/open_sell.rb +11 -10
- data/lib/bitex_bot/models/opening_flow.rb +157 -99
- data/lib/bitex_bot/models/order_book_simulator.rb +62 -67
- data/lib/bitex_bot/models/sell_closing_flow.rb +25 -20
- data/lib/bitex_bot/models/sell_opening_flow.rb +47 -54
- data/lib/bitex_bot/models/store.rb +3 -1
- data/lib/bitex_bot/robot.rb +203 -176
- data/lib/bitex_bot/settings.rb +71 -12
- data/lib/bitex_bot/version.rb +1 -1
- data/lib/bitex_bot.rb +40 -16
- data/settings.rb.sample +43 -66
- data/spec/bitex_bot/settings_spec.rb +87 -15
- data/spec/factories/bitex_buy.rb +3 -3
- data/spec/factories/bitex_sell.rb +3 -3
- data/spec/factories/buy_opening_flow.rb +1 -1
- data/spec/factories/open_buy.rb +12 -10
- data/spec/factories/open_sell.rb +12 -10
- data/spec/factories/sell_opening_flow.rb +1 -1
- data/spec/models/api_wrappers/bitstamp_api_wrapper_spec.rb +200 -0
- data/spec/models/api_wrappers/itbit_api_wrapper_spec.rb +176 -0
- data/spec/models/api_wrappers/kraken_api_wrapper_spec.rb +209 -0
- data/spec/models/bitex_api_spec.rb +1 -1
- data/spec/models/buy_closing_flow_spec.rb +140 -71
- data/spec/models/buy_opening_flow_spec.rb +126 -56
- data/spec/models/order_book_simulator_spec.rb +10 -10
- data/spec/models/robot_spec.rb +61 -47
- data/spec/models/sell_closing_flow_spec.rb +130 -62
- data/spec/models/sell_opening_flow_spec.rb +129 -60
- data/spec/spec_helper.rb +19 -16
- data/spec/support/bitex_stubs.rb +13 -14
- data/spec/support/bitstamp/bitstamp_api_wrapper_stubs.rb +35 -0
- data/spec/support/bitstamp/bitstamp_stubs.rb +91 -0
- metadata +60 -42
- data/lib/bitex_bot/models/bitfinex_api_wrapper.rb +0 -118
- data/lib/bitex_bot/models/bitstamp_api_wrapper.rb +0 -82
- data/lib/bitex_bot/models/itbit_api_wrapper.rb +0 -68
- data/lib/bitex_bot/models/kraken_api_wrapper.rb +0 -188
- data/spec/models/bitfinex_api_wrapper_spec.rb +0 -17
- data/spec/models/bitstamp_api_wrapper_spec.rb +0 -15
- data/spec/models/itbit_api_wrapper_spec.rb +0 -15
- data/spec/support/bitstamp_stubs.rb +0 -110
@@ -0,0 +1,116 @@
|
|
1
|
+
# Wrapper implementation for Itbit API.
|
2
|
+
# https://api.itbit.com/docs
|
3
|
+
class ItbitApiWrapper < ApiWrapper
|
4
|
+
def self.setup(settings)
|
5
|
+
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
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.amount_and_quantity(order_id, _transactions)
|
15
|
+
order = Itbit::Order.find(order_id)
|
16
|
+
amount = order.volume_weighted_average_price * order.amount_filled
|
17
|
+
quantity = order.amount_filled
|
18
|
+
|
19
|
+
[amount, quantity]
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.balance
|
23
|
+
balance_summary_parser(wallet[:balances])
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.find_lost(type, price, _quantity)
|
27
|
+
orders.find { |o| o.type == type && o.price == price && o.timestamp >= 5.minutes.ago.to_i }
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.order_book
|
31
|
+
order_book_parser(Itbit::XBTUSDMarketData.orders)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.orders
|
35
|
+
Itbit::Order.all(status: :open).map { |o| order_parser(o) }
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.place_order(type, price, quantity)
|
39
|
+
Itbit::Order.create!(type, :xbtusd, quantity.round(4), price.round(2), wait: true)
|
40
|
+
rescue RestClient::RequestTimeout => e
|
41
|
+
# On timeout errors, we still look for the latest active closing order that may be available.
|
42
|
+
# We have a magic threshold of 5 minutes and also use the price to recognize an order as the current one.
|
43
|
+
# TODO: Maybe we can identify the order using metadata instead of price.
|
44
|
+
BitexBot::Robot.log(:error, 'Captured Timeout on itbit')
|
45
|
+
latest = last_order_by(price)
|
46
|
+
return latest if latest.present?
|
47
|
+
|
48
|
+
BitexBot::Robot.log(:error, 'Could not find my order')
|
49
|
+
raise e
|
50
|
+
end
|
51
|
+
|
52
|
+
def self.transactions
|
53
|
+
Itbit::XBTUSDMarketData.trades.map { |t| transaction_parser(t.symbolize_keys) }
|
54
|
+
end
|
55
|
+
|
56
|
+
# We don't need to fetch the list of transaction for itbit since we wont actually use them later.
|
57
|
+
def self.user_transactions
|
58
|
+
[]
|
59
|
+
end
|
60
|
+
|
61
|
+
private_class_method
|
62
|
+
|
63
|
+
# [
|
64
|
+
# { total_balance: 0.2e2, currency: :usd, available_balance: 0.1e2 },
|
65
|
+
# { total_balance: 0.0, currency: :xbt, available_balance: 0.0 },
|
66
|
+
# { total_balance: 0.0, currency: :eur, available_balance: 0.0 },
|
67
|
+
# { total_balance: 0.0, currency: :sgd, available_balance: 0.0 }
|
68
|
+
# ]
|
69
|
+
def self.balance_summary_parser(balances)
|
70
|
+
BalanceSummary.new(balance_parser(balances, :xbt), balance_parser(balances, :usd), 0.5.to_d)
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.wallet
|
74
|
+
Itbit::Wallet.all.find { |w| w[:id] == Itbit.default_wallet_id }
|
75
|
+
end
|
76
|
+
|
77
|
+
def self.balance_parser(balances, currency)
|
78
|
+
currency_balance = balances.find { |balance| balance[:currency] == currency }
|
79
|
+
Balance.new(
|
80
|
+
currency_balance[:total_balance].to_d,
|
81
|
+
currency_balance[:total_balance].to_d - currency_balance[:available_balance].to_d,
|
82
|
+
currency_balance[:available_balance].to_d
|
83
|
+
)
|
84
|
+
end
|
85
|
+
|
86
|
+
def self.last_order_by(price)
|
87
|
+
Itbit::Order.all.select { |o| o.price == price && (o.created_time - Time.now.to_i).abs < 500 }.first
|
88
|
+
end
|
89
|
+
|
90
|
+
# {
|
91
|
+
# bids: [[0.63921e3, 0.195e1], [0.637e3, 0.47e0], [0.63e3, 0.158e1]],
|
92
|
+
# asks: [[0.6424e3, 0.4e0], [0.6433e3, 0.95e0], [0.6443e3, 0.25e0]]
|
93
|
+
# }
|
94
|
+
def self.order_book_parser(book)
|
95
|
+
OrderBook.new(Time.now.to_i, order_summary_parser(book[:bids]), order_summary_parser(book[:asks]))
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.order_summary_parser(orders)
|
99
|
+
orders.map { |order| OrderSummary.new(order[0], order[1]) }
|
100
|
+
end
|
101
|
+
|
102
|
+
# <Itbit::Order:
|
103
|
+
# @id='8fd820d3-baff-4d6f-9439-ff03d816c7ce', @wallet_id='b440efce-a83c-4873-8833-802a1022b476', @side=:buy,
|
104
|
+
# @instrument=:xbtusd, @type=:limit, @amount=0.1005e1, @display_amount=0.1005e1, @price=0.1e3,
|
105
|
+
# @volume_weighted_average_price=0.0, @amount_filled=0.0, @created_time=1415290187, @status=:open,
|
106
|
+
# @metadata={foo: 'bar'}, @client_order_identifier='o'
|
107
|
+
# >
|
108
|
+
def self.order_parser(order)
|
109
|
+
Order.new(order.id, order.side, order.price, order.amount, order.created_time, order)
|
110
|
+
end
|
111
|
+
|
112
|
+
# { tid: 601855, price: 0.41814e3, amount: 0.19e-1, date: 1460161126 }
|
113
|
+
def self.transaction_parser(transaction)
|
114
|
+
Transaction.new(transaction[:tid], transaction[:price], transaction[:amount], transaction[:date])
|
115
|
+
end
|
116
|
+
end
|
@@ -0,0 +1,111 @@
|
|
1
|
+
# Wrapper implementation for Kraken API.
|
2
|
+
# https://www.kraken.com/en-us/help/api
|
3
|
+
class KrakenApiWrapper < ApiWrapper
|
4
|
+
MIN_AMOUNT = 0.002
|
5
|
+
|
6
|
+
def self.setup(settings)
|
7
|
+
HTTParty::Basement.headers('User-Agent' => BitexBot.user_agent)
|
8
|
+
@settings = settings
|
9
|
+
end
|
10
|
+
|
11
|
+
def self.client
|
12
|
+
@client ||= KrakenClient.load(@settings)
|
13
|
+
end
|
14
|
+
|
15
|
+
def self.amount_and_quantity(order_id, _transactions)
|
16
|
+
KrakenOrder.amount_and_quantity(order_id)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.balance
|
20
|
+
balance_summary_parser(client.private.balance)
|
21
|
+
rescue KrakenClient::ErrorResponse, Net::ReadTimeout
|
22
|
+
retry
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.enough_order_size?(quantity, _price)
|
26
|
+
quantity >= MIN_AMOUNT
|
27
|
+
end
|
28
|
+
|
29
|
+
def self.find_lost(type, price, quantity)
|
30
|
+
KrakenOrder.find_lost(type, price, quantity)
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.order_book
|
34
|
+
order_book_parser(client.public.order_book('XBTUSD')[:XXBTZUSD])
|
35
|
+
rescue NoMethodError
|
36
|
+
retry
|
37
|
+
end
|
38
|
+
|
39
|
+
def self.orders
|
40
|
+
KrakenOrder.open.map { |ko| order_parser(ko) }
|
41
|
+
end
|
42
|
+
|
43
|
+
def self.send_order(type, price, quantity)
|
44
|
+
KrakenOrder.create!(type, price, quantity)
|
45
|
+
end
|
46
|
+
|
47
|
+
def self.transactions
|
48
|
+
client.public.trades('XBTUSD')[:XXBTZUSD].reverse.map { |t| transaction_parser(t) }
|
49
|
+
rescue NoMethodError
|
50
|
+
retry
|
51
|
+
end
|
52
|
+
|
53
|
+
# We don't need to fetch the list of transactions for Kraken
|
54
|
+
def self.user_transactions
|
55
|
+
[]
|
56
|
+
end
|
57
|
+
|
58
|
+
private_class_method
|
59
|
+
|
60
|
+
# { ZEUR: '1433.0939', XXBT: '0.0000000000', 'XETH': '99.7497224800' }
|
61
|
+
def self.balance_summary_parser(balances)
|
62
|
+
open_orders = KrakenOrder.open
|
63
|
+
BalanceSummary.new(
|
64
|
+
balance_parser(balances, :XXBT, btc_reserved(open_orders)),
|
65
|
+
balance_parser(balances, :ZUSD, usd_reserved(open_orders)),
|
66
|
+
client.private.trade_volume(pair: 'XBTUSD')[:fees][:XXBTZUSD][:fee].to_d
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.balance_parser(balances, currency, reserved)
|
71
|
+
Balance.new(balances[currency].to_d, reserved, balances[currency].to_d - reserved)
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.btc_reserved(open_orders)
|
75
|
+
orders_by(open_orders, :sell).map { |o| (o.amount - o.executed_amount).to_d }.sum
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.usd_reserved(open_orders)
|
79
|
+
orders_by(open_orders, :buy).map { |o| (o.amount - o.executed_amount) * o.price.to_d }.sum
|
80
|
+
end
|
81
|
+
|
82
|
+
def self.orders_by(open_orders, order_type)
|
83
|
+
open_orders.select { |o| o.type == order_type }
|
84
|
+
end
|
85
|
+
|
86
|
+
# {
|
87
|
+
# 'asks': [['204.52893', '0.010', 1440291148], ['204.78790', '0.312', 1440291132]],
|
88
|
+
# 'bids': [['204.24000', '0.100', 1440291016], ['204.23010', '0.312', 1440290699]]
|
89
|
+
# }
|
90
|
+
def self.order_book_parser(book)
|
91
|
+
OrderBook.new(Time.now.to_i, order_summary_parser(book[:bids]), order_summary_parser(book[:asks]))
|
92
|
+
end
|
93
|
+
|
94
|
+
def self.order_summary_parser(stock_market)
|
95
|
+
stock_market.map { |stock| OrderSummary.new(stock[0].to_d, stock[1].to_d) }
|
96
|
+
end
|
97
|
+
|
98
|
+
# <KrakenOrder: @id='O5TDV2-WDYB2-6OGJRD', @type=:buy, @price='1.01', @amount='1.00000000', @datetime='2013-09-26 23:15:04'>
|
99
|
+
def self.order_parser(order)
|
100
|
+
Order.new(order.id.to_s, order.type, order.price, order.amount, order.datetime, order)
|
101
|
+
end
|
102
|
+
|
103
|
+
# [
|
104
|
+
# ['price', 'amount', 'timestamp', 'buy/sell', 'market/limit', 'miscellaneous']
|
105
|
+
# ['202.51626', '0.01440000', 1440277319.1922, 'b', 'l', ''],
|
106
|
+
# ['202.54000', '0.10000000', 1440277322.8993, 'b', 'l', '']
|
107
|
+
# ]
|
108
|
+
def self.transaction_parser(transaction)
|
109
|
+
Transaction.new(transaction[2].to_i, transaction[0].to_d, transaction[1].to_d, transaction[2].to_i)
|
110
|
+
end
|
111
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
require 'kraken_client'
|
2
|
+
|
3
|
+
# Wrapper for Kraken orders.
|
4
|
+
class KrakenOrder
|
5
|
+
cattr_accessor :last_closed_order
|
6
|
+
attr_accessor :id, :amount, :executed_amount, :price, :avg_price, :type, :datetime
|
7
|
+
|
8
|
+
# rubocop:disable Metrics/AbcSize
|
9
|
+
def self.create!(type, price, quantity)
|
10
|
+
self.last_closed_order = closed.first.try(:id) || Time.now.to_i
|
11
|
+
find(order_info_by(type, price.truncate(1), quantity.truncate(8))['txid'].first)
|
12
|
+
rescue KrakenClient::ErrorResponse => e
|
13
|
+
# Order could not be placed
|
14
|
+
if e.message == 'EService:Unavailable'
|
15
|
+
BitexBot::Robot.log(:debug, 'Captured EService:Unavailable error when placing order on Kraken. Retrying...')
|
16
|
+
retry
|
17
|
+
elsif e.message.start_with?('EGeneral:Invalid')
|
18
|
+
BitexBot::Robot.log(:debug, "Captured #{e.message}: type: #{type}, price: #{price}, quantity: #{quantity}")
|
19
|
+
raise OrderArgumentError, e.message
|
20
|
+
elsif e.message != 'error'
|
21
|
+
raise
|
22
|
+
end
|
23
|
+
end
|
24
|
+
# rubocop:enable Metrics/AbcSize
|
25
|
+
|
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)
|
28
|
+
end
|
29
|
+
|
30
|
+
def self.find(id)
|
31
|
+
new(*KrakenApiWrapper.client.private.query_orders(txid: id).first)
|
32
|
+
rescue KrakenClient::ErrorResponse
|
33
|
+
retry
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.amount_and_quantity(order_id)
|
37
|
+
order = find(order_id)
|
38
|
+
amount = order.avg_price * order.executed_amount
|
39
|
+
quantity = order.executed_amount
|
40
|
+
|
41
|
+
[amount, quantity]
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.open
|
45
|
+
KrakenApiWrapper.client.private.open_orders['open'].map { |o| new(*o) }
|
46
|
+
rescue KrakenClient::ErrorResponse
|
47
|
+
retry
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.closed(start: 1.hour.ago.to_i)
|
51
|
+
KrakenApiWrapper.client.private.closed_orders(start: start)[:closed].map { |o| new(*o) }
|
52
|
+
rescue KrakenClient::ErrorResponse
|
53
|
+
retry
|
54
|
+
end
|
55
|
+
|
56
|
+
def self.find_lost(type, price, quantity)
|
57
|
+
BitexBot::Robot.log(:debug, "Looking for #{type} order in open orders...")
|
58
|
+
order = open_order_by(type, price, quantity)
|
59
|
+
return log_and_return(order, :open) if order.present?
|
60
|
+
|
61
|
+
BitexBot::Robot.log(:debug, "Looking for #{type} order in closed orders...")
|
62
|
+
order = closed_order_by(type, price, quantity)
|
63
|
+
return log_and_return(order, :closed) if order && order.id != last_closed_order
|
64
|
+
end
|
65
|
+
|
66
|
+
def self.log_and_return(order, status)
|
67
|
+
BitexBot::Robot.log(:debug, "Found open #{status} with ID #{order.id}")
|
68
|
+
order
|
69
|
+
end
|
70
|
+
|
71
|
+
# description: [type, price, quantity]
|
72
|
+
def self.open_order_by(type, price, quantity)
|
73
|
+
open.detect { |o| o == [type, price, quantity] }
|
74
|
+
end
|
75
|
+
|
76
|
+
# description: [type, price, quantity]
|
77
|
+
def self.closed_order_by(type, price, quantity)
|
78
|
+
closed(start: last_closed_order).detect { |o| o == [type, price, quantity] }
|
79
|
+
end
|
80
|
+
|
81
|
+
# id: 'O5TDV2-WDYB2-6OGJRD'
|
82
|
+
# order_data: {
|
83
|
+
# 'refid': nil, 'userref': nil, 'status': 'open', 'opentm': 1440292821.4839, 'starttm': 0, 'expiretm': 0,
|
84
|
+
# 'descr': {
|
85
|
+
# 'pair': 'ETHEUR', 'type': 'buy', 'ordertype': 'limit', 'price': '1.19000', 'price2': '0', 'leverage': 'none',
|
86
|
+
# 'order': 'buy 1204.00000000 ETHEUR @ limit 1.19000'
|
87
|
+
# },
|
88
|
+
# 'vol': '1204.00000000', 'vol_exec': '0.00000000', 'cost': '0.00000', 'fee': '0.00000', 'price': '0.00000',
|
89
|
+
# 'misc': '', 'oflags': 'fciq'
|
90
|
+
# }
|
91
|
+
# }
|
92
|
+
def initialize(id, order_data)
|
93
|
+
self.id = id
|
94
|
+
self.type = order_data[:descr][:type].to_sym
|
95
|
+
self.datetime = order_data[:opentm].to_i
|
96
|
+
self.amount = order_data[:vol].to_d
|
97
|
+
self.executed_amount = order_data[:vol_exec].to_d
|
98
|
+
self.price = order_data[:descr][:price].to_d
|
99
|
+
self.avg_price = order_data[:price].to_d
|
100
|
+
end
|
101
|
+
|
102
|
+
def cancel!
|
103
|
+
KrakenApiWrapper.client.private.cancel_order(txid: id)
|
104
|
+
rescue KrakenClient::ErrorResponse => e
|
105
|
+
e.message == 'EService:Unavailable' ? retry : raise
|
106
|
+
end
|
107
|
+
|
108
|
+
def ==(other)
|
109
|
+
if other.is_a?(self.class)
|
110
|
+
other.id == id
|
111
|
+
elsif other.is_a?(Array)
|
112
|
+
other == [type, price, amount]
|
113
|
+
end
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
class OrderArgumentError < StandardError; end
|
@@ -1,34 +1,41 @@
|
|
1
1
|
module BitexBot
|
2
|
+
# It sold at Bitex and needs to close (buy) in the other market.
|
2
3
|
class BuyClosingFlow < ClosingFlow
|
3
4
|
has_many :open_positions, class_name: 'OpenBuy', foreign_key: :closing_flow_id
|
4
5
|
has_many :close_positions, class_name: 'CloseBuy', foreign_key: :closing_flow_id
|
5
|
-
|
6
|
+
|
7
|
+
scope :active, -> { where(done: false) }
|
6
8
|
|
7
9
|
def self.open_position_class
|
8
10
|
OpenBuy
|
9
11
|
end
|
10
|
-
|
11
|
-
def order_method
|
12
|
-
:sell
|
13
|
-
end
|
14
12
|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
# The coins we actually bought minus the coins we were supposed
|
22
|
-
# to re-buy
|
23
|
-
def get_btc_profit
|
13
|
+
private
|
14
|
+
|
15
|
+
# create_or_cancel! hookers
|
16
|
+
# The coins we actually bought minus the coins we were supposed to re-buy
|
17
|
+
def estimate_btc_profit
|
24
18
|
quantity - close_positions.sum(:quantity)
|
25
19
|
end
|
26
20
|
|
27
|
-
|
21
|
+
# The amount received when selling initially, minus the amount spent re-buying the sold coins.
|
22
|
+
def estimate_fiat_profit
|
23
|
+
positions_balance_amount - open_positions.sum(:amount)
|
24
|
+
end
|
25
|
+
|
26
|
+
def next_price_and_quantity
|
28
27
|
closes = close_positions
|
29
|
-
next_price = desired_price - (
|
28
|
+
next_price = desired_price - price_variation(closes.count)
|
30
29
|
next_quantity = quantity - closes.sum(:quantity)
|
30
|
+
|
31
31
|
[next_price, next_quantity]
|
32
32
|
end
|
33
|
+
# end: create_or_cancel! hookers
|
34
|
+
|
35
|
+
# create_order_and_close_position hookers
|
36
|
+
def order_method
|
37
|
+
:sell
|
38
|
+
end
|
39
|
+
# end: create_order_and_close_position hookers
|
33
40
|
end
|
34
41
|
end
|
@@ -1,83 +1,77 @@
|
|
1
1
|
module BitexBot
|
2
|
-
# A workflow for buying bitcoin in Bitex and selling on another exchange.
|
3
|
-
#
|
4
|
-
#
|
5
|
-
# orderbook and the recent operated volume.
|
2
|
+
# A workflow for buying bitcoin in Bitex and selling on another exchange. The BuyOpeningFlow factory function estimates how
|
3
|
+
# much you could sell on the other exchange and calculates a reasonable price taking into account the remote order book and the
|
4
|
+
# recent operated volume.
|
6
5
|
#
|
7
|
-
# When created, a BuyOpeningFlow places a Bid on Bitex for the calculated amount and
|
8
|
-
#
|
9
|
-
# matched amount for a higher price on the other exchange.
|
6
|
+
# When created, a BuyOpeningFlow places a Bid on Bitex for the calculated amount and price, when the Bid is matched on Bitex an
|
7
|
+
# OpenBuy is created to sell the matched amount for a higher price on the other exchange.
|
10
8
|
#
|
11
|
-
# A BuyOpeningFlow can be cancelled at any point, which will cancel the Bitex order
|
12
|
-
#
|
13
|
-
#
|
14
|
-
# @attr order_id The first thing a BuyOpeningFlow does is placing a Bid on Bitex,
|
15
|
-
# this is its unique id.
|
9
|
+
# A BuyOpeningFlow can be cancelled at any point, which will cancel the Bitex order and any orders on the remote exchange
|
10
|
+
# created from its OpenBuy's
|
11
|
+
#
|
12
|
+
# @attr order_id The first thing a BuyOpeningFlow does is placing a Bid on Bitex, this is its unique id.
|
16
13
|
class BuyOpeningFlow < OpeningFlow
|
17
|
-
|
18
|
-
#
|
19
|
-
#
|
20
|
-
#
|
21
|
-
#
|
22
|
-
#
|
23
|
-
#
|
24
|
-
#
|
25
|
-
# @param
|
26
|
-
#
|
27
|
-
# @param
|
28
|
-
#
|
29
|
-
# @param transactions [Hash] a list of hashes representing
|
30
|
-
# all transactions in the other exchange. Each hash contains 'date', 'tid',
|
31
|
-
# 'price' and 'amount', where 'amount' is the BTC transacted.
|
32
|
-
# @param bitex_fee [BigDecimal] the transaction fee to pay on bitex.
|
33
|
-
# @param other_fee [BigDecimal] the transaction fee to pay on the other
|
34
|
-
# exchange.
|
14
|
+
# Start a workflow for buying bitcoin on bitex and selling on the other exchange. The amount to be spent on bitex is
|
15
|
+
# retrieved from Settings, if there is not enough USD on bitex or BTC on the other exchange then no order will be placed
|
16
|
+
# and an exception will be raised instead.
|
17
|
+
#
|
18
|
+
# The amount a BuyOpeningFlow will try to buy and the price it will try to buy at are derived from these parameters:
|
19
|
+
#
|
20
|
+
# @param btc_balance [BigDecimal] amount of btc available in the other exchange that can be sold to balance this purchase.
|
21
|
+
# @param order_book [[price, quantity]] a list of lists representing a bid order book in the other exchange.
|
22
|
+
# @param transactions [Hash] a list of hashes representing all transactions in the other exchange:
|
23
|
+
# Each hash contains 'date', 'tid', 'price' and 'amount', where 'amount' is the BTC transacted.
|
24
|
+
# @param maker_fee [BigDecimal] the transaction fee to pay on maker exchange.
|
25
|
+
# @param taker_fee [BigDecimal] the transaction fee to pay on taker exchange.
|
35
26
|
# @param store [Store] An updated config for this robot, mainly to use for profit.
|
36
27
|
#
|
37
28
|
# @return [BuyOpeningFlow] The newly created flow.
|
38
|
-
# @raise [CannotCreateFlow] If there's any problem creating this flow, for
|
39
|
-
#
|
40
|
-
|
41
|
-
def self.create_for_market(btc_balance, order_book, transactions,
|
42
|
-
bitex_fee, other_fee, store)
|
29
|
+
# @raise [CannotCreateFlow] If there's any problem creating this flow, for example when you run out of USD on bitex or out
|
30
|
+
# of BTC on the other exchange.
|
31
|
+
def self.create_for_market(btc_balance, order_book, transactions, maker_fee, taker_fee, store)
|
43
32
|
super
|
44
33
|
end
|
45
|
-
|
34
|
+
|
35
|
+
# sync_open_positions helpers
|
36
|
+
def self.transaction_order_id(transaction)
|
37
|
+
transaction.bid_id
|
38
|
+
end
|
39
|
+
|
46
40
|
def self.open_position_class
|
47
41
|
OpenBuy
|
48
42
|
end
|
49
|
-
|
43
|
+
# end: sync_open_positions helpers
|
44
|
+
|
45
|
+
# sought_transaction helpers
|
50
46
|
def self.transaction_class
|
51
47
|
Bitex::Buy
|
52
48
|
end
|
53
|
-
|
54
|
-
|
55
|
-
|
49
|
+
# end: sought_transaction helpers
|
50
|
+
|
51
|
+
# create_for_market helpers
|
52
|
+
def self.maker_price(bitcoin_to_resell)
|
53
|
+
value_to_use / bitcoin_to_resell * (1 - profit / 100)
|
56
54
|
end
|
57
55
|
|
58
56
|
def self.order_class
|
59
57
|
Bitex::Bid
|
60
58
|
end
|
61
|
-
|
62
|
-
def self.value_to_use
|
63
|
-
store.buying_amount_to_spend_per_order || Settings.buying.amount_to_spend_per_order
|
64
|
-
end
|
65
|
-
|
59
|
+
|
66
60
|
def self.profit
|
67
61
|
store.buying_profit || Settings.buying.profit
|
68
62
|
end
|
69
|
-
|
70
|
-
def self.
|
71
|
-
OrderBookSimulator.run(Settings.time_to_live, transactions,
|
72
|
-
order_book, dollars_to_use, nil)
|
73
|
-
end
|
74
|
-
|
75
|
-
def self.get_remote_value_to_use(value_to_use_needed, safest_price)
|
63
|
+
|
64
|
+
def self.remote_value_to_use(value_to_use_needed, safest_price)
|
76
65
|
value_to_use_needed / safest_price
|
77
66
|
end
|
78
67
|
|
79
|
-
def self.
|
80
|
-
|
68
|
+
def self.safest_price(transactions, order_book, dollars_to_use)
|
69
|
+
OrderBookSimulator.run(Settings.time_to_live, transactions, order_book, dollars_to_use, nil)
|
70
|
+
end
|
71
|
+
|
72
|
+
def self.value_to_use
|
73
|
+
store.buying_amount_to_spend_per_order || Settings.buying.amount_to_spend_per_order
|
81
74
|
end
|
75
|
+
# end: create_for_market helpers
|
82
76
|
end
|
83
77
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module BitexBot
|
2
|
-
# A CloseBuy represents an Ask on the remote exchange intended
|
3
|
-
# to close one or several OpenBuy positions.
|
2
|
+
# A CloseBuy represents an Ask on the remote exchange intended to close one or several OpenBuy positions.
|
4
3
|
# TODO: document attributes.
|
4
|
+
#
|
5
5
|
class CloseBuy < ActiveRecord::Base
|
6
6
|
end
|
7
7
|
end
|