bitex_bot 0.0.1

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 (41) hide show
  1. data/.gitignore +18 -0
  2. data/.rspec +1 -0
  3. data/Gemfile +4 -0
  4. data/LICENSE.txt +22 -0
  5. data/README.md +50 -0
  6. data/Rakefile +1 -0
  7. data/bin/bitex_bot +5 -0
  8. data/bitex_bot.gemspec +41 -0
  9. data/lib/bitex_bot/database.rb +93 -0
  10. data/lib/bitex_bot/models/buy_closing_flow.rb +34 -0
  11. data/lib/bitex_bot/models/buy_opening_flow.rb +86 -0
  12. data/lib/bitex_bot/models/close_buy.rb +7 -0
  13. data/lib/bitex_bot/models/close_sell.rb +4 -0
  14. data/lib/bitex_bot/models/closing_flow.rb +80 -0
  15. data/lib/bitex_bot/models/open_buy.rb +11 -0
  16. data/lib/bitex_bot/models/open_sell.rb +11 -0
  17. data/lib/bitex_bot/models/opening_flow.rb +114 -0
  18. data/lib/bitex_bot/models/order_book_simulator.rb +75 -0
  19. data/lib/bitex_bot/models/sell_closing_flow.rb +36 -0
  20. data/lib/bitex_bot/models/sell_opening_flow.rb +82 -0
  21. data/lib/bitex_bot/robot.rb +173 -0
  22. data/lib/bitex_bot/settings.rb +13 -0
  23. data/lib/bitex_bot/version.rb +3 -0
  24. data/lib/bitex_bot.rb +18 -0
  25. data/settings.yml.sample +84 -0
  26. data/spec/factories/bitex_buy.rb +12 -0
  27. data/spec/factories/bitex_sell.rb +12 -0
  28. data/spec/factories/buy_opening_flow.rb +17 -0
  29. data/spec/factories/open_buy.rb +17 -0
  30. data/spec/factories/open_sell.rb +17 -0
  31. data/spec/factories/sell_opening_flow.rb +17 -0
  32. data/spec/models/buy_closing_flow_spec.rb +150 -0
  33. data/spec/models/buy_opening_flow_spec.rb +154 -0
  34. data/spec/models/order_book_simulator_spec.rb +57 -0
  35. data/spec/models/robot_spec.rb +103 -0
  36. data/spec/models/sell_closing_flow_spec.rb +160 -0
  37. data/spec/models/sell_opening_flow_spec.rb +156 -0
  38. data/spec/spec_helper.rb +43 -0
  39. data/spec/support/bitex_stubs.rb +66 -0
  40. data/spec/support/bitstamp_stubs.rb +110 -0
  41. metadata +363 -0
@@ -0,0 +1,36 @@
1
+ module BitexBot
2
+ class SellClosingFlow < ClosingFlow
3
+ has_many :open_positions, class_name: 'OpenSell', foreign_key: :closing_flow_id
4
+ has_many :close_positions, class_name: 'CloseSell', foreign_key: :closing_flow_id
5
+ scope :active, lambda { where(done: false) }
6
+
7
+ def self.open_position_class
8
+ OpenSell
9
+ end
10
+
11
+ def order_method
12
+ :buy
13
+ end
14
+
15
+ # The amount received when selling initially, minus
16
+ # the amount spent re-buying the sold coins.
17
+ def get_usd_profit
18
+ open_positions.sum(:amount) - close_positions.sum(:amount)
19
+ end
20
+
21
+ # The coins we actually bought minus the coins we were supposed
22
+ # to re-buy
23
+ def get_btc_profit
24
+ close_positions.sum(:quantity) - quantity
25
+ end
26
+
27
+ def get_next_price_and_quantity
28
+ closes = close_positions
29
+ next_price =
30
+ desired_price + ((closes.count * (closes.count * 2)) / 100.0)
31
+ next_quantity =
32
+ ((quantity * desired_price) - closes.sum(:amount)) / next_price
33
+ [next_price, next_quantity]
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,82 @@
1
+ module BitexBot
2
+ # A workflow for selling bitcoin in Bitex and buying on another exchange. The
3
+ # SellOpeningFlow factory function estimates how much you could buy on the other
4
+ # exchange and calculates a reasonable price taking into account the remote
5
+ # orderbook and the recent operated volume.
6
+ #
7
+ # When created, a SellOpeningFlow places an Ask on Bitex for the calculated
8
+ # quantity and price, when the Ask is matched on Bitex an OpenSell is
9
+ # created to buy the same quantity for a lower price on the other exchange.
10
+ #
11
+ # A SellOpeningFlow can be cancelled at any point, which will cancel the Bitex
12
+ # order and any orders on the remote exchange created from its OpenSell's
13
+ #
14
+ # @attr order_id The first thing a SellOpeningFlow does is placing an Ask on Bitex,
15
+ # this is its unique id.
16
+ class SellOpeningFlow < OpeningFlow
17
+
18
+ # Start a workflow for selling bitcoin on bitex and buying on the other
19
+ # exchange. The quantity to be sold on bitex is retrieved from Settings, if
20
+ # there is not enough BTC on bitex or USD on the other exchange then no
21
+ # order will be placed and an exception will be raised instead.
22
+ # The amount a SellOpeningFlow will try to sell and the price it will try to
23
+ # charge are derived from these parameters:
24
+ #
25
+ # @param usd_balance [BigDecimal] amount of usd available in the other
26
+ # exchange that can be spent to balance this sale.
27
+ # @param order_book [[price, quantity]] a list of lists representing an ask
28
+ # order book in the other exchange.
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.
35
+ #
36
+ # @return [SellOpeningFlow] The newly created flow.
37
+ # @raise [CannotCreateFlow] If there's any problem creating this flow, for
38
+ # example when you run out of BTC on bitex or out of USD on the other
39
+ # exchange.
40
+ def self.create_for_market(usd_balance, order_book, transactions,
41
+ bitex_fee, other_fee)
42
+ super
43
+ end
44
+
45
+ def self.open_position_class
46
+ OpenSell
47
+ end
48
+
49
+ def self.transaction_class
50
+ Bitex::Sell
51
+ end
52
+
53
+ def self.transaction_order_id(transaction)
54
+ transaction.ask_id
55
+ end
56
+
57
+ def self.order_class
58
+ Bitex::Ask
59
+ end
60
+
61
+ def self.value_to_use
62
+ Settings.selling.quantity_to_sell_per_order
63
+ end
64
+
65
+ def self.get_safest_price(transactions, order_book, bitcoins_to_use)
66
+ OrderBookSimulator.run(Settings.time_to_live, transactions,
67
+ order_book, nil, bitcoins_to_use)
68
+ end
69
+
70
+ def self.get_remote_value_to_use(value_to_use_needed, safest_price)
71
+ value_to_use_needed * safest_price
72
+ end
73
+
74
+ def self.order_is_executing?(order)
75
+ order.status != :executing && order.remaining_quantity == order.quantity
76
+ end
77
+
78
+ def self.get_bitex_price(btc_to_sell, usd_to_spend_re_buying)
79
+ (usd_to_spend_re_buying / btc_to_sell) * (1 + Settings.selling.profit / 100.0)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,173 @@
1
+ trap "INT" do
2
+ if BitexBot::Robot.graceful_shutdown
3
+ print "\b"
4
+ BitexBot::Robot.logger.info("Ok, ok, I'm out.")
5
+ exit 1
6
+ end
7
+ BitexBot::Robot.graceful_shutdown = true
8
+ BitexBot::Robot.logger.info("Shutting down as soon as I've cleaned up.")
9
+ end
10
+
11
+ module BitexBot
12
+ class Robot
13
+ cattr_accessor :graceful_shutdown
14
+ cattr_accessor :cooldown_until
15
+ cattr_accessor :test_mode
16
+ cattr_accessor :logger do
17
+ Logger.new(Settings.log.file || STDOUT, 10, 10240000).tap do |l|
18
+ l.level = Logger.const_get(Settings.log.level.upcase)
19
+ l.formatter = proc do |severity, datetime, progname, msg|
20
+ date = datetime.strftime("%m/%d %H:%M:%S.%L")
21
+ "#{ '%-6s' % severity } #{date}: #{msg}\n"
22
+ end
23
+ end
24
+ end
25
+ cattr_accessor :current_cooldowns do 0 end
26
+
27
+ # Trade constantly respecting cooldown times so that we don't get
28
+ # banned by api clients.
29
+ def self.run!
30
+ setup
31
+ logger.info("Loading trading robot, ctrl+c *once* to exit gracefully.")
32
+ self.cooldown_until = Time.now
33
+ bot = new
34
+
35
+ while true
36
+ start_time = Time.now
37
+ return if start_time < cooldown_until
38
+ self.current_cooldowns = 0
39
+ bot.trade!
40
+ self.cooldown_until = start_time + current_cooldowns.seconds
41
+ end
42
+ end
43
+
44
+ def self.setup
45
+ Bitex.api_key = Settings.bitex
46
+ Bitstamp.setup do |config|
47
+ config.key = Settings.bitstamp.key
48
+ config.secret = Settings.bitstamp.secret
49
+ config.client_id = Settings.bitstamp.client_id.to_s
50
+ end
51
+ end
52
+
53
+ def self.with_cooldown(&block)
54
+ result = block.call
55
+ return result if test_mode
56
+ self.current_cooldowns += 1
57
+ sleep 0.1
58
+ return result
59
+ end
60
+
61
+ def with_cooldown(&block)
62
+ self.class.with_cooldown(&block)
63
+ end
64
+
65
+ def trade!
66
+ finalise_some_opening_flows
67
+ if(!active_opening_flows? && !open_positions? &&
68
+ !active_closing_flows? && self.class.graceful_shutdown)
69
+ self.class.logger.info("Shutdown completed")
70
+ exit
71
+ end
72
+ sync_opening_flows if active_opening_flows?
73
+ start_closing_flows if open_positions?
74
+ sync_closing_flows if active_closing_flows?
75
+ start_opening_flows_if_needed
76
+ rescue CannotCreateFlow => e
77
+ self.notify("#{e.message}:\n\n#{e.backtrace.join("\n")}")
78
+ BitexBot::Robot.graceful_shutdown = true
79
+ rescue StandardError => e
80
+ self.notify("#{e.message}:\n\n#{e.backtrace.join("\n")}")
81
+ sleep 30 unless self.class.test_mode
82
+ end
83
+
84
+ def finalise_some_opening_flows
85
+ [BuyOpeningFlow, SellOpeningFlow].each do |kind|
86
+ flows = self.class.graceful_shutdown ? kind.active : kind.old_active
87
+ flows.each{|flow| flow.finalise! }
88
+ end
89
+ end
90
+
91
+ def start_closing_flows
92
+ [BuyClosingFlow, SellClosingFlow].each{|kind| kind.close_open_positions}
93
+ end
94
+
95
+ def open_positions?
96
+ OpenBuy.open.exists? || OpenSell.open.exists?
97
+ end
98
+
99
+ def sync_closing_flows
100
+ orders = with_cooldown{ Bitstamp.orders.all }
101
+ transactions = with_cooldown{ Bitstamp.user_transactions.all }
102
+
103
+ [BuyClosingFlow, SellClosingFlow].each do |kind|
104
+ kind.active.each do |flow|
105
+ flow.sync_closed_positions(orders, transactions)
106
+ end
107
+ end
108
+ end
109
+
110
+ def active_closing_flows?
111
+ BuyClosingFlow.active.exists? || SellClosingFlow.active.exists?
112
+ end
113
+
114
+ def start_opening_flows_if_needed
115
+ return if open_positions?
116
+ return if active_closing_flows?
117
+ return if self.class.graceful_shutdown
118
+
119
+
120
+ recent_buying, recent_selling =
121
+ [BuyOpeningFlow, SellOpeningFlow].collect do |kind|
122
+ threshold = (Settings.time_to_live / 2).seconds.ago
123
+ kind.active.where('created_at > ?', threshold).first
124
+ end
125
+
126
+ return if recent_buying && recent_selling
127
+
128
+ balances = with_cooldown{ Bitstamp.balance }
129
+ order_book = with_cooldown{ Bitstamp.order_book }
130
+ transactions = with_cooldown{ Bitstamp.transactions }
131
+
132
+ unless recent_buying
133
+ BuyOpeningFlow.create_for_market(
134
+ balances['btc_available'].to_d,
135
+ order_book['bids'],
136
+ transactions,
137
+ Bitex::Profile.get[:fee],
138
+ balances['fee'].to_d )
139
+ end
140
+ unless recent_selling
141
+ SellOpeningFlow.create_for_market(
142
+ balances['usd_available'].to_d,
143
+ order_book['asks'],
144
+ transactions,
145
+ Bitex::Profile.get[:fee],
146
+ balances['fee'].to_d )
147
+ end
148
+ end
149
+
150
+ def sync_opening_flows
151
+ [SellOpeningFlow, BuyOpeningFlow].each{|o| o.sync_open_positions }
152
+ end
153
+
154
+ def active_opening_flows?
155
+ BuyOpeningFlow.active.exists? || SellOpeningFlow.active.exists?
156
+ end
157
+
158
+ def notify(message)
159
+ self.class.logger.error(message)
160
+ if Settings.mailer
161
+ mail = Mail.new do
162
+ from Settings.mailer.from
163
+ to Settings.mailer.to
164
+ subject 'Notice from your robot trader'
165
+ body message
166
+ end
167
+ mail.delivery_method(Settings.mailer.method.to_sym,
168
+ Settings.mailer.options.symbolize_keys)
169
+ mail.deliver!
170
+ end
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,13 @@
1
+ require 'settingslogic'
2
+ require 'debugger'
3
+ class BitexBot::Settings < Settingslogic
4
+ path = File.expand_path('bitex_bot_settings.yml', Dir.pwd)
5
+ unless FileTest.exists?(path)
6
+ sample_path = File.expand_path('../../../settings.yml.sample', __FILE__)
7
+ FileUtils.cp(sample_path, path)
8
+ puts "No settings found, I've created a new one with sample "\
9
+ "values at #{path}. Please go ahead and edit it before running this again."
10
+ exit 1
11
+ end
12
+ source path
13
+ end
@@ -0,0 +1,3 @@
1
+ module BitexBot
2
+ VERSION = "0.0.1"
3
+ end
data/lib/bitex_bot.rb ADDED
@@ -0,0 +1,18 @@
1
+ require "bitex_bot/version"
2
+ require "active_support"
3
+ require "active_record"
4
+ require "active_model"
5
+ require "mail"
6
+ require "logger"
7
+ require "bitex"
8
+ require "bitstamp"
9
+ require "bitex_bot/settings"
10
+ require "bitex_bot/database"
11
+ require "debugger"
12
+ require "bitex_bot/models/opening_flow.rb"
13
+ require "bitex_bot/models/closing_flow.rb"
14
+ Dir[File.dirname(__FILE__) + '/bitex_bot/models/*.rb'].each {|file| require file }
15
+ require "bitex_bot/robot"
16
+
17
+ module BitexBot
18
+ end
@@ -0,0 +1,84 @@
1
+ log:
2
+ # File to write log to. The log is rotated every 10240000 bytes, 10 oldest
3
+ # logs are kept around.
4
+ # If you want to log to stdout you can comment this line.
5
+ file: bitex_bot.log
6
+ # How much information to show in the log, valid values are debug, info, error
7
+ level: info
8
+
9
+ # Seconds to keep an order alive before recalculating the price and replacing it.
10
+ # Given the way in which the robot tries to find the safest price to place an
11
+ # order, if the time to live is too long the price is not going to be
12
+ # competitive. About 20 seconds is a good number here.
13
+ time_to_live: 20
14
+
15
+ # Settings for buying on bitex and selling on bitstamp.
16
+ buying:
17
+ # Dollars to spend on each initial bitex bid.
18
+ # Should at least be 10.0 which is the current minimum order size on Bitex.
19
+ # if it's too large you're taking a higher risk if whatever happens and you
20
+ # cannot re sell it afterwards. Also, higher amounts result in your bitex
21
+ # bid price not being competitive.
22
+ # A number between 10.0 and 1000.0 is reccommended.
23
+ amount_to_spend_per_order: 10.0
24
+
25
+ # Your profit when buying on bitex, 0.5 means 0.5%.
26
+ # After calculating the price at which bitcoins can be sold on bitstamp, the
27
+ # robot deduces your profit from that price, and places a bid on bitex paying
28
+ # a lower price.
29
+ profit: 0.5
30
+
31
+ # Settings for selling on bitex and buying on bitstamp.
32
+ selling:
33
+ # Quantity to sell on each initial bitex Ask.
34
+ # It should be at least 10 USD worth of BTC at current market prices, a bit
35
+ # more just to be safe. Otherwise Bitex will reject your orders for being too
36
+ # small.
37
+ # If it's too large then you're taking a risk if whatever happens and you
38
+ # If it's too small then the robot is pointless, and if it's too large you're
39
+ # taking a higher risk if whatever happens ad you cannot re buy afterwards.
40
+ # Also, higher amounts result in your bitex bid price not being competitive.
41
+ # A number between 0.05 and 2.0 is recommended.
42
+ quantity_to_sell_per_order: 0.1
43
+
44
+ # Your profit when selling on bitex, 0.5 means 0.5%.
45
+ # After calculating the price at which bitcoins can be bought on bitstamp,
46
+ # the robot deduces your profit from that price and places an ask on bitex
47
+ # charging a higher price.
48
+ profit: 0.5
49
+
50
+ # This is your bitex api key, it's passed in to the
51
+ # bitex gem: https://github.com/bitex-la/bitex-ruby
52
+ bitex: your_bitex_api_key_which_should_be_kept_safe
53
+
54
+ # These are passed in to the bitstamp gem:
55
+ # see https://github.com/kojnapp/bitstamp for more info.
56
+ bitstamp:
57
+ key: YOUR_API_KEY
58
+ secret: YOUR_API_SECRET
59
+ client_id: YOUR_BITSTAMP_USERNAME
60
+
61
+ # Settings for the ActiveRecord Database to use.
62
+ # sqlite is just fine. Check this link for more options:
63
+ # http://apidock.com/rails/ActiveRecord/Base/establish_connection/class
64
+ database:
65
+ adapter: sqlite3
66
+ database: bitex_bot.db
67
+
68
+ # The robot sends you emails whenever a problem occurs.
69
+ # If you do not want to receive emails just remove this 'mailer'
70
+ # key and everything under it.
71
+ # It uses https://github.com/mikel/mail under the hood, so method
72
+ # is any valid delivery_method for teh mail gem.
73
+ # Options is the options hash passed in to delivery_method.
74
+ mailer:
75
+ from: 'robot@example.com'
76
+ to: 'you@example.com'
77
+ method: smtp
78
+ options:
79
+ address: "your_smtp_server_address.com"
80
+ port: 587
81
+ authentication: "plain"
82
+ enable_starttls_auto: true
83
+ user_name: 'your_user_name'
84
+ password: 'your_smtp_password'
@@ -0,0 +1,12 @@
1
+ FactoryGirl.define do
2
+ factory :bitex_buy, class: Bitex::Buy do
3
+ id 12345678
4
+ created_at{ Time.now }
5
+ specie :btc
6
+ quantity 2.0
7
+ amount 600.0
8
+ fee 0.05
9
+ price 300.0
10
+ bid_id 12345
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ FactoryGirl.define do
2
+ factory :bitex_sell, class: Bitex::Sell do
3
+ id 12345678
4
+ created_at{ Time.now }
5
+ specie :btc
6
+ quantity 2.0
7
+ amount 600.0
8
+ fee 0.05
9
+ price 300.0
10
+ ask_id 12345
11
+ end
12
+ end
@@ -0,0 +1,17 @@
1
+ FactoryGirl.define do
2
+ factory :buy_opening_flow, class: BitexBot::BuyOpeningFlow do
3
+ price 300.0
4
+ value_to_use 600.0
5
+ suggested_closing_price 310.0
6
+ status 'executing'
7
+ order_id 12345
8
+ end
9
+
10
+ factory :other_buy_opening_flow, class: BitexBot::BuyOpeningFlow do
11
+ price 400.0
12
+ value_to_use 400.0
13
+ suggested_closing_price 410.0
14
+ status 'executing'
15
+ order_id 2
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ FactoryGirl.define do
2
+ factory :open_buy, class: BitexBot::OpenBuy do
3
+ price 300.0
4
+ amount 600.0
5
+ quantity 2.0
6
+ transaction_id 12345678
7
+ association :opening_flow, factory: :buy_opening_flow
8
+ end
9
+
10
+ factory :tiny_open_buy, class: BitexBot::OpenBuy do
11
+ price 400.0
12
+ amount 4.0
13
+ quantity 0.01
14
+ transaction_id 23456789
15
+ association :opening_flow, factory: :other_buy_opening_flow
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ FactoryGirl.define do
2
+ factory :open_sell, class: BitexBot::OpenSell do
3
+ price 300.0
4
+ amount 600.0
5
+ quantity 2.0
6
+ transaction_id 12345678
7
+ association :opening_flow, factory: :sell_opening_flow
8
+ end
9
+
10
+ factory :tiny_open_sell, class: BitexBot::OpenSell do
11
+ price 400.0
12
+ amount 4.0
13
+ quantity 0.01
14
+ transaction_id 23456789
15
+ association :opening_flow, factory: :other_sell_opening_flow
16
+ end
17
+ end
@@ -0,0 +1,17 @@
1
+ FactoryGirl.define do
2
+ factory :sell_opening_flow, class: BitexBot::SellOpeningFlow do
3
+ price 300.0
4
+ value_to_use 2.0
5
+ suggested_closing_price 290.0
6
+ status 'executing'
7
+ order_id 12345
8
+ end
9
+
10
+ factory :other_sell_opening_flow, class: BitexBot::SellOpeningFlow do
11
+ price 400.0
12
+ value_to_use 1.0
13
+ suggested_closing_price 390.0
14
+ status 'executing'
15
+ order_id 2
16
+ end
17
+ end
@@ -0,0 +1,150 @@
1
+ require 'spec_helper'
2
+
3
+ describe BitexBot::BuyClosingFlow do
4
+ it "closes a single open position completely" do
5
+ stub_bitstamp_sell
6
+ open = create :open_buy
7
+ flow = BitexBot::BuyClosingFlow.close_open_positions
8
+ open.reload.closing_flow.should == flow
9
+ flow.open_positions.should == [open]
10
+ flow.desired_price.should == 310
11
+ flow.quantity.should == 2
12
+ flow.btc_profit.should be_nil
13
+ flow.usd_profit.should be_nil
14
+ close = flow.close_positions.first
15
+ close.order_id.should == 1
16
+ close.amount.should be_nil
17
+ close.quantity.should be_nil
18
+ end
19
+
20
+ it "closes an aggregate of several open positions" do
21
+ stub_bitstamp_sell
22
+ open_one = create :tiny_open_buy
23
+ open_two = create :open_buy
24
+ flow = BitexBot::BuyClosingFlow.close_open_positions
25
+ close = flow.close_positions.first
26
+ open_one.reload.closing_flow.should == flow
27
+ open_two.reload.closing_flow.should == flow
28
+ flow.open_positions.should == [open_one, open_two]
29
+ flow.desired_price.should == '310.497512437810945273631840797'.to_d
30
+ flow.quantity.should == 2.01
31
+ flow.btc_profit.should be_nil
32
+ flow.usd_profit.should be_nil
33
+ close.order_id.should == 1
34
+ close.amount.should be_nil
35
+ close.quantity.should be_nil
36
+ end
37
+
38
+ it "does not try to close if the amount is too low" do
39
+ open = create :tiny_open_buy
40
+ expect do
41
+ BitexBot::BuyClosingFlow.close_open_positions.should be_nil
42
+ end.not_to change{ BitexBot::BuyClosingFlow.count }
43
+ end
44
+
45
+ it "does not try to close if there are no open positions" do
46
+ expect do
47
+ BitexBot::BuyClosingFlow.close_open_positions.should be_nil
48
+ end.not_to change{ BitexBot::BuyClosingFlow.count }
49
+ end
50
+
51
+ describe "when syncinc executed orders" do
52
+ before(:each) do
53
+ stub_bitstamp_sell
54
+ stub_bitstamp_user_transactions
55
+ create :tiny_open_buy
56
+ create :open_buy
57
+ end
58
+
59
+ it "syncs the executed orders, calculates profit" do
60
+ flow = BitexBot::BuyClosingFlow.close_open_positions
61
+ stub_bitstamp_orders_into_transactions
62
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
63
+ close = flow.close_positions.last
64
+ close.amount.should == 624.1
65
+ close.quantity.should == 2.01
66
+ flow.should be_done
67
+ flow.btc_profit.should == 0
68
+ flow.usd_profit.should == 20.1
69
+ end
70
+
71
+ it "retries closing at a lower price every minute" do
72
+ flow = BitexBot::BuyClosingFlow.close_open_positions
73
+ expect do
74
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
75
+ end.not_to change{ BitexBot::CloseBuy.count }
76
+ flow.should_not be_done
77
+
78
+ # Immediately calling sync again does not try to cancel the ask.
79
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
80
+ Bitstamp.orders.all.size.should == 1
81
+
82
+ # Partially executes order, and 61 seconds after that
83
+ # sync_closed_positions tries to cancel it.
84
+ stub_bitstamp_orders_into_transactions(ratio: 0.5)
85
+ Timecop.travel 61.seconds.from_now
86
+ Bitstamp.orders.all.size.should == 1
87
+ expect do
88
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
89
+ end.not_to change{ BitexBot::CloseBuy.count }
90
+ Bitstamp.orders.all.size.should == 0
91
+ flow.should_not be_done
92
+
93
+ # Next time we try to sync_closed_positions the flow
94
+ # detects the previous close_buy was cancelled correctly so
95
+ # it syncs it's total amounts and tries to place a new one.
96
+ expect do
97
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
98
+ end.to change{ BitexBot::CloseBuy.count }.by(1)
99
+ flow.close_positions.first.tap do |close|
100
+ close.amount.should == 312.05
101
+ close.quantity.should == 1.005
102
+ end
103
+
104
+ # The second ask is executed completely so we can wrap it up and consider
105
+ # this closing flow done.
106
+ stub_bitstamp_orders_into_transactions
107
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
108
+ flow.close_positions.last.tap do |close|
109
+ close.amount.should == 312.0299
110
+ close.quantity.should == 1.005
111
+ end
112
+ flow.should be_done
113
+ flow.btc_profit.should == 0
114
+ flow.usd_profit.should == 20.0799
115
+ end
116
+
117
+ it "does not retry for an amount less than minimum_for_closing" do
118
+ flow = BitexBot::BuyClosingFlow.close_open_positions
119
+
120
+ stub_bitstamp_orders_into_transactions(ratio: 0.999)
121
+ Bitstamp.orders.all.first.cancel!
122
+
123
+ expect do
124
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
125
+ end.not_to change{ BitexBot::CloseBuy.count }
126
+
127
+ flow.should be_done
128
+ flow.btc_profit.should == 0.00201
129
+ flow.usd_profit.should == 19.4759
130
+ end
131
+
132
+ it "can lose USD if price had to be dropped dramatically" do
133
+ # This flow is forced to sell the original BTC quantity for less, thus regaining
134
+ # less USD than what was spent on bitex.
135
+ flow = BitexBot::BuyClosingFlow.close_open_positions
136
+ 60.times do
137
+ Timecop.travel 60.seconds.from_now
138
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
139
+ end
140
+
141
+ stub_bitstamp_orders_into_transactions
142
+
143
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
144
+
145
+ flow.reload.should be_done
146
+ flow.btc_profit.should == 0
147
+ flow.usd_profit.should == -16.08
148
+ end
149
+ end
150
+ end