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