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
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ settings.yml
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --color
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in bitex_bot.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Nubis
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,50 @@
1
+ # BitexBot
2
+
3
+ A robot to do arbitrage between bitex.la and other exchanges.
4
+
5
+ ## Installation
6
+
7
+ Add this line to your application's Gemfile:
8
+
9
+ gem 'bitex_bot'
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install bitex_bot
18
+
19
+ ## Usage
20
+
21
+ This software is provided as is, and it serves as an example to the
22
+ https://github.com/bitex-la/bitex-ruby API wrapper for bitex.
23
+
24
+ With that said, you can use this robot yourself to take advantage of price
25
+ differences between bitex and bitstamp.
26
+
27
+ Before you can start using this robot you need to have approved accounts in
28
+ https://bitex.la and https://bitstamp.net. Both accounts should have enough
29
+ BTC and USD as the robot will try to sell on bitex and buy on bitstamp
30
+ and to buy on bitex and sell on bitstamp.
31
+
32
+ This gem provides bitex_bot executable, it will create a config file on the first run.
33
+
34
+ Edit the config file to include your bitex and bitstmap api_keys, configure your
35
+ trading parameters and desired profit on successfull trades.
36
+
37
+ Optionally, configure your outgoing email credentials to receive emails when your
38
+ robot needs attention.
39
+
40
+ Once you're done, run bitex_bot again and it will start placing orders on bitex,
41
+ looking for opportunities to make a profit. Start with low amounts and then
42
+ start trading more as you become more comfortable with how the robot operates.
43
+
44
+ ## Contributing
45
+
46
+ 1. Fork it
47
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
48
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
49
+ 4. Push to the branch (`git push origin my-new-feature`)
50
+ 5. Create new Pull Request
data/Rakefile ADDED
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/bitex_bot ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bitex_bot'
4
+
5
+ BitexBot::Robot.run!
data/bitex_bot.gemspec ADDED
@@ -0,0 +1,41 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'bitex_bot/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "bitex_bot"
8
+ spec.version = BitexBot::VERSION
9
+ spec.authors = ["Nubis", "Eromirou"]
10
+ spec.email = ["nb@bitex.la", "tr@bitex.la"]
11
+ spec.description = %q{Both a trading robot and a library to build trading
12
+ robots. The bitex-bot lets you buy cheap on bitex and
13
+ sell on another exchange and vice versa.}
14
+ spec.summary = %q{A trading robot to do arbitrage between bitex.la and
15
+ other exchanges!}
16
+ spec.homepage = ""
17
+ spec.license = "MIT"
18
+
19
+ spec.files = `git ls-files`.split($/)
20
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
22
+ spec.require_paths = ["lib"]
23
+
24
+ spec.add_dependency "settingslogic"
25
+ spec.add_dependency "activerecord"
26
+ spec.add_dependency "activesupport"
27
+ spec.add_dependency "sqlite3"
28
+ spec.add_dependency "bitstamp"
29
+ spec.add_dependency "bitex"
30
+ spec.add_dependency "debugger"
31
+ spec.add_dependency "mail"
32
+
33
+ spec.add_development_dependency "bundler", "~> 1.3"
34
+ spec.add_development_dependency "rake"
35
+ spec.add_development_dependency "rspec"
36
+ spec.add_development_dependency "rspec-mocks"
37
+ spec.add_development_dependency "database_cleaner"
38
+ spec.add_development_dependency "factory_girl"
39
+ spec.add_development_dependency "timecop"
40
+ spec.add_development_dependency "shoulda-matchers"
41
+ end
@@ -0,0 +1,93 @@
1
+ module BitexBot
2
+ module Database
3
+ ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'w'))
4
+ ActiveRecord::Base.establish_connection(Settings.database)
5
+
6
+ if ActiveRecord::Base.connection.tables.empty?
7
+ ActiveRecord::Schema.define(version: 1) do
8
+ create_table :buy_opening_flows do |t|
9
+ t.decimal :price, precision: 30, scale: 15
10
+ t.decimal :value_to_use, precision: 30, scale: 15
11
+ t.decimal :suggested_closing_price, precision: 30, scale: 15
12
+ t.integer :order_id
13
+ t.string :status, null: false, default: 'executing'
14
+ t.index :status
15
+ t.timestamps
16
+ end
17
+ add_index :buy_opening_flows, :order_id
18
+
19
+ create_table :sell_opening_flows do |t|
20
+ t.decimal :price, precision: 30, scale: 15
21
+ t.decimal :value_to_use, precision: 30, scale: 15
22
+ t.decimal :suggested_closing_price, precision: 30, scale: 15
23
+ t.integer :order_id
24
+ t.string :status, null: false, default: 'executing'
25
+ t.index :status
26
+ t.timestamps
27
+ end
28
+ add_index :sell_opening_flows, :order_id
29
+
30
+ create_table :open_buys do |t|
31
+ t.belongs_to :opening_flow
32
+ t.belongs_to :closing_flow
33
+ t.decimal :price, precision: 30, scale: 15
34
+ t.decimal :amount, precision: 30, scale: 15
35
+ t.decimal :quantity, precision: 30, scale: 15
36
+ t.integer :transaction_id
37
+ t.timestamps
38
+ end
39
+ add_index :open_buys, :transaction_id
40
+
41
+ create_table :open_sells do |t|
42
+ t.belongs_to :opening_flow
43
+ t.belongs_to :closing_flow
44
+ t.decimal :price, precision: 30, scale: 15
45
+ t.decimal :quantity, precision: 30, scale: 15
46
+ t.decimal :amount, precision: 30, scale: 15
47
+ t.integer :transaction_id
48
+ t.timestamps
49
+ end
50
+ add_index :open_sells, :transaction_id
51
+
52
+ create_table :buy_closing_flows do |t|
53
+ t.decimal :desired_price, precision: 30, scale: 15
54
+ t.decimal :quantity, precision: 30, scale: 15
55
+ t.decimal :amount, precision: 30, scale: 15
56
+ t.boolean :done, null: false, default: false
57
+ t.decimal :btc_profit, precision: 30, scale: 15
58
+ t.decimal :usd_profit, precision: 30, scale: 15
59
+ t.timestamps
60
+ end
61
+
62
+ create_table :sell_closing_flows do |t|
63
+ t.decimal :desired_price, precision: 30, scale: 15
64
+ t.decimal :quantity, precision: 30, scale: 15
65
+ t.decimal :amount, precision: 30, scale: 15
66
+ t.boolean :done, null: false, default: false
67
+ t.decimal :btc_profit, precision: 30, scale: 15
68
+ t.decimal :usd_profit, precision: 30, scale: 15
69
+ t.timestamps
70
+ end
71
+
72
+ create_table :close_buys do |t|
73
+ t.belongs_to :closing_flow
74
+ t.decimal :amount, precision: 30, scale: 15
75
+ t.decimal :quantity, precision: 30, scale: 15
76
+ t.integer :order_id
77
+ t.timestamps
78
+ end
79
+ add_index :close_buys, :order_id
80
+
81
+ create_table :close_sells do |t|
82
+ t.belongs_to :closing_flow
83
+ t.decimal :amount, precision: 30, scale: 15
84
+ t.decimal :quantity, precision: 30, scale: 15
85
+ t.integer :order_id
86
+ t.timestamps
87
+ end
88
+ add_index :close_sells, :order_id
89
+ end
90
+ end
91
+ end
92
+ end
93
+
@@ -0,0 +1,34 @@
1
+ module BitexBot
2
+ class BuyClosingFlow < ClosingFlow
3
+ has_many :open_positions, class_name: 'OpenBuy', foreign_key: :closing_flow_id
4
+ has_many :close_positions, class_name: 'CloseBuy', foreign_key: :closing_flow_id
5
+ scope :active, lambda { where(done: false) }
6
+
7
+ def self.open_position_class
8
+ OpenBuy
9
+ end
10
+
11
+ def order_method
12
+ :sell
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
+ close_positions.sum(:amount) - open_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
+ quantity - close_positions.sum(:quantity)
25
+ end
26
+
27
+ def get_next_price_and_quantity
28
+ closes = close_positions
29
+ next_price = desired_price - ((closes.count * (closes.count * 2)) / 100.0)
30
+ next_quantity = quantity - closes.sum(:quantity)
31
+ [next_price, next_quantity]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,86 @@
1
+ module BitexBot
2
+ # A workflow for buying bitcoin in Bitex and selling on another exchange. The
3
+ # BuyOpeningFlow factory function estimates how much you could sell 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 BuyOpeningFlow places a Bid on Bitex for the calculated amount and
8
+ # price, when the Bid is matched on Bitex an OpenBuy is created to sell the
9
+ # matched amount for a higher price on the other exchange.
10
+ #
11
+ # A BuyOpeningFlow can be cancelled at any point, which will cancel the Bitex order
12
+ # and any orders on the remote exchange created from its OpenBuy's
13
+ #
14
+ # @attr order_id The first thing a BuyOpeningFlow does is placing a Bid on Bitex,
15
+ # this is its unique id.
16
+ class BuyOpeningFlow < OpeningFlow
17
+
18
+ # Start a workflow for buying bitcoin on bitex and selling on the other
19
+ # exchange. The amount to be spent on bitex is retrieved from Settings, if
20
+ # there is not enough USD on bitex or BTC on the other exchange then no
21
+ # order will be placed and an exception will be raised instead.
22
+ # The amount a BuyOpeningFlow will try to buy and the price it will try to buy at
23
+ # are derived from these parameters:
24
+ #
25
+ # @param btc_balance [BigDecimal] amount of btc available in the other
26
+ # exchange that can be sold to balance this purchase.
27
+ # @param order_book [[price, quantity]] a list of lists representing a bid
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 [BuyOpeningFlow] The newly created flow.
37
+ # @raise [CannotCreateFlow] If there's any problem creating this flow, for
38
+ # example when you run out of USD on bitex or out of BTC on the other
39
+ # exchange.
40
+ def self.create_for_market(btc_balance, order_book, transactions,
41
+ bitex_fee, other_fee)
42
+ super
43
+ end
44
+
45
+ def self.open_position_class
46
+ OpenBuy
47
+ end
48
+
49
+ def self.transaction_class
50
+ Bitex::Buy
51
+ end
52
+
53
+ def self.transaction_order_id(transaction)
54
+ transaction.bid_id
55
+ end
56
+
57
+ def self.order_class
58
+ Bitex::Bid
59
+ end
60
+
61
+ def self.value_to_use
62
+ Settings.buying.amount_to_spend_per_order
63
+ end
64
+
65
+ def self.profit
66
+ Settings.buying.profit
67
+ end
68
+
69
+ def self.get_safest_price(transactions, order_book, dollars_to_use)
70
+ OrderBookSimulator.run(Settings.time_to_live, transactions,
71
+ order_book, dollars_to_use, nil)
72
+ end
73
+
74
+ def self.get_remote_value_to_use(value_to_use_needed, safest_price)
75
+ value_to_use_needed / safest_price
76
+ end
77
+
78
+ def self.order_is_executing?(order)
79
+ order.status != :executing && order.remaining_amount == order.amount
80
+ end
81
+
82
+ def self.get_bitex_price(usd_to_spend, bitcoin_to_resell)
83
+ (usd_to_spend / bitcoin_to_resell) * (1 - Settings.buying.profit / 100.0)
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,7 @@
1
+ module BitexBot
2
+ # A CloseBuy represents an Ask on the remote exchange intended
3
+ # to close one or several OpenBuy positions.
4
+ # TODO: document attributes.
5
+ class CloseBuy < ActiveRecord::Base
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ module BitexBot
2
+ class CloseSell < ActiveRecord::Base
3
+ end
4
+ end
@@ -0,0 +1,80 @@
1
+ module BitexBot
2
+ class ClosingFlow < ActiveRecord::Base
3
+ self.abstract_class = true
4
+
5
+ # Start a new CloseBuy that closes exising OpenBuy's by selling
6
+ # on another exchange what was just bought on bitex.
7
+ def self.close_open_positions
8
+ open_positions = open_position_class.open
9
+ return if open_positions.empty?
10
+
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
17
+
18
+ # 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_order_and_close_position(quantity, price)
28
+
29
+ return flow
30
+ end
31
+
32
+ def create_order_and_close_position(quantity, price)
33
+ order = Bitstamp.orders.send(order_method,
34
+ amount: quantity.round(8), price: price.round(8))
35
+ Robot.logger.info("Closing: Going to #{order_method} ##{order.id} for"\
36
+ "#{self.class.name} ##{id} #{quantity} BTC @ $#{price}")
37
+ close_positions.create!(order_id: order.id.to_i)
38
+ end
39
+
40
+ def sync_closed_positions(orders, transactions)
41
+ latest_close = close_positions.last
42
+ order = orders.find{|x| x.id.to_s == latest_close.order_id.to_s }
43
+
44
+ # When ask is nil it means the other exchange is done executing it
45
+ # so we can now have a look of all the sales that were spawned from it.
46
+ if order.nil?
47
+ closes = transactions.select{|t| t.order_id.to_s == latest_close.order_id.to_s}
48
+ latest_close.amount = closes.collect{|x| x.usd.to_d }.sum.abs
49
+ latest_close.quantity = closes.collect{|x| x.btc.to_d }.sum.abs
50
+ latest_close.save!
51
+
52
+ next_price, next_quantity = get_next_price_and_quantity
53
+ if (next_quantity * next_price) > self.class.minimum_amount_for_closing
54
+ create_order_and_close_position(next_quantity, next_price)
55
+ else
56
+ self.btc_profit = get_btc_profit
57
+ self.usd_profit = get_usd_profit
58
+ self.done = true
59
+ Robot.logger.info("Closing: Finished #{self.class.name} ##{id}"\
60
+ "earned $#{self.usd_profit} and #{self.btc_profit} BTC. ")
61
+ save!
62
+ end
63
+ elsif latest_close.created_at < self.class.close_time_to_live.seconds.ago
64
+ Robot.with_cooldown{ order.cancel! }
65
+ end
66
+ end
67
+
68
+ # When placing a closing order we need to be aware of the smallest order
69
+ # amount permitted by the other exchange.
70
+ # If the other order is less than this USD amount then we do not attempt
71
+ # to close the positions yet.
72
+ def self.minimum_amount_for_closing
73
+ 5
74
+ end
75
+
76
+ def self.close_time_to_live
77
+ 60
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,11 @@
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') }
11
+ end
@@ -0,0 +1,11 @@
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') }
11
+ end
@@ -0,0 +1,114 @@
1
+ module BitexBot
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.
5
+ class OpeningFlow < ActiveRecord::Base
6
+ self.abstract_class = true
7
+
8
+ def self.active
9
+ where('status != "finalised"')
10
+ end
11
+
12
+ def self.old_active
13
+ where('status != "finalised" AND created_at < ?',
14
+ Settings.time_to_live.seconds.ago)
15
+ end
16
+
17
+ # @!group Statuses
18
+
19
+ # All possible flow statuses
20
+ # @return [Array<String>]
21
+ def self.statuses
22
+ %w(executing settling finalised)
23
+ end
24
+
25
+ # The Bitex order has been placed, its id stored as order_id.
26
+ def executing?; status == 'executing'; end
27
+
28
+ # In process of cancelling the Bitex order and any other outstanding order in the
29
+ # other exchange.
30
+ def settling?; status == 'settling'; end
31
+
32
+ # Successfully settled or finished executing.
33
+ def finalised?; status == 'finalised'; end
34
+ # @!endgroup
35
+
36
+ validates :status, presence: true, inclusion: {in: statuses}
37
+ validates :order_id, presence: true
38
+ validates_presence_of :price, :value_to_use
39
+
40
+ def self.create_for_market(remote_balance, order_book, transactions,
41
+ bitex_fee, other_fee)
42
+
43
+ plus_bitex = value_to_use + (value_to_use * bitex_fee / 100.0)
44
+ value_to_use_needed = plus_bitex / (1 - other_fee / 100.0)
45
+
46
+ safest_price = get_safest_price(transactions, order_book,
47
+ value_to_use_needed)
48
+
49
+ remote_value_to_use = get_remote_value_to_use(value_to_use_needed, safest_price)
50
+
51
+ if remote_value_to_use > remote_balance
52
+ raise CannotCreateFlow.new(
53
+ "Needed #{remote_value_to_use} but you only have #{remote_balance}")
54
+ end
55
+
56
+ bitex_price = get_bitex_price(value_to_use, remote_value_to_use)
57
+
58
+ order = order_class.create!(:btc, value_to_use, bitex_price, true)
59
+ if order_is_executing?(order)
60
+ raise CannotCreateFlow.new(
61
+ "You need to have #{value_to_use} on bitex to place this
62
+ #{order_class.name}.")
63
+ end
64
+ Robot.logger.info("Opening: Placed #{order_class.name} ##{order.id} " \
65
+ "#{value_to_use} @ $#{bitex_price} (#{remote_value_to_use})")
66
+ begin
67
+ self.create!(price: bitex_price, value_to_use: value_to_use,
68
+ suggested_closing_price: safest_price, status: 'executing', order_id: order.id)
69
+ rescue StandardError => e
70
+ raise CannotCreateFlow.new(e.message)
71
+ end
72
+ end
73
+
74
+ # Buys on bitex represent open positions, we mirror them locally
75
+ # so that we can plan on how to close them.
76
+ def self.sync_open_positions
77
+ threshold = open_position_class.order('created_at DESC').first.try(:created_at)
78
+ Bitex::Transaction.all.collect do |transaction|
79
+ next unless transaction.is_a?(transaction_class)
80
+ next if threshold && transaction.created_at < threshold
81
+ next if open_position_class.find_by_transaction_id(transaction.id)
82
+ next if transaction.specie != :btc
83
+ next unless flow = find_by_order_id(transaction_order_id(transaction))
84
+ Robot.logger.info("Opening: #{name} ##{flow.id} "\
85
+ "was hit for #{transaction.quantity} BTC @ $#{transaction.price}")
86
+ open_position_class.create!(
87
+ transaction_id: transaction.id,
88
+ price: transaction.price,
89
+ amount: transaction.amount,
90
+ quantity: transaction.quantity,
91
+ opening_flow: flow)
92
+ end.compact
93
+ end
94
+
95
+ def finalise!
96
+ order = self.class.order_class.find(order_id)
97
+ if order.nil?
98
+ Robot.logger.info(
99
+ "Opening: #{self.class.order_class.name} ##{order_id} was too old.")
100
+ self.status = 'finalised'
101
+ save!
102
+ else
103
+ order.cancel!
104
+ unless settling?
105
+ self.status = 'settling'
106
+ save!
107
+ end
108
+ end
109
+ end
110
+ end
111
+
112
+ # @visibility private
113
+ class CannotCreateFlow < StandardError; end
114
+ end
@@ -0,0 +1,75 @@
1
+ # Simulates hitting an order-book to find a price at which an order can be
2
+ # assumed to get executed completely.
3
+ # It essentially drops the start of the order book, to account for price
4
+ # volatility (assuming those orders may be taken by someone else), and then
5
+ # digs until the given USD amount or BTC quantity are reached, finally returning
6
+ # the last price seen, which is the 'safest' price at which we can expect this
7
+ # order to get executed quickly.
8
+ class BitexBot::OrderBookSimulator
9
+
10
+ # @param volatility [Integer] How many seconds of recent volume we need to
11
+ # skip from the start of the order book to be more certain that our order
12
+ # will get executed.
13
+ # @param transactions [Hash] a list of hashes representing
14
+ # all transactions in the other exchange. Each hash contains 'date', 'tid',
15
+ # 'price' and 'amount', where 'amount' is the BTC transacted.
16
+ # @param order_book [[price, quantity]] a list of lists representing the
17
+ # order book to dig in.
18
+ # @param amount_target [BigDecimal] stop when this amount has been reached,
19
+ # leave as nil if looking for a quantity_target.
20
+ # @param quantity_target [BigDecimal] stop when this quantity has been
21
+ # reached, leave as nil if looking for an amount_target.
22
+ # @return [Decimal] Returns the price that we're more likely to get when
23
+ # executing an order for the given amount or quantity.
24
+ def self.run(volatility, transactions, order_book,
25
+ amount_target, quantity_target)
26
+
27
+ to_skip = estimate_quantity_to_skip(volatility, transactions)
28
+ BitexBot::Robot.logger.debug("Skipping #{to_skip} BTC")
29
+ seen = 0
30
+ safest_price = 0
31
+
32
+ order_book.each do |price, quantity|
33
+ price = price.to_d
34
+ quantity = quantity.to_d
35
+
36
+ # An order may be partially or completely skipped due to volatility.
37
+ if to_skip > 0
38
+ dropped = [quantity, to_skip].min
39
+ to_skip -= dropped
40
+ quantity -= dropped
41
+ BitexBot::Robot.logger.debug("Skipped #{dropped} BTC @ $#{price}")
42
+ next if quantity == 0
43
+ end
44
+
45
+ if quantity_target
46
+ if quantity >= (quantity_target - seen)
47
+ BitexBot::Robot.logger.debug("Best price to get "\
48
+ "#{quantity_target} BTC is $#{price}")
49
+ return price
50
+ else
51
+ seen += quantity
52
+ end
53
+ elsif amount_target
54
+ amount = price * quantity
55
+ if amount >= (amount_target - seen)
56
+ BitexBot::Robot.logger.debug("Best price to get "\
57
+ "$#{amount_target} is $#{price}")
58
+ return price
59
+ else
60
+ seen += amount
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ private
67
+
68
+ def self.estimate_quantity_to_skip(volatility, transactions)
69
+ threshold = transactions.first.date.to_i - volatility
70
+ transactions
71
+ .select{|t| t.date.to_i > threshold}
72
+ .collect{|t| t.amount.to_d }
73
+ .sum
74
+ end
75
+ end