bitex_bot 0.0.6 → 0.1.0

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.
@@ -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