bitex_bot 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
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