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