bitex_bot 0.3.7 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|