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
@@ -1,106 +1,125 @@
|
|
1
1
|
module BitexBot
|
2
|
+
# Close buy/sell positions.
|
2
3
|
class ClosingFlow < ActiveRecord::Base
|
3
4
|
self.abstract_class = true
|
4
5
|
|
5
|
-
|
6
|
-
|
6
|
+
cattr_reader(:close_time_to_live) { 30 }
|
7
|
+
|
8
|
+
# Start a new CloseBuy that closes exising OpenBuy's by selling on another exchange what was just bought on bitex.
|
7
9
|
def self.close_open_positions
|
8
10
|
open_positions = open_position_class.open
|
9
11
|
return if open_positions.empty?
|
10
12
|
|
11
|
-
quantity = open_positions.
|
12
|
-
amount = open_positions.
|
13
|
-
|
14
|
-
open.quantity * open.opening_flow.suggested_closing_price
|
15
|
-
end.sum
|
16
|
-
price = suggested_amount / quantity
|
13
|
+
quantity = open_positions.map(&:quantity).sum
|
14
|
+
amount = open_positions.map(&:amount).sum
|
15
|
+
price = suggested_amount(open_positions) / quantity
|
17
16
|
|
18
17
|
# Don't even bother trying to close a position that's too small.
|
19
|
-
return
|
20
|
-
|
21
|
-
flow = create!(
|
22
|
-
desired_price: price,
|
23
|
-
quantity: quantity,
|
24
|
-
amount: amount,
|
25
|
-
open_positions: open_positions)
|
26
|
-
|
27
|
-
flow.create_initial_order_and_close_position
|
28
|
-
|
29
|
-
return flow
|
18
|
+
return unless Robot.taker.enough_order_size?(quantity, price)
|
19
|
+
create_closing_flow!(price, quantity, amount, open_positions)
|
30
20
|
end
|
31
|
-
|
32
|
-
|
33
|
-
|
21
|
+
|
22
|
+
# close_open_positions helpers
|
23
|
+
def self.suggested_amount(positions)
|
24
|
+
positions.map { |p| p.quantity * p.opening_flow.suggested_closing_price }.sum
|
34
25
|
end
|
35
26
|
|
36
|
-
def
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
Robot.logger.error("Closing: Error on #{order_method} for "\
|
41
|
-
"#{self.class.name} ##{id} #{quantity} BTC @ $#{price}."\
|
42
|
-
"#{order.to_s}")
|
43
|
-
return
|
44
|
-
end
|
45
|
-
Robot.logger.info("Closing: Going to #{order_method} ##{order.id} for "\
|
46
|
-
"#{self.class.name} ##{id} #{order.amount} BTC @ $#{order.price}")
|
47
|
-
close_positions.create!(order_id: order.id)
|
27
|
+
def self.create_closing_flow!(price, quantity, amount, open_positions)
|
28
|
+
create!(desired_price: price, quantity: quantity, amount: amount, open_positions: open_positions)
|
29
|
+
.create_initial_order_and_close_position!
|
30
|
+
nil
|
48
31
|
end
|
32
|
+
# end: close_open_positions helpers
|
49
33
|
|
34
|
+
def create_initial_order_and_close_position!
|
35
|
+
create_order_and_close_position(quantity, desired_price)
|
36
|
+
end
|
37
|
+
|
38
|
+
# TODO: should receive a order_ids and user_transaccions array, then each Wrapper should know how to search for them.
|
50
39
|
def sync_closed_positions(orders, transactions)
|
51
|
-
|
40
|
+
# Maybe we couldn't create the bitstamp order when this flow was created, so we try again when syncing.
|
41
|
+
latest_close.nil? ? create_initial_order_and_close_position! : create_or_cancel!(orders, transactions)
|
42
|
+
end
|
52
43
|
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
44
|
+
def estimate_fiat_profit
|
45
|
+
raise 'self subclass responsibility'
|
46
|
+
end
|
47
|
+
|
48
|
+
def positions_balance_amount
|
49
|
+
close_positions.sum(:amount) * Settings.fx_rate
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
59
53
|
|
54
|
+
# sync_closed_positions helpers
|
55
|
+
# rubocop:disable Metrics/AbcSize
|
56
|
+
# Metrics/AbcSize: Assignment Branch Condition size for create_or_cancel! is too high. [17.23/16]
|
57
|
+
def create_or_cancel!(orders, transactions)
|
60
58
|
order_id = latest_close.order_id.to_s
|
61
|
-
order = orders.find{|
|
59
|
+
order = orders.find { |o| o.id.to_s == order_id }
|
62
60
|
|
63
|
-
# When
|
64
|
-
#
|
61
|
+
# 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
|
+
# spawned from it.
|
65
63
|
if order.nil?
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
next_price, next_quantity = get_next_price_and_quantity
|
71
|
-
if (next_quantity * next_price) > self.class.minimum_amount_for_closing
|
72
|
-
create_order_and_close_position(next_quantity, next_price)
|
73
|
-
else
|
74
|
-
self.btc_profit = get_btc_profit
|
75
|
-
self.usd_profit = get_usd_profit
|
76
|
-
self.done = true
|
77
|
-
Robot.logger.info("Closing: Finished #{self.class.name} ##{id} "\
|
78
|
-
"earned $#{self.usd_profit} and #{self.btc_profit} BTC. ")
|
79
|
-
save!
|
80
|
-
end
|
81
|
-
elsif latest_close.created_at < self.class.close_time_to_live.seconds.ago
|
82
|
-
Robot.with_cooldown do
|
83
|
-
begin
|
84
|
-
Robot.logger.debug("Finalising #{order.class}##{order.id}")
|
85
|
-
order.cancel!
|
86
|
-
Robot.logger.debug("Finalised #{order.class}##{order.id}")
|
87
|
-
rescue StandardError => e
|
88
|
-
nil # just pass, we'll keep on trying until it's not in orders anymore.
|
89
|
-
end
|
90
|
-
end
|
64
|
+
sync_position(order_id, transactions)
|
65
|
+
create_next_position!
|
66
|
+
elsif latest_close.created_at < close_time_to_live.seconds.ago
|
67
|
+
cancel!(order)
|
91
68
|
end
|
92
69
|
end
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
70
|
+
# rubocop:enable Metrics/AbcSize
|
71
|
+
|
72
|
+
def latest_close
|
73
|
+
close_positions.last
|
74
|
+
end
|
75
|
+
# end: sync_closed_positions helpers
|
76
|
+
|
77
|
+
# create_or_cancel! helpers
|
78
|
+
def cancel!(order)
|
79
|
+
Robot.with_cooldown do
|
80
|
+
Robot.log(:debug, "Finalising #{order.class}##{order.id}")
|
81
|
+
order.cancel!
|
82
|
+
Robot.log(:debug, "Finalised #{order.class}##{order.id}")
|
83
|
+
end
|
84
|
+
rescue StandardError => error
|
85
|
+
Robot.log(:debug, error)
|
86
|
+
nil # just pass, we'll keep on trying until it's not in orders anymore.
|
87
|
+
end
|
88
|
+
|
89
|
+
# This use hooks methods, these must be defined in the subclass:
|
90
|
+
# estimate_btc_profit
|
91
|
+
# amount_positions_balance
|
92
|
+
# next_price_and_quantity
|
93
|
+
def create_next_position!
|
94
|
+
next_price, next_quantity = next_price_and_quantity
|
95
|
+
if Robot.taker.enough_order_size?(next_quantity, next_price)
|
96
|
+
create_order_and_close_position(next_quantity, next_price)
|
97
|
+
else
|
98
|
+
update!(btc_profit: estimate_btc_profit, fiat_profit: estimate_fiat_profit, fx_rate: Settings.fx_rate, done: true)
|
99
|
+
Robot.logger.info("Closing: Finished #{self.class.name} ##{id} earned $#{fiat_profit} and #{btc_profit} BTC.")
|
100
|
+
end
|
100
101
|
end
|
101
|
-
|
102
|
-
def
|
103
|
-
|
102
|
+
|
103
|
+
def sync_position(order_id, transactions)
|
104
|
+
latest = latest_close
|
105
|
+
latest.amount, latest.quantity = Robot.taker.amount_and_quantity(order_id, transactions)
|
106
|
+
latest.save!
|
107
|
+
end
|
108
|
+
# end: create_or_cancel! helpers
|
109
|
+
|
110
|
+
# next_price_and_quantity helpers
|
111
|
+
def price_variation(closes_count)
|
112
|
+
closes_count**2 * 0.03
|
113
|
+
end
|
114
|
+
# end: next_price_and_quantity helpers
|
115
|
+
|
116
|
+
# This use hooks methods, these must be defined in the subclass:
|
117
|
+
# order_method
|
118
|
+
def create_order_and_close_position(quantity, price)
|
119
|
+
# TODO: investigate how to generate an ID to insert in the fields of goals where possible.
|
120
|
+
Robot.log(:info, "Closing: Going to place #{order_method} order for #{self.class.name} ##{id} #{quantity} BTC @ $#{price}")
|
121
|
+
order = Robot.taker.place_order(order_method, price, quantity)
|
122
|
+
close_positions.create!(order_id: order.id)
|
104
123
|
end
|
105
124
|
end
|
106
125
|
end
|
@@ -1,11 +1,12 @@
|
|
1
|
-
|
2
|
-
#
|
3
|
-
# CloseBuys
|
4
|
-
# TODO: document attributes.
|
5
|
-
|
6
|
-
|
7
|
-
foreign_key: :opening_flow_id
|
8
|
-
|
9
|
-
|
10
|
-
|
1
|
+
module BitexBot
|
2
|
+
# An OpenBuy represents a Buy transaction on Bitex.
|
3
|
+
# OpenBuys are open buy positions that are closed by one or several CloseBuys.
|
4
|
+
# TODO: document attributes.
|
5
|
+
#
|
6
|
+
class OpenBuy < ActiveRecord::Base
|
7
|
+
belongs_to :opening_flow, class_name: 'BuyOpeningFlow', foreign_key: :opening_flow_id
|
8
|
+
belongs_to :closing_flow, class_name: 'BuyClosingFlow', foreign_key: :closing_flow_id
|
9
|
+
|
10
|
+
scope :open, -> { where('closing_flow_id IS NULL') }
|
11
|
+
end
|
11
12
|
end
|
@@ -1,11 +1,12 @@
|
|
1
|
-
|
2
|
-
#
|
3
|
-
# SellClosingFlow
|
4
|
-
# TODO: document attributes.
|
5
|
-
|
6
|
-
|
7
|
-
foreign_key: :opening_flow_id
|
8
|
-
|
9
|
-
|
10
|
-
|
1
|
+
module BitexBot
|
2
|
+
# An OpenSell represents a Sell transaction on Bitex.
|
3
|
+
# OpenSells are open sell positions that are closed by one SellClosingFlow.
|
4
|
+
# TODO: document attributes.
|
5
|
+
#
|
6
|
+
class OpenSell < ActiveRecord::Base
|
7
|
+
belongs_to :opening_flow, class_name: 'SellOpeningFlow', foreign_key: :opening_flow_id
|
8
|
+
belongs_to :closing_flow, class_name: 'SellClosingFlow', foreign_key: :closing_flow_id
|
9
|
+
|
10
|
+
scope :open, -> { where('closing_flow_id IS NULL') }
|
11
|
+
end
|
11
12
|
end
|
@@ -1,127 +1,185 @@
|
|
1
1
|
module BitexBot
|
2
2
|
# Any arbitrage workflow has 2 stages, opening positions and then closing them.
|
3
|
-
# The OpeningFlow stage places an order on bitex, detecting and storing all
|
4
|
-
#
|
3
|
+
# The OpeningFlow stage places an order on bitex, detecting and storing all transactions spawn from that order as
|
4
|
+
# Open positions.
|
5
5
|
class OpeningFlow < ActiveRecord::Base
|
6
6
|
self.abstract_class = true
|
7
7
|
|
8
8
|
# The updated config store as passed from the robot
|
9
9
|
cattr_accessor :store
|
10
|
-
|
10
|
+
|
11
|
+
# @!group Statuses
|
12
|
+
# All possible flow statuses
|
13
|
+
# @return [Array<String>]
|
14
|
+
cattr_accessor(:statuses) { %w[executing settling finalised] }
|
15
|
+
|
11
16
|
def self.active
|
12
|
-
where(
|
17
|
+
where.not(status: :finalised)
|
13
18
|
end
|
14
|
-
|
19
|
+
|
15
20
|
def self.old_active
|
16
|
-
where('
|
17
|
-
Settings.time_to_live.seconds.ago)
|
21
|
+
active.where('created_at < ?', Settings.time_to_live.seconds.ago)
|
18
22
|
end
|
23
|
+
# @!endgroup
|
19
24
|
|
20
|
-
#
|
25
|
+
# This use hooks methods, these must be defined in the subclass:
|
26
|
+
# #maker_price
|
27
|
+
# #order_class
|
28
|
+
# #remote_value_to_use
|
29
|
+
# #safest_price
|
30
|
+
# #value_to_use
|
31
|
+
# rubocop:disable Metrics/AbcSize
|
32
|
+
def self.create_for_market(remote_balance, order_book, transactions, maker_fee, taker_fee, store)
|
33
|
+
self.store = store
|
21
34
|
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
35
|
+
remote_value, safest_price = calc_remote_value(maker_fee, taker_fee, order_book, transactions)
|
36
|
+
raise CannotCreateFlow, "Needed #{remote_value} but you only have #{remote_balance}" unless
|
37
|
+
enough_remote_funds?(remote_balance, remote_value)
|
38
|
+
|
39
|
+
bitex_price = maker_price(remote_value) * Settings.fx_rate
|
40
|
+
order = create_order!(bitex_price)
|
41
|
+
raise CannotCreateFlow, "You need to have #{value_to_use} on bitex to place this #{order_class.name}." unless
|
42
|
+
enough_funds?(order)
|
43
|
+
|
44
|
+
Robot.log(
|
45
|
+
:info,
|
46
|
+
"Opening: Placed #{order_class.name} ##{order.id} #{value_to_use} @ #{Settings.quote.upcase} #{bitex_price}"\
|
47
|
+
" (#{remote_value})"
|
48
|
+
)
|
49
|
+
|
50
|
+
create!(
|
51
|
+
price: bitex_price,
|
52
|
+
value_to_use: value_to_use,
|
53
|
+
suggested_closing_price: safest_price,
|
54
|
+
status: 'executing',
|
55
|
+
order_id: order.id
|
56
|
+
)
|
57
|
+
rescue StandardError => e
|
58
|
+
raise CannotCreateFlow, e.message
|
26
59
|
end
|
60
|
+
# rubocop:enable Metrics/AbcSize
|
27
61
|
|
28
|
-
#
|
29
|
-
def
|
62
|
+
# create_for_market helpers
|
63
|
+
def self.calc_remote_value(maker_fee, taker_fee, order_book, transactions)
|
64
|
+
value_to_use_needed = (value_to_use + maker_plus(maker_fee)) / (1 - taker_fee / 100)
|
65
|
+
safest_price = safest_price(transactions, order_book, value_to_use_needed)
|
66
|
+
remote_value = remote_value_to_use(value_to_use_needed, safest_price)
|
30
67
|
|
31
|
-
|
32
|
-
|
33
|
-
def settling?; status == 'settling'; end
|
68
|
+
[remote_value, safest_price]
|
69
|
+
end
|
34
70
|
|
35
|
-
|
36
|
-
|
37
|
-
|
71
|
+
def self.create_order!(bitex_price)
|
72
|
+
order_class.create!(Settings.maker_settings.order_book, value_to_use, bitex_price, true)
|
73
|
+
rescue StandardError => e
|
74
|
+
raise CannotCreateFlow, e.message
|
75
|
+
end
|
38
76
|
|
39
|
-
|
40
|
-
|
41
|
-
|
77
|
+
def self.enough_funds?(order)
|
78
|
+
!order.reason.to_s.inquiry.not_enough_funds?
|
79
|
+
end
|
42
80
|
|
43
|
-
def self.
|
44
|
-
|
81
|
+
def self.enough_remote_funds?(remote_balance, remote_value)
|
82
|
+
remote_balance >= remote_value
|
83
|
+
end
|
45
84
|
|
46
|
-
|
85
|
+
def self.maker_plus(fee)
|
86
|
+
value_to_use * fee / 100
|
87
|
+
end
|
88
|
+
# end: create_for_market helpers
|
47
89
|
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
value_to_use_needed)
|
53
|
-
|
54
|
-
remote_value_to_use = get_remote_value_to_use(value_to_use_needed, safest_price)
|
55
|
-
|
56
|
-
if remote_value_to_use > remote_balance
|
57
|
-
raise CannotCreateFlow.new(
|
58
|
-
"Needed #{remote_value_to_use} but you only have #{remote_balance}")
|
59
|
-
end
|
60
|
-
|
61
|
-
bitex_price = get_bitex_price(value_to_use, remote_value_to_use)
|
62
|
-
|
63
|
-
begin
|
64
|
-
order = order_class.create!(:btc, value_to_use, bitex_price, true)
|
65
|
-
rescue StandardError => e
|
66
|
-
raise CannotCreateFlow.new(e.message)
|
67
|
-
end
|
68
|
-
|
69
|
-
if order.reason == :not_enough_funds
|
70
|
-
raise CannotCreateFlow.new(
|
71
|
-
"You need to have #{value_to_use} on bitex to place this
|
72
|
-
#{order_class.name}.")
|
73
|
-
end
|
74
|
-
|
75
|
-
Robot.logger.info("Opening: Placed #{order_class.name} ##{order.id} " \
|
76
|
-
"#{value_to_use} @ $#{bitex_price} (#{remote_value_to_use})")
|
77
|
-
|
78
|
-
begin
|
79
|
-
self.create!(price: bitex_price, value_to_use: value_to_use,
|
80
|
-
suggested_closing_price: safest_price, status: 'executing', order_id: order.id)
|
81
|
-
rescue StandardError => e
|
82
|
-
raise CannotCreateFlow.new(e.message)
|
83
|
-
end
|
84
|
-
end
|
85
|
-
|
86
|
-
# Buys on bitex represent open positions, we mirror them locally
|
87
|
-
# so that we can plan on how to close them.
|
90
|
+
# Buys on bitex represent open positions, we mirror them locally so that we can plan on how to close them.
|
91
|
+
# This use hooks methods, these must be defined in the subclass:
|
92
|
+
# #transaction_order_id(transaction)
|
93
|
+
# #open_position_class
|
88
94
|
def self.sync_open_positions
|
89
|
-
threshold = open_position_class
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
next
|
95
|
-
|
96
|
-
|
97
|
-
Robot.logger.info("Opening: #{name} ##{flow.id} "\
|
98
|
-
"was hit for #{transaction.quantity} BTC @ $#{transaction.price}")
|
99
|
-
open_position_class.create!(
|
100
|
-
transaction_id: transaction.id,
|
101
|
-
price: transaction.price,
|
102
|
-
amount: transaction.amount,
|
103
|
-
quantity: transaction.quantity,
|
104
|
-
opening_flow: flow)
|
95
|
+
threshold = open_position_class.order('created_at DESC').first.try(:created_at)
|
96
|
+
Bitex::Trade.all.map do |transaction|
|
97
|
+
next if sought_transaction?(threshold, transaction)
|
98
|
+
|
99
|
+
flow = find_by_order_id(transaction_order_id(transaction))
|
100
|
+
next unless flow.present?
|
101
|
+
|
102
|
+
create_open_position!(transaction, flow)
|
105
103
|
end.compact
|
106
104
|
end
|
107
|
-
|
105
|
+
|
106
|
+
# sync_open_positions helpers
|
107
|
+
def self.create_open_position!(transaction, flow)
|
108
|
+
Robot.log(
|
109
|
+
:info,
|
110
|
+
"Opening: #{name} ##{flow.id} was hit for #{transaction.quantity} #{Settings.base.upcase} @ #{Settings.quote.upcase}"\
|
111
|
+
" #{transaction.price}"
|
112
|
+
)
|
113
|
+
open_position_class.create!(
|
114
|
+
transaction_id: transaction.id,
|
115
|
+
price: transaction.price,
|
116
|
+
amount: transaction.amount,
|
117
|
+
quantity: transaction.quantity,
|
118
|
+
opening_flow: flow
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
# This use hooks methods, these must be defined in the subclass:
|
123
|
+
# #transaction_class
|
124
|
+
def self.sought_transaction?(threshold, transaction)
|
125
|
+
!transaction.is_a?(transaction_class) ||
|
126
|
+
active_transaction?(transaction, threshold) ||
|
127
|
+
open_position?(transaction) ||
|
128
|
+
!expected_order_book?(transaction)
|
129
|
+
end
|
130
|
+
# end: sync_open_positions helpers
|
131
|
+
|
132
|
+
# sought_transaction helpers
|
133
|
+
def self.active_transaction?(transaction, threshold)
|
134
|
+
threshold && transaction.created_at < (threshold - 30.minutes)
|
135
|
+
end
|
136
|
+
|
137
|
+
def self.open_position?(transaction)
|
138
|
+
open_position_class.find_by_transaction_id(transaction.id)
|
139
|
+
end
|
140
|
+
|
141
|
+
def self.expected_order_book?(transaction)
|
142
|
+
transaction.order_book == Settings.maker_settings.order_book
|
143
|
+
end
|
144
|
+
# end: sought_transaction helpers
|
145
|
+
|
146
|
+
validates :status, presence: true, inclusion: { in: statuses }
|
147
|
+
validates :order_id, presence: true
|
148
|
+
validates_presence_of :price, :value_to_use
|
149
|
+
|
150
|
+
# Statuses:
|
151
|
+
# executing: The Bitex order has been placed, its id stored as order_id.
|
152
|
+
# setting: In process of cancelling the Bitex order and any other outstanding order in the other exchange.
|
153
|
+
# finalised: Successfully settled or finished executing.
|
154
|
+
statuses.each do |status_name|
|
155
|
+
define_method("#{status_name}?") { status == status_name }
|
156
|
+
define_method("#{status_name}!") { update!(status: status_name) }
|
157
|
+
end
|
158
|
+
|
108
159
|
def finalise!
|
109
160
|
order = self.class.order_class.find(order_id)
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
161
|
+
canceled_or_completed?(order) ? do_finalize : do_cancel(order)
|
162
|
+
end
|
163
|
+
|
164
|
+
private
|
165
|
+
|
166
|
+
# finalise! helpers
|
167
|
+
def canceled_or_completed?(order)
|
168
|
+
%i[cancelled completed].any? { |status| order.status == status }
|
169
|
+
end
|
170
|
+
|
171
|
+
def do_finalize
|
172
|
+
Robot.log(:info, "Opening: #{self.class.order_class.name} ##{order_id} finalised.")
|
173
|
+
finalised!
|
174
|
+
end
|
175
|
+
|
176
|
+
def do_cancel(order)
|
177
|
+
Robot.log(:info, "Opening: #{self.class.order_class.name} ##{order_id} canceled.")
|
178
|
+
order.cancel!
|
179
|
+
settling! unless settling?
|
180
|
+
end
|
181
|
+
# end: finalise! helpers
|
182
|
+
end
|
183
|
+
|
126
184
|
class CannotCreateFlow < StandardError; end
|
127
185
|
end
|