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
@@ -0,0 +1,36 @@
|
|
1
|
+
module BitexBot
|
2
|
+
class SellClosingFlow < ClosingFlow
|
3
|
+
has_many :open_positions, class_name: 'OpenSell', foreign_key: :closing_flow_id
|
4
|
+
has_many :close_positions, class_name: 'CloseSell', foreign_key: :closing_flow_id
|
5
|
+
scope :active, lambda { where(done: false) }
|
6
|
+
|
7
|
+
def self.open_position_class
|
8
|
+
OpenSell
|
9
|
+
end
|
10
|
+
|
11
|
+
def order_method
|
12
|
+
:buy
|
13
|
+
end
|
14
|
+
|
15
|
+
# The amount received when selling initially, minus
|
16
|
+
# the amount spent re-buying the sold coins.
|
17
|
+
def get_usd_profit
|
18
|
+
open_positions.sum(:amount) - close_positions.sum(:amount)
|
19
|
+
end
|
20
|
+
|
21
|
+
# The coins we actually bought minus the coins we were supposed
|
22
|
+
# to re-buy
|
23
|
+
def get_btc_profit
|
24
|
+
close_positions.sum(:quantity) - quantity
|
25
|
+
end
|
26
|
+
|
27
|
+
def get_next_price_and_quantity
|
28
|
+
closes = close_positions
|
29
|
+
next_price =
|
30
|
+
desired_price + ((closes.count * (closes.count * 2)) / 100.0)
|
31
|
+
next_quantity =
|
32
|
+
((quantity * desired_price) - closes.sum(:amount)) / next_price
|
33
|
+
[next_price, next_quantity]
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
module BitexBot
|
2
|
+
# A workflow for selling bitcoin in Bitex and buying on another exchange. The
|
3
|
+
# SellOpeningFlow factory function estimates how much you could buy on the other
|
4
|
+
# exchange and calculates a reasonable price taking into account the remote
|
5
|
+
# orderbook and the recent operated volume.
|
6
|
+
#
|
7
|
+
# When created, a SellOpeningFlow places an Ask on Bitex for the calculated
|
8
|
+
# quantity and price, when the Ask is matched on Bitex an OpenSell is
|
9
|
+
# created to buy the same quantity for a lower price on the other exchange.
|
10
|
+
#
|
11
|
+
# A SellOpeningFlow can be cancelled at any point, which will cancel the Bitex
|
12
|
+
# order and any orders on the remote exchange created from its OpenSell's
|
13
|
+
#
|
14
|
+
# @attr order_id The first thing a SellOpeningFlow does is placing an Ask on Bitex,
|
15
|
+
# this is its unique id.
|
16
|
+
class SellOpeningFlow < OpeningFlow
|
17
|
+
|
18
|
+
# Start a workflow for selling bitcoin on bitex and buying on the other
|
19
|
+
# exchange. The quantity to be sold on bitex is retrieved from Settings, if
|
20
|
+
# there is not enough BTC on bitex or USD on the other exchange then no
|
21
|
+
# order will be placed and an exception will be raised instead.
|
22
|
+
# The amount a SellOpeningFlow will try to sell and the price it will try to
|
23
|
+
# charge are derived from these parameters:
|
24
|
+
#
|
25
|
+
# @param usd_balance [BigDecimal] amount of usd available in the other
|
26
|
+
# exchange that can be spent to balance this sale.
|
27
|
+
# @param order_book [[price, quantity]] a list of lists representing an ask
|
28
|
+
# order book in the other exchange.
|
29
|
+
# @param transactions [Hash] a list of hashes representing
|
30
|
+
# all transactions in the other exchange. Each hash contains 'date', 'tid',
|
31
|
+
# 'price' and 'amount', where 'amount' is the BTC transacted.
|
32
|
+
# @param bitex_fee [BigDecimal] the transaction fee to pay on bitex.
|
33
|
+
# @param other_fee [BigDecimal] the transaction fee to pay on the other
|
34
|
+
# exchange.
|
35
|
+
#
|
36
|
+
# @return [SellOpeningFlow] The newly created flow.
|
37
|
+
# @raise [CannotCreateFlow] If there's any problem creating this flow, for
|
38
|
+
# example when you run out of BTC on bitex or out of USD on the other
|
39
|
+
# exchange.
|
40
|
+
def self.create_for_market(usd_balance, order_book, transactions,
|
41
|
+
bitex_fee, other_fee)
|
42
|
+
super
|
43
|
+
end
|
44
|
+
|
45
|
+
def self.open_position_class
|
46
|
+
OpenSell
|
47
|
+
end
|
48
|
+
|
49
|
+
def self.transaction_class
|
50
|
+
Bitex::Sell
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.transaction_order_id(transaction)
|
54
|
+
transaction.ask_id
|
55
|
+
end
|
56
|
+
|
57
|
+
def self.order_class
|
58
|
+
Bitex::Ask
|
59
|
+
end
|
60
|
+
|
61
|
+
def self.value_to_use
|
62
|
+
Settings.selling.quantity_to_sell_per_order
|
63
|
+
end
|
64
|
+
|
65
|
+
def self.get_safest_price(transactions, order_book, bitcoins_to_use)
|
66
|
+
OrderBookSimulator.run(Settings.time_to_live, transactions,
|
67
|
+
order_book, nil, bitcoins_to_use)
|
68
|
+
end
|
69
|
+
|
70
|
+
def self.get_remote_value_to_use(value_to_use_needed, safest_price)
|
71
|
+
value_to_use_needed * safest_price
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.order_is_executing?(order)
|
75
|
+
order.status != :executing && order.remaining_quantity == order.quantity
|
76
|
+
end
|
77
|
+
|
78
|
+
def self.get_bitex_price(btc_to_sell, usd_to_spend_re_buying)
|
79
|
+
(usd_to_spend_re_buying / btc_to_sell) * (1 + Settings.selling.profit / 100.0)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
@@ -0,0 +1,173 @@
|
|
1
|
+
trap "INT" do
|
2
|
+
if BitexBot::Robot.graceful_shutdown
|
3
|
+
print "\b"
|
4
|
+
BitexBot::Robot.logger.info("Ok, ok, I'm out.")
|
5
|
+
exit 1
|
6
|
+
end
|
7
|
+
BitexBot::Robot.graceful_shutdown = true
|
8
|
+
BitexBot::Robot.logger.info("Shutting down as soon as I've cleaned up.")
|
9
|
+
end
|
10
|
+
|
11
|
+
module BitexBot
|
12
|
+
class Robot
|
13
|
+
cattr_accessor :graceful_shutdown
|
14
|
+
cattr_accessor :cooldown_until
|
15
|
+
cattr_accessor :test_mode
|
16
|
+
cattr_accessor :logger do
|
17
|
+
Logger.new(Settings.log.file || STDOUT, 10, 10240000).tap do |l|
|
18
|
+
l.level = Logger.const_get(Settings.log.level.upcase)
|
19
|
+
l.formatter = proc do |severity, datetime, progname, msg|
|
20
|
+
date = datetime.strftime("%m/%d %H:%M:%S.%L")
|
21
|
+
"#{ '%-6s' % severity } #{date}: #{msg}\n"
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
cattr_accessor :current_cooldowns do 0 end
|
26
|
+
|
27
|
+
# Trade constantly respecting cooldown times so that we don't get
|
28
|
+
# banned by api clients.
|
29
|
+
def self.run!
|
30
|
+
setup
|
31
|
+
logger.info("Loading trading robot, ctrl+c *once* to exit gracefully.")
|
32
|
+
self.cooldown_until = Time.now
|
33
|
+
bot = new
|
34
|
+
|
35
|
+
while true
|
36
|
+
start_time = Time.now
|
37
|
+
return if start_time < cooldown_until
|
38
|
+
self.current_cooldowns = 0
|
39
|
+
bot.trade!
|
40
|
+
self.cooldown_until = start_time + current_cooldowns.seconds
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.setup
|
45
|
+
Bitex.api_key = Settings.bitex
|
46
|
+
Bitstamp.setup do |config|
|
47
|
+
config.key = Settings.bitstamp.key
|
48
|
+
config.secret = Settings.bitstamp.secret
|
49
|
+
config.client_id = Settings.bitstamp.client_id.to_s
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.with_cooldown(&block)
|
54
|
+
result = block.call
|
55
|
+
return result if test_mode
|
56
|
+
self.current_cooldowns += 1
|
57
|
+
sleep 0.1
|
58
|
+
return result
|
59
|
+
end
|
60
|
+
|
61
|
+
def with_cooldown(&block)
|
62
|
+
self.class.with_cooldown(&block)
|
63
|
+
end
|
64
|
+
|
65
|
+
def trade!
|
66
|
+
finalise_some_opening_flows
|
67
|
+
if(!active_opening_flows? && !open_positions? &&
|
68
|
+
!active_closing_flows? && self.class.graceful_shutdown)
|
69
|
+
self.class.logger.info("Shutdown completed")
|
70
|
+
exit
|
71
|
+
end
|
72
|
+
sync_opening_flows if active_opening_flows?
|
73
|
+
start_closing_flows if open_positions?
|
74
|
+
sync_closing_flows if active_closing_flows?
|
75
|
+
start_opening_flows_if_needed
|
76
|
+
rescue CannotCreateFlow => e
|
77
|
+
self.notify("#{e.message}:\n\n#{e.backtrace.join("\n")}")
|
78
|
+
BitexBot::Robot.graceful_shutdown = true
|
79
|
+
rescue StandardError => e
|
80
|
+
self.notify("#{e.message}:\n\n#{e.backtrace.join("\n")}")
|
81
|
+
sleep 30 unless self.class.test_mode
|
82
|
+
end
|
83
|
+
|
84
|
+
def finalise_some_opening_flows
|
85
|
+
[BuyOpeningFlow, SellOpeningFlow].each do |kind|
|
86
|
+
flows = self.class.graceful_shutdown ? kind.active : kind.old_active
|
87
|
+
flows.each{|flow| flow.finalise! }
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def start_closing_flows
|
92
|
+
[BuyClosingFlow, SellClosingFlow].each{|kind| kind.close_open_positions}
|
93
|
+
end
|
94
|
+
|
95
|
+
def open_positions?
|
96
|
+
OpenBuy.open.exists? || OpenSell.open.exists?
|
97
|
+
end
|
98
|
+
|
99
|
+
def sync_closing_flows
|
100
|
+
orders = with_cooldown{ Bitstamp.orders.all }
|
101
|
+
transactions = with_cooldown{ Bitstamp.user_transactions.all }
|
102
|
+
|
103
|
+
[BuyClosingFlow, SellClosingFlow].each do |kind|
|
104
|
+
kind.active.each do |flow|
|
105
|
+
flow.sync_closed_positions(orders, transactions)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def active_closing_flows?
|
111
|
+
BuyClosingFlow.active.exists? || SellClosingFlow.active.exists?
|
112
|
+
end
|
113
|
+
|
114
|
+
def start_opening_flows_if_needed
|
115
|
+
return if open_positions?
|
116
|
+
return if active_closing_flows?
|
117
|
+
return if self.class.graceful_shutdown
|
118
|
+
|
119
|
+
|
120
|
+
recent_buying, recent_selling =
|
121
|
+
[BuyOpeningFlow, SellOpeningFlow].collect do |kind|
|
122
|
+
threshold = (Settings.time_to_live / 2).seconds.ago
|
123
|
+
kind.active.where('created_at > ?', threshold).first
|
124
|
+
end
|
125
|
+
|
126
|
+
return if recent_buying && recent_selling
|
127
|
+
|
128
|
+
balances = with_cooldown{ Bitstamp.balance }
|
129
|
+
order_book = with_cooldown{ Bitstamp.order_book }
|
130
|
+
transactions = with_cooldown{ Bitstamp.transactions }
|
131
|
+
|
132
|
+
unless recent_buying
|
133
|
+
BuyOpeningFlow.create_for_market(
|
134
|
+
balances['btc_available'].to_d,
|
135
|
+
order_book['bids'],
|
136
|
+
transactions,
|
137
|
+
Bitex::Profile.get[:fee],
|
138
|
+
balances['fee'].to_d )
|
139
|
+
end
|
140
|
+
unless recent_selling
|
141
|
+
SellOpeningFlow.create_for_market(
|
142
|
+
balances['usd_available'].to_d,
|
143
|
+
order_book['asks'],
|
144
|
+
transactions,
|
145
|
+
Bitex::Profile.get[:fee],
|
146
|
+
balances['fee'].to_d )
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def sync_opening_flows
|
151
|
+
[SellOpeningFlow, BuyOpeningFlow].each{|o| o.sync_open_positions }
|
152
|
+
end
|
153
|
+
|
154
|
+
def active_opening_flows?
|
155
|
+
BuyOpeningFlow.active.exists? || SellOpeningFlow.active.exists?
|
156
|
+
end
|
157
|
+
|
158
|
+
def notify(message)
|
159
|
+
self.class.logger.error(message)
|
160
|
+
if Settings.mailer
|
161
|
+
mail = Mail.new do
|
162
|
+
from Settings.mailer.from
|
163
|
+
to Settings.mailer.to
|
164
|
+
subject 'Notice from your robot trader'
|
165
|
+
body message
|
166
|
+
end
|
167
|
+
mail.delivery_method(Settings.mailer.method.to_sym,
|
168
|
+
Settings.mailer.options.symbolize_keys)
|
169
|
+
mail.deliver!
|
170
|
+
end
|
171
|
+
end
|
172
|
+
end
|
173
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'settingslogic'
|
2
|
+
require 'debugger'
|
3
|
+
class BitexBot::Settings < Settingslogic
|
4
|
+
path = File.expand_path('bitex_bot_settings.yml', Dir.pwd)
|
5
|
+
unless FileTest.exists?(path)
|
6
|
+
sample_path = File.expand_path('../../../settings.yml.sample', __FILE__)
|
7
|
+
FileUtils.cp(sample_path, path)
|
8
|
+
puts "No settings found, I've created a new one with sample "\
|
9
|
+
"values at #{path}. Please go ahead and edit it before running this again."
|
10
|
+
exit 1
|
11
|
+
end
|
12
|
+
source path
|
13
|
+
end
|
data/lib/bitex_bot.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require "bitex_bot/version"
|
2
|
+
require "active_support"
|
3
|
+
require "active_record"
|
4
|
+
require "active_model"
|
5
|
+
require "mail"
|
6
|
+
require "logger"
|
7
|
+
require "bitex"
|
8
|
+
require "bitstamp"
|
9
|
+
require "bitex_bot/settings"
|
10
|
+
require "bitex_bot/database"
|
11
|
+
require "debugger"
|
12
|
+
require "bitex_bot/models/opening_flow.rb"
|
13
|
+
require "bitex_bot/models/closing_flow.rb"
|
14
|
+
Dir[File.dirname(__FILE__) + '/bitex_bot/models/*.rb'].each {|file| require file }
|
15
|
+
require "bitex_bot/robot"
|
16
|
+
|
17
|
+
module BitexBot
|
18
|
+
end
|
data/settings.yml.sample
ADDED
@@ -0,0 +1,84 @@
|
|
1
|
+
log:
|
2
|
+
# File to write log to. The log is rotated every 10240000 bytes, 10 oldest
|
3
|
+
# logs are kept around.
|
4
|
+
# If you want to log to stdout you can comment this line.
|
5
|
+
file: bitex_bot.log
|
6
|
+
# How much information to show in the log, valid values are debug, info, error
|
7
|
+
level: info
|
8
|
+
|
9
|
+
# Seconds to keep an order alive before recalculating the price and replacing it.
|
10
|
+
# Given the way in which the robot tries to find the safest price to place an
|
11
|
+
# order, if the time to live is too long the price is not going to be
|
12
|
+
# competitive. About 20 seconds is a good number here.
|
13
|
+
time_to_live: 20
|
14
|
+
|
15
|
+
# Settings for buying on bitex and selling on bitstamp.
|
16
|
+
buying:
|
17
|
+
# Dollars to spend on each initial bitex bid.
|
18
|
+
# Should at least be 10.0 which is the current minimum order size on Bitex.
|
19
|
+
# if it's too large you're taking a higher risk if whatever happens and you
|
20
|
+
# cannot re sell it afterwards. Also, higher amounts result in your bitex
|
21
|
+
# bid price not being competitive.
|
22
|
+
# A number between 10.0 and 1000.0 is reccommended.
|
23
|
+
amount_to_spend_per_order: 10.0
|
24
|
+
|
25
|
+
# Your profit when buying on bitex, 0.5 means 0.5%.
|
26
|
+
# After calculating the price at which bitcoins can be sold on bitstamp, the
|
27
|
+
# robot deduces your profit from that price, and places a bid on bitex paying
|
28
|
+
# a lower price.
|
29
|
+
profit: 0.5
|
30
|
+
|
31
|
+
# Settings for selling on bitex and buying on bitstamp.
|
32
|
+
selling:
|
33
|
+
# Quantity to sell on each initial bitex Ask.
|
34
|
+
# It should be at least 10 USD worth of BTC at current market prices, a bit
|
35
|
+
# more just to be safe. Otherwise Bitex will reject your orders for being too
|
36
|
+
# small.
|
37
|
+
# If it's too large then you're taking a risk if whatever happens and you
|
38
|
+
# If it's too small then the robot is pointless, and if it's too large you're
|
39
|
+
# taking a higher risk if whatever happens ad you cannot re buy afterwards.
|
40
|
+
# Also, higher amounts result in your bitex bid price not being competitive.
|
41
|
+
# A number between 0.05 and 2.0 is recommended.
|
42
|
+
quantity_to_sell_per_order: 0.1
|
43
|
+
|
44
|
+
# Your profit when selling on bitex, 0.5 means 0.5%.
|
45
|
+
# After calculating the price at which bitcoins can be bought on bitstamp,
|
46
|
+
# the robot deduces your profit from that price and places an ask on bitex
|
47
|
+
# charging a higher price.
|
48
|
+
profit: 0.5
|
49
|
+
|
50
|
+
# This is your bitex api key, it's passed in to the
|
51
|
+
# bitex gem: https://github.com/bitex-la/bitex-ruby
|
52
|
+
bitex: your_bitex_api_key_which_should_be_kept_safe
|
53
|
+
|
54
|
+
# These are passed in to the bitstamp gem:
|
55
|
+
# see https://github.com/kojnapp/bitstamp for more info.
|
56
|
+
bitstamp:
|
57
|
+
key: YOUR_API_KEY
|
58
|
+
secret: YOUR_API_SECRET
|
59
|
+
client_id: YOUR_BITSTAMP_USERNAME
|
60
|
+
|
61
|
+
# Settings for the ActiveRecord Database to use.
|
62
|
+
# sqlite is just fine. Check this link for more options:
|
63
|
+
# http://apidock.com/rails/ActiveRecord/Base/establish_connection/class
|
64
|
+
database:
|
65
|
+
adapter: sqlite3
|
66
|
+
database: bitex_bot.db
|
67
|
+
|
68
|
+
# The robot sends you emails whenever a problem occurs.
|
69
|
+
# If you do not want to receive emails just remove this 'mailer'
|
70
|
+
# key and everything under it.
|
71
|
+
# It uses https://github.com/mikel/mail under the hood, so method
|
72
|
+
# is any valid delivery_method for teh mail gem.
|
73
|
+
# Options is the options hash passed in to delivery_method.
|
74
|
+
mailer:
|
75
|
+
from: 'robot@example.com'
|
76
|
+
to: 'you@example.com'
|
77
|
+
method: smtp
|
78
|
+
options:
|
79
|
+
address: "your_smtp_server_address.com"
|
80
|
+
port: 587
|
81
|
+
authentication: "plain"
|
82
|
+
enable_starttls_auto: true
|
83
|
+
user_name: 'your_user_name'
|
84
|
+
password: 'your_smtp_password'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :buy_opening_flow, class: BitexBot::BuyOpeningFlow do
|
3
|
+
price 300.0
|
4
|
+
value_to_use 600.0
|
5
|
+
suggested_closing_price 310.0
|
6
|
+
status 'executing'
|
7
|
+
order_id 12345
|
8
|
+
end
|
9
|
+
|
10
|
+
factory :other_buy_opening_flow, class: BitexBot::BuyOpeningFlow do
|
11
|
+
price 400.0
|
12
|
+
value_to_use 400.0
|
13
|
+
suggested_closing_price 410.0
|
14
|
+
status 'executing'
|
15
|
+
order_id 2
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :open_buy, class: BitexBot::OpenBuy do
|
3
|
+
price 300.0
|
4
|
+
amount 600.0
|
5
|
+
quantity 2.0
|
6
|
+
transaction_id 12345678
|
7
|
+
association :opening_flow, factory: :buy_opening_flow
|
8
|
+
end
|
9
|
+
|
10
|
+
factory :tiny_open_buy, class: BitexBot::OpenBuy do
|
11
|
+
price 400.0
|
12
|
+
amount 4.0
|
13
|
+
quantity 0.01
|
14
|
+
transaction_id 23456789
|
15
|
+
association :opening_flow, factory: :other_buy_opening_flow
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :open_sell, class: BitexBot::OpenSell do
|
3
|
+
price 300.0
|
4
|
+
amount 600.0
|
5
|
+
quantity 2.0
|
6
|
+
transaction_id 12345678
|
7
|
+
association :opening_flow, factory: :sell_opening_flow
|
8
|
+
end
|
9
|
+
|
10
|
+
factory :tiny_open_sell, class: BitexBot::OpenSell do
|
11
|
+
price 400.0
|
12
|
+
amount 4.0
|
13
|
+
quantity 0.01
|
14
|
+
transaction_id 23456789
|
15
|
+
association :opening_flow, factory: :other_sell_opening_flow
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
FactoryGirl.define do
|
2
|
+
factory :sell_opening_flow, class: BitexBot::SellOpeningFlow do
|
3
|
+
price 300.0
|
4
|
+
value_to_use 2.0
|
5
|
+
suggested_closing_price 290.0
|
6
|
+
status 'executing'
|
7
|
+
order_id 12345
|
8
|
+
end
|
9
|
+
|
10
|
+
factory :other_sell_opening_flow, class: BitexBot::SellOpeningFlow do
|
11
|
+
price 400.0
|
12
|
+
value_to_use 1.0
|
13
|
+
suggested_closing_price 390.0
|
14
|
+
status 'executing'
|
15
|
+
order_id 2
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BitexBot::BuyClosingFlow do
|
4
|
+
it "closes a single open position completely" do
|
5
|
+
stub_bitstamp_sell
|
6
|
+
open = create :open_buy
|
7
|
+
flow = BitexBot::BuyClosingFlow.close_open_positions
|
8
|
+
open.reload.closing_flow.should == flow
|
9
|
+
flow.open_positions.should == [open]
|
10
|
+
flow.desired_price.should == 310
|
11
|
+
flow.quantity.should == 2
|
12
|
+
flow.btc_profit.should be_nil
|
13
|
+
flow.usd_profit.should be_nil
|
14
|
+
close = flow.close_positions.first
|
15
|
+
close.order_id.should == 1
|
16
|
+
close.amount.should be_nil
|
17
|
+
close.quantity.should be_nil
|
18
|
+
end
|
19
|
+
|
20
|
+
it "closes an aggregate of several open positions" do
|
21
|
+
stub_bitstamp_sell
|
22
|
+
open_one = create :tiny_open_buy
|
23
|
+
open_two = create :open_buy
|
24
|
+
flow = BitexBot::BuyClosingFlow.close_open_positions
|
25
|
+
close = flow.close_positions.first
|
26
|
+
open_one.reload.closing_flow.should == flow
|
27
|
+
open_two.reload.closing_flow.should == flow
|
28
|
+
flow.open_positions.should == [open_one, open_two]
|
29
|
+
flow.desired_price.should == '310.497512437810945273631840797'.to_d
|
30
|
+
flow.quantity.should == 2.01
|
31
|
+
flow.btc_profit.should be_nil
|
32
|
+
flow.usd_profit.should be_nil
|
33
|
+
close.order_id.should == 1
|
34
|
+
close.amount.should be_nil
|
35
|
+
close.quantity.should be_nil
|
36
|
+
end
|
37
|
+
|
38
|
+
it "does not try to close if the amount is too low" do
|
39
|
+
open = create :tiny_open_buy
|
40
|
+
expect do
|
41
|
+
BitexBot::BuyClosingFlow.close_open_positions.should be_nil
|
42
|
+
end.not_to change{ BitexBot::BuyClosingFlow.count }
|
43
|
+
end
|
44
|
+
|
45
|
+
it "does not try to close if there are no open positions" do
|
46
|
+
expect do
|
47
|
+
BitexBot::BuyClosingFlow.close_open_positions.should be_nil
|
48
|
+
end.not_to change{ BitexBot::BuyClosingFlow.count }
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "when syncinc executed orders" do
|
52
|
+
before(:each) do
|
53
|
+
stub_bitstamp_sell
|
54
|
+
stub_bitstamp_user_transactions
|
55
|
+
create :tiny_open_buy
|
56
|
+
create :open_buy
|
57
|
+
end
|
58
|
+
|
59
|
+
it "syncs the executed orders, calculates profit" do
|
60
|
+
flow = BitexBot::BuyClosingFlow.close_open_positions
|
61
|
+
stub_bitstamp_orders_into_transactions
|
62
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
63
|
+
close = flow.close_positions.last
|
64
|
+
close.amount.should == 624.1
|
65
|
+
close.quantity.should == 2.01
|
66
|
+
flow.should be_done
|
67
|
+
flow.btc_profit.should == 0
|
68
|
+
flow.usd_profit.should == 20.1
|
69
|
+
end
|
70
|
+
|
71
|
+
it "retries closing at a lower price every minute" do
|
72
|
+
flow = BitexBot::BuyClosingFlow.close_open_positions
|
73
|
+
expect do
|
74
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
75
|
+
end.not_to change{ BitexBot::CloseBuy.count }
|
76
|
+
flow.should_not be_done
|
77
|
+
|
78
|
+
# Immediately calling sync again does not try to cancel the ask.
|
79
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
80
|
+
Bitstamp.orders.all.size.should == 1
|
81
|
+
|
82
|
+
# Partially executes order, and 61 seconds after that
|
83
|
+
# sync_closed_positions tries to cancel it.
|
84
|
+
stub_bitstamp_orders_into_transactions(ratio: 0.5)
|
85
|
+
Timecop.travel 61.seconds.from_now
|
86
|
+
Bitstamp.orders.all.size.should == 1
|
87
|
+
expect do
|
88
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
89
|
+
end.not_to change{ BitexBot::CloseBuy.count }
|
90
|
+
Bitstamp.orders.all.size.should == 0
|
91
|
+
flow.should_not be_done
|
92
|
+
|
93
|
+
# Next time we try to sync_closed_positions the flow
|
94
|
+
# detects the previous close_buy was cancelled correctly so
|
95
|
+
# it syncs it's total amounts and tries to place a new one.
|
96
|
+
expect do
|
97
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
98
|
+
end.to change{ BitexBot::CloseBuy.count }.by(1)
|
99
|
+
flow.close_positions.first.tap do |close|
|
100
|
+
close.amount.should == 312.05
|
101
|
+
close.quantity.should == 1.005
|
102
|
+
end
|
103
|
+
|
104
|
+
# The second ask is executed completely so we can wrap it up and consider
|
105
|
+
# this closing flow done.
|
106
|
+
stub_bitstamp_orders_into_transactions
|
107
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
108
|
+
flow.close_positions.last.tap do |close|
|
109
|
+
close.amount.should == 312.0299
|
110
|
+
close.quantity.should == 1.005
|
111
|
+
end
|
112
|
+
flow.should be_done
|
113
|
+
flow.btc_profit.should == 0
|
114
|
+
flow.usd_profit.should == 20.0799
|
115
|
+
end
|
116
|
+
|
117
|
+
it "does not retry for an amount less than minimum_for_closing" do
|
118
|
+
flow = BitexBot::BuyClosingFlow.close_open_positions
|
119
|
+
|
120
|
+
stub_bitstamp_orders_into_transactions(ratio: 0.999)
|
121
|
+
Bitstamp.orders.all.first.cancel!
|
122
|
+
|
123
|
+
expect do
|
124
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
125
|
+
end.not_to change{ BitexBot::CloseBuy.count }
|
126
|
+
|
127
|
+
flow.should be_done
|
128
|
+
flow.btc_profit.should == 0.00201
|
129
|
+
flow.usd_profit.should == 19.4759
|
130
|
+
end
|
131
|
+
|
132
|
+
it "can lose USD if price had to be dropped dramatically" do
|
133
|
+
# This flow is forced to sell the original BTC quantity for less, thus regaining
|
134
|
+
# less USD than what was spent on bitex.
|
135
|
+
flow = BitexBot::BuyClosingFlow.close_open_positions
|
136
|
+
60.times do
|
137
|
+
Timecop.travel 60.seconds.from_now
|
138
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
139
|
+
end
|
140
|
+
|
141
|
+
stub_bitstamp_orders_into_transactions
|
142
|
+
|
143
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
144
|
+
|
145
|
+
flow.reload.should be_done
|
146
|
+
flow.btc_profit.should == 0
|
147
|
+
flow.usd_profit.should == -16.08
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|