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.
- checksums.yaml +4 -4
- data/lib/bitfinex.rb +28 -30
- data/lib/{bitfinex/errors.rb → errors.rb} +2 -2
- data/lib/models/alert.rb +27 -0
- data/lib/models/balance_info.rb +25 -0
- data/lib/models/candle.rb +29 -0
- data/lib/models/currency.rb +27 -0
- data/lib/models/funding_credit.rb +41 -0
- data/lib/models/funding_info.rb +40 -0
- data/lib/models/funding_loan.rb +40 -0
- data/lib/models/funding_offer.rb +38 -0
- data/lib/models/funding_ticker.rb +50 -0
- data/lib/models/funding_trade.rb +31 -0
- data/lib/models/ledger_entry.rb +33 -0
- data/lib/models/margin_info.rb +67 -0
- data/lib/models/model.rb +56 -0
- data/lib/models/movement.rb +33 -0
- data/lib/models/notification.rb +30 -0
- data/lib/models/order.rb +197 -0
- data/lib/models/order_book.rb +265 -0
- data/lib/models/position.rb +33 -0
- data/lib/models/public_trade.rb +27 -0
- data/lib/models/trade.rb +34 -0
- data/lib/models/trading_ticker.rb +33 -0
- data/lib/models/user_info.rb +27 -0
- data/lib/models/wallet.rb +28 -0
- data/lib/rest/rest_client.rb +103 -0
- data/lib/rest/v1.rb +63 -0
- data/lib/rest/v1/account_info.rb +23 -0
- data/lib/{bitfinex → rest}/v1/deposit.rb +2 -3
- data/lib/{bitfinex → rest}/v1/funding_book.rb +2 -4
- data/lib/{bitfinex → rest}/v1/historical_data.rb +2 -2
- data/lib/{bitfinex → rest}/v1/lends.rb +1 -2
- data/lib/{bitfinex → rest}/v1/margin_funding.rb +1 -2
- data/lib/rest/v1/order_book.rb +17 -0
- data/lib/{bitfinex → rest}/v1/orders.rb +6 -8
- data/lib/{bitfinex → rest}/v1/positions.rb +6 -2
- data/lib/{bitfinex → rest}/v1/stats.rb +1 -3
- data/lib/{bitfinex → rest}/v1/symbols.rb +1 -2
- data/lib/rest/v1/ticker.rb +13 -0
- data/lib/{bitfinex → rest}/v1/trades.rb +1 -16
- data/lib/{bitfinex → rest}/v1/wallet.rb +1 -2
- data/lib/rest/v2.rb +47 -0
- data/lib/{bitfinex → rest}/v2/margin.rb +1 -2
- data/lib/{bitfinex → rest}/v2/personal.rb +2 -18
- data/lib/{bitfinex → rest}/v2/stats.rb +1 -4
- data/lib/rest/v2/ticker.rb +20 -0
- data/lib/{bitfinex → rest}/v2/trading.rb +3 -49
- data/lib/{bitfinex → rest}/v2/utils.rb +1 -3
- data/lib/ws/ws2.rb +529 -0
- metadata +74 -56
- data/lib/bitfinex-api-rb.rb +0 -1
- data/lib/bitfinex-rb.rb +0 -1
- data/lib/bitfinex/api_versions.rb +0 -7
- data/lib/bitfinex/authenticated_rest.rb +0 -57
- data/lib/bitfinex/client.rb +0 -38
- data/lib/bitfinex/configurable.rb +0 -48
- data/lib/bitfinex/connection.rb +0 -52
- data/lib/bitfinex/v1/account_info.rb +0 -38
- data/lib/bitfinex/v1/orderbook.rb +0 -36
- data/lib/bitfinex/v1/ticker.rb +0 -27
- data/lib/bitfinex/v2/ticker.rb +0 -58
- data/lib/bitfinex/version.rb +0 -3
- data/lib/bitfinex/websocket_connection.rb +0 -231
@@ -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
|
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
|
-
|
61
|
+
authenticated_post("order/cancel/multi", params: {order_ids: ids.map(&:to_i)}).body
|
63
62
|
when Numeric, String
|
64
|
-
|
63
|
+
authenticated_post("order/cancel", params: {order_id: ids.to_i}).body
|
65
64
|
when NilClass
|
66
|
-
|
65
|
+
authenticated_post("order/cancel/all").body
|
67
66
|
else
|
68
|
-
|
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
|
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
|
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
|
@@ -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
|
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
|
data/lib/rest/v2.rb
ADDED
@@ -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
|
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
|
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
|
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
|
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
|
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 = "
|
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
|
data/lib/ws/ws2.rb
ADDED
@@ -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
|