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