bitex_bot 0.3.6 → 0.3.7

Sign up to get free protection for your applications and to get access to all the features.
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