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.
- data/.gitignore +18 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +50 -0
- data/Rakefile +1 -0
- data/bin/bitex_bot +5 -0
- data/bitex_bot.gemspec +41 -0
- data/lib/bitex_bot/database.rb +93 -0
- data/lib/bitex_bot/models/buy_closing_flow.rb +34 -0
- data/lib/bitex_bot/models/buy_opening_flow.rb +86 -0
- data/lib/bitex_bot/models/close_buy.rb +7 -0
- data/lib/bitex_bot/models/close_sell.rb +4 -0
- data/lib/bitex_bot/models/closing_flow.rb +80 -0
- data/lib/bitex_bot/models/open_buy.rb +11 -0
- data/lib/bitex_bot/models/open_sell.rb +11 -0
- data/lib/bitex_bot/models/opening_flow.rb +114 -0
- data/lib/bitex_bot/models/order_book_simulator.rb +75 -0
- data/lib/bitex_bot/models/sell_closing_flow.rb +36 -0
- data/lib/bitex_bot/models/sell_opening_flow.rb +82 -0
- data/lib/bitex_bot/robot.rb +173 -0
- data/lib/bitex_bot/settings.rb +13 -0
- data/lib/bitex_bot/version.rb +3 -0
- data/lib/bitex_bot.rb +18 -0
- data/settings.yml.sample +84 -0
- data/spec/factories/bitex_buy.rb +12 -0
- data/spec/factories/bitex_sell.rb +12 -0
- data/spec/factories/buy_opening_flow.rb +17 -0
- data/spec/factories/open_buy.rb +17 -0
- data/spec/factories/open_sell.rb +17 -0
- data/spec/factories/sell_opening_flow.rb +17 -0
- data/spec/models/buy_closing_flow_spec.rb +150 -0
- data/spec/models/buy_opening_flow_spec.rb +154 -0
- data/spec/models/order_book_simulator_spec.rb +57 -0
- data/spec/models/robot_spec.rb +103 -0
- data/spec/models/sell_closing_flow_spec.rb +160 -0
- data/spec/models/sell_opening_flow_spec.rb +156 -0
- data/spec/spec_helper.rb +43 -0
- data/spec/support/bitex_stubs.rb +66 -0
- data/spec/support/bitstamp_stubs.rb +110 -0
- metadata +363 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
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
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,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
|