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.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/.circleci/config.yml +63 -0
  3. data/.rubocop.yml +33 -0
  4. data/Gemfile +1 -1
  5. data/Rakefile +1 -1
  6. data/bin/bitex_bot +1 -1
  7. data/bitex_bot.gemspec +34 -34
  8. data/lib/bitex_bot/database.rb +67 -67
  9. data/lib/bitex_bot/models/api_wrappers/api_wrapper.rb +142 -0
  10. data/lib/bitex_bot/models/api_wrappers/bitstamp/bitstamp_api_wrapper.rb +137 -0
  11. data/lib/bitex_bot/models/api_wrappers/itbit/itbit_api_wrapper.rb +116 -0
  12. data/lib/bitex_bot/models/api_wrappers/kraken/kraken_api_wrapper.rb +111 -0
  13. data/lib/bitex_bot/models/api_wrappers/kraken/kraken_order.rb +117 -0
  14. data/lib/bitex_bot/models/buy_closing_flow.rb +23 -16
  15. data/lib/bitex_bot/models/buy_opening_flow.rb +48 -54
  16. data/lib/bitex_bot/models/close_buy.rb +2 -2
  17. data/lib/bitex_bot/models/closing_flow.rb +98 -79
  18. data/lib/bitex_bot/models/open_buy.rb +11 -10
  19. data/lib/bitex_bot/models/open_sell.rb +11 -10
  20. data/lib/bitex_bot/models/opening_flow.rb +157 -99
  21. data/lib/bitex_bot/models/order_book_simulator.rb +62 -67
  22. data/lib/bitex_bot/models/sell_closing_flow.rb +25 -20
  23. data/lib/bitex_bot/models/sell_opening_flow.rb +47 -54
  24. data/lib/bitex_bot/models/store.rb +3 -1
  25. data/lib/bitex_bot/robot.rb +203 -176
  26. data/lib/bitex_bot/settings.rb +71 -12
  27. data/lib/bitex_bot/version.rb +1 -1
  28. data/lib/bitex_bot.rb +40 -16
  29. data/settings.rb.sample +43 -66
  30. data/spec/bitex_bot/settings_spec.rb +87 -15
  31. data/spec/factories/bitex_buy.rb +3 -3
  32. data/spec/factories/bitex_sell.rb +3 -3
  33. data/spec/factories/buy_opening_flow.rb +1 -1
  34. data/spec/factories/open_buy.rb +12 -10
  35. data/spec/factories/open_sell.rb +12 -10
  36. data/spec/factories/sell_opening_flow.rb +1 -1
  37. data/spec/models/api_wrappers/bitstamp_api_wrapper_spec.rb +200 -0
  38. data/spec/models/api_wrappers/itbit_api_wrapper_spec.rb +176 -0
  39. data/spec/models/api_wrappers/kraken_api_wrapper_spec.rb +209 -0
  40. data/spec/models/bitex_api_spec.rb +1 -1
  41. data/spec/models/buy_closing_flow_spec.rb +140 -71
  42. data/spec/models/buy_opening_flow_spec.rb +126 -56
  43. data/spec/models/order_book_simulator_spec.rb +10 -10
  44. data/spec/models/robot_spec.rb +61 -47
  45. data/spec/models/sell_closing_flow_spec.rb +130 -62
  46. data/spec/models/sell_opening_flow_spec.rb +129 -60
  47. data/spec/spec_helper.rb +19 -16
  48. data/spec/support/bitex_stubs.rb +13 -14
  49. data/spec/support/bitstamp/bitstamp_api_wrapper_stubs.rb +35 -0
  50. data/spec/support/bitstamp/bitstamp_stubs.rb +91 -0
  51. metadata +60 -42
  52. data/lib/bitex_bot/models/bitfinex_api_wrapper.rb +0 -118
  53. data/lib/bitex_bot/models/bitstamp_api_wrapper.rb +0 -82
  54. data/lib/bitex_bot/models/itbit_api_wrapper.rb +0 -68
  55. data/lib/bitex_bot/models/kraken_api_wrapper.rb +0 -188
  56. data/spec/models/bitfinex_api_wrapper_spec.rb +0 -17
  57. data/spec/models/bitstamp_api_wrapper_spec.rb +0 -15
  58. data/spec/models/itbit_api_wrapper_spec.rb +0 -15
  59. 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
- # Start a new CloseBuy that closes exising OpenBuy's by selling
6
- # on another exchange what was just bought on bitex.
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.collect(&:quantity).sum
12
- amount = open_positions.collect(&:amount).sum
13
- suggested_amount = open_positions.collect do |open|
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 if quantity * price < minimum_amount_for_closing
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
- def create_initial_order_and_close_position
33
- create_order_and_close_position(quantity, desired_price)
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 create_order_and_close_position(quantity, price)
37
- order = BitexBot::Robot.taker.place_order(
38
- order_method, price, quantity)
39
- if order.nil? || order.id.nil?
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
- latest_close = close_positions.last
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
- # Maybe we couldn't create the bitstamp order when this flow
54
- # was created, so we try again when syncing.
55
- if latest_close.nil?
56
- create_initial_order_and_close_position
57
- return
58
- end
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{|x| x.id.to_s == order_id }
59
+ order = orders.find { |o| o.id.to_s == order_id }
62
60
 
63
- # When ask is nil it means the other exchange is done executing it
64
- # so we can now have a look of all the sales that were spawned from it.
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
- latest_close.amount, latest_close.quantity =
67
- BitexBot::Robot.taker.amount_and_quantity(order_id, transactions)
68
- latest_close.save!
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
- # When placing a closing order we need to be aware of the smallest order
95
- # amount permitted by the other exchange.
96
- # If the other order is less than this USD amount then we do not attempt
97
- # to close the positions yet.
98
- def self.minimum_amount_for_closing
99
- 5
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 self.close_time_to_live
103
- 30
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
- # An OpenBuy represents a Buy transaction on Bitex.
2
- # OpenBuys are open buy positions that are closed by one or several
3
- # CloseBuys
4
- # TODO: document attributes.
5
- class BitexBot::OpenBuy < ActiveRecord::Base
6
- belongs_to :opening_flow, class_name: 'BuyOpeningFlow',
7
- foreign_key: :opening_flow_id
8
- belongs_to :closing_flow, class_name: 'BuyClosingFlow',
9
- foreign_key: :closing_flow_id
10
- scope :open, lambda{ where('closing_flow_id IS NULL') }
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
- # An OpenSell represents a Sell transaction on Bitex.
2
- # OpenSells are open sell positions that are closed by one
3
- # SellClosingFlow
4
- # TODO: document attributes.
5
- class BitexBot::OpenSell < ActiveRecord::Base
6
- belongs_to :opening_flow, class_name: 'SellOpeningFlow',
7
- foreign_key: :opening_flow_id
8
- belongs_to :closing_flow, class_name: 'SellClosingFlow',
9
- foreign_key: :closing_flow_id
10
- scope :open, lambda{ where('closing_flow_id IS NULL') }
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
- # transactions spawn from that order as Open positions.
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('status != "finalised"')
17
+ where.not(status: :finalised)
13
18
  end
14
-
19
+
15
20
  def self.old_active
16
- where('status != "finalised" AND created_at < ?',
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
- # @!group Statuses
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
- # All possible flow statuses
23
- # @return [Array<String>]
24
- def self.statuses
25
- %w(executing settling finalised)
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
- # The Bitex order has been placed, its id stored as order_id.
29
- def executing?; status == 'executing'; end
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
- # In process of cancelling the Bitex order and any other outstanding order in the
32
- # other exchange.
33
- def settling?; status == 'settling'; end
68
+ [remote_value, safest_price]
69
+ end
34
70
 
35
- # Successfully settled or finished executing.
36
- def finalised?; status == 'finalised'; end
37
- # @!endgroup
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
- validates :status, presence: true, inclusion: {in: statuses}
40
- validates :order_id, presence: true
41
- validates_presence_of :price, :value_to_use
77
+ def self.enough_funds?(order)
78
+ !order.reason.to_s.inquiry.not_enough_funds?
79
+ end
42
80
 
43
- def self.create_for_market(remote_balance, order_book, transactions,
44
- bitex_fee, other_fee, store)
81
+ def self.enough_remote_funds?(remote_balance, remote_value)
82
+ remote_balance >= remote_value
83
+ end
45
84
 
46
- self.store = store
85
+ def self.maker_plus(fee)
86
+ value_to_use * fee / 100
87
+ end
88
+ # end: create_for_market helpers
47
89
 
48
- plus_bitex = value_to_use + (value_to_use * bitex_fee / 100.0)
49
- value_to_use_needed = plus_bitex / (1 - other_fee / 100.0)
50
-
51
- safest_price = get_safest_price(transactions, order_book,
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
- .order('created_at DESC').first.try(:created_at)
91
- Bitex::Trade.all.collect do |transaction|
92
- next unless transaction.is_a?(transaction_class)
93
- next if threshold && transaction.created_at < (threshold - 30.minutes)
94
- next if open_position_class.find_by_transaction_id(transaction.id)
95
- next if transaction.specie != :btc
96
- next unless flow = find_by_order_id(transaction_order_id(transaction))
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
- if order.status == :cancelled || order.status == :completed
111
- Robot.logger.info(
112
- "Opening: #{self.class.order_class.name} ##{order_id} finalised.")
113
- self.status = 'finalised'
114
- save!
115
- else
116
- order.cancel!
117
- unless settling?
118
- self.status = 'settling'
119
- save!
120
- end
121
- end
122
- end
123
- end
124
-
125
- # @visibility private
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