bitex_bot 0.3.6 → 0.3.7

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 4361404809a875ed432da4a24e3d7730bb405864
4
- data.tar.gz: 8799ebd00c18ccfd4e4066e81fc5f182ff4e93da
3
+ metadata.gz: c1bcc53a7b4d8a6382173bdd864dd306b3a2bdc5
4
+ data.tar.gz: e4254d0ffe025a95c5eba3429814b2b60405abb7
5
5
  SHA512:
6
- metadata.gz: 1131f6f01e2b216854bbac7981b1ab6219732809047e544694dbac065b171c2cb2648c2db58f6a45c63356a604d945ebda582176de92a131e310af6def71ba09
7
- data.tar.gz: 132ad4b745e70eb8bbb2d1179aae3e5cc4310909b766e4112c825cebe6f09723813d6b25fff50176de581f9a70b3b10c2fcee12e22fc2f7347e01ffb2de6a599
6
+ metadata.gz: 0b64dae4070f6fb5ce595bfb0d5cac5c88d9d853264b948ae62241e812a1946ed83e9f3af344b4fa8fb7a215d61f9cc8c2b960c388b820d2b880f3dc72cc32b7
7
+ data.tar.gz: a8ac2e4ace3edaa00b529d6aafd1808d4bb8cfb8139b6588789a80e8455b38505be31afb6a813048905d9f8c32f3738b979c23c45a1ab1340a49454180347ecc
data/bitex_bot.gemspec CHANGED
@@ -27,6 +27,7 @@ Gem::Specification.new do |spec|
27
27
  spec.add_dependency "bitex", "0.3"
28
28
  spec.add_dependency "itbit", "0.0.6"
29
29
  spec.add_dependency "bitfinex-rb", "0.0.6"
30
+ spec.add_dependency "kraken_client", "~> 1.2.1"
30
31
  spec.add_dependency "mail"
31
32
  spec.add_dependency "hashie", "~> 3.5.4"
32
33
 
@@ -95,7 +95,7 @@ class BitfinexApiWrapper
95
95
  def self.place_order(type, price, quantity)
96
96
  with_retry "place order #{type} #{price} #{quantity}" do
97
97
  order_data = Bitfinex::Client.new
98
- .new_order('btcusd', quantity, 'exchange limit', type.to_s, price)
98
+ .new_order('btcusd', quantity.round(4), 'exchange limit', type.to_s, price.round(2))
99
99
  BitfinexOrder.new(order_data)
100
100
  end
101
101
  end
@@ -77,6 +77,6 @@ class BitstampApiWrapper
77
77
  end
78
78
 
79
79
  def self.place_order(type, price, quantity)
80
- Bitstamp.orders.send(type, amount: quantity, price: price)
80
+ Bitstamp.orders.send(type, amount: quantity.round(4), price: price.round(2))
81
81
  end
82
82
  end
@@ -35,15 +35,15 @@ module BitexBot
35
35
 
36
36
  def create_order_and_close_position(quantity, price)
37
37
  order = BitexBot::Robot.taker.place_order(
38
- order_method, price.round(2), quantity.round(4))
38
+ order_method, price, quantity)
39
39
  if order.nil? || order.id.nil?
40
40
  Robot.logger.error("Closing: Error on #{order_method} for "\
41
41
  "#{self.class.name} ##{id} #{quantity} BTC @ $#{price}."\
42
42
  "#{order.to_s}")
43
43
  return
44
44
  end
45
- Robot.logger.info("Closing: Going to #{order_method} ##{order.id} for"\
46
- "#{self.class.name} ##{id} #{quantity} BTC @ $#{price}")
45
+ Robot.logger.info("Closing: Going to #{order_method} ##{order.id} for "\
46
+ "#{self.class.name} ##{id} #{order.amount} BTC @ $#{order.price}")
47
47
  close_positions.create!(order_id: order.id)
48
48
  end
49
49
 
@@ -74,14 +74,16 @@ module BitexBot
74
74
  self.btc_profit = get_btc_profit
75
75
  self.usd_profit = get_usd_profit
76
76
  self.done = true
77
- Robot.logger.info("Closing: Finished #{self.class.name} ##{id}"\
77
+ Robot.logger.info("Closing: Finished #{self.class.name} ##{id} "\
78
78
  "earned $#{self.usd_profit} and #{self.btc_profit} BTC. ")
79
79
  save!
80
80
  end
81
81
  elsif latest_close.created_at < self.class.close_time_to_live.seconds.ago
82
82
  Robot.with_cooldown do
83
83
  begin
84
+ Robot.logger.debug("Finalising #{order.class}##{order.id}")
84
85
  order.cancel!
86
+ Robot.logger.debug("Finalised #{order.class}##{order.id}")
85
87
  rescue StandardError => e
86
88
  nil # just pass, we'll keep on trying until it's not in orders anymore.
87
89
  end
@@ -47,7 +47,7 @@ class ItbitApiWrapper
47
47
 
48
48
  def self.place_order(type, price, quantity)
49
49
  begin
50
- return Itbit::Order.create!(type, :xbtusd, quantity, price, wait: true)
50
+ return Itbit::Order.create!(type, :xbtusd, quantity.round(4), price.round(2), wait: true)
51
51
  rescue RestClient::RequestTimeout => e
52
52
  # On timeout errors, we still look for the latest active closing order
53
53
  # that may be available. We have a magic threshold of 5 minutes
@@ -0,0 +1,188 @@
1
+ require 'kraken_client'
2
+
3
+ class KrakenApiWrapper
4
+ def self.setup(settings)
5
+ HTTParty::Basement.headers('User-Agent' => BitexBot.user_agent)
6
+ @settings = settings.kraken
7
+ end
8
+
9
+ def self.client
10
+ @client ||= KrakenClient.load(@settings)
11
+ end
12
+
13
+ #{
14
+ # tid:i,
15
+ # date: (i+1).seconds.ago.to_i.to_s,
16
+ # price: price.to_s,
17
+ # amount: amount.to_s
18
+ #}
19
+ def self.transactions
20
+ client.public.trades('XBTUSD')[:XXBTZUSD].reverse.collect do |t|
21
+ Hashie::Mash.new({
22
+ tid: t[2].to_s,
23
+ price: t[0],
24
+ amount: t[1],
25
+ date: t[2]
26
+ })
27
+ end
28
+ rescue NoMethodError => e
29
+ retry
30
+ end
31
+
32
+ # { 'timestamp' => DateTime.now.to_i.to_s,
33
+ # 'bids' =>
34
+ # [['30', '3'], ['25', '2'], ['20', '1.5'], ['15', '4'], ['10', '5']],
35
+ # 'asks' =>
36
+ # [['10', '2'], ['15', '3'], ['20', '1.5'], ['25', '3'], ['30', '3']]
37
+ # }
38
+ def self.order_book(retries = 20)
39
+ book = client.public.order_book('XBTUSD')[:XXBTZUSD]
40
+ { 'bids' => book[:bids].collect { |b| [ b[0], b[1] ] },
41
+ 'asks' => book[:asks].collect { |a| [ a[0], a[1] ] } }
42
+ rescue NoMethodError => e
43
+ retry
44
+ end
45
+
46
+ # {"btc_balance"=> "10.0", "btc_reserved"=> "0", "btc_available"=> "10.0",
47
+ # "usd_balance"=> "100.0", "usd_reserved"=>"0", "usd_available"=> "100.0",
48
+ # "fee"=> "0.5000"}
49
+ def self.balance
50
+ balances = client.private.balance
51
+ open_orders = KrakenOrder.open
52
+ sell_orders = open_orders.select { |o| o.type == :sell }
53
+ btc_reserved = sell_orders.collect { |o| o.amount - o.executed_amount }.sum
54
+ buy_orders = open_orders - sell_orders
55
+ usd_reserved = buy_orders.collect { |o| (o.amount - o.executed_amount) * o.price }.sum
56
+ { 'btc_balance' => balances['XXBT'].to_d,
57
+ 'btc_reserved' => btc_reserved,
58
+ 'btc_available' => balances['XXBT'].to_d - btc_reserved,
59
+ 'usd_balance' => balances['ZUSD'].to_d,
60
+ 'usd_reserved' => usd_reserved,
61
+ 'usd_available' => balances['ZUSD'].to_d - usd_reserved,
62
+ 'fee' => client.private.trade_volume(pair: 'XBTUSD')[:fees][:XXBTZUSD][:fee].to_d
63
+ }
64
+ rescue KrakenClient::ErrorResponse, Net::ReadTimeout => e
65
+ retry
66
+ end
67
+
68
+ # ask = double(amount: args[:amount], price: args[:price],
69
+ # type: 1, id: remote_id, datetime: DateTime.now.to_s)
70
+ # ask.stub(:cancel!) do
71
+ def self.orders
72
+ KrakenOrder.open
73
+ end
74
+
75
+ # We don't need to fetch the list of transactions
76
+ # for Kraken
77
+ def self.user_transactions
78
+ [ ]
79
+ end
80
+
81
+ def self.amount_and_quantity(order_id, transactions)
82
+ KrakenOrder.amount_and_quantity(order_id, transactions)
83
+ end
84
+
85
+ def self.place_order(type, price, quantity)
86
+ KrakenOrder.create(type, price, quantity)
87
+ end
88
+ end
89
+
90
+ class KrakenOrder
91
+ attr_accessor :id, :amount, :executed_amount, :price, :avg_price, :type, :datetime
92
+ def initialize(id, order_data)
93
+ self.id = id
94
+ self.amount = order_data['vol'].to_d
95
+ self.executed_amount = order_data['vol_exec'].to_d
96
+ self.price = order_data['descr']['price'].to_d
97
+ self.avg_price = order_data['price'].to_d
98
+ self.type = order_data['descr']['type'].to_sym
99
+ self.datetime = order_data['opentm'].to_i
100
+ end
101
+
102
+ def cancel!
103
+ self.class.client.private.cancel_order(txid: id)
104
+ rescue KrakenClient::ErrorResponse => e
105
+ retry if e.message == 'EService:Unavailable'
106
+ raise
107
+ end
108
+
109
+ def ==(order)
110
+ if order.is_a?(self.class)
111
+ id == order.id
112
+ elsif order.is_a?(Array)
113
+ [ type, price, amount ] == order
114
+ end
115
+ end
116
+
117
+ def self.client
118
+ KrakenApiWrapper.client
119
+ end
120
+
121
+ def self.find(id)
122
+ new(*client.private.query_orders(txid: id).first)
123
+ rescue KrakenClient::ErrorResponse => e
124
+ retry
125
+ end
126
+
127
+ def self.amount_and_quantity(order_id, transactions)
128
+ order = find(order_id)
129
+ [ order.avg_price * order.executed_amount, order.executed_amount ]
130
+ end
131
+
132
+ def self.open
133
+ client.private.open_orders['open'].collect { |o| new(*o) }
134
+ rescue KrakenClient::ErrorResponse => e
135
+ retry
136
+ end
137
+
138
+ def self.closed(start: 1.hour.ago.to_i)
139
+ client.private.closed_orders(start: start)[:closed].collect { |o| new(*o) }
140
+ rescue KrakenClient::ErrorResponse => e
141
+ retry
142
+ end
143
+
144
+ def self.find_lost(type, price, quantity, last_closed_order)
145
+ order_descr = [ type, price, quantity ]
146
+
147
+ BitexBot::Robot.logger.debug("Looking for #{type} order in open orders...")
148
+ if order = self.open.detect { |o| o == order_descr }
149
+ BitexBot::Robot.logger.debug("Found open order with ID #{order.id}")
150
+ return order
151
+ end
152
+
153
+ BitexBot::Robot.logger.debug("Looking for #{type} order in closed orders...")
154
+ order = closed(start: last_closed_order).detect { |o| o == order_descr }
155
+ if order && order.id != last_closed_order
156
+ BitexBot::Robot.logger.debug("Found closed order with ID #{order.id}")
157
+ return order
158
+ end
159
+ end
160
+
161
+ def self.create(type, price, quantity)
162
+ last_closed_order = closed.first.try(:id) || Time.now.to_i
163
+ price = price.truncate(1)
164
+ quantity = quantity.truncate(8)
165
+ order_info = client.private.add_order(pair: 'XBTUSD', type: type, ordertype: 'limit',
166
+ price: price, volume: quantity)
167
+ find(order_info['txid'].first)
168
+ rescue KrakenClient::ErrorResponse => e
169
+ # Order could not be placed
170
+ if e.message == 'EService:Unavailable'
171
+ BitexBot::Robot.logger.debug('Captured EService:Unavailable error when placing order on Kraken. Retrying...')
172
+ retry
173
+ elsif e.message.start_with?('EGeneral:Invalid')
174
+ BitexBot::Robot.logger.debug("Captured #{e.message}: type: #{type}, price: #{price}, quantity: #{quantity}")
175
+ return
176
+ end
177
+ raise unless e.message == 'error'
178
+ BitexBot::Robot.logger.debug('Captured error when placing order on Kraken')
179
+ # Order may have gone through and be stuck somewhere in Kraken's
180
+ # pipeline. We just sleep for a bit and then look for the order.
181
+ 8.times do
182
+ sleep 15
183
+ order = find_lost(type, price, quantity, last_closed_order)
184
+ return order if order
185
+ end
186
+ raise
187
+ end
188
+ end
@@ -21,6 +21,8 @@ module BitexBot
21
21
  BitstampApiWrapper
22
22
  when 'bitfinex'
23
23
  BitfinexApiWrapper
24
+ when 'kraken'
25
+ KrakenApiWrapper
24
26
  end
25
27
  end
26
28
  cattr_accessor :logger do
@@ -1,3 +1,3 @@
1
1
  module BitexBot
2
- VERSION = "0.3.6"
2
+ VERSION = "0.3.7"
3
3
  end
data/settings.rb.sample CHANGED
@@ -15,7 +15,7 @@ time_to_live 20
15
15
  sandbox false
16
16
 
17
17
  # Which market to use for taking (we're always makers on bitex)
18
- # 'itbit', 'bitstamp' or 'bitfinex'
18
+ # 'itbit', 'bitstamp', 'bitfinex' or 'kraken'
19
19
  taker 'bitstamp'
20
20
 
21
21
  # Settings for buying on bitex and selling on bitstamp.
@@ -73,6 +73,11 @@ itbit client_key: 'the-client-key',
73
73
  bitfinex api_key: 'your_api_key',
74
74
  api_secret: 'your_api_secret'
75
75
 
76
+ # These are passed in to the kraken gem:
77
+ # see https://github.com/shideneyu/kraken_client for more info.
78
+ kraken api_key: 'your_api_key',
79
+ api_secret: 'your_api_secret'
80
+
76
81
  # Settings for the ActiveRecord Database to use.
77
82
  # sqlite is just fine. Check this link for more options:
78
83
  # http://apidock.com/rails/ActiveRecord/Base/establish_connection/class
@@ -10,6 +10,7 @@ describe BitexBot::Settings do
10
10
  :buying => {:amount_to_spend_per_order=>10.0, :profit=>0.5},
11
11
  :database => {:adapter=>:sqlite3, :database=>"bitex_bot.db"},
12
12
  :itbit => {:client_key=>"the-client-key", :secret=>"the-secret", :user_id=>"the-user-id", :default_wallet_id=>"wallet-000"},
13
+ :kraken => {:api_key=>"your_api_key", :api_secret=>"your_api_secret"},
13
14
  :log => {:file=>"bitex_bot.log", :level=>:info},
14
15
  :mailer => {:from=>"robot@example.com", :to=>"you@example.com", :delivery_method=>:smtp, :options=>{:address=>"your_smtp_server_address.com", :port=>587, :authentication=>"plain", :enable_starttls_auto=>true, :user_name=>"your_user_name", :password=>"your_smtp_password"}},
15
16
  :sandbox => false,
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.3.6
4
+ version: 0.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Nubis
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
- date: 2017-10-03 00:00:00.000000000 Z
12
+ date: 2017-11-09 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activerecord
@@ -95,6 +95,20 @@ dependencies:
95
95
  - - '='
96
96
  - !ruby/object:Gem::Version
97
97
  version: 0.0.6
98
+ - !ruby/object:Gem::Dependency
99
+ name: kraken_client
100
+ requirement: !ruby/object:Gem::Requirement
101
+ requirements:
102
+ - - "~>"
103
+ - !ruby/object:Gem::Version
104
+ version: 1.2.1
105
+ type: :runtime
106
+ prerelease: false
107
+ version_requirements: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - "~>"
110
+ - !ruby/object:Gem::Version
111
+ version: 1.2.1
98
112
  - !ruby/object:Gem::Dependency
99
113
  name: mail
100
114
  requirement: !ruby/object:Gem::Requirement
@@ -293,6 +307,7 @@ files:
293
307
  - lib/bitex_bot/models/close_sell.rb
294
308
  - lib/bitex_bot/models/closing_flow.rb
295
309
  - lib/bitex_bot/models/itbit_api_wrapper.rb
310
+ - lib/bitex_bot/models/kraken_api_wrapper.rb
296
311
  - lib/bitex_bot/models/open_buy.rb
297
312
  - lib/bitex_bot/models/open_sell.rb
298
313
  - lib/bitex_bot/models/opening_flow.rb