bitfinex-rb 0.1.0 → 1.0.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.
Files changed (64) hide show
  1. checksums.yaml +4 -4
  2. data/lib/bitfinex.rb +28 -30
  3. data/lib/{bitfinex/errors.rb → errors.rb} +2 -2
  4. data/lib/models/alert.rb +27 -0
  5. data/lib/models/balance_info.rb +25 -0
  6. data/lib/models/candle.rb +29 -0
  7. data/lib/models/currency.rb +27 -0
  8. data/lib/models/funding_credit.rb +41 -0
  9. data/lib/models/funding_info.rb +40 -0
  10. data/lib/models/funding_loan.rb +40 -0
  11. data/lib/models/funding_offer.rb +38 -0
  12. data/lib/models/funding_ticker.rb +50 -0
  13. data/lib/models/funding_trade.rb +31 -0
  14. data/lib/models/ledger_entry.rb +33 -0
  15. data/lib/models/margin_info.rb +67 -0
  16. data/lib/models/model.rb +56 -0
  17. data/lib/models/movement.rb +33 -0
  18. data/lib/models/notification.rb +30 -0
  19. data/lib/models/order.rb +197 -0
  20. data/lib/models/order_book.rb +265 -0
  21. data/lib/models/position.rb +33 -0
  22. data/lib/models/public_trade.rb +27 -0
  23. data/lib/models/trade.rb +34 -0
  24. data/lib/models/trading_ticker.rb +33 -0
  25. data/lib/models/user_info.rb +27 -0
  26. data/lib/models/wallet.rb +28 -0
  27. data/lib/rest/rest_client.rb +103 -0
  28. data/lib/rest/v1.rb +63 -0
  29. data/lib/rest/v1/account_info.rb +23 -0
  30. data/lib/{bitfinex → rest}/v1/deposit.rb +2 -3
  31. data/lib/{bitfinex → rest}/v1/funding_book.rb +2 -4
  32. data/lib/{bitfinex → rest}/v1/historical_data.rb +2 -2
  33. data/lib/{bitfinex → rest}/v1/lends.rb +1 -2
  34. data/lib/{bitfinex → rest}/v1/margin_funding.rb +1 -2
  35. data/lib/rest/v1/order_book.rb +17 -0
  36. data/lib/{bitfinex → rest}/v1/orders.rb +6 -8
  37. data/lib/{bitfinex → rest}/v1/positions.rb +6 -2
  38. data/lib/{bitfinex → rest}/v1/stats.rb +1 -3
  39. data/lib/{bitfinex → rest}/v1/symbols.rb +1 -2
  40. data/lib/rest/v1/ticker.rb +13 -0
  41. data/lib/{bitfinex → rest}/v1/trades.rb +1 -16
  42. data/lib/{bitfinex → rest}/v1/wallet.rb +1 -2
  43. data/lib/rest/v2.rb +47 -0
  44. data/lib/{bitfinex → rest}/v2/margin.rb +1 -2
  45. data/lib/{bitfinex → rest}/v2/personal.rb +2 -18
  46. data/lib/{bitfinex → rest}/v2/stats.rb +1 -4
  47. data/lib/rest/v2/ticker.rb +20 -0
  48. data/lib/{bitfinex → rest}/v2/trading.rb +3 -49
  49. data/lib/{bitfinex → rest}/v2/utils.rb +1 -3
  50. data/lib/ws/ws2.rb +529 -0
  51. metadata +74 -56
  52. data/lib/bitfinex-api-rb.rb +0 -1
  53. data/lib/bitfinex-rb.rb +0 -1
  54. data/lib/bitfinex/api_versions.rb +0 -7
  55. data/lib/bitfinex/authenticated_rest.rb +0 -57
  56. data/lib/bitfinex/client.rb +0 -38
  57. data/lib/bitfinex/configurable.rb +0 -48
  58. data/lib/bitfinex/connection.rb +0 -52
  59. data/lib/bitfinex/v1/account_info.rb +0 -38
  60. data/lib/bitfinex/v1/orderbook.rb +0 -36
  61. data/lib/bitfinex/v1/ticker.rb +0 -27
  62. data/lib/bitfinex/v2/ticker.rb +0 -58
  63. data/lib/bitfinex/version.rb +0 -3
  64. data/lib/bitfinex/websocket_connection.rb +0 -231
@@ -1,6 +1,5 @@
1
1
  module Bitfinex
2
- module V1::MarginFundingClient
3
-
2
+ module RESTv1MarginFunding
4
3
  # Submit a new offer
5
4
  #
6
5
  # @param currency [string] The name of the currency, es: 'USD'
@@ -0,0 +1,17 @@
1
+ module Bitfinex
2
+ module RESTv1OrderBook
3
+ # Get the full order book
4
+ #
5
+ # @param symbol [string]
6
+ # @param params :limit_bids [int] (optional) Limit the number of bids returned. May be 0 in which case the array of bids is empty. Default 50.
7
+ # @param params :limit_asks [int] (optional) Limit the number of asks returned. May be 0 in which case the array of asks is empty. Default 50.
8
+ # @param params :group [0/1] (optional) If 1, orders are grouped by price in the orderbook. If 0, orders are not grouped and sorted individually. Default 1
9
+ # @return [Hash] :bids [Array], :asks [Array]
10
+ # @example:
11
+ # client.orderbook("btcusd")
12
+ def orderbook(symbol="btcusd", params = {})
13
+ check_params(params, %i{limit_bids limit_asks group})
14
+ get("book/#{symbol}", params).body
15
+ end
16
+ end
17
+ end
@@ -1,6 +1,5 @@
1
1
  module Bitfinex
2
- module V1::OrdersClient
3
-
2
+ module RESTv1Orders
4
3
  # Submit a new order
5
4
  # @param symbol [string] The name of the symbol (see `#symbols`)
6
5
  # @param amount [decimal] Order size: how much to buy or sell
@@ -15,7 +14,7 @@ module Bitfinex
15
14
  # @example:
16
15
  # client.new_order("usdbtc", 100, "market", "sell", 0)
17
16
  def new_order(symbol, amount, type, side, price = nil, params = {})
18
- check_params(params, %i{is_hidden is_postonly ocoorder buy_price_oco})
17
+ check_params(params, %i{is_hidden is_postonly ocoorder buy_price_oco use_all_available})
19
18
 
20
19
  # for 'market' order, we need to pass a random positive price, not nil
21
20
  price ||= 0.001 if type == "market" || type == "exchange market"
@@ -59,13 +58,13 @@ module Bitfinex
59
58
  def cancel_orders(ids=nil)
60
59
  case ids
61
60
  when Array
62
- authenticated_post("order/cancel/multi", params: {order_ids: ids.map(&:to_i)}).body
61
+ authenticated_post("order/cancel/multi", params: {order_ids: ids.map(&:to_i)}).body
63
62
  when Numeric, String
64
- authenticated_post("order/cancel", params: {order_id: ids.to_i}).body
63
+ authenticated_post("order/cancel", params: {order_id: ids.to_i}).body
65
64
  when NilClass
66
- authenticated_post("order/cancel/all").body
65
+ authenticated_post("order/cancel/all").body
67
66
  else
68
- raise ParamsError
67
+ raise ParamsError
69
68
  end
70
69
  end
71
70
 
@@ -116,6 +115,5 @@ module Bitfinex
116
115
  def orders
117
116
  authenticated_post("orders").body
118
117
  end
119
-
120
118
  end
121
119
  end
@@ -1,6 +1,5 @@
1
1
  module Bitfinex
2
- module V1::PositionsClient
3
-
2
+ module RESTv1Positions
4
3
  # View your active positions.
5
4
  #
6
5
  # @return [Array]
@@ -23,5 +22,10 @@ module Bitfinex
23
22
  def claim_position(position_id, amount)
24
23
  authenticated_post("position/claim", params: {position_id: position_id, amount: amount}).body
25
24
  end
25
+
26
+ # Closes the specified position with a market order
27
+ def close_position(position_id)
28
+ authenticated_post("position/close", params: {position_id: position_id}).body
29
+ end
26
30
  end
27
31
  end
@@ -1,6 +1,5 @@
1
1
  module Bitfinex
2
- module V1::StatsClient
3
-
2
+ module RESTv1Stats
4
3
  # Various statistics about the requested pair.
5
4
  #
6
5
  # @param symbol [string] Symbol of the pair you want info about. Default 'btcusd'
@@ -10,6 +9,5 @@ module Bitfinex
10
9
  def stats(symbol = "btcusd")
11
10
  get("stats/#{symbol}").body
12
11
  end
13
-
14
12
  end
15
13
  end
@@ -1,6 +1,5 @@
1
1
  module Bitfinex
2
- module V1::SymbolsClient
3
-
2
+ module RESTv1Symbols
4
3
  # Get a list of valid symbol IDs.
5
4
  #
6
5
  # @return [Array]
@@ -0,0 +1,13 @@
1
+ module Bitfinex
2
+ module RESTv1Ticker
3
+ # Gives innermost bid and asks and information on the most recent trade, as well as high, low and volume of the last 24 hours.
4
+ #
5
+ # @param symbol [string] The name of hthe symbol
6
+ # @return [Hash]
7
+ # @example:
8
+ # client.ticker
9
+ def ticker(symbol = "btcusd")
10
+ get("pubticker/#{symbol}").body
11
+ end
12
+ end
13
+ end
@@ -1,6 +1,5 @@
1
1
  module Bitfinex
2
- module V1::TradesClient
3
-
2
+ module RESTv1Trades
4
3
  # Get a list of the most recent trades for the given symbol.
5
4
  #
6
5
  # @param symbol [string] the name of the symbol
@@ -13,19 +12,5 @@ module Bitfinex
13
12
  check_params(params, %i{timestamp limit_trades})
14
13
  get("trades/#{symbol}", params).body
15
14
  end
16
-
17
- # Listen to the trades using websocket.
18
- #
19
- # @param pair [string]
20
- # @param block [Block] The code to be executed when a new trade is executed
21
- # @example:
22
- # client.listen_trades do |trade|
23
- # puts trade.inspect
24
- # end
25
- def listen_trades(pair="BTCUSD", &block)
26
- raise BlockMissingError unless block_given?
27
- register_channel pair:pair, channel: 'trades', &block
28
- end
29
-
30
15
  end
31
16
  end
@@ -1,6 +1,5 @@
1
1
  module Bitfinex
2
- module V1::WalletClient
3
-
2
+ module RESTv1Wallet
4
3
  # See your balances.
5
4
  #
6
5
  # @param params :type [string] “trading”, “deposit” or “exchange”.
@@ -0,0 +1,47 @@
1
+ require_relative './rest_client'
2
+ require_relative './v2/margin'
3
+ require_relative './v2/personal'
4
+ require_relative './v2/stats'
5
+ require_relative './v2/ticker'
6
+ require_relative './v2/trading'
7
+ require_relative './v2/utils'
8
+
9
+ module Bitfinex
10
+ class RESTv2
11
+ attr_accessor :api_endpoint, :debug, :debug_connection, :api_version
12
+ attr_accessor :rest_timeout, :rest_open_timeout, :proxy
13
+ attr_accessor :api_key, :api_secret
14
+
15
+ include Bitfinex::RESTClient
16
+ include Bitfinex::RESTv2Margin
17
+ include Bitfinex::RESTv2Personal
18
+ include Bitfinex::RESTv2Stats
19
+ include Bitfinex::RESTv2Ticker
20
+ include Bitfinex::RESTv2Trading
21
+ include Bitfinex::RESTv2Utils
22
+
23
+ def initialize(args = {})
24
+ self.api_endpoint = args[:url] ? "#{args[:url]}/v2/" : "https://api.bitfinex.com/v2/"
25
+ self.proxy = args[:proxy] || nil
26
+ self.debug_connection = false
27
+ self.api_version = 2
28
+ self.rest_timeout = 30
29
+ self.rest_open_timeout = 30
30
+ self.api_key = args[:api_key]
31
+ self.api_secret = args[:api_secret]
32
+ end
33
+
34
+ def config
35
+ {
36
+ :api_endpoint => self.api_endpoint,
37
+ :debug_connection => self.debug_connection,
38
+ :api_version => self.api_version,
39
+ :rest_timeout => self.rest_timeout,
40
+ :rest_open_timeout => self.rest_open_timeout,
41
+ :proxy => self.proxy,
42
+ :api_key => self.api_key,
43
+ :api_secret => self.api_secret
44
+ }
45
+ end
46
+ end
47
+ end
@@ -1,5 +1,5 @@
1
1
  module Bitfinex
2
- module V2::MarginClient
2
+ module RESTv2Margin
3
3
 
4
4
  # Get active offers
5
5
  #
@@ -20,7 +20,6 @@ module Bitfinex
20
20
  authenticated_post("auth/r/margin/#{symbol}").body
21
21
  end
22
22
 
23
-
24
23
  # Get account funding info
25
24
  #
26
25
  # @param symbol [string] default fUSD
@@ -1,5 +1,5 @@
1
1
  module Bitfinex
2
- module V2::PersonalClient
2
+ module RESTv2Personal
3
3
 
4
4
  # Get account wallets
5
5
  #
@@ -14,7 +14,7 @@ module Bitfinex
14
14
  # @example:
15
15
  # client.performance
16
16
  def performance
17
- authenticated_post("auth/r/stats/perf:1D/hist")
17
+ authenticated_post("auth/r/stats/perf::1D/hist")
18
18
  end
19
19
 
20
20
  # Get the list of alerts
@@ -53,7 +53,6 @@ module Bitfinex
53
53
  authenticated_post("auth/w/alert/price:#{symbol}:#{price}/del").body
54
54
  end
55
55
 
56
-
57
56
  # Calculate available balance for order/offer
58
57
  #
59
58
  # @param rate [int] Rate of the order/offer
@@ -75,20 +74,5 @@ module Bitfinex
75
74
  }
76
75
  authenticated_post("auth/calc/order/avail", params: params).body
77
76
  end
78
-
79
- # Listen to authenticated channel
80
- #
81
- # Documentation:
82
- # https://docs.bitfinex.com/v2/reference#account-info
83
- #
84
- # example:
85
- # client.listen_account do |account|
86
- # puts account
87
- # end
88
- def listen_account(&block)
89
- raise BlockMissingError unless block_given?
90
- ws_auth(&block)
91
- end
92
-
93
77
  end
94
78
  end
@@ -1,7 +1,5 @@
1
- # coding: utf-8
2
-
3
1
  module Bitfinex
4
- module V2::StatsClient
2
+ module RESTv2Stats
5
3
 
6
4
  # Various statistics about the requested pair.
7
5
  #
@@ -22,6 +20,5 @@ module Bitfinex
22
20
  check_params(params, %i{sort})
23
21
  get("stats1/#{key}:#{size}:#{symbol}:#{side}/#{section}").body
24
22
  end
25
-
26
23
  end
27
24
  end
@@ -0,0 +1,20 @@
1
+ module Bitfinex
2
+ module RESTv2Ticker
3
+
4
+ # Gives innermost bid and asks and information on
5
+ # the most recent trade, as well as high, low and
6
+ # volume of the last 24 hours.
7
+ #
8
+ # @param symbols a list of symbols
9
+ # @return [Hash]
10
+ # @example:
11
+ # client.ticker("tBTCUSD","tLTCUSD","fUSD")
12
+ def ticker(*symbols)
13
+ if symbols.size == 1
14
+ get("ticker/#{symbols.first}").body
15
+ else
16
+ get("tickers", symbols: "#{symbols.flatten.join(",")}").body
17
+ end
18
+ end
19
+ end
20
+ end
@@ -1,5 +1,5 @@
1
1
  module Bitfinex
2
- module V2::TradingClient
2
+ module RESTv2Trading
3
3
 
4
4
  # Provides a way to access charting candle info
5
5
  #
@@ -17,7 +17,7 @@ module Bitfinex
17
17
  #
18
18
  # @example:
19
19
  # client.candles('tBTCUSD')
20
- def candles(symbol = 'tBTCUSD', timeframe = '1m', section = "last", params = {})
20
+ def candles(symbol = 'tBTCUSD', timeframe = '1m', section = "hist", params = {})
21
21
  check_params(params, %i{limit start end sort})
22
22
  get("candles/trade:#{timeframe}:#{symbol}/#{section}", params).body
23
23
  end
@@ -87,7 +87,6 @@ module Bitfinex
87
87
  authenticated_post("auth/r/order/#{symbol}:#{order_id}/trades").body
88
88
  end
89
89
 
90
-
91
90
  # Get active positions
92
91
  #
93
92
  # return [Array]
@@ -95,52 +94,7 @@ module Bitfinex
95
94
  # @example:
96
95
  # client.active_positions
97
96
  def active_positions
98
- authenticated_post("auth/positions").body
97
+ authenticated_post("auth/r/positions").body
99
98
  end
100
-
101
-
102
- # This channel sends a trade message whenever a trade occurs at Bitfinex.
103
- # It includes all the pertinent details of the trade, such as price, size and time.
104
- #
105
- # @param symbol [string]
106
- # @param block [Block] The code to be executed when a new ticker is sent by the server
107
- #
108
- # Documentation:
109
- # https://docs.bitfinex.com/v2/reference#ws-public-trades
110
- #
111
- # @example:
112
- # client.listen_trades("tBTCUSD") do |trade|
113
- # puts "traded #{trade[2][2]} BTC for #{trade[2][3]} USD"
114
- # end
115
- def listen_trades(symbol="tBTCUSD", &block)
116
- raise BlockMissingError unless block_given?
117
- register_channel symbol: symbol, channel: "trades", &block
118
- end
119
-
120
- # The Order Books channel allow you to keep track of the state of the Bitfinex order book.
121
- # It is provided on a price aggregated basis, with customizable precision.
122
- # After receiving the response, you will receive a snapshot of the book,
123
- # followed by updates upon any changes to the book.
124
- #
125
- # @param symbol [string]
126
- # @param precision [string] Level of price aggregation (P0, P1, P2, P3, R0).
127
- # (default P0) R0 is raw books - These are the most granular books.
128
- # @param frequency [string] Frequency of updates (F0, F1, F2, F3).
129
- # F0=realtime / F1=2sec / F2=5sec / F3=10sec (default F0)
130
- # @param length [int] Number of price points ("25", "100") [default="25"]
131
- #
132
- # Documentation:
133
- # https://docs.bitfinex.com/v2/reference#ws-public-order-books
134
- # https://docs.bitfinex.com/v2/reference#ws-public-raw-order-books
135
- #
136
- # @example:
137
- # client.listen_book("tBTCUSD") do |trade|
138
- # puts "traded #{trade[2][2]} BTC for #{trade[2][3]} USD"
139
- # end
140
- def listen_book(symbol="tBTCUSD", frequency="F0", length=25, precision="P0", &block)
141
- raise BlockMissingError unless block_given?
142
- register_channel symbol: symbol, channel: "book", prec: precision, freq: frequency, len: length, &block
143
- end
144
-
145
99
  end
146
100
  end
@@ -1,7 +1,5 @@
1
- # coding: utf-8
2
-
3
1
  module Bitfinex
4
- module V2::UtilsClient
2
+ module RESTv2Utils
5
3
 
6
4
  # Calculate the average execution rate for Trading or Margin funding.
7
5
  #
@@ -0,0 +1,529 @@
1
+ require 'faye/websocket'
2
+ require 'eventmachine'
3
+ require 'logger'
4
+ require 'emittr'
5
+ require_relative '../models/alert'
6
+ require_relative '../models/balance_info'
7
+ require_relative '../models/candle'
8
+ require_relative '../models/currency'
9
+ require_relative '../models/funding_credit'
10
+ require_relative '../models/funding_info'
11
+ require_relative '../models/funding_loan'
12
+ require_relative '../models/funding_offer'
13
+ require_relative '../models/funding_ticker'
14
+ require_relative '../models/funding_trade'
15
+ require_relative '../models/ledger_entry'
16
+ require_relative '../models/margin_info'
17
+ require_relative '../models/movement'
18
+ require_relative '../models/notification'
19
+ require_relative '../models/order_book'
20
+ require_relative '../models/order'
21
+ require_relative '../models/position'
22
+ require_relative '../models/public_trade'
23
+ require_relative '../models/trade'
24
+ require_relative '../models/trading_ticker'
25
+ require_relative '../models/user_info'
26
+ require_relative '../models/wallet'
27
+
28
+ module Bitfinex
29
+ class WSv2
30
+ include Emittr::Events
31
+
32
+ INFO_SERVER_RESTART = 20051
33
+ INFO_MAINTENANCE_START = 20060
34
+ INFO_MAINTENANCE_END = 20061
35
+
36
+ FLAG_DEC_S = 8, # enables all decimals as strings
37
+ FLAG_TIME_S = 32, # enables all timestamps as strings
38
+ FLAG_TIMESTAMP = 32768, # timestamps in milliseconds
39
+ FLAG_SEQ_ALL = 65536, # enable sequencing
40
+ FLAG_CHECKSUM = 131072 # enable OB checksums, top 25 levels per side
41
+
42
+ def initialize (params = {})
43
+ @l = Logger.new(STDOUT)
44
+ @l.progname = 'ws2'
45
+
46
+ @url = params[:url] || 'wss://api.bitfinex.com/ws/2'
47
+ @api_key = params[:api_key]
48
+ @api_secret = params[:api_secret]
49
+ @manage_obs = params[:manage_order_books]
50
+ @transform = !!params[:transform]
51
+ @seq_audit = !!params[:seq_audit]
52
+ @checksum_audit = !!params[:checksum_audit]
53
+
54
+ @enabled_flags = 0
55
+ @is_open = false
56
+ @is_authenticated = false
57
+ @channel_map = {}
58
+ @order_books = {}
59
+ @last_pub_seq = nil
60
+ @last_auth_seq = nil
61
+ end
62
+
63
+ def on_open (e)
64
+ @l.info 'client open'
65
+ @is_open = true
66
+ emit(:open)
67
+
68
+ enable_sequencing if @seq_audit
69
+ enable_ob_checksums if @checksum_audit
70
+ end
71
+
72
+ def on_message (e)
73
+ @l.info "recv #{e.data}"
74
+
75
+ msg = JSON.parse(e.data)
76
+ process_message(msg)
77
+
78
+ emit(:message, msg)
79
+ end
80
+
81
+ def on_close (e)
82
+ @l.info 'client closed'
83
+ @is_open = false
84
+ emit(:close)
85
+ end
86
+
87
+ def open!
88
+ if @is_open
89
+ raise Exception, 'already open'
90
+ end
91
+
92
+ EM.run {
93
+ @ws = Faye::WebSocket::Client.new(@url)
94
+
95
+ @ws.on(:open) do |e|
96
+ on_open(e)
97
+ end
98
+
99
+ @ws.on(:message) do |e|
100
+ on_message(e)
101
+ end
102
+
103
+ @ws.on(:close) do |e|
104
+ on_close(e)
105
+ end
106
+ }
107
+ end
108
+
109
+ def close!
110
+ @ws.close
111
+ end
112
+
113
+ def process_message (msg)
114
+ if @seq_audit
115
+ validate_message_seq(msg)
116
+ end
117
+
118
+ if msg.kind_of?(Array)
119
+ process_channel_message(msg)
120
+ elsif msg.kind_of?(Hash)
121
+ process_event_message(msg)
122
+ end
123
+ end
124
+
125
+ def validate_message_seq (msg)
126
+ return unless @seq_audit
127
+ return unless msg.kind_of?(Array)
128
+ return unless msg.size > 2
129
+
130
+ # The auth sequence # is the last value in channel 0 non-hb packets
131
+ if msg[0] == 0 && msg[1] != 'hb'
132
+ auth_seq = msg[-1]
133
+ else
134
+ auth_seq = nil
135
+ end
136
+
137
+ # all other packets provide a public sequence # as the last value. For
138
+ # chan 0 packets, these are included as the 2nd to last value
139
+ #
140
+ # note that error notifications lack seq
141
+ if msg[0] == 0 && msg[1] != 'hb' && !(msg[1] && msg[2][6] == 'ERROR')
142
+ pub_seq = msg[-2]
143
+ else
144
+ pub_seq = msg[-1]
145
+ end
146
+
147
+ return unless pub_seq.is_a?(Numeric)
148
+
149
+ if @last_pub_seq.nil?
150
+ @last_pub_seq = pub_seq
151
+ return
152
+ end
153
+
154
+ if pub_seq != (@last_pub_seq + 1) # check pub seq
155
+ @l.warn "invalid pub seq #; last #{@last_pub_seq}, got #{pub_seq}"
156
+ end
157
+
158
+ @last_pub_seq = pub_seq
159
+
160
+ return unless auth_seq.is_a?(Numeric)
161
+ return if auth_seq == 0
162
+ return if msg[1] == 'n' && msg[2][6] == 'ERROR' # error notifications
163
+ return if auth_seq == @last_auth_seq # seq didn't advance
164
+
165
+ if !@last_auth_seq.nil? && auth_seq != @last_auth_seq + 1
166
+ @l.warn "invalid auth seq #; last #{@last_auth_seq}, got #{auth_seq}"
167
+ end
168
+
169
+ @last_auth_seq = auth_seq
170
+ end
171
+
172
+ def process_channel_message (msg)
173
+ if !@channel_map.include?(msg[0])
174
+ @l.error "recv message on unknown channel: #{msg[0]}"
175
+ return
176
+ end
177
+
178
+ chan = @channel_map[msg[0]]
179
+ type = msg[1]
180
+
181
+ if msg.size < 2 || type == 'hb'
182
+ return
183
+ end
184
+
185
+ case chan['channel']
186
+ when 'ticker'
187
+ handle_ticker_message(msg, chan)
188
+ when 'trades'
189
+ handle_trades_message(msg, chan)
190
+ when 'candles'
191
+ handle_candles_message(msg, chan)
192
+ when 'book'
193
+ if type == 'cs'
194
+ handle_order_book_checksum_message(msg, chan)
195
+ else
196
+ handle_order_book_message(msg, chan)
197
+ end
198
+ when 'auth'
199
+ handle_auth_message(msg, chan)
200
+ end
201
+ end
202
+
203
+ def handle_ticker_message (msg, chan)
204
+ payload = msg[1]
205
+
206
+ if chan['symbol'][0] === 't'
207
+ emit(:ticker, chan['symbol'], @transform ? Models::TradingTicker.new(payload) : payload)
208
+ else
209
+ emit(:ticker, chan['symbol'], @transform ? Models::FundingTicker.new(payload) : payload)
210
+ end
211
+ end
212
+
213
+ def handle_trades_message (msg, chan)
214
+ if msg[1].kind_of?(Array)
215
+ payload = msg[1]
216
+ emit(:public_trades, chan['symbol'], @transform ? payload.map { |t| Models::PublicTrade.new(t) } : payload)
217
+ else
218
+ payload = @transform ? Models::PublicTrade.new(msg[2]) : msg[2]
219
+ type = msg[1]
220
+
221
+ emit(:public_trades, chan['symbol'], payload)
222
+
223
+ if type == 'te'
224
+ emit(:public_trade_entry, chan['symbol'], payload)
225
+ elsif type == 'tu'
226
+ emit(:public_trade_update, chan['symbol'], payload)
227
+ end
228
+ end
229
+ end
230
+
231
+ def handle_candles_message (msg, chan)
232
+ payload = msg[1]
233
+
234
+ if payload[0].kind_of?(Array)
235
+ emit(:candles, chan['key'], @transform ? payload.map { |c| Models::Candle.new(c) } : payload)
236
+ else
237
+ emit(:candles, chan['key'], @transform ? Models::Candle.new(payload) : payload)
238
+ end
239
+ end
240
+
241
+ def handle_order_book_checksum_message (msg, chan)
242
+ key = "#{chan['symbol']}:#{chan['prec']}:#{chan['len']}"
243
+ emit(:checksum, chan['symbol'], msg)
244
+
245
+ return unless @manage_obs
246
+ return unless @order_books.has_key?(key)
247
+
248
+ remote_cs = msg[2]
249
+ local_cs = @order_books[key].checksum
250
+
251
+ if local_cs != remote_cs
252
+ err = "OB checksum mismatch, have #{local_cs} want #{remote_cs} [#{chan['symbol']}"
253
+ @l.error err
254
+ emit(:error, err)
255
+ else
256
+ @l.info "OB checksum OK #{local_cs} [#{chan['symbol']}]"
257
+ end
258
+ end
259
+
260
+ def handle_order_book_message (msg, chan)
261
+ ob = msg[1]
262
+
263
+ if @manage_obs
264
+ key = "#{chan['symbol']}:#{chan['prec']}:#{chan['len']}"
265
+
266
+ if !@order_books.has_key?(key)
267
+ @order_books[key] = Models::OrderBook.new(ob, chan['prec'][0] == 'R')
268
+ else
269
+ @order_books[key].update_with(ob)
270
+ end
271
+
272
+ data = @order_books[key]
273
+ elsif @transform
274
+ data = Models::OrderBook.new(ob)
275
+ else
276
+ data = ob
277
+ end
278
+
279
+ emit(:order_book, chan['symbol'], data)
280
+ end
281
+
282
+ def handle_auth_message (msg, chan)
283
+ type = msg[1]
284
+ return if type == 'hb'
285
+ payload = msg[2]
286
+
287
+ case type
288
+ when 'n'
289
+ emit(:notification, @transform ? Models::Notification.new(payload) : payload)
290
+ when 'te'
291
+ emit(:trade_entry, @transform ? Models::Trade.new(payload) : payload)
292
+ when 'tu'
293
+ emit(:trade_update, @transform ? Models::Trade.new(payload) : payload)
294
+ when 'os'
295
+ emit(:order_snapshot, @transform ? payload.map { |o| Models::Order.new(o) } : payload)
296
+ when 'ou'
297
+ emit(:order_update, @transform ? Models::Order.new(payload) : payload)
298
+ when 'on'
299
+ emit(:order_new, @transform ? Models::Order.new(payload) : payload)
300
+ when 'oc'
301
+ emit(:order_close, @transform ? Models::Order.new(payload) : payload)
302
+ when 'ps'
303
+ emit(:position_snapshot, @transform ? payload.map { |p| Models::Position.new(p) } : payload)
304
+ when 'pn'
305
+ emit(:position_new, @transform ? Models::Position.new(payload) : payload)
306
+ when 'pu'
307
+ emit(:position_update, @transform ? Models::Position.new(payload) : payload)
308
+ when 'pc'
309
+ emit(:position_close, @transform ? Models::Position.new(payload) : payload)
310
+ when 'fos'
311
+ emit(:funding_offer_snapshot, @transform ? payload.map { |fo| Models::FundingOffer.new(fo) } : payload)
312
+ when 'fon'
313
+ emit(:funding_offer_new, @transform ? Models::FundingOffer.new(payload) : payload)
314
+ when 'fou'
315
+ emit(:funding_offer_update, @transform ? Models::FundingOffer.new(payload) : payload)
316
+ when 'foc'
317
+ emit(:funding_offer_close, @transform ? Models::FundingOffer.new(payload) : payload)
318
+ when 'fcs'
319
+ emit(:funding_credit_snapshot, @transform ? payload.map { |fc| Models::FundingCredit.new(fc) } : payload)
320
+ when 'fcn'
321
+ emit(:funding_credit_new, @transform ? Models::FundingCredit.new(payload) : payload)
322
+ when 'fcu'
323
+ emit(:funding_credit_update, @transform ? Models::FundingCredit.new(payload) : payload)
324
+ when 'fcc'
325
+ emit(:funding_credit_close, @transform ? Models::FundingCredit.new(payload) : payload)
326
+ when 'fls'
327
+ emit(:funding_loan_snapshot, @transform ? payload.map { |fl| Models::FundingLoan.new(fl) } : payload)
328
+ when 'fln'
329
+ emit(:funding_loan_new, @transform ? Models::FundingLoan.new(payload) : payload)
330
+ when 'flu'
331
+ emit(:funding_loan_update, @transform ? Models::FundingLoan.new(payload) : payload)
332
+ when 'flc'
333
+ emit(:funding_loan_close, @transform ? Models::FundingLoan.new(payload) : payload)
334
+ when 'ws'
335
+ emit(:wallet_snapshot, @transform ? payload.map { |w| Models::Wallet.new(payload) } : payload)
336
+ when 'wu'
337
+ emit(:wallet_update, @transform ? Models::Wallet.new(payload) : payload)
338
+ when 'bu'
339
+ emit(:balance_update, @transform ? Models::BalanceInfo.new(payload) : payload)
340
+ when 'miu'
341
+ emit(:margin_info_update, @transform ? Models::MarginInfo.new(payload) : payload)
342
+ when 'fiu'
343
+ emit(:funding_info_update, @transform ? Models::FundingInfo.new(payload) : payload)
344
+ when 'fte'
345
+ emit(:funding_trade_entry, @transform ? Models::FundingTrade.new(payload) : payload)
346
+ when 'ftu'
347
+ emit(:funding_trade_update, @transform ? Models::FundingTrade.new(payload) : payload)
348
+ end
349
+ end
350
+
351
+ def subscribe (channel, params = {})
352
+ @l.info 'subscribing to channel %s [%s]' % [channel, params]
353
+ @ws.send(JSON.generate(params.merge({
354
+ :event => 'subscribe',
355
+ :channel => channel,
356
+ })))
357
+ end
358
+
359
+ def subscribe_ticker (sym)
360
+ subscribe('ticker', { :symbol => sym })
361
+ end
362
+
363
+ def subscribe_trades (sym)
364
+ subscribe('trades', { :symbol => sym })
365
+ end
366
+
367
+ def subscribe_candles (key)
368
+ subscribe('candles', { :key => key })
369
+ end
370
+
371
+ def subscribe_order_book (sym, prec, len)
372
+ subscribe('book', {
373
+ :symbol => sym,
374
+ :prec => prec,
375
+ :len => len
376
+ })
377
+ end
378
+
379
+ def process_event_message (msg)
380
+ case msg['event']
381
+ when 'auth'
382
+ handle_auth_event(msg)
383
+ when 'subscribed'
384
+ handle_subscribed_event(msg)
385
+ when 'unsubscribed'
386
+ handle_unsubscribed_event(msg)
387
+ when 'info'
388
+ handle_info_event(msg)
389
+ when 'conf'
390
+ handle_config_event(msg)
391
+ when 'error'
392
+ handle_error_event(msg)
393
+ end
394
+ end
395
+
396
+ def handle_auth_event (msg)
397
+ if msg['status'] != 'OK'
398
+ @l.error "auth failed: #{msg['message']}"
399
+ return
400
+ end
401
+
402
+ @channel_map[msg['chanId']] = { 'channel' => 'auth' }
403
+ @is_authenticated = true
404
+ emit(:auth, msg)
405
+
406
+ @l.info 'authenticated'
407
+ end
408
+
409
+ def handle_info_event (msg)
410
+ if msg.include?('version')
411
+ if msg['version'] != 2
412
+ close!
413
+ raise Exception, "server not running API v2: #{msg['version']}"
414
+ end
415
+
416
+ platform = msg['platform']
417
+
418
+ @l.info "server running API v2 (platform: %s (%d))" % [
419
+ platform['status'] == 0 ? 'under maintenance' : 'operating normally',
420
+ platform['status']
421
+ ]
422
+ elsif msg.include?('code')
423
+ code = msg['code']
424
+
425
+ if code == INFO_SERVER_RESTART
426
+ @l.info 'server restarted, please reconnect'
427
+ emit(:server_restart)
428
+ elsif code == INFO_MAINTENANCE_START
429
+ @l.info 'server maintenance period started!'
430
+ emit(:maintenance_start)
431
+ elsif code == INFO_MAINTENANCE_END
432
+ @l.info 'server maintenance period ended!'
433
+ emit(:maintenance_end)
434
+ end
435
+ end
436
+ end
437
+
438
+ def handle_error_event (msg)
439
+ @l.error msg
440
+ end
441
+
442
+ def handle_config_event (msg)
443
+ if msg['status'] != 'OK'
444
+ @l.error "config failed: #{msg['message']}"
445
+ else
446
+ @l.info "flags updated to #{msg['flags']}"
447
+ @enabled_flags = msg['flags']
448
+ end
449
+ end
450
+
451
+ def handle_subscribed_event (msg)
452
+ @l.info "subscribed to #{msg['channel']} [#{msg['chanId']}]"
453
+ @channel_map[msg['chanId']] = msg
454
+ emit(:subscribed, msg['chanId'])
455
+ end
456
+
457
+ def handle_unsubscribed_event (msg)
458
+ @l.info "unsubscribed from #{msg['chanId']}"
459
+ @channel_map.delete(msg['chanId'])
460
+ emit(:unsubscribed, msg['chanId'])
461
+ end
462
+
463
+ def enable_flag (flag)
464
+ return unless @is_open
465
+
466
+ @ws.send(JSON.generate({
467
+ :event => 'conf',
468
+ :flags => @enabled_flags | flag
469
+ }))
470
+ end
471
+
472
+ def is_flag_enabled (flag)
473
+ (@enabled_flags & flag) == flag
474
+ end
475
+
476
+ def enable_sequencing (audit = true)
477
+ @seq_audit = audit
478
+ enable_flag(FLAG_SEQ_ALL)
479
+ end
480
+
481
+ def enable_ob_checksums
482
+ enable_flag(FLAG_CHECKSUM)
483
+ end
484
+
485
+ def auth! (calc = 0, dms = 0)
486
+ if @is_authenticated
487
+ raise Exception, 'already authenticated'
488
+ end
489
+
490
+ auth_nonce = new_nonce
491
+ auth_payload = "AUTH#{auth_nonce}#{auth_nonce}"
492
+ sig = sign(auth_payload)
493
+
494
+ @ws.send(JSON.generate({
495
+ :event => 'auth',
496
+ :apiKey => @api_key,
497
+ :authSig => sig,
498
+ :authPayload => auth_payload,
499
+ :authNonce => auth_nonce,
500
+ :dms => dms,
501
+ :calc => calc
502
+ }))
503
+ end
504
+
505
+ def new_nonce
506
+ Time.now.to_i.to_s
507
+ end
508
+
509
+ def sign (payload)
510
+ OpenSSL::HMAC.hexdigest('sha384', @api_secret, payload)
511
+ end
512
+
513
+ def submit_order (order)
514
+ return if !@is_authenticated
515
+
516
+ if order.kind_of?(Array)
517
+ packet = order
518
+ elsif order.instance_of?(Models::Order)
519
+ packet = order.to_new_order_packet
520
+ elsif order.kind_of?(Hash)
521
+ packet = Models::Order.new(order).to_new_order_packet
522
+ else
523
+ raise Exception, 'tried to submit order of unkown type'
524
+ end
525
+
526
+ @ws.send(JSON.generate([0, 'on', nil, packet]))
527
+ end
528
+ end
529
+ end