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