bitex_bot 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -26,7 +26,7 @@ Gem::Specification.new do |spec|
26
26
  spec.add_dependency "activesupport"
27
27
  spec.add_dependency "sqlite3"
28
28
  spec.add_dependency "bitstamp"
29
- spec.add_dependency "bitex", "0.0.8"
29
+ spec.add_dependency "bitex", "0.1.0"
30
30
  spec.add_dependency "mail"
31
31
 
32
32
  spec.add_development_dependency "bundler", "~> 1.3"
@@ -3,8 +3,8 @@ module BitexBot
3
3
  #ActiveRecord::Base.logger = Logger.new(File.open('database.log', 'w'))
4
4
  ActiveRecord::Base.establish_connection(Settings.database)
5
5
 
6
- if ActiveRecord::Base.connection.tables.empty?
7
- ActiveRecord::Schema.define(version: 1) do
6
+ ActiveRecord::Schema.define(version: 1) do
7
+ if ActiveRecord::Base.connection.tables.empty?
8
8
  create_table :buy_opening_flows do |t|
9
9
  t.decimal :price, precision: 30, scale: 15
10
10
  t.decimal :value_to_use, precision: 30, scale: 15
@@ -87,7 +87,23 @@ module BitexBot
87
87
  end
88
88
  add_index :close_sells, :order_id
89
89
  end
90
+
91
+ unless ActiveRecord::Base.connection.table_exists?('stores')
92
+ create_table "stores", force: true do |t|
93
+ t.decimal "bitstamp_usd", precision: 20, scale: 8
94
+ t.decimal "bitstamp_btc", precision: 20, scale: 8
95
+ t.boolean "hold", default: false
96
+ t.text "log"
97
+ t.decimal "usd_stop", precision: 20, scale: 8
98
+ t.decimal "usd_warning", precision: 20, scale: 8
99
+ t.decimal "btc_stop", precision: 20, scale: 8
100
+ t.decimal "btc_warning", precision: 20, scale: 8
101
+ t.datetime "last_warning"
102
+ t.timestamps
103
+ end
104
+ end
90
105
  end
106
+
91
107
  end
92
108
  end
93
109
 
@@ -23,15 +23,25 @@ module BitexBot
23
23
  quantity: quantity,
24
24
  amount: amount,
25
25
  open_positions: open_positions)
26
-
27
- flow.create_order_and_close_position(quantity, price)
28
-
26
+
27
+ flow.create_initial_order_and_close_position
28
+
29
29
  return flow
30
30
  end
31
31
 
32
+ def create_initial_order_and_close_position
33
+ create_order_and_close_position(quantity, desired_price)
34
+ end
35
+
32
36
  def create_order_and_close_position(quantity, price)
33
37
  order = Bitstamp.orders.send(order_method,
34
- amount: quantity.round(8), price: price.round(8))
38
+ amount: quantity.round(4), price: price.round(2))
39
+ if order.nil? || order.id.nil?
40
+ Robot.logger.error("Closing: Error on #{order_method} for "\
41
+ "#{self.class.name} ##{id} #{quantity} BTC @ $#{price}."\
42
+ "#{order.to_s}")
43
+ return
44
+ end
35
45
  Robot.logger.info("Closing: Going to #{order_method} ##{order.id} for"\
36
46
  "#{self.class.name} ##{id} #{quantity} BTC @ $#{price}")
37
47
  close_positions.create!(order_id: order.id.to_i)
@@ -39,6 +49,14 @@ module BitexBot
39
49
 
40
50
  def sync_closed_positions(orders, transactions)
41
51
  latest_close = close_positions.last
52
+
53
+ # Maybe we couldn't create the bitstamp order when this flow
54
+ # was created, so we try again when syncing.
55
+ if latest_close.nil?
56
+ create_initial_order_and_close_position
57
+ return
58
+ end
59
+
42
60
  order = orders.find{|x| x.id.to_s == latest_close.order_id.to_s }
43
61
 
44
62
  # When ask is nil it means the other exchange is done executing it
@@ -101,9 +101,9 @@ module BitexBot
101
101
 
102
102
  def finalise!
103
103
  order = self.class.order_class.find(order_id)
104
- if order.status == :cancelled
104
+ if order.status == :cancelled || order.status == :completed
105
105
  Robot.logger.info(
106
- "Opening: #{self.class.order_class.name} ##{order_id} was too old.")
106
+ "Opening: #{self.class.order_class.name} ##{order_id} finalised.")
107
107
  self.status = 'finalised'
108
108
  save!
109
109
  else
@@ -0,0 +1,2 @@
1
+ class BitexBot::Store < ActiveRecord::Base
2
+ end
@@ -1,3 +1,5 @@
1
+ require 'debugger'
2
+
1
3
  trap "INT" do
2
4
  if BitexBot::Robot.graceful_shutdown
3
5
  print "\b"
@@ -14,7 +16,10 @@ module BitexBot
14
16
  cattr_accessor :cooldown_until
15
17
  cattr_accessor :test_mode
16
18
  cattr_accessor :logger do
17
- Logger.new(Settings.log.try(:file) || STDOUT, 10, 10240000).tap do |l|
19
+ logfile = Settings.log.try(:file) ? File.open(Settings.log.file, 'a') : STDOUT
20
+ logfile.sync = true
21
+ $stderr = logfile
22
+ Logger.new(logfile, 10, 10240000).tap do |l|
18
23
  l.level = Logger.const_get(Settings.log.level.upcase)
19
24
  l.formatter = proc do |severity, datetime, progname, msg|
20
25
  date = datetime.strftime("%m/%d %H:%M:%S.%L")
@@ -23,7 +28,7 @@ module BitexBot
23
28
  end
24
29
  end
25
30
  cattr_accessor :current_cooldowns do 0 end
26
-
31
+
27
32
  # Trade constantly respecting cooldown times so that we don't get
28
33
  # banned by api clients.
29
34
  def self.run!
@@ -112,11 +117,10 @@ module BitexBot
112
117
  end
113
118
 
114
119
  def start_opening_flows_if_needed
115
- return if open_positions?
120
+ return if store.reload.hold?
116
121
  return if active_closing_flows?
117
122
  return if self.class.graceful_shutdown
118
123
 
119
-
120
124
  recent_buying, recent_selling =
121
125
  [BuyOpeningFlow, SellOpeningFlow].collect do |kind|
122
126
  threshold = (Settings.time_to_live / 2).seconds.ago
@@ -126,6 +130,31 @@ module BitexBot
126
130
  return if recent_buying && recent_selling
127
131
 
128
132
  balances = with_cooldown{ Bitstamp.balance }
133
+ profile = Bitex::Profile.get
134
+
135
+ total_usd = balances['usd_balance'].to_d + profile[:usd_balance]
136
+ total_btc = balances['btc_balance'].to_d + profile[:btc_balance]
137
+
138
+ store.update_attributes(bitstamp_usd: balances['usd_balance'],
139
+ bitstamp_btc: balances['btc_balance'])
140
+
141
+ if store.last_warning.nil? || store.last_warning < 30.minutes.ago
142
+ if store.usd_warning && total_usd <= store.usd_warning
143
+ notify("USD balance is too low, it's #{total_usd},"\
144
+ "make it #{store.usd_warning} to stop this warning.")
145
+ store.update_attributes(last_warning: Time.now)
146
+ end
147
+
148
+ if store.btc_warning && total_btc <= store.btc_warning
149
+ notify("BTC balance is too low, it's #{total_btc},"\
150
+ "make it #{store.btc_warning} to stop this warning.")
151
+ store.update_attributes(last_warning: Time.now)
152
+ end
153
+ end
154
+
155
+ return if store.usd_stop && total_usd <= store.usd_stop
156
+ return if store.btc_stop && total_btc <= store.btc_stop
157
+
129
158
  order_book = with_cooldown{ Bitstamp.order_book }
130
159
  transactions = with_cooldown{ Bitstamp.transactions }
131
160
 
@@ -134,7 +163,7 @@ module BitexBot
134
163
  balances['btc_available'].to_d,
135
164
  order_book['bids'],
136
165
  transactions,
137
- Bitex::Profile.get[:fee],
166
+ profile[:fee],
138
167
  balances['fee'].to_d )
139
168
  end
140
169
  unless recent_selling
@@ -142,7 +171,7 @@ module BitexBot
142
171
  balances['usd_available'].to_d,
143
172
  order_book['asks'],
144
173
  transactions,
145
- Bitex::Profile.get[:fee],
174
+ profile[:fee],
146
175
  balances['fee'].to_d )
147
176
  end
148
177
  end
@@ -169,5 +198,10 @@ module BitexBot
169
198
  mail.deliver!
170
199
  end
171
200
  end
201
+
202
+ # The trader has a Store
203
+ def store
204
+ @store ||= Store.first || Store.create
205
+ end
172
206
  end
173
207
  end
@@ -1,3 +1,3 @@
1
1
  module BitexBot
2
- VERSION = "0.0.6"
2
+ VERSION = "0.1.0"
3
3
  end
@@ -35,6 +35,30 @@ describe BitexBot::BuyClosingFlow do
35
35
  close.quantity.should be_nil
36
36
  end
37
37
 
38
+ it 'keeps trying to place a closed position on bitstamp errors' do
39
+ stub_bitstamp_sell
40
+ Bitstamp.orders.stub(:sell) do
41
+ double(id: nil, error: {price: ['Ensure it is good']})
42
+ end
43
+ open = create :open_buy
44
+ flow = BitexBot::BuyClosingFlow.close_open_positions
45
+ open.reload.closing_flow.should == flow
46
+ flow.open_positions.should == [open]
47
+ flow.desired_price.should == 310
48
+ flow.quantity.should == 2
49
+ flow.btc_profit.should be_nil
50
+ flow.usd_profit.should be_nil
51
+ flow.close_positions.should be_empty
52
+
53
+ stub_bitstamp_user_transactions
54
+ stub_bitstamp_sell
55
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
56
+ close = flow.close_positions.first
57
+ close.order_id.should == 1
58
+ close.amount.should be_nil
59
+ close.quantity.should be_nil
60
+ end
61
+
38
62
  it "does not try to close if the amount is too low" do
39
63
  open = create :tiny_open_buy
40
64
  expect do
@@ -61,11 +85,11 @@ describe BitexBot::BuyClosingFlow do
61
85
  stub_bitstamp_orders_into_transactions
62
86
  flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
63
87
  close = flow.close_positions.last
64
- close.amount.should == '624.1000000044'.to_d
88
+ close.amount.should == '624.105'.to_d
65
89
  close.quantity.should == 2.01
66
90
  flow.should be_done
67
91
  flow.btc_profit.should == 0
68
- flow.usd_profit.should == '20.1000000044'.to_d
92
+ flow.usd_profit.should == '20.105'.to_d
69
93
  end
70
94
 
71
95
  it "retries closing at a lower price every minute" do
@@ -97,7 +121,7 @@ describe BitexBot::BuyClosingFlow do
97
121
  flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
98
122
  end.to change{ BitexBot::CloseBuy.count }.by(1)
99
123
  flow.close_positions.first.tap do |close|
100
- close.amount.should == '312.0500000022'.to_d
124
+ close.amount.should == '312.0525'.to_d
101
125
  close.quantity.should == 1.005
102
126
  end
103
127
 
@@ -106,12 +130,12 @@ describe BitexBot::BuyClosingFlow do
106
130
  stub_bitstamp_orders_into_transactions
107
131
  flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
108
132
  flow.close_positions.last.tap do |close|
109
- close.amount.should == '312.0299000022'.to_d
133
+ close.amount.should == '312.0324'.to_d
110
134
  close.quantity.should == 1.005
111
135
  end
112
136
  flow.should be_done
113
137
  flow.btc_profit.should == 0
114
- flow.usd_profit.should == '20.0799000044001'.to_d
138
+ flow.usd_profit.should == '20.0849000000001'.to_d
115
139
  end
116
140
 
117
141
  it "does not retry for an amount less than minimum_for_closing" do
@@ -126,7 +150,7 @@ describe BitexBot::BuyClosingFlow do
126
150
 
127
151
  flow.should be_done
128
152
  flow.btc_profit.should == 0.00201
129
- flow.usd_profit.should == '19.4759000043956'.to_d
153
+ flow.usd_profit.should == '19.480895'.to_d
130
154
  end
131
155
 
132
156
  it "can lose USD if price had to be dropped dramatically" do
@@ -144,7 +168,7 @@ describe BitexBot::BuyClosingFlow do
144
168
 
145
169
  flow.reload.should be_done
146
170
  flow.btc_profit.should == 0
147
- flow.usd_profit.should == '-16.0799999956'.to_d
171
+ flow.usd_profit.should == '-16.075'.to_d
148
172
  end
149
173
  end
150
174
  end
@@ -18,7 +18,18 @@ describe BitexBot::Robot do
18
18
  )
19
19
  )
20
20
  Bitex.api_key = "valid_key"
21
- Bitex::Profile.stub(get: {fee: 0.5})
21
+ Bitex::Profile.stub(get: {
22
+ fee: 0.5,
23
+ usd_balance: 10000.00, # Total USD balance
24
+ usd_reserved: 2000.00, # USD reserved in open orders
25
+ usd_available: 8000.00, # USD available for trading
26
+ btc_balance: 20.00000000, # Total BTC balance
27
+ btc_reserved: 5.00000000, # BTC reserved in open orders
28
+ btc_available: 15.00000000, # BTC available for trading
29
+ ltc_balance: 250.00000000, # Total LTC balance
30
+ ltc_reserved: 100.00000000, # LTC reserved in open orders
31
+ ltc_available: 150.00000000,
32
+ })
22
33
  stub_bitex_orders
23
34
  stub_bitstamp_sell
24
35
  stub_bitstamp_buy
@@ -92,16 +103,83 @@ describe BitexBot::Robot do
92
103
  bot.should_not be_active_closing_flows
93
104
  end.to change{ BitexBot::BuyOpeningFlow.count }.by(1)
94
105
  end
106
+
107
+ it 'does not place new opening flows when ordered to hold' do
108
+ other_bot = BitexBot::Robot.new
109
+ other_bot.store.hold = true
110
+ other_bot.store.save!
111
+ expect do
112
+ bot.trade!
113
+ end.not_to change{ BitexBot::BuyOpeningFlow.count }
114
+ end
115
+
116
+ it 'stops trading when btc stop is reached' do
117
+ other_bot = BitexBot::Robot.new
118
+ other_bot.store.usd_stop = 11000
119
+ other_bot.store.save!
120
+ expect do
121
+ bot.trade!
122
+ end.not_to change{ BitexBot::BuyOpeningFlow.count }
123
+ end
124
+
125
+ it 'stops trading when usd stop is reached' do
126
+ other_bot = BitexBot::Robot.new
127
+ other_bot.store.btc_stop = 30
128
+ other_bot.store.save!
129
+ expect do
130
+ bot.trade!
131
+ end.not_to change{ BitexBot::BuyOpeningFlow.count }
132
+ end
133
+
134
+ it 'warns every 30 minutes when usd warn is reached' do
135
+ Bitex::Transaction.stub(all: [])
136
+ other_bot = BitexBot::Robot.new
137
+ other_bot.store.usd_warning = 11000
138
+ other_bot.store.save!
139
+ expect do
140
+ bot.trade!
141
+ end.to change{ Mail::TestMailer.deliveries.count }.by(1)
142
+ Timecop.travel 1.minute.from_now
143
+ expect do
144
+ bot.trade!
145
+ end.not_to change{ Mail::TestMailer.deliveries.count }
146
+ Timecop.travel 31.minutes.from_now
147
+ expect do
148
+ bot.trade!
149
+ end.to change{ Mail::TestMailer.deliveries.count }.by(1)
150
+ end
151
+
152
+ it 'warns every 30 minutes when btc warn is reached' do
153
+ Bitex::Transaction.stub(all: [])
154
+ other_bot = BitexBot::Robot.new
155
+ other_bot.store.btc_warning = 30
156
+ other_bot.store.save!
157
+ expect do
158
+ bot.trade!
159
+ end.to change{ Mail::TestMailer.deliveries.count }.by(1)
160
+ Timecop.travel 1.minute.from_now
161
+ expect do
162
+ bot.trade!
163
+ end.not_to change{ Mail::TestMailer.deliveries.count }
164
+ Timecop.travel 31.minutes.from_now
165
+ expect do
166
+ bot.trade!
167
+ end.to change{ Mail::TestMailer.deliveries.count }.by(1)
168
+ end
169
+
170
+ it 'updates bitstamp_usd and bitstamp_btc' do
171
+ bot.trade!
172
+ bot.store.bitstamp_usd.should_not be_nil
173
+ bot.store.bitstamp_btc.should_not be_nil
174
+ end
95
175
 
96
176
  it 'notifies exceptions and sleeps' do
97
177
  Bitstamp.stub(:balance) do
98
178
  raise StandardError.new('oh moova')
99
179
  end
100
- bot.trade!
101
- Mail::TestMailer.deliveries.count.should == 1
180
+ expect do
181
+ bot.trade!
182
+ end.to change{ Mail::TestMailer.deliveries.count }.by(1)
102
183
  end
103
184
 
104
- #it 'goes through all the motions buying and selling' do
105
- # pending
106
- #end
107
185
  end
@@ -36,6 +36,30 @@ describe BitexBot::SellClosingFlow do
36
36
  close.amount.should be_nil
37
37
  close.quantity.should be_nil
38
38
  end
39
+
40
+ it 'keeps trying to place a closed position on bitstamp errors' do
41
+ stub_bitstamp_buy
42
+ Bitstamp.orders.stub(:buy) do
43
+ double(id: nil, error: {price: ['Ensure it is good']})
44
+ end
45
+ open = create :open_sell
46
+ flow = BitexBot::SellClosingFlow.close_open_positions
47
+ open.reload.closing_flow.should == flow
48
+ flow.open_positions.should == [open]
49
+ flow.desired_price.should == 290
50
+ flow.quantity.should == 2
51
+ flow.btc_profit.should be_nil
52
+ flow.usd_profit.should be_nil
53
+ flow.close_positions.should be_empty
54
+
55
+ stub_bitstamp_user_transactions
56
+ stub_bitstamp_buy
57
+ flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
58
+ close = flow.close_positions.first
59
+ close.order_id.should == 1
60
+ close.amount.should be_nil
61
+ close.quantity.should be_nil
62
+ end
39
63
 
40
64
  it "does not try to close if the amount is too low" do
41
65
  open = create :tiny_open_sell
@@ -63,11 +87,11 @@ describe BitexBot::SellClosingFlow do
63
87
  stub_bitstamp_orders_into_transactions
64
88
  flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
65
89
  close = flow.close_positions.last
66
- close.amount.should == "583.9000000044".to_d
90
+ close.amount.should == "583.905".to_d
67
91
  close.quantity.should == 2.01
68
92
  flow.should be_done
69
93
  flow.btc_profit.should == 0
70
- flow.usd_profit.should == "20.0999999956".to_d
94
+ flow.usd_profit.should == "20.095".to_d
71
95
  end
72
96
 
73
97
  it "retries closing at a higher price every minute" do
@@ -99,7 +123,7 @@ describe BitexBot::SellClosingFlow do
99
123
  flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
100
124
  end.to change{ BitexBot::CloseSell.count }.by(1)
101
125
  flow.close_positions.first.tap do |close|
102
- close.amount.should == "291.9500000022".to_d
126
+ close.amount.should == "291.9525".to_d
103
127
  close.quantity.should == 1.005
104
128
  end
105
129
 
@@ -108,12 +132,13 @@ describe BitexBot::SellClosingFlow do
108
132
  stub_bitstamp_orders_into_transactions
109
133
  flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
110
134
  flow.close_positions.last.tap do |close|
111
- close.amount.should == "291.9499990955143".to_d
112
- close.quantity.should == '1.00493081'.to_d
135
+ close.amount.should == "291.943548".to_d
136
+ close.quantity.should == '1.0049'.to_d
113
137
  end
114
138
  flow.should be_done
115
- flow.btc_profit.should == '-0.00006919'.to_d
116
- flow.usd_profit.should == '20.1000009022857'.to_d
139
+ flow.btc_profit.should == '-0.0001'.to_d
140
+ flow.usd_profit.should == '20.1039519999999'.to_d
141
+
117
142
  end
118
143
 
119
144
  it "does not retry for an amount less than minimum_for_closing" do
@@ -132,8 +157,8 @@ describe BitexBot::SellClosingFlow do
132
157
  end.not_to change{ BitexBot::CloseSell.count }
133
158
 
134
159
  flow.should be_done
135
- flow.btc_profit.should == '-0.01573996629'.to_d
136
- flow.usd_profit.should == '20.6839009813144'.to_d
160
+ flow.btc_profit.should == '-0.0156963'.to_d
161
+ flow.usd_profit.should == '20.66616775'.to_d
137
162
  end
138
163
 
139
164
  it "can lose BTC if price had to be raised dramatically" do
@@ -150,11 +175,11 @@ describe BitexBot::SellClosingFlow do
150
175
  flow.sync_closed_positions(Bitstamp.orders.all, Bitstamp.user_transactions.all)
151
176
 
152
177
  flow.reload.should be_done
153
- flow.btc_profit.should == "-0.11727809".to_d
154
- flow.usd_profit.should == "20.0999990243145".to_d
178
+ flow.btc_profit.should == "-0.1173".to_d
179
+ flow.usd_profit.should == "20.10205".to_d
155
180
  close = flow.close_positions.last
156
181
  (close.amount / close.quantity)
157
- .should == '308.497512439999968088286144476448735145'.to_d
182
+ .should == '308.5'.to_d
158
183
  end
159
184
  end
160
185
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bitex_bot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.6
4
+ version: 0.1.0
5
5
  prerelease:
6
6
  platform: ruby
7
7
  authors:
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2014-07-31 00:00:00.000000000 Z
13
+ date: 2014-08-08 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: settingslogic
@@ -99,7 +99,7 @@ dependencies:
99
99
  requirements:
100
100
  - - '='
101
101
  - !ruby/object:Gem::Version
102
- version: 0.0.8
102
+ version: 0.1.0
103
103
  type: :runtime
104
104
  prerelease: false
105
105
  version_requirements: !ruby/object:Gem::Requirement
@@ -107,7 +107,7 @@ dependencies:
107
107
  requirements:
108
108
  - - '='
109
109
  - !ruby/object:Gem::Version
110
- version: 0.0.8
110
+ version: 0.1.0
111
111
  - !ruby/object:Gem::Dependency
112
112
  name: mail
113
113
  requirement: !ruby/object:Gem::Requirement
@@ -300,6 +300,7 @@ files:
300
300
  - lib/bitex_bot/models/order_book_simulator.rb
301
301
  - lib/bitex_bot/models/sell_closing_flow.rb
302
302
  - lib/bitex_bot/models/sell_opening_flow.rb
303
+ - lib/bitex_bot/models/store.rb
303
304
  - lib/bitex_bot/robot.rb
304
305
  - lib/bitex_bot/settings.rb
305
306
  - lib/bitex_bot/version.rb