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
@@ -0,0 +1,154 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BitexBot::BuyOpeningFlow do
|
4
|
+
before(:each) do
|
5
|
+
Bitex.api_key = "valid_key"
|
6
|
+
end
|
7
|
+
|
8
|
+
it { should validate_presence_of :status }
|
9
|
+
it { should validate_presence_of :price }
|
10
|
+
it { should validate_presence_of :value_to_use }
|
11
|
+
it { should validate_presence_of :order_id }
|
12
|
+
it { should(ensure_inclusion_of(:status)
|
13
|
+
.in_array(BitexBot::BuyOpeningFlow.statuses)) }
|
14
|
+
|
15
|
+
describe "when creating a buying flow" do
|
16
|
+
it "spends 50 usd" do
|
17
|
+
stub_bitex_bid_create
|
18
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
19
|
+
buying: double(amount_to_spend_per_order: 50, profit: 0))
|
20
|
+
|
21
|
+
flow = BitexBot::BuyOpeningFlow.create_for_market(100,
|
22
|
+
bitstamp_order_book_stub['bids'], bitstamp_transactions_stub, 0.5, 0.25)
|
23
|
+
|
24
|
+
flow.value_to_use.should == 50
|
25
|
+
flow.price.should <= flow.suggested_closing_price
|
26
|
+
flow.price.should == "19.85074626865672".to_d
|
27
|
+
flow.suggested_closing_price.should == 20
|
28
|
+
flow.order_id.should == 12345
|
29
|
+
end
|
30
|
+
|
31
|
+
it "spends 100 usd" do
|
32
|
+
stub_bitex_bid_create
|
33
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
34
|
+
buying: double(amount_to_spend_per_order: 100, profit: 0))
|
35
|
+
|
36
|
+
flow = BitexBot::BuyOpeningFlow.create_for_market(100000,
|
37
|
+
bitstamp_order_book_stub['bids'], bitstamp_transactions_stub, 0.5, 0.25)
|
38
|
+
flow.value_to_use.should == 100
|
39
|
+
flow.price.should <= flow.suggested_closing_price
|
40
|
+
flow.price.should == "14.88805970149254".to_d
|
41
|
+
flow.suggested_closing_price.should == 15
|
42
|
+
flow.order_id.should == 12345
|
43
|
+
end
|
44
|
+
|
45
|
+
it "lowers the price to pay on bitex to take a profit" do
|
46
|
+
stub_bitex_bid_create
|
47
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
48
|
+
buying: double(amount_to_spend_per_order: 100, profit: 50))
|
49
|
+
|
50
|
+
flow = BitexBot::BuyOpeningFlow.create_for_market(100000,
|
51
|
+
bitstamp_order_book_stub['bids'], bitstamp_transactions_stub, 0.5, 0.25)
|
52
|
+
flow.value_to_use.should == 100
|
53
|
+
flow.price.should <= flow.suggested_closing_price
|
54
|
+
flow.price.should == "7.444029850746269".to_d
|
55
|
+
flow.suggested_closing_price.should == 15
|
56
|
+
flow.order_id.should == 12345
|
57
|
+
end
|
58
|
+
|
59
|
+
it "fails when there is a problem placing the bid on bitex" do
|
60
|
+
Bitex::Bid.stub(:create!) do
|
61
|
+
raise StandardError.new("Cannot Create")
|
62
|
+
end
|
63
|
+
|
64
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
65
|
+
buying: double(amount_to_spend_per_order: 100, profit: 0))
|
66
|
+
|
67
|
+
expect do
|
68
|
+
flow = BitexBot::BuyOpeningFlow.create_for_market(100000,
|
69
|
+
bitstamp_order_book_stub['bids'], bitstamp_transactions_stub, 0.5, 0.25)
|
70
|
+
flow.should be_nil
|
71
|
+
BitexBot::BuyOpeningFlow.count.should == 0
|
72
|
+
end.to raise_exception(BitexBot::CannotCreateFlow)
|
73
|
+
end
|
74
|
+
|
75
|
+
it "fails when there are not enough bitcoin to sell in the other exchange" do
|
76
|
+
stub_bitex_bid_create
|
77
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
78
|
+
buying: double(amount_to_spend_per_order: 100, profit: 0))
|
79
|
+
|
80
|
+
expect do
|
81
|
+
flow = BitexBot::BuyOpeningFlow.create_for_market(1,
|
82
|
+
bitstamp_order_book_stub['bids'], bitstamp_transactions_stub, 0.5, 0.25)
|
83
|
+
flow.should be_nil
|
84
|
+
BitexBot::BuyOpeningFlow.count.should == 0
|
85
|
+
end.to raise_exception(BitexBot::CannotCreateFlow)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
describe "when fetching open positions" do
|
90
|
+
let(:flow){ create(:buy_opening_flow) }
|
91
|
+
|
92
|
+
it 'only gets buys' do
|
93
|
+
flow.order_id.should == 12345
|
94
|
+
stub_bitex_transactions
|
95
|
+
expect do
|
96
|
+
all = BitexBot::BuyOpeningFlow.sync_open_positions
|
97
|
+
all.size.should == 1
|
98
|
+
all.first.tap do |o|
|
99
|
+
o.price == 300.0
|
100
|
+
o.amount == 600.0
|
101
|
+
o.quantity == 2
|
102
|
+
o.transaction_id.should == 12345678
|
103
|
+
o.opening_flow.should == flow
|
104
|
+
end
|
105
|
+
end.to change{ BitexBot::OpenBuy.count }.by(1)
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'does not register the same buy twice' do
|
109
|
+
flow.order_id.should == 12345
|
110
|
+
stub_bitex_transactions
|
111
|
+
BitexBot::BuyOpeningFlow.sync_open_positions
|
112
|
+
BitexBot::OpenBuy.count.should == 1
|
113
|
+
Timecop.travel 1.second.from_now
|
114
|
+
stub_bitex_transactions(build(:bitex_buy, id: 23456))
|
115
|
+
expect do
|
116
|
+
news = BitexBot::BuyOpeningFlow.sync_open_positions
|
117
|
+
news.first.transaction_id.should == 23456
|
118
|
+
end.to change{ BitexBot::OpenBuy.count }.by(1)
|
119
|
+
end
|
120
|
+
|
121
|
+
it 'does not register litecoin buys' do
|
122
|
+
flow.order_id.should == 12345
|
123
|
+
Bitex::Transaction.stub(all: [build(:bitex_buy, id: 23456, specie: :ltc)])
|
124
|
+
expect do
|
125
|
+
BitexBot::BuyOpeningFlow.sync_open_positions.should be_empty
|
126
|
+
end.not_to change{ BitexBot::OpenBuy.count }
|
127
|
+
BitexBot::OpenBuy.count.should == 0
|
128
|
+
end
|
129
|
+
|
130
|
+
it 'does not register buys from unknown bids' do
|
131
|
+
stub_bitex_transactions
|
132
|
+
expect do
|
133
|
+
BitexBot::BuyOpeningFlow.sync_open_positions.should be_empty
|
134
|
+
end.not_to change{ BitexBot::OpenBuy.count }
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
it 'cancels the associated bitex bid' do
|
139
|
+
stub_bitex_bid_create
|
140
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
141
|
+
buying: double(amount_to_spend_per_order: 50, profit: 0))
|
142
|
+
|
143
|
+
flow = BitexBot::BuyOpeningFlow.create_for_market(100,
|
144
|
+
bitstamp_order_book_stub['bids'], bitstamp_transactions_stub, 0.5, 0.25)
|
145
|
+
|
146
|
+
flow.finalise!
|
147
|
+
flow.should be_settling
|
148
|
+
flow.finalise!
|
149
|
+
flow.should be_settling
|
150
|
+
Bitex::Order.stub(active: [])
|
151
|
+
flow.finalise!
|
152
|
+
flow.should be_finalised
|
153
|
+
end
|
154
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BitexBot::OrderBookSimulator do
|
4
|
+
describe 'when buying on bitex to sell somewhere else' do
|
5
|
+
def simulate(volatility, amount)
|
6
|
+
BitexBot::OrderBookSimulator.run(volatility, bitstamp_transactions_stub,
|
7
|
+
bitstamp_order_book_stub['bids'], amount, nil)
|
8
|
+
end
|
9
|
+
|
10
|
+
it 'gets the safest price' do
|
11
|
+
simulate(0, 20).should == 30
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'adjusts for medium volatility' do
|
15
|
+
simulate(3, 20).should == 25
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'adjusts for high volatility' do
|
19
|
+
simulate(6, 20).should == 20
|
20
|
+
end
|
21
|
+
|
22
|
+
it 'big orders dig deep' do
|
23
|
+
simulate(0, 180).should == 15
|
24
|
+
end
|
25
|
+
|
26
|
+
it 'big orders with high volatility' do
|
27
|
+
simulate(6, 100).should == 10
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe 'when selling on bitex to buy somewhere else' do
|
32
|
+
def simulate(volatility, quantity)
|
33
|
+
BitexBot::OrderBookSimulator.run(volatility, bitstamp_transactions_stub,
|
34
|
+
bitstamp_order_book_stub['asks'], nil, quantity)
|
35
|
+
end
|
36
|
+
|
37
|
+
it 'gets the safest price' do
|
38
|
+
simulate(0, 2).should == 10
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'adjusts for medium volatility' do
|
42
|
+
simulate(3, 2).should == 15
|
43
|
+
end
|
44
|
+
|
45
|
+
it 'adjusts for high volatility' do
|
46
|
+
simulate(6, 2).should == 25
|
47
|
+
end
|
48
|
+
|
49
|
+
it 'big orders dig deep' do
|
50
|
+
simulate(0, 8).should == 25
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'big orders with high volatility dig deep' do
|
54
|
+
simulate(6, 6).should == 30
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BitexBot::Robot do
|
4
|
+
before(:each) do
|
5
|
+
BitexBot::Settings.stub(
|
6
|
+
time_to_live: 10,
|
7
|
+
buying: double(
|
8
|
+
amount_to_spend_per_order: 50,
|
9
|
+
profit: 0),
|
10
|
+
selling: double(
|
11
|
+
quantity_to_sell_per_order: 1,
|
12
|
+
profit: 0),
|
13
|
+
mailer: double(
|
14
|
+
from: 'test@test.com',
|
15
|
+
to: 'test@test.com',
|
16
|
+
method: :test,
|
17
|
+
options: {}
|
18
|
+
)
|
19
|
+
)
|
20
|
+
Bitex.api_key = "valid_key"
|
21
|
+
Bitex::Profile.stub(get: {fee: 0.5})
|
22
|
+
stub_bitex_bid_create
|
23
|
+
stub_bitex_ask_create
|
24
|
+
stub_bitstamp_sell
|
25
|
+
stub_bitstamp_buy
|
26
|
+
stub_bitstamp_balance
|
27
|
+
stub_bitstamp_order_book
|
28
|
+
stub_bitstamp_transactions
|
29
|
+
stub_bitstamp_user_transactions
|
30
|
+
end
|
31
|
+
let(:bot){ BitexBot::Robot.new }
|
32
|
+
|
33
|
+
it 'Starts out by creating opening flows that timeout' do
|
34
|
+
bot.trade!
|
35
|
+
stub_bitex_transactions
|
36
|
+
buying = BitexBot::BuyOpeningFlow.last
|
37
|
+
selling = BitexBot::SellOpeningFlow.last
|
38
|
+
|
39
|
+
Timecop.travel 10.minutes.from_now
|
40
|
+
bot.trade!
|
41
|
+
|
42
|
+
buying.reload.should be_settling
|
43
|
+
selling.reload.should be_settling
|
44
|
+
|
45
|
+
Bitex::Order.stub(active: [])
|
46
|
+
bot.trade!
|
47
|
+
buying.reload.should be_finalised
|
48
|
+
selling.reload.should be_finalised
|
49
|
+
end
|
50
|
+
|
51
|
+
it 'creates alternating opening flows' do
|
52
|
+
Bitex::Transaction.stub(all: [])
|
53
|
+
bot.trade!
|
54
|
+
BitexBot::BuyOpeningFlow.active.count.should == 1
|
55
|
+
Timecop.travel 2.seconds.from_now
|
56
|
+
bot.trade!
|
57
|
+
BitexBot::BuyOpeningFlow.active.count.should == 1
|
58
|
+
Timecop.travel 5.seconds.from_now
|
59
|
+
bot.trade!
|
60
|
+
BitexBot::BuyOpeningFlow.active.count.should == 2
|
61
|
+
|
62
|
+
stub_bitex_transactions
|
63
|
+
Bitex::Order.stub(active: [])
|
64
|
+
Timecop.travel 5.seconds.from_now
|
65
|
+
bot.trade!
|
66
|
+
BitexBot::BuyOpeningFlow.active.count.should == 1
|
67
|
+
Timecop.travel 5.seconds.from_now
|
68
|
+
bot.trade!
|
69
|
+
BitexBot::BuyOpeningFlow.active.count.should == 0
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'does not place new opening flows until all closing flows are done' do
|
73
|
+
bot.trade!
|
74
|
+
stub_bitex_transactions
|
75
|
+
Bitex::Order.stub(active: [])
|
76
|
+
expect do
|
77
|
+
bot.trade!
|
78
|
+
end.to change{ BitexBot::BuyClosingFlow.count }.by(1)
|
79
|
+
|
80
|
+
Timecop.travel 15.seconds.from_now
|
81
|
+
bot.trade!
|
82
|
+
bot.should be_active_closing_flows
|
83
|
+
bot.should_not be_active_opening_flows
|
84
|
+
|
85
|
+
stub_bitstamp_orders_into_transactions
|
86
|
+
expect do
|
87
|
+
bot.trade!
|
88
|
+
bot.should_not be_active_closing_flows
|
89
|
+
end.to change{ BitexBot::BuyOpeningFlow.count }.by(1)
|
90
|
+
end
|
91
|
+
|
92
|
+
it 'notifies exceptions and sleeps' do
|
93
|
+
Bitstamp.stub(:balance) do
|
94
|
+
raise StandardError.new('oh moova')
|
95
|
+
end
|
96
|
+
bot.trade!
|
97
|
+
Mail::TestMailer.deliveries.count.should == 1
|
98
|
+
end
|
99
|
+
|
100
|
+
#it 'goes through all the motions buying and selling' do
|
101
|
+
# pending
|
102
|
+
#end
|
103
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BitexBot::SellClosingFlow do
|
4
|
+
it "closes a single open position completely" do
|
5
|
+
stub_bitstamp_buy
|
6
|
+
open = create :open_sell
|
7
|
+
flow = BitexBot::SellClosingFlow.close_open_positions
|
8
|
+
open.reload.closing_flow.should == flow
|
9
|
+
flow.open_positions.should == [open]
|
10
|
+
flow.desired_price.should == 290
|
11
|
+
flow.quantity.should == 2
|
12
|
+
flow.amount.should == 600
|
13
|
+
flow.btc_profit.should be_nil
|
14
|
+
flow.usd_profit.should be_nil
|
15
|
+
close = flow.close_positions.first
|
16
|
+
close.order_id.should == 1
|
17
|
+
close.amount.should be_nil
|
18
|
+
close.quantity.should be_nil
|
19
|
+
end
|
20
|
+
|
21
|
+
it "closes an aggregate of several open positions" do
|
22
|
+
stub_bitstamp_buy
|
23
|
+
open_one = create :tiny_open_sell
|
24
|
+
open_two = create :open_sell
|
25
|
+
flow = BitexBot::SellClosingFlow.close_open_positions
|
26
|
+
close = flow.close_positions.first
|
27
|
+
open_one.reload.closing_flow.should == flow
|
28
|
+
open_two.reload.closing_flow.should == flow
|
29
|
+
flow.open_positions.should == [open_one, open_two]
|
30
|
+
flow.desired_price.should == '290.497512437810945273631840797'.to_d
|
31
|
+
flow.quantity.should == 2.01
|
32
|
+
flow.amount.should == 604
|
33
|
+
flow.btc_profit.should be_nil
|
34
|
+
flow.usd_profit.should be_nil
|
35
|
+
close.order_id.should == 1
|
36
|
+
close.amount.should be_nil
|
37
|
+
close.quantity.should be_nil
|
38
|
+
end
|
39
|
+
|
40
|
+
it "does not try to close if the amount is too low" do
|
41
|
+
open = create :tiny_open_sell
|
42
|
+
expect do
|
43
|
+
BitexBot::SellClosingFlow.close_open_positions.should be_nil
|
44
|
+
end.not_to change{ BitexBot::SellClosingFlow.count }
|
45
|
+
end
|
46
|
+
|
47
|
+
it "does not try to close if there are no open positions" do
|
48
|
+
expect do
|
49
|
+
BitexBot::SellClosingFlow.close_open_positions.should be_nil
|
50
|
+
end.not_to change{ BitexBot::SellClosingFlow.count }
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "when syncinc executed orders" do
|
54
|
+
before(:each) do
|
55
|
+
stub_bitstamp_buy
|
56
|
+
stub_bitstamp_user_transactions
|
57
|
+
create :tiny_open_sell
|
58
|
+
create :open_sell
|
59
|
+
end
|
60
|
+
|
61
|
+
it "syncs the executed orders, calculates profit" do
|
62
|
+
flow = BitexBot::SellClosingFlow.close_open_positions
|
63
|
+
stub_bitstamp_orders_into_transactions
|
64
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
65
|
+
close = flow.close_positions.last
|
66
|
+
close.amount.should == 583.9
|
67
|
+
close.quantity.should == 2.01
|
68
|
+
flow.should be_done
|
69
|
+
flow.btc_profit.should == 0
|
70
|
+
flow.usd_profit.should == 20.1
|
71
|
+
end
|
72
|
+
|
73
|
+
it "retries closing at a higher price every minute" do
|
74
|
+
flow = BitexBot::SellClosingFlow.close_open_positions
|
75
|
+
expect do
|
76
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
77
|
+
end.not_to change{ BitexBot::CloseSell.count }
|
78
|
+
flow.should_not be_done
|
79
|
+
|
80
|
+
# Immediately calling sync again does not try to cancel the ask.
|
81
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
82
|
+
Bitstamp.orders.all.size.should == 1
|
83
|
+
|
84
|
+
# Partially executes order, and 61 seconds after that
|
85
|
+
# sync_closed_positions tries to cancel it.
|
86
|
+
stub_bitstamp_orders_into_transactions(ratio: 0.5)
|
87
|
+
Timecop.travel 61.seconds.from_now
|
88
|
+
Bitstamp.orders.all.size.should == 1
|
89
|
+
expect do
|
90
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
91
|
+
end.not_to change{ BitexBot::CloseSell.count }
|
92
|
+
Bitstamp.orders.all.size.should == 0
|
93
|
+
flow.should_not be_done
|
94
|
+
|
95
|
+
# Next time we try to sync_closed_positions the flow
|
96
|
+
# detects the previous close_buy was cancelled correctly so
|
97
|
+
# it syncs it's total amounts and tries to place a new one.
|
98
|
+
expect do
|
99
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
100
|
+
end.to change{ BitexBot::CloseSell.count }.by(1)
|
101
|
+
flow.close_positions.first.tap do |close|
|
102
|
+
close.amount.should == 291.95
|
103
|
+
close.quantity.should == 1.005
|
104
|
+
end
|
105
|
+
|
106
|
+
# The second ask is executed completely so we can wrap it up and consider
|
107
|
+
# this closing flow done.
|
108
|
+
stub_bitstamp_orders_into_transactions
|
109
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
110
|
+
flow.close_positions.last.tap do |close|
|
111
|
+
close.amount.should == 291.95
|
112
|
+
close.quantity.should == '1.004930813120933'.to_d
|
113
|
+
end
|
114
|
+
flow.should be_done
|
115
|
+
flow.btc_profit.should == '-0.000069186879067'.to_d
|
116
|
+
flow.usd_profit.should == 20.1
|
117
|
+
end
|
118
|
+
|
119
|
+
it "does not retry for an amount less than minimum_for_closing" do
|
120
|
+
flow = BitexBot::SellClosingFlow.close_open_positions
|
121
|
+
|
122
|
+
20.times do
|
123
|
+
Timecop.travel 60.seconds.from_now
|
124
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
125
|
+
end
|
126
|
+
|
127
|
+
stub_bitstamp_orders_into_transactions(ratio: 0.999)
|
128
|
+
Bitstamp.orders.all.first.cancel!
|
129
|
+
|
130
|
+
expect do
|
131
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
132
|
+
end.not_to change{ BitexBot::CloseSell.count }
|
133
|
+
|
134
|
+
flow.should be_done
|
135
|
+
flow.btc_profit.should == '-0.015739962920125'.to_d
|
136
|
+
flow.usd_profit.should == '20.6839'.to_d
|
137
|
+
end
|
138
|
+
|
139
|
+
it "can lose BTC if price had to be raised dramatically" do
|
140
|
+
# This flow is forced to spend the original USD amount paying more than
|
141
|
+
# expected, thus regaining less BTC than what was sold on bitex.
|
142
|
+
flow = BitexBot::SellClosingFlow.close_open_positions
|
143
|
+
60.times do
|
144
|
+
Timecop.travel 60.seconds.from_now
|
145
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
146
|
+
end
|
147
|
+
|
148
|
+
stub_bitstamp_orders_into_transactions
|
149
|
+
|
150
|
+
flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
|
151
|
+
|
152
|
+
flow.reload.should be_done
|
153
|
+
flow.btc_profit.should == "-0.117278093149271".to_d
|
154
|
+
flow.usd_profit.should == "20.1".to_d
|
155
|
+
close = flow.close_positions.last
|
156
|
+
(close.amount / close.quantity)
|
157
|
+
.should == '308.497512437810935201007569945345172662'.to_d
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,156 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe BitexBot::SellOpeningFlow do
|
4
|
+
before(:each) do
|
5
|
+
Bitex.api_key = "valid_key"
|
6
|
+
end
|
7
|
+
|
8
|
+
it { should validate_presence_of :status }
|
9
|
+
it { should validate_presence_of :price }
|
10
|
+
it { should validate_presence_of :value_to_use }
|
11
|
+
it { should validate_presence_of :order_id }
|
12
|
+
it { should(ensure_inclusion_of(:status)
|
13
|
+
.in_array(BitexBot::SellOpeningFlow.statuses)) }
|
14
|
+
|
15
|
+
describe "when creating a selling flow" do
|
16
|
+
it "sells 2 bitcoin" do
|
17
|
+
stub_bitex_ask_create
|
18
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
19
|
+
selling: double(quantity_to_sell_per_order: 2, profit: 0))
|
20
|
+
|
21
|
+
flow = BitexBot::SellOpeningFlow.create_for_market(1000,
|
22
|
+
bitstamp_order_book_stub['asks'], bitstamp_transactions_stub, 0.5, 0.25)
|
23
|
+
|
24
|
+
flow.value_to_use.should == 2
|
25
|
+
flow.price.should >= flow.suggested_closing_price
|
26
|
+
flow.price.should == "20.15037593984962".to_d
|
27
|
+
flow.suggested_closing_price.should == 20
|
28
|
+
flow.order_id.should == 12345
|
29
|
+
end
|
30
|
+
|
31
|
+
it "sells 4 bitcoin" do
|
32
|
+
stub_bitex_ask_create
|
33
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
34
|
+
selling: double(quantity_to_sell_per_order: 4, profit: 0))
|
35
|
+
|
36
|
+
flow = BitexBot::SellOpeningFlow.create_for_market(1000,
|
37
|
+
bitstamp_order_book_stub['asks'], bitstamp_transactions_stub, 0.5, 0.25)
|
38
|
+
|
39
|
+
flow.value_to_use.should == 4
|
40
|
+
flow.price.should >= flow.suggested_closing_price
|
41
|
+
flow.price.should == "25.18796992481203".to_d
|
42
|
+
flow.suggested_closing_price.should == 25
|
43
|
+
flow.order_id.should == 12345
|
44
|
+
end
|
45
|
+
|
46
|
+
it "raises the price to charge on bitex to take a profit" do
|
47
|
+
stub_bitex_ask_create
|
48
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
49
|
+
selling: double(quantity_to_sell_per_order: 4, profit: 50))
|
50
|
+
|
51
|
+
flow = BitexBot::SellOpeningFlow.create_for_market(1000,
|
52
|
+
bitstamp_order_book_stub['asks'], bitstamp_transactions_stub, 0.5, 0.25)
|
53
|
+
|
54
|
+
flow.value_to_use.should == 4
|
55
|
+
flow.price.should >= flow.suggested_closing_price
|
56
|
+
flow.price.should == "37.78195488721804".to_d
|
57
|
+
flow.suggested_closing_price.should == 25
|
58
|
+
flow.order_id.should == 12345
|
59
|
+
end
|
60
|
+
|
61
|
+
it "fails when there is a problem placing the ask on bitex" do
|
62
|
+
Bitex::Ask.stub(:create!) do
|
63
|
+
raise StandardError.new("Cannot Create")
|
64
|
+
end
|
65
|
+
|
66
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
67
|
+
selling: double(quantity_to_sell_per_order: 4, profit: 50))
|
68
|
+
|
69
|
+
expect do
|
70
|
+
flow = BitexBot::SellOpeningFlow.create_for_market(100000,
|
71
|
+
bitstamp_order_book_stub['asks'], bitstamp_transactions_stub, 0.5, 0.25)
|
72
|
+
flow.should be_nil
|
73
|
+
BitexBot::SellOpeningFlow.count.should == 0
|
74
|
+
end.to raise_exception(BitexBot::CannotCreateFlow)
|
75
|
+
end
|
76
|
+
|
77
|
+
it "fails when there are not enough USD to re-buy in the other exchange" do
|
78
|
+
stub_bitex_bid_create
|
79
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
80
|
+
selling: double(quantity_to_sell_per_order: 4, profit: 50))
|
81
|
+
|
82
|
+
expect do
|
83
|
+
flow = BitexBot::SellOpeningFlow.create_for_market(1,
|
84
|
+
bitstamp_order_book_stub['asks'], bitstamp_transactions_stub, 0.5, 0.25)
|
85
|
+
flow.should be_nil
|
86
|
+
BitexBot::SellOpeningFlow.count.should == 0
|
87
|
+
end.to raise_exception(BitexBot::CannotCreateFlow)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "when fetching open positions" do
|
92
|
+
let(:flow){ create(:sell_opening_flow) }
|
93
|
+
|
94
|
+
it 'only gets sells' do
|
95
|
+
flow.order_id.should == 12345
|
96
|
+
stub_bitex_transactions
|
97
|
+
expect do
|
98
|
+
all = BitexBot::SellOpeningFlow.sync_open_positions
|
99
|
+
all.size.should == 1
|
100
|
+
all.first.tap do |o|
|
101
|
+
o.price.should == 300.0
|
102
|
+
o.amount.should == 600.0
|
103
|
+
o.quantity.should == 2
|
104
|
+
o.transaction_id.should == 12345678
|
105
|
+
o.opening_flow.should == flow
|
106
|
+
end
|
107
|
+
end.to change{ BitexBot::OpenSell.count }.by(1)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'does not register the same buy twice' do
|
111
|
+
flow.order_id.should == 12345
|
112
|
+
stub_bitex_transactions
|
113
|
+
BitexBot::SellOpeningFlow.sync_open_positions
|
114
|
+
BitexBot::OpenSell.count.should == 1
|
115
|
+
Timecop.travel 1.second.from_now
|
116
|
+
stub_bitex_transactions(build(:bitex_sell, id: 23456))
|
117
|
+
expect do
|
118
|
+
news = BitexBot::SellOpeningFlow.sync_open_positions
|
119
|
+
news.first.transaction_id.should == 23456
|
120
|
+
end.to change{ BitexBot::OpenSell.count }.by(1)
|
121
|
+
end
|
122
|
+
|
123
|
+
it 'does not register litecoin buys' do
|
124
|
+
flow.order_id.should == 12345
|
125
|
+
Bitex::Transaction.stub(all: [build(:bitex_sell, id: 23456, specie: :ltc)])
|
126
|
+
expect do
|
127
|
+
BitexBot::SellOpeningFlow.sync_open_positions.should be_empty
|
128
|
+
end.not_to change{ BitexBot::OpenSell.count }
|
129
|
+
BitexBot::OpenSell.count.should == 0
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'does not register buys from unknown bids' do
|
133
|
+
stub_bitex_transactions
|
134
|
+
expect do
|
135
|
+
BitexBot::SellOpeningFlow.sync_open_positions.should be_empty
|
136
|
+
end.not_to change{ BitexBot::OpenSell.count }
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
it 'cancels the associated bitex bid' do
|
141
|
+
stub_bitex_ask_create
|
142
|
+
BitexBot::Settings.stub(time_to_live: 3,
|
143
|
+
selling: double(quantity_to_sell_per_order: 4, profit: 50))
|
144
|
+
|
145
|
+
flow = BitexBot::SellOpeningFlow.create_for_market(1000,
|
146
|
+
bitstamp_order_book_stub['asks'], bitstamp_transactions_stub, 0.5, 0.25)
|
147
|
+
|
148
|
+
flow.finalise!
|
149
|
+
flow.should be_settling
|
150
|
+
flow.finalise!
|
151
|
+
flow.should be_settling
|
152
|
+
Bitex::Order.stub(active: [])
|
153
|
+
flow.finalise!
|
154
|
+
flow.should be_finalised
|
155
|
+
end
|
156
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,43 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
Bundler.setup
|
3
|
+
|
4
|
+
require 'bitex_bot'
|
5
|
+
require 'factory_girl'
|
6
|
+
require 'database_cleaner'
|
7
|
+
require 'shoulda/matchers'
|
8
|
+
require 'timecop'
|
9
|
+
FactoryGirl.find_definitions
|
10
|
+
|
11
|
+
Dir[File.dirname(__FILE__) + '/support/*.rb'].each {|file| require file }
|
12
|
+
|
13
|
+
# Automatically do rake db:test:prepare
|
14
|
+
ActiveRecord::Migration.maintain_test_schema!
|
15
|
+
|
16
|
+
# Transactional fixtures do not work with Selenium tests, because Capybara
|
17
|
+
# uses a separate server thread, which the transactions would be hidden
|
18
|
+
# from. We hence use DatabaseCleaner to truncate our test database.
|
19
|
+
DatabaseCleaner.strategy = :truncation
|
20
|
+
|
21
|
+
RSpec.configure do |config|
|
22
|
+
config.include(FactoryGirl::Syntax::Methods)
|
23
|
+
config.mock_with :rspec do |mocks|
|
24
|
+
mocks.yield_receiver_to_any_instance_implementation_blocks = true
|
25
|
+
mocks.syntax = [:expect, :should]
|
26
|
+
end
|
27
|
+
config.expect_with :rspec do |c|
|
28
|
+
c.syntax = [:expect, :should]
|
29
|
+
end
|
30
|
+
|
31
|
+
config.before(:all) do
|
32
|
+
BitexBot::Robot.logger = Logger.new('/dev/null')
|
33
|
+
BitexBot::Robot.test_mode = true
|
34
|
+
end
|
35
|
+
|
36
|
+
config.after(:each) do
|
37
|
+
DatabaseCleaner.clean # Truncate the database
|
38
|
+
Timecop.return
|
39
|
+
end
|
40
|
+
|
41
|
+
config.order = "random"
|
42
|
+
end
|
43
|
+
|