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