ccxt 4.1.47__py2.py3-none-any.whl → 4.1.49__py2.py3-none-any.whl
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.
Potentially problematic release.
This version of ccxt might be problematic. Click here for more details.
- ccxt/__init__.py +3 -1
- ccxt/abstract/binance.py +2 -0
- ccxt/abstract/binancecoinm.py +2 -0
- ccxt/abstract/binanceus.py +2 -0
- ccxt/abstract/binanceusdm.py +2 -0
- ccxt/abstract/htx.py +541 -0
- ccxt/ace.py +2 -2
- ccxt/ascendex.py +2 -2
- ccxt/async_support/__init__.py +3 -1
- ccxt/async_support/ace.py +2 -2
- ccxt/async_support/ascendex.py +2 -2
- ccxt/async_support/base/exchange.py +1 -1
- ccxt/async_support/bigone.py +2 -2
- ccxt/async_support/binance.py +5 -3
- ccxt/async_support/bingx.py +3 -3
- ccxt/async_support/bitbns.py +2 -2
- ccxt/async_support/bitfinex.py +2 -2
- ccxt/async_support/bitfinex2.py +2 -2
- ccxt/async_support/bitget.py +3 -3
- ccxt/async_support/bithumb.py +2 -2
- ccxt/async_support/bitmart.py +157 -53
- ccxt/async_support/bitmex.py +2 -2
- ccxt/async_support/bitopro.py +2 -2
- ccxt/async_support/bitpanda.py +2 -2
- ccxt/async_support/bitrue.py +2 -2
- ccxt/async_support/bitstamp.py +2 -2
- ccxt/async_support/bittrex.py +2 -2
- ccxt/async_support/bitvavo.py +2 -2
- ccxt/async_support/blockchaincom.py +2 -2
- ccxt/async_support/btcalpha.py +2 -2
- ccxt/async_support/btcturk.py +2 -2
- ccxt/async_support/bybit.py +2 -2
- ccxt/async_support/cex.py +2 -2
- ccxt/async_support/coinbase.py +2 -2
- ccxt/async_support/coinbasepro.py +2 -2
- ccxt/async_support/coinex.py +63 -20
- ccxt/async_support/coinfalcon.py +2 -2
- ccxt/async_support/coinlist.py +2 -2
- ccxt/async_support/coinone.py +2 -2
- ccxt/async_support/coinsph.py +2 -2
- ccxt/async_support/coinspot.py +2 -2
- ccxt/async_support/cryptocom.py +2 -2
- ccxt/async_support/currencycom.py +2 -2
- ccxt/async_support/delta.py +2 -2
- ccxt/async_support/deribit.py +2 -2
- ccxt/async_support/digifinex.py +2 -2
- ccxt/async_support/exmo.py +2 -2
- ccxt/async_support/gate.py +2 -2
- ccxt/async_support/gemini.py +3 -3
- ccxt/async_support/hitbtc.py +2 -2
- ccxt/async_support/hollaex.py +2 -2
- ccxt/async_support/htx.py +7853 -0
- ccxt/async_support/huobi.py +3 -7846
- ccxt/async_support/huobijp.py +2 -2
- ccxt/async_support/idex.py +2 -2
- ccxt/async_support/indodax.py +2 -2
- ccxt/async_support/kraken.py +2 -6
- ccxt/async_support/krakenfutures.py +2 -2
- ccxt/async_support/kucoin.py +2 -2
- ccxt/async_support/kuna.py +2 -2
- ccxt/async_support/latoken.py +2 -2
- ccxt/async_support/lbank.py +2 -2
- ccxt/async_support/lbank2.py +2 -2
- ccxt/async_support/luno.py +2 -2
- ccxt/async_support/lykke.py +2 -2
- ccxt/async_support/mexc.py +23 -23
- ccxt/async_support/novadax.py +2 -2
- ccxt/async_support/oceanex.py +2 -2
- ccxt/async_support/okcoin.py +3 -3
- ccxt/async_support/okx.py +3 -3
- ccxt/async_support/phemex.py +2 -2
- ccxt/async_support/poloniex.py +2 -2
- ccxt/async_support/poloniexfutures.py +2 -2
- ccxt/async_support/probit.py +2 -2
- ccxt/async_support/tidex.py +2 -2
- ccxt/async_support/timex.py +4 -4
- ccxt/async_support/tokocrypto.py +2 -2
- ccxt/async_support/upbit.py +2 -2
- ccxt/async_support/wavesexchange.py +2 -2
- ccxt/async_support/wazirx.py +2 -2
- ccxt/async_support/whitebit.py +2 -2
- ccxt/async_support/yobit.py +2 -2
- ccxt/async_support/zonda.py +2 -2
- ccxt/base/exchange.py +1 -1
- ccxt/base/types.py +43 -4
- ccxt/bigone.py +2 -2
- ccxt/binance.py +5 -3
- ccxt/bingx.py +3 -3
- ccxt/bitbns.py +2 -2
- ccxt/bitfinex.py +2 -2
- ccxt/bitfinex2.py +2 -2
- ccxt/bitget.py +3 -3
- ccxt/bithumb.py +2 -2
- ccxt/bitmart.py +157 -53
- ccxt/bitmex.py +2 -2
- ccxt/bitopro.py +2 -2
- ccxt/bitpanda.py +2 -2
- ccxt/bitrue.py +2 -2
- ccxt/bitstamp.py +2 -2
- ccxt/bittrex.py +2 -2
- ccxt/bitvavo.py +2 -2
- ccxt/blockchaincom.py +2 -2
- ccxt/btcalpha.py +2 -2
- ccxt/btcturk.py +2 -2
- ccxt/bybit.py +2 -2
- ccxt/cex.py +2 -2
- ccxt/coinbase.py +2 -2
- ccxt/coinbasepro.py +2 -2
- ccxt/coinex.py +63 -20
- ccxt/coinfalcon.py +2 -2
- ccxt/coinlist.py +2 -2
- ccxt/coinone.py +2 -2
- ccxt/coinsph.py +2 -2
- ccxt/coinspot.py +2 -2
- ccxt/cryptocom.py +2 -2
- ccxt/currencycom.py +2 -2
- ccxt/delta.py +2 -2
- ccxt/deribit.py +2 -2
- ccxt/digifinex.py +2 -2
- ccxt/exmo.py +2 -2
- ccxt/gate.py +2 -2
- ccxt/gemini.py +3 -3
- ccxt/hitbtc.py +2 -2
- ccxt/hollaex.py +2 -2
- ccxt/htx.py +7852 -0
- ccxt/huobi.py +3 -7845
- ccxt/huobijp.py +2 -2
- ccxt/idex.py +2 -2
- ccxt/indodax.py +2 -2
- ccxt/kraken.py +2 -6
- ccxt/krakenfutures.py +2 -2
- ccxt/kucoin.py +2 -2
- ccxt/kuna.py +2 -2
- ccxt/latoken.py +2 -2
- ccxt/lbank.py +2 -2
- ccxt/lbank2.py +2 -2
- ccxt/luno.py +2 -2
- ccxt/lykke.py +2 -2
- ccxt/mexc.py +23 -23
- ccxt/novadax.py +2 -2
- ccxt/oceanex.py +2 -2
- ccxt/okcoin.py +3 -3
- ccxt/okx.py +3 -3
- ccxt/phemex.py +2 -2
- ccxt/poloniex.py +2 -2
- ccxt/poloniexfutures.py +2 -2
- ccxt/pro/__init__.py +3 -1
- ccxt/pro/htx.py +2177 -0
- ccxt/pro/huobi.py +4 -2166
- ccxt/probit.py +2 -2
- ccxt/test/test_async.py +15 -1
- ccxt/test/test_sync.py +15 -1
- ccxt/tidex.py +2 -2
- ccxt/timex.py +4 -4
- ccxt/tokocrypto.py +2 -2
- ccxt/upbit.py +2 -2
- ccxt/wavesexchange.py +2 -2
- ccxt/wazirx.py +2 -2
- ccxt/whitebit.py +2 -2
- ccxt/yobit.py +2 -2
- ccxt/zonda.py +2 -2
- ccxt-4.1.49.dist-info/METADATA +624 -0
- {ccxt-4.1.47.dist-info → ccxt-4.1.49.dist-info}/RECORD +165 -161
- ccxt-4.1.47.dist-info/METADATA +0 -624
- {ccxt-4.1.47.dist-info → ccxt-4.1.49.dist-info}/WHEEL +0 -0
- {ccxt-4.1.47.dist-info → ccxt-4.1.49.dist-info}/top_level.txt +0 -0
ccxt/pro/huobi.py
CHANGED
@@ -3,2175 +3,13 @@
|
|
3
3
|
# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
|
4
4
|
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
|
5
5
|
|
6
|
-
|
7
|
-
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp
|
8
|
-
import hashlib
|
9
|
-
from ccxt.async_support.base.ws.client import Client
|
10
|
-
from typing import Optional
|
11
|
-
from typing import List
|
12
|
-
from ccxt.base.errors import ExchangeError
|
13
|
-
from ccxt.base.errors import ArgumentsRequired
|
14
|
-
from ccxt.base.errors import BadRequest
|
15
|
-
from ccxt.base.errors import BadSymbol
|
16
|
-
from ccxt.base.errors import NetworkError
|
17
|
-
from ccxt.base.errors import InvalidNonce
|
18
|
-
from ccxt.base.errors import AuthenticationError
|
6
|
+
from ccxt.pro.htx import htx
|
19
7
|
|
20
8
|
|
21
|
-
class huobi(
|
9
|
+
class huobi(htx):
|
22
10
|
|
23
11
|
def describe(self):
|
24
12
|
return self.deep_extend(super(huobi, self).describe(), {
|
25
|
-
'
|
26
|
-
|
27
|
-
'watchOrderBook': True,
|
28
|
-
'watchOrders': True,
|
29
|
-
'watchTickers': False,
|
30
|
-
'watchTicker': True,
|
31
|
-
'watchTrades': True,
|
32
|
-
'watchMyTrades': True,
|
33
|
-
'watchBalance': True,
|
34
|
-
'watchOHLCV': True,
|
35
|
-
},
|
36
|
-
'urls': {
|
37
|
-
'api': {
|
38
|
-
'ws': {
|
39
|
-
'api': {
|
40
|
-
'spot': {
|
41
|
-
'public': 'wss://{hostname}/ws',
|
42
|
-
'private': 'wss://{hostname}/ws/v2',
|
43
|
-
},
|
44
|
-
'future': {
|
45
|
-
'linear': {
|
46
|
-
'public': 'wss://api.hbdm.com/linear-swap-ws',
|
47
|
-
'private': 'wss://api.hbdm.com/linear-swap-notification',
|
48
|
-
},
|
49
|
-
'inverse': {
|
50
|
-
'public': 'wss://api.hbdm.com/ws',
|
51
|
-
'private': 'wss://api.hbdm.com/notification',
|
52
|
-
},
|
53
|
-
},
|
54
|
-
'swap': {
|
55
|
-
'inverse': {
|
56
|
-
'public': 'wss://api.hbdm.com/swap-ws',
|
57
|
-
'private': 'wss://api.hbdm.com/swap-notification',
|
58
|
-
},
|
59
|
-
'linear': {
|
60
|
-
'public': 'wss://api.hbdm.com/linear-swap-ws',
|
61
|
-
'private': 'wss://api.hbdm.com/linear-swap-notification',
|
62
|
-
},
|
63
|
-
},
|
64
|
-
},
|
65
|
-
# these settings work faster for clients hosted on AWS
|
66
|
-
'api-aws': {
|
67
|
-
'spot': {
|
68
|
-
'public': 'wss://api-aws.huobi.pro/ws',
|
69
|
-
'private': 'wss://api-aws.huobi.pro/ws/v2',
|
70
|
-
},
|
71
|
-
'future': {
|
72
|
-
'linear': {
|
73
|
-
'public': 'wss://api.hbdm.vn/linear-swap-ws',
|
74
|
-
'private': 'wss://api.hbdm.vn/linear-swap-notification',
|
75
|
-
},
|
76
|
-
'inverse': {
|
77
|
-
'public': 'wss://api.hbdm.vn/ws',
|
78
|
-
'private': 'wss://api.hbdm.vn/notification',
|
79
|
-
},
|
80
|
-
},
|
81
|
-
'swap': {
|
82
|
-
'linear': {
|
83
|
-
'public': 'wss://api.hbdm.vn/linear-swap-ws',
|
84
|
-
'private': 'wss://api.hbdm.vn/linear-swap-notification',
|
85
|
-
},
|
86
|
-
'inverse': {
|
87
|
-
'public': 'wss://api.hbdm.vn/swap-ws',
|
88
|
-
'private': 'wss://api.hbdm.vn/swap-notification',
|
89
|
-
},
|
90
|
-
},
|
91
|
-
},
|
92
|
-
},
|
93
|
-
},
|
94
|
-
},
|
95
|
-
'options': {
|
96
|
-
'tradesLimit': 1000,
|
97
|
-
'OHLCVLimit': 1000,
|
98
|
-
'api': 'api', # or api-aws for clients hosted on AWS
|
99
|
-
'watchOrderBook': {
|
100
|
-
'maxRetries': 3,
|
101
|
-
},
|
102
|
-
'ws': {
|
103
|
-
'gunzip': True,
|
104
|
-
},
|
105
|
-
'watchTicker': {
|
106
|
-
'name': 'market.{marketId}.detail', # 'market.{marketId}.bbo' or 'market.{marketId}.ticker'
|
107
|
-
},
|
108
|
-
},
|
109
|
-
'exceptions': {
|
110
|
-
'ws': {
|
111
|
-
'exact': {
|
112
|
-
'bad-request': BadRequest, # { ts: 1586323747018, status: 'error', 'err-code': 'bad-request', err-msg': 'invalid mbp.150.symbol linkusdt', id: '2'}
|
113
|
-
'2002': AuthenticationError, # {action: 'sub', code: 2002, ch: 'accounts.update#2', message: 'invalid.auth.state'}
|
114
|
-
'2021': BadRequest,
|
115
|
-
'2001': BadSymbol, # {action: 'sub', code: 2001, ch: 'orders#2ltcusdt', message: 'invalid.symbol'}
|
116
|
-
'2011': BadSymbol, # {op: 'sub', cid: '1649149285', topic: 'orders_cross.hereltc-usdt', 'err-code': 2011, 'err-msg': "Contract doesn't exist.", ts: 1649149287637}
|
117
|
-
'2040': BadRequest, # {op: 'sub', cid: '1649152947', 'err-code': 2040, 'err-msg': 'Missing required parameter.', ts: 1649152948684}
|
118
|
-
'4007': BadRequest, # {op: 'sub', cid: '1', topic: 'accounts_unify.USDT', 'err-code': 4007, 'err-msg': 'Non - single account user is not available, please check through the cross and isolated account asset interface', ts: 1698419318540}
|
119
|
-
},
|
120
|
-
},
|
121
|
-
},
|
13
|
+
'alias': True,
|
14
|
+
'id': 'huobi',
|
122
15
|
})
|
123
|
-
|
124
|
-
def request_id(self):
|
125
|
-
requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1)
|
126
|
-
self.options['requestId'] = requestId
|
127
|
-
return str(requestId)
|
128
|
-
|
129
|
-
async def watch_ticker(self, symbol: str, params={}):
|
130
|
-
"""
|
131
|
-
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
132
|
-
:param str symbol: unified symbol of the market to fetch the ticker for
|
133
|
-
:param dict [params]: extra parameters specific to the huobi api endpoint
|
134
|
-
:returns dict: a `ticker structure <https://github.com/ccxt/ccxt/wiki/Manual#ticker-structure>`
|
135
|
-
"""
|
136
|
-
await self.load_markets()
|
137
|
-
market = self.market(symbol)
|
138
|
-
symbol = market['symbol']
|
139
|
-
options = self.safe_value(self.options, 'watchTicker', {})
|
140
|
-
topic = self.safe_string(options, 'name', 'market.{marketId}.detail')
|
141
|
-
if topic == 'market.{marketId}.ticker' and market['type'] != 'spot':
|
142
|
-
raise BadRequest(self.id + ' watchTicker() with name market.{marketId}.ticker is only allowed for spot markets, use market.{marketId}.detail instead')
|
143
|
-
messageHash = self.implode_params(topic, {'marketId': market['id']})
|
144
|
-
url = self.get_url_by_market_type(market['type'], market['linear'])
|
145
|
-
return await self.subscribe_public(url, symbol, messageHash, None, params)
|
146
|
-
|
147
|
-
def handle_ticker(self, client: Client, message):
|
148
|
-
#
|
149
|
-
# "market.btcusdt.detail"
|
150
|
-
# {
|
151
|
-
# "ch": "market.btcusdt.detail",
|
152
|
-
# "ts": 1583494163784,
|
153
|
-
# "tick": {
|
154
|
-
# "id": 209988464418,
|
155
|
-
# "low": 8988,
|
156
|
-
# "high": 9155.41,
|
157
|
-
# "open": 9078.91,
|
158
|
-
# "close": 9136.46,
|
159
|
-
# "vol": 237813910.5928412,
|
160
|
-
# "amount": 26184.202558551195,
|
161
|
-
# "version": 209988464418,
|
162
|
-
# "count": 265673
|
163
|
-
# }
|
164
|
-
# }
|
165
|
-
# "market.btcusdt.bbo"
|
166
|
-
# {
|
167
|
-
# "ch": "market.btcusdt.bbo",
|
168
|
-
# "ts": 1671941599613,
|
169
|
-
# "tick": {
|
170
|
-
# "seqId": 161499562790,
|
171
|
-
# "ask": 16829.51,
|
172
|
-
# "askSize": 0.707776,
|
173
|
-
# "bid": 16829.5,
|
174
|
-
# "bidSize": 1.685945,
|
175
|
-
# "quoteTime": 1671941599612,
|
176
|
-
# "symbol": "btcusdt"
|
177
|
-
# }
|
178
|
-
# }
|
179
|
-
#
|
180
|
-
tick = self.safe_value(message, 'tick', {})
|
181
|
-
ch = self.safe_string(message, 'ch')
|
182
|
-
parts = ch.split('.')
|
183
|
-
marketId = self.safe_string(parts, 1)
|
184
|
-
market = self.safe_market(marketId)
|
185
|
-
ticker = self.parse_ticker(tick, market)
|
186
|
-
timestamp = self.safe_value(message, 'ts')
|
187
|
-
ticker['timestamp'] = timestamp
|
188
|
-
ticker['datetime'] = self.iso8601(timestamp)
|
189
|
-
symbol = ticker['symbol']
|
190
|
-
self.tickers[symbol] = ticker
|
191
|
-
client.resolve(ticker, ch)
|
192
|
-
return message
|
193
|
-
|
194
|
-
async def watch_trades(self, symbol: str, since: Optional[int] = None, limit: Optional[int] = None, params={}):
|
195
|
-
"""
|
196
|
-
get the list of most recent trades for a particular symbol
|
197
|
-
:param str symbol: unified symbol of the market to fetch trades for
|
198
|
-
:param int [since]: timestamp in ms of the earliest trade to fetch
|
199
|
-
:param int [limit]: the maximum amount of trades to fetch
|
200
|
-
:param dict [params]: extra parameters specific to the huobi api endpoint
|
201
|
-
:returns dict[]: a list of `trade structures <https://github.com/ccxt/ccxt/wiki/Manual#public-trades>`
|
202
|
-
"""
|
203
|
-
await self.load_markets()
|
204
|
-
market = self.market(symbol)
|
205
|
-
symbol = market['symbol']
|
206
|
-
messageHash = 'market.' + market['id'] + '.trade.detail'
|
207
|
-
url = self.get_url_by_market_type(market['type'], market['linear'])
|
208
|
-
trades = await self.subscribe_public(url, symbol, messageHash, None, params)
|
209
|
-
if self.newUpdates:
|
210
|
-
limit = trades.getLimit(symbol, limit)
|
211
|
-
return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
212
|
-
|
213
|
-
def handle_trades(self, client: Client, message):
|
214
|
-
#
|
215
|
-
# {
|
216
|
-
# "ch": "market.btcusdt.trade.detail",
|
217
|
-
# "ts": 1583495834011,
|
218
|
-
# "tick": {
|
219
|
-
# "id": 105004645372,
|
220
|
-
# "ts": 1583495833751,
|
221
|
-
# "data": [
|
222
|
-
# {
|
223
|
-
# "id": 1.050046453727319e+22,
|
224
|
-
# "ts": 1583495833751,
|
225
|
-
# "tradeId": 102090727790,
|
226
|
-
# "amount": 0.003893,
|
227
|
-
# "price": 9150.01,
|
228
|
-
# "direction": "sell"
|
229
|
-
# }
|
230
|
-
# ]
|
231
|
-
# }
|
232
|
-
# }
|
233
|
-
#
|
234
|
-
tick = self.safe_value(message, 'tick', {})
|
235
|
-
data = self.safe_value(tick, 'data', {})
|
236
|
-
ch = self.safe_string(message, 'ch')
|
237
|
-
parts = ch.split('.')
|
238
|
-
marketId = self.safe_string(parts, 1)
|
239
|
-
market = self.safe_market(marketId)
|
240
|
-
symbol = market['symbol']
|
241
|
-
tradesCache = self.safe_value(self.trades, symbol)
|
242
|
-
if tradesCache is None:
|
243
|
-
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
244
|
-
tradesCache = ArrayCache(limit)
|
245
|
-
self.trades[symbol] = tradesCache
|
246
|
-
for i in range(0, len(data)):
|
247
|
-
trade = self.parse_trade(data[i], market)
|
248
|
-
tradesCache.append(trade)
|
249
|
-
client.resolve(tradesCache, ch)
|
250
|
-
return message
|
251
|
-
|
252
|
-
async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Optional[int] = None, limit: Optional[int] = None, params={}):
|
253
|
-
"""
|
254
|
-
watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
255
|
-
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
256
|
-
:param str timeframe: the length of time each candle represents
|
257
|
-
:param int [since]: timestamp in ms of the earliest candle to fetch
|
258
|
-
:param int [limit]: the maximum amount of candles to fetch
|
259
|
-
:param dict [params]: extra parameters specific to the huobi api endpoint
|
260
|
-
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
261
|
-
"""
|
262
|
-
await self.load_markets()
|
263
|
-
market = self.market(symbol)
|
264
|
-
symbol = market['symbol']
|
265
|
-
interval = self.safe_string(self.timeframes, timeframe, timeframe)
|
266
|
-
messageHash = 'market.' + market['id'] + '.kline.' + interval
|
267
|
-
url = self.get_url_by_market_type(market['type'], market['linear'])
|
268
|
-
ohlcv = await self.subscribe_public(url, symbol, messageHash, None, params)
|
269
|
-
if self.newUpdates:
|
270
|
-
limit = ohlcv.getLimit(symbol, limit)
|
271
|
-
return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
|
272
|
-
|
273
|
-
def handle_ohlcv(self, client: Client, message):
|
274
|
-
#
|
275
|
-
# {
|
276
|
-
# "ch": "market.btcusdt.kline.1min",
|
277
|
-
# "ts": 1583501786794,
|
278
|
-
# "tick": {
|
279
|
-
# "id": 1583501760,
|
280
|
-
# "open": 9094.5,
|
281
|
-
# "close": 9094.51,
|
282
|
-
# "low": 9094.5,
|
283
|
-
# "high": 9094.51,
|
284
|
-
# "amount": 0.44639786263800907,
|
285
|
-
# "vol": 4059.76919054,
|
286
|
-
# "count": 16
|
287
|
-
# }
|
288
|
-
# }
|
289
|
-
#
|
290
|
-
ch = self.safe_string(message, 'ch')
|
291
|
-
parts = ch.split('.')
|
292
|
-
marketId = self.safe_string(parts, 1)
|
293
|
-
market = self.safe_market(marketId)
|
294
|
-
symbol = market['symbol']
|
295
|
-
interval = self.safe_string(parts, 3)
|
296
|
-
timeframe = self.find_timeframe(interval)
|
297
|
-
self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
|
298
|
-
stored = self.safe_value(self.ohlcvs[symbol], timeframe)
|
299
|
-
if stored is None:
|
300
|
-
limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
|
301
|
-
stored = ArrayCacheByTimestamp(limit)
|
302
|
-
self.ohlcvs[symbol][timeframe] = stored
|
303
|
-
tick = self.safe_value(message, 'tick')
|
304
|
-
parsed = self.parse_ohlcv(tick, market)
|
305
|
-
stored.append(parsed)
|
306
|
-
client.resolve(stored, ch)
|
307
|
-
|
308
|
-
async def watch_order_book(self, symbol: str, limit: Optional[int] = None, params={}):
|
309
|
-
"""
|
310
|
-
:see: https://huobiapi.github.io/docs/dm/v1/en/#subscribe-market-depth-data
|
311
|
-
:see: https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#subscribe-incremental-market-depth-data
|
312
|
-
:see: https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-incremental-market-depth-data
|
313
|
-
watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
314
|
-
:param str symbol: unified symbol of the market to fetch the order book for
|
315
|
-
:param int [limit]: the maximum amount of order book entries to return
|
316
|
-
:param dict [params]: extra parameters specific to the huobi api endpoint
|
317
|
-
:returns dict: A dictionary of `order book structures <https://github.com/ccxt/ccxt/wiki/Manual#order-book-structure>` indexed by market symbols
|
318
|
-
"""
|
319
|
-
await self.load_markets()
|
320
|
-
market = self.market(symbol)
|
321
|
-
symbol = market['symbol']
|
322
|
-
allowedSpotLimits = [150]
|
323
|
-
allowedSwapLimits = [20, 150]
|
324
|
-
limit = 150 if (limit is None) else limit
|
325
|
-
if market['spot'] and not self.in_array(limit, allowedSpotLimits):
|
326
|
-
raise ExchangeError(self.id + ' watchOrderBook spot market accepts limits of 150 only')
|
327
|
-
if not market['spot'] and not self.in_array(limit, allowedSwapLimits):
|
328
|
-
raise ExchangeError(self.id + ' watchOrderBook swap market accepts limits of 20 and 150 only')
|
329
|
-
messageHash = None
|
330
|
-
if market['spot']:
|
331
|
-
messageHash = 'market.' + market['id'] + '.mbp.' + str(limit)
|
332
|
-
else:
|
333
|
-
messageHash = 'market.' + market['id'] + '.depth.size_' + str(limit) + '.high_freq'
|
334
|
-
url = self.get_url_by_market_type(market['type'], market['linear'])
|
335
|
-
method = self.handle_order_book_subscription
|
336
|
-
if not market['spot']:
|
337
|
-
params = self.extend(params)
|
338
|
-
params['data_type'] = 'incremental'
|
339
|
-
method = None
|
340
|
-
orderbook = await self.subscribe_public(url, symbol, messageHash, method, params)
|
341
|
-
return orderbook.limit()
|
342
|
-
|
343
|
-
def handle_order_book_snapshot(self, client: Client, message, subscription):
|
344
|
-
#
|
345
|
-
# {
|
346
|
-
# "id": 1583473663565,
|
347
|
-
# "rep": "market.btcusdt.mbp.150",
|
348
|
-
# "status": "ok",
|
349
|
-
# "ts": 1698359289261,
|
350
|
-
# "data": {
|
351
|
-
# "seqNum": 104999417756,
|
352
|
-
# "bids": [
|
353
|
-
# [9058.27, 0],
|
354
|
-
# [9058.43, 0],
|
355
|
-
# [9058.99, 0],
|
356
|
-
# ],
|
357
|
-
# "asks": [
|
358
|
-
# [9084.27, 0.2],
|
359
|
-
# [9085.69, 0],
|
360
|
-
# [9085.81, 0],
|
361
|
-
# ]
|
362
|
-
# }
|
363
|
-
# }
|
364
|
-
#
|
365
|
-
symbol = self.safe_string(subscription, 'symbol')
|
366
|
-
messageHash = self.safe_string(subscription, 'messageHash')
|
367
|
-
id = self.safe_string(message, 'id')
|
368
|
-
try:
|
369
|
-
orderbook = self.orderbooks[symbol]
|
370
|
-
data = self.safe_value(message, 'data')
|
371
|
-
messages = orderbook.cache
|
372
|
-
firstMessage = self.safe_value(messages, 0, {})
|
373
|
-
snapshot = self.parse_order_book(data, symbol)
|
374
|
-
tick = self.safe_value(firstMessage, 'tick')
|
375
|
-
sequence = self.safe_integer(tick, 'seqNum')
|
376
|
-
nonce = self.safe_integer(data, 'seqNum')
|
377
|
-
snapshot['nonce'] = nonce
|
378
|
-
timestamp = self.safe_integer(message, 'ts')
|
379
|
-
snapshot['timestamp'] = timestamp
|
380
|
-
snapshot['datetime'] = self.iso8601(timestamp)
|
381
|
-
snapshotLimit = self.safe_integer(subscription, 'limit')
|
382
|
-
snapshotOrderBook = self.order_book(snapshot, snapshotLimit)
|
383
|
-
client.resolve(snapshotOrderBook, id)
|
384
|
-
if (sequence is not None) and (nonce < sequence):
|
385
|
-
maxAttempts = self.handle_option('watchOrderBook', 'maxRetries', 3)
|
386
|
-
numAttempts = self.safe_integer(subscription, 'numAttempts', 0)
|
387
|
-
# retry to synchronize if we have not reached maxAttempts yet
|
388
|
-
if numAttempts < maxAttempts:
|
389
|
-
# safety guard
|
390
|
-
if messageHash in client.subscriptions:
|
391
|
-
numAttempts = self.sum(numAttempts, 1)
|
392
|
-
subscription['numAttempts'] = numAttempts
|
393
|
-
client.subscriptions[messageHash] = subscription
|
394
|
-
self.spawn(self.watch_order_book_snapshot, client, message, subscription)
|
395
|
-
else:
|
396
|
-
# raise upon failing to synchronize in maxAttempts
|
397
|
-
raise InvalidNonce(self.id + ' failed to synchronize WebSocket feed with the snapshot for symbol ' + symbol + ' in ' + str(maxAttempts) + ' attempts')
|
398
|
-
else:
|
399
|
-
orderbook.reset(snapshot)
|
400
|
-
# unroll the accumulated deltas
|
401
|
-
for i in range(0, len(messages)):
|
402
|
-
self.handle_order_book_message(client, messages[i], orderbook)
|
403
|
-
self.orderbooks[symbol] = orderbook
|
404
|
-
client.resolve(orderbook, messageHash)
|
405
|
-
except Exception as e:
|
406
|
-
client.reject(e, messageHash)
|
407
|
-
|
408
|
-
async def watch_order_book_snapshot(self, client, message, subscription):
|
409
|
-
messageHash = self.safe_string(subscription, 'messageHash')
|
410
|
-
try:
|
411
|
-
symbol = self.safe_string(subscription, 'symbol')
|
412
|
-
limit = self.safe_integer(subscription, 'limit')
|
413
|
-
params = self.safe_value(subscription, 'params')
|
414
|
-
attempts = self.safe_integer(subscription, 'numAttempts', 0)
|
415
|
-
market = self.market(symbol)
|
416
|
-
url = self.get_url_by_market_type(market['type'], market['linear'])
|
417
|
-
requestId = self.request_id()
|
418
|
-
request = {
|
419
|
-
'req': messageHash,
|
420
|
-
'id': requestId,
|
421
|
-
}
|
422
|
-
# self is a temporary subscription by a specific requestId
|
423
|
-
# it has a very short lifetime until the snapshot is received over ws
|
424
|
-
snapshotSubscription = {
|
425
|
-
'id': requestId,
|
426
|
-
'messageHash': messageHash,
|
427
|
-
'symbol': symbol,
|
428
|
-
'limit': limit,
|
429
|
-
'params': params,
|
430
|
-
'numAttempts': attempts,
|
431
|
-
'method': self.handle_order_book_snapshot,
|
432
|
-
}
|
433
|
-
orderbook = await self.watch(url, requestId, request, requestId, snapshotSubscription)
|
434
|
-
return orderbook.limit()
|
435
|
-
except Exception as e:
|
436
|
-
del client.subscriptions[messageHash]
|
437
|
-
client.reject(e, messageHash)
|
438
|
-
|
439
|
-
def handle_delta(self, bookside, delta):
|
440
|
-
price = self.safe_float(delta, 0)
|
441
|
-
amount = self.safe_float(delta, 1)
|
442
|
-
bookside.store(price, amount)
|
443
|
-
|
444
|
-
def handle_deltas(self, bookside, deltas):
|
445
|
-
for i in range(0, len(deltas)):
|
446
|
-
self.handle_delta(bookside, deltas[i])
|
447
|
-
|
448
|
-
def handle_order_book_message(self, client: Client, message, orderbook):
|
449
|
-
# spot markets
|
450
|
-
#
|
451
|
-
# {
|
452
|
-
# "ch": "market.btcusdt.mbp.150",
|
453
|
-
# "ts": 1583472025885,
|
454
|
-
# "tick": {
|
455
|
-
# "seqNum": 104998984994,
|
456
|
-
# "prevSeqNum": 104998984977,
|
457
|
-
# "bids": [
|
458
|
-
# [9058.27, 0],
|
459
|
-
# [9058.43, 0],
|
460
|
-
# [9058.99, 0],
|
461
|
-
# ],
|
462
|
-
# "asks": [
|
463
|
-
# [9084.27, 0.2],
|
464
|
-
# [9085.69, 0],
|
465
|
-
# [9085.81, 0],
|
466
|
-
# ]
|
467
|
-
# }
|
468
|
-
# }
|
469
|
-
#
|
470
|
-
# non-spot market update
|
471
|
-
#
|
472
|
-
# {
|
473
|
-
# "ch":"market.BTC220218.depth.size_150.high_freq",
|
474
|
-
# "tick":{
|
475
|
-
# "asks":[],
|
476
|
-
# "bids":[
|
477
|
-
# [43445.74,1],
|
478
|
-
# [43444.48,0],
|
479
|
-
# [40593.92,9]
|
480
|
-
# ],
|
481
|
-
# "ch":"market.BTC220218.depth.size_150.high_freq",
|
482
|
-
# "event":"update",
|
483
|
-
# "id":152727500274,
|
484
|
-
# "mrid":152727500274,
|
485
|
-
# "ts":1645023376098,
|
486
|
-
# "version":37536690
|
487
|
-
# },
|
488
|
-
# "ts":1645023376098
|
489
|
-
# }
|
490
|
-
# non-spot market snapshot
|
491
|
-
#
|
492
|
-
# {
|
493
|
-
# "ch":"market.BTC220218.depth.size_150.high_freq",
|
494
|
-
# "tick":{
|
495
|
-
# "asks":[
|
496
|
-
# [43445.74,1],
|
497
|
-
# [43444.48,0],
|
498
|
-
# [40593.92,9]
|
499
|
-
# ],
|
500
|
-
# "bids":[
|
501
|
-
# [43445.74,1],
|
502
|
-
# [43444.48,0],
|
503
|
-
# [40593.92,9]
|
504
|
-
# ],
|
505
|
-
# "ch":"market.BTC220218.depth.size_150.high_freq",
|
506
|
-
# "event":"snapshot",
|
507
|
-
# "id":152727500274,
|
508
|
-
# "mrid":152727500274,
|
509
|
-
# "ts":1645023376098,
|
510
|
-
# "version":37536690
|
511
|
-
# },
|
512
|
-
# "ts":1645023376098
|
513
|
-
# }
|
514
|
-
#
|
515
|
-
ch = self.safe_value(message, 'ch')
|
516
|
-
parts = ch.split('.')
|
517
|
-
marketId = self.safe_string(parts, 1)
|
518
|
-
symbol = self.safe_symbol(marketId)
|
519
|
-
tick = self.safe_value(message, 'tick', {})
|
520
|
-
seqNum = self.safe_integer_2(tick, 'seqNum', 'version')
|
521
|
-
prevSeqNum = self.safe_integer(tick, 'prevSeqNum')
|
522
|
-
event = self.safe_string(tick, 'event')
|
523
|
-
timestamp = self.safe_integer(message, 'ts')
|
524
|
-
if event == 'snapshot':
|
525
|
-
snapshot = self.parse_order_book(tick, symbol, timestamp)
|
526
|
-
orderbook.reset(snapshot)
|
527
|
-
orderbook['nonce'] = seqNum
|
528
|
-
if prevSeqNum is not None and prevSeqNum > orderbook['nonce']:
|
529
|
-
raise InvalidNonce(self.id + ' watchOrderBook() received a mesage out of order')
|
530
|
-
if (prevSeqNum is None or prevSeqNum <= orderbook['nonce']) and (seqNum > orderbook['nonce']):
|
531
|
-
asks = self.safe_value(tick, 'asks', [])
|
532
|
-
bids = self.safe_value(tick, 'bids', [])
|
533
|
-
self.handle_deltas(orderbook['asks'], asks)
|
534
|
-
self.handle_deltas(orderbook['bids'], bids)
|
535
|
-
orderbook['nonce'] = seqNum
|
536
|
-
orderbook['timestamp'] = timestamp
|
537
|
-
orderbook['datetime'] = self.iso8601(timestamp)
|
538
|
-
return orderbook
|
539
|
-
|
540
|
-
def handle_order_book(self, client: Client, message):
|
541
|
-
#
|
542
|
-
# deltas
|
543
|
-
#
|
544
|
-
# spot markets
|
545
|
-
#
|
546
|
-
# {
|
547
|
-
# "ch": "market.btcusdt.mbp.150",
|
548
|
-
# "ts": 1583472025885,
|
549
|
-
# "tick": {
|
550
|
-
# "seqNum": 104998984994,
|
551
|
-
# "prevSeqNum": 104998984977,
|
552
|
-
# "bids": [
|
553
|
-
# [9058.27, 0],
|
554
|
-
# [9058.43, 0],
|
555
|
-
# [9058.99, 0],
|
556
|
-
# ],
|
557
|
-
# "asks": [
|
558
|
-
# [9084.27, 0.2],
|
559
|
-
# [9085.69, 0],
|
560
|
-
# [9085.81, 0],
|
561
|
-
# ]
|
562
|
-
# }
|
563
|
-
# }
|
564
|
-
#
|
565
|
-
# non spot markets
|
566
|
-
#
|
567
|
-
# {
|
568
|
-
# "ch":"market.BTC220218.depth.size_150.high_freq",
|
569
|
-
# "tick":{
|
570
|
-
# "asks":[],
|
571
|
-
# "bids":[
|
572
|
-
# [43445.74,1],
|
573
|
-
# [43444.48,0],
|
574
|
-
# [40593.92,9]
|
575
|
-
# ],
|
576
|
-
# "ch":"market.BTC220218.depth.size_150.high_freq",
|
577
|
-
# "event":"update",
|
578
|
-
# "id":152727500274,
|
579
|
-
# "mrid":152727500274,
|
580
|
-
# "ts":1645023376098,
|
581
|
-
# "version":37536690
|
582
|
-
# },
|
583
|
-
# "ts":1645023376098
|
584
|
-
# }
|
585
|
-
#
|
586
|
-
tick = self.safe_value(message, 'tick', {})
|
587
|
-
event = self.safe_string(tick, 'event')
|
588
|
-
messageHash = self.safe_string(message, 'ch')
|
589
|
-
ch = self.safe_value(message, 'ch')
|
590
|
-
parts = ch.split('.')
|
591
|
-
marketId = self.safe_string(parts, 1)
|
592
|
-
symbol = self.safe_symbol(marketId)
|
593
|
-
orderbook = self.safe_value(self.orderbooks, symbol)
|
594
|
-
if orderbook is None:
|
595
|
-
size = self.safe_string(parts, 3)
|
596
|
-
sizeParts = size.split('_')
|
597
|
-
limit = self.safe_integer(sizeParts, 1)
|
598
|
-
orderbook = self.order_book({}, limit)
|
599
|
-
if orderbook['nonce'] is None:
|
600
|
-
orderbook.cache.append(message)
|
601
|
-
if event is not None or orderbook['nonce'] is not None:
|
602
|
-
self.orderbooks[symbol] = self.handle_order_book_message(client, message, orderbook)
|
603
|
-
client.resolve(orderbook, messageHash)
|
604
|
-
|
605
|
-
def handle_order_book_subscription(self, client: Client, message, subscription):
|
606
|
-
symbol = self.safe_string(subscription, 'symbol')
|
607
|
-
limit = self.safe_integer(subscription, 'limit')
|
608
|
-
if symbol in self.orderbooks:
|
609
|
-
del self.orderbooks[symbol]
|
610
|
-
self.orderbooks[symbol] = self.order_book({}, limit)
|
611
|
-
if self.markets[symbol]['spot'] is True:
|
612
|
-
self.spawn(self.watch_order_book_snapshot, client, message, subscription)
|
613
|
-
|
614
|
-
async def watch_my_trades(self, symbol: Optional[str] = None, since: Optional[int] = None, limit: Optional[int] = None, params={}):
|
615
|
-
"""
|
616
|
-
watches information on multiple trades made by the user
|
617
|
-
:param str symbol: unified market symbol of the market trades were made in
|
618
|
-
:param int [since]: the earliest time in ms to fetch trades for
|
619
|
-
:param int [limit]: the maximum number of trade structures to retrieve
|
620
|
-
:param dict [params]: extra parameters specific to the huobi api endpoint
|
621
|
-
:returns dict[]: a list of [trade structures]{@link https://github.com/ccxt/ccxt/wiki/Manual#trade-structure
|
622
|
-
"""
|
623
|
-
self.check_required_credentials()
|
624
|
-
await self.load_markets()
|
625
|
-
type = None
|
626
|
-
marketId = '*' # wildcard
|
627
|
-
market = None
|
628
|
-
messageHash = None
|
629
|
-
channel = None
|
630
|
-
trades = None
|
631
|
-
subType = None
|
632
|
-
if symbol is not None:
|
633
|
-
market = self.market(symbol)
|
634
|
-
symbol = market['symbol']
|
635
|
-
type = market['type']
|
636
|
-
subType = 'linear' if market['linear'] else 'inverse'
|
637
|
-
marketId = market['lowercaseId']
|
638
|
-
else:
|
639
|
-
type = self.safe_string(self.options, 'defaultType', 'spot')
|
640
|
-
type = self.safe_string(params, 'type', type)
|
641
|
-
subType = self.safe_string_2(self.options, 'subType', 'defaultSubType', 'linear')
|
642
|
-
subType = self.safe_string(params, 'subType', subType)
|
643
|
-
params = self.omit(params, ['type', 'subType'])
|
644
|
-
if type == 'spot':
|
645
|
-
mode = None
|
646
|
-
if mode is None:
|
647
|
-
mode = self.safe_string_2(self.options, 'watchMyTrades', 'mode', '0')
|
648
|
-
mode = self.safe_string(params, 'mode', mode)
|
649
|
-
params = self.omit(params, 'mode')
|
650
|
-
messageHash = 'trade.clearing' + '#' + marketId + '#' + mode
|
651
|
-
channel = messageHash
|
652
|
-
else:
|
653
|
-
channelAndMessageHash = self.get_order_channel_and_message_hash(type, subType, market, params)
|
654
|
-
channel = self.safe_string(channelAndMessageHash, 0)
|
655
|
-
orderMessageHash = self.safe_string(channelAndMessageHash, 1)
|
656
|
-
# we will take advantage of the order messageHash because already handles stuff
|
657
|
-
# like symbol/margin/subtype/type variations
|
658
|
-
messageHash = orderMessageHash + ':' + 'trade'
|
659
|
-
trades = await self.subscribe_private(channel, messageHash, type, subType, params)
|
660
|
-
if self.newUpdates:
|
661
|
-
limit = trades.getLimit(symbol, limit)
|
662
|
-
return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)
|
663
|
-
|
664
|
-
def get_order_channel_and_message_hash(self, type, subType, market=None, params={}):
|
665
|
-
messageHash = None
|
666
|
-
channel = None
|
667
|
-
orderType = self.safe_string(self.options, 'orderType', 'orders') # orders or matchOrders
|
668
|
-
orderType = self.safe_string(params, 'orderType', orderType)
|
669
|
-
params = self.omit(params, 'orderType')
|
670
|
-
marketCode = market['lowercaseId'] if (market is not None) else None
|
671
|
-
baseId = market['lowercaseBaseId'] if (market is not None) else None
|
672
|
-
prefix = orderType
|
673
|
-
messageHash = prefix
|
674
|
-
if subType == 'linear':
|
675
|
-
# USDT Margined Contracts Example: LTC/USDT:USDT
|
676
|
-
marginMode = self.safe_string(params, 'margin', 'cross')
|
677
|
-
marginPrefix = prefix + '_cross' if (marginMode == 'cross') else prefix
|
678
|
-
messageHash = marginPrefix
|
679
|
-
if marketCode is not None:
|
680
|
-
messageHash += '.' + marketCode
|
681
|
-
channel = messageHash
|
682
|
-
else:
|
683
|
-
channel = marginPrefix + '.' + '*'
|
684
|
-
elif type == 'future':
|
685
|
-
# inverse futures Example: BCH/USD:BCH-220408
|
686
|
-
if baseId is not None:
|
687
|
-
channel = prefix + '.' + baseId
|
688
|
-
messageHash = channel
|
689
|
-
else:
|
690
|
-
channel = prefix + '.' + '*'
|
691
|
-
else:
|
692
|
-
# inverse swaps: Example: BTC/USD:BTC
|
693
|
-
if marketCode is not None:
|
694
|
-
channel = prefix + '.' + marketCode
|
695
|
-
messageHash = channel
|
696
|
-
else:
|
697
|
-
channel = prefix + '.' + '*'
|
698
|
-
return [channel, messageHash]
|
699
|
-
|
700
|
-
async def watch_orders(self, symbol: Optional[str] = None, since: Optional[int] = None, limit: Optional[int] = None, params={}):
|
701
|
-
"""
|
702
|
-
watches information on multiple orders made by the user
|
703
|
-
:param str symbol: unified market symbol of the market orders were made in
|
704
|
-
:param int [since]: the earliest time in ms to fetch orders for
|
705
|
-
:param int [limit]: the maximum number of orde structures to retrieve
|
706
|
-
:param dict [params]: extra parameters specific to the huobi api endpoint
|
707
|
-
:returns dict[]: a list of `order structures <https://github.com/ccxt/ccxt/wiki/Manual#order-structure>`
|
708
|
-
"""
|
709
|
-
await self.load_markets()
|
710
|
-
type = None
|
711
|
-
subType = None
|
712
|
-
market = None
|
713
|
-
suffix = '*' # wildcard
|
714
|
-
if symbol is not None:
|
715
|
-
market = self.market(symbol)
|
716
|
-
symbol = market['symbol']
|
717
|
-
type = market['type']
|
718
|
-
suffix = market['lowercaseId']
|
719
|
-
subType = 'linear' if market['linear'] else 'inverse'
|
720
|
-
else:
|
721
|
-
type = self.safe_string(self.options, 'defaultType', 'spot')
|
722
|
-
type = self.safe_string(params, 'type', type)
|
723
|
-
subType = self.safe_string_2(self.options, 'subType', 'defaultSubType', 'linear')
|
724
|
-
subType = self.safe_string(params, 'subType', subType)
|
725
|
-
params = self.omit(params, ['type', 'subType'])
|
726
|
-
messageHash = None
|
727
|
-
channel = None
|
728
|
-
if type == 'spot':
|
729
|
-
messageHash = 'orders' + '#' + suffix
|
730
|
-
channel = messageHash
|
731
|
-
else:
|
732
|
-
channelAndMessageHash = self.get_order_channel_and_message_hash(type, subType, market, params)
|
733
|
-
channel = self.safe_string(channelAndMessageHash, 0)
|
734
|
-
messageHash = self.safe_string(channelAndMessageHash, 1)
|
735
|
-
orders = await self.subscribe_private(channel, messageHash, type, subType, params)
|
736
|
-
if self.newUpdates:
|
737
|
-
limit = orders.getLimit(symbol, limit)
|
738
|
-
return self.filter_by_since_limit(orders, since, limit, 'timestamp', True)
|
739
|
-
|
740
|
-
def handle_order(self, client: Client, message):
|
741
|
-
#
|
742
|
-
# spot
|
743
|
-
#
|
744
|
-
# {
|
745
|
-
# "action":"push",
|
746
|
-
# "ch":"orders#btcusdt", # or "orders#*" for global subscriptions
|
747
|
-
# "data": {
|
748
|
-
# "orderSource": "spot-web",
|
749
|
-
# "orderCreateTime": 1645116048355,
|
750
|
-
# "accountId": 44234548,
|
751
|
-
# "orderPrice": "100",
|
752
|
-
# "orderSize": "0.05",
|
753
|
-
# "symbol": "ethusdt",
|
754
|
-
# "type": "buy-limit",
|
755
|
-
# "orderId": "478861479986886",
|
756
|
-
# "eventType": "creation",
|
757
|
-
# "clientOrderId": '',
|
758
|
-
# "orderStatus": "submitted"
|
759
|
-
# }
|
760
|
-
# }
|
761
|
-
#
|
762
|
-
# spot wrapped trade
|
763
|
-
#
|
764
|
-
# {
|
765
|
-
# "action": "push",
|
766
|
-
# "ch": "orders#ltcusdt",
|
767
|
-
# "data": {
|
768
|
-
# "tradePrice": "130.01",
|
769
|
-
# "tradeVolume": "0.0385",
|
770
|
-
# "tradeTime": 1648714741525,
|
771
|
-
# "aggressor": True,
|
772
|
-
# "execAmt": "0.0385",
|
773
|
-
# "orderSource": "spot-web",
|
774
|
-
# "orderSize": "0.0385",
|
775
|
-
# "remainAmt": "0",
|
776
|
-
# "tradeId": 101541578884,
|
777
|
-
# "symbol": "ltcusdt",
|
778
|
-
# "type": "sell-market",
|
779
|
-
# "eventType": "trade",
|
780
|
-
# "clientOrderId": '',
|
781
|
-
# "orderStatus": "filled",
|
782
|
-
# "orderId": 509835753860328
|
783
|
-
# }
|
784
|
-
# }
|
785
|
-
#
|
786
|
-
# non spot order
|
787
|
-
#
|
788
|
-
# {
|
789
|
-
# "contract_type": "swap",
|
790
|
-
# "pair": "LTC-USDT",
|
791
|
-
# "business_type": "swap",
|
792
|
-
# "op": "notify",
|
793
|
-
# "topic": "orders_cross.ltc-usdt",
|
794
|
-
# "ts": 1650354508696,
|
795
|
-
# "symbol": "LTC",
|
796
|
-
# "contract_code": "LTC-USDT",
|
797
|
-
# "volume": 1,
|
798
|
-
# "price": 110.34,
|
799
|
-
# "order_price_type": "lightning",
|
800
|
-
# "direction": "sell",
|
801
|
-
# "offset": "close",
|
802
|
-
# "status": 6,
|
803
|
-
# "lever_rate": 1,
|
804
|
-
# "order_id": "966002354015051776",
|
805
|
-
# "order_id_str": "966002354015051776",
|
806
|
-
# "client_order_id": null,
|
807
|
-
# "order_source": "web",
|
808
|
-
# "order_type": 1,
|
809
|
-
# "created_at": 1650354508649,
|
810
|
-
# "trade_volume": 1,
|
811
|
-
# "trade_turnover": 11.072,
|
812
|
-
# "fee": -0.005536,
|
813
|
-
# "trade_avg_price": 110.72,
|
814
|
-
# "margin_frozen": 0,
|
815
|
-
# "profit": -0.045,
|
816
|
-
# "trade": [
|
817
|
-
# {
|
818
|
-
# "trade_fee": -0.005536,
|
819
|
-
# "fee_asset": "USDT",
|
820
|
-
# "real_profit": 0.473,
|
821
|
-
# "profit": -0.045,
|
822
|
-
# "trade_id": 86678766507,
|
823
|
-
# "id": "86678766507-966002354015051776-1",
|
824
|
-
# "trade_volume": 1,
|
825
|
-
# "trade_price": 110.72,
|
826
|
-
# "trade_turnover": 11.072,
|
827
|
-
# "created_at": 1650354508656,
|
828
|
-
# "role": "taker"
|
829
|
-
# }
|
830
|
-
# ],
|
831
|
-
# "canceled_at": 0,
|
832
|
-
# "fee_asset": "USDT",
|
833
|
-
# "margin_asset": "USDT",
|
834
|
-
# "uid": "359305390",
|
835
|
-
# "liquidation_type": "0",
|
836
|
-
# "margin_mode": "cross",
|
837
|
-
# "margin_account": "USDT",
|
838
|
-
# "is_tpsl": 0,
|
839
|
-
# "real_profit": 0.473,
|
840
|
-
# "trade_partition": "USDT",
|
841
|
-
# "reduce_only": 1
|
842
|
-
# }
|
843
|
-
#
|
844
|
-
#
|
845
|
-
messageHash = self.safe_string_2(message, 'ch', 'topic')
|
846
|
-
data = self.safe_value(message, 'data')
|
847
|
-
marketId = self.safe_string(message, 'contract_code')
|
848
|
-
if marketId is None:
|
849
|
-
marketId = self.safe_string(data, 'symbol')
|
850
|
-
market = self.safe_market(marketId)
|
851
|
-
parsedOrder = None
|
852
|
-
if data is not None:
|
853
|
-
# spot updates
|
854
|
-
eventType = self.safe_string(data, 'eventType')
|
855
|
-
if eventType == 'trade':
|
856
|
-
# when a spot order is filled we get an update message
|
857
|
-
# with the trade info
|
858
|
-
parsedTrade = self.parse_order_trade(data, market)
|
859
|
-
# inject trade in existing order by faking an order object
|
860
|
-
orderId = self.safe_string(parsedTrade, 'order')
|
861
|
-
trades = [parsedTrade]
|
862
|
-
order = {
|
863
|
-
'id': orderId,
|
864
|
-
'trades': trades,
|
865
|
-
'status': 'closed',
|
866
|
-
'symbol': market['symbol'],
|
867
|
-
}
|
868
|
-
parsedOrder = order
|
869
|
-
else:
|
870
|
-
parsedOrder = self.parse_ws_order(data, market)
|
871
|
-
else:
|
872
|
-
# contract branch
|
873
|
-
parsedOrder = self.parse_ws_order(message, market)
|
874
|
-
rawTrades = self.safe_value(message, 'trade', [])
|
875
|
-
tradesLength = len(rawTrades)
|
876
|
-
if tradesLength > 0:
|
877
|
-
tradesObject = {
|
878
|
-
'trades': rawTrades,
|
879
|
-
'ch': messageHash,
|
880
|
-
'symbol': marketId,
|
881
|
-
}
|
882
|
-
# inject order params in every trade
|
883
|
-
extendTradeParams = {
|
884
|
-
'order': self.safe_string(parsedOrder, 'id'),
|
885
|
-
'type': self.safe_string(parsedOrder, 'type'),
|
886
|
-
'side': self.safe_string(parsedOrder, 'side'),
|
887
|
-
}
|
888
|
-
# trades arrive inside an order update
|
889
|
-
# we're forwarding them to handleMyTrade
|
890
|
-
# so they can be properly resolved
|
891
|
-
self.handle_my_trade(client, tradesObject, extendTradeParams)
|
892
|
-
if self.orders is None:
|
893
|
-
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
894
|
-
self.orders = ArrayCacheBySymbolById(limit)
|
895
|
-
cachedOrders = self.orders
|
896
|
-
cachedOrders.append(parsedOrder)
|
897
|
-
client.resolve(self.orders, messageHash)
|
898
|
-
# when we make a global subscription(for contracts only) our message hash can't have a symbol/currency attached
|
899
|
-
# so we're removing it here
|
900
|
-
genericMessageHash = messageHash.replace('.' + market['lowercaseId'], '')
|
901
|
-
genericMessageHash = genericMessageHash.replace('.' + market['lowercaseBaseId'], '')
|
902
|
-
client.resolve(self.orders, genericMessageHash)
|
903
|
-
|
904
|
-
def parse_ws_order(self, order, market=None):
|
905
|
-
#
|
906
|
-
# spot
|
907
|
-
#
|
908
|
-
# {
|
909
|
-
# "orderSource": "spot-web",
|
910
|
-
# "orderCreateTime": 1645116048355, # creating only
|
911
|
-
# "accountId": 44234548,
|
912
|
-
# "orderPrice": "100",
|
913
|
-
# "orderSize": "0.05",
|
914
|
-
# "orderValue": "3.71676361", # market-buy only
|
915
|
-
# "symbol": "ethusdt",
|
916
|
-
# "type": "buy-limit",
|
917
|
-
# "orderId": "478861479986886",
|
918
|
-
# "eventType": "creation",
|
919
|
-
# "clientOrderId": '',
|
920
|
-
# "orderStatus": "submitted"
|
921
|
-
# "lastActTime":1645118621810 # except creating
|
922
|
-
# "execAmt":"0"
|
923
|
-
# }
|
924
|
-
#
|
925
|
-
# swap order
|
926
|
-
#
|
927
|
-
# {
|
928
|
-
# "contract_type": "swap",
|
929
|
-
# "pair": "LTC-USDT",
|
930
|
-
# "business_type": "swap",
|
931
|
-
# "op": "notify",
|
932
|
-
# "topic": "orders_cross.ltc-usdt",
|
933
|
-
# "ts": 1648717911384,
|
934
|
-
# "symbol": "LTC",
|
935
|
-
# "contract_code": "LTC-USDT",
|
936
|
-
# "volume": 1,
|
937
|
-
# "price": 129.13,
|
938
|
-
# "order_price_type": "lightning",
|
939
|
-
# "direction": "sell",
|
940
|
-
# "offset": "close",
|
941
|
-
# "status": 6,
|
942
|
-
# "lever_rate": 5,
|
943
|
-
# "order_id": "959137967397068800",
|
944
|
-
# "order_id_str": "959137967397068800",
|
945
|
-
# "client_order_id": null,
|
946
|
-
# "order_source": "web",
|
947
|
-
# "order_type": 1,
|
948
|
-
# "created_at": 1648717911344,
|
949
|
-
# "trade_volume": 1,
|
950
|
-
# "trade_turnover": 12.952,
|
951
|
-
# "fee": -0.006476,
|
952
|
-
# "trade_avg_price": 129.52,
|
953
|
-
# "margin_frozen": 0,
|
954
|
-
# "profit": -0.005,
|
955
|
-
# "trade": [
|
956
|
-
# {
|
957
|
-
# "trade_fee": -0.006476,
|
958
|
-
# "fee_asset": "USDT",
|
959
|
-
# "real_profit": -0.005,
|
960
|
-
# "profit": -0.005,
|
961
|
-
# "trade_id": 83619995370,
|
962
|
-
# "id": "83619995370-959137967397068800-1",
|
963
|
-
# "trade_volume": 1,
|
964
|
-
# "trade_price": 129.52,
|
965
|
-
# "trade_turnover": 12.952,
|
966
|
-
# "created_at": 1648717911352,
|
967
|
-
# "role": "taker"
|
968
|
-
# }
|
969
|
-
# ],
|
970
|
-
# "canceled_at": 0,
|
971
|
-
# "fee_asset": "USDT",
|
972
|
-
# "margin_asset": "USDT",
|
973
|
-
# "uid": "359305390",
|
974
|
-
# "liquidation_type": "0",
|
975
|
-
# "margin_mode": "cross",
|
976
|
-
# "margin_account": "USDT",
|
977
|
-
# "is_tpsl": 0,
|
978
|
-
# "real_profit": -0.005,
|
979
|
-
# "trade_partition": "USDT",
|
980
|
-
# "reduce_only": 1
|
981
|
-
# }
|
982
|
-
#
|
983
|
-
# {
|
984
|
-
# "op":"notify",
|
985
|
-
# "topic":"orders.ada",
|
986
|
-
# "ts":1604388667226,
|
987
|
-
# "symbol":"ADA",
|
988
|
-
# "contract_type":"quarter",
|
989
|
-
# "contract_code":"ADA201225",
|
990
|
-
# "volume":1,
|
991
|
-
# "price":0.0905,
|
992
|
-
# "order_price_type":"post_only",
|
993
|
-
# "direction":"sell",
|
994
|
-
# "offset":"open",
|
995
|
-
# "status":6,
|
996
|
-
# "lever_rate":20,
|
997
|
-
# "order_id":773207641127878656,
|
998
|
-
# "order_id_str":"773207641127878656",
|
999
|
-
# "client_order_id":null,
|
1000
|
-
# "order_source":"web",
|
1001
|
-
# "order_type":1,
|
1002
|
-
# "created_at":1604388667146,
|
1003
|
-
# "trade_volume":1,
|
1004
|
-
# "trade_turnover":10,
|
1005
|
-
# "fee":-0.022099447513812154,
|
1006
|
-
# "trade_avg_price":0.0905,
|
1007
|
-
# "margin_frozen":0,
|
1008
|
-
# "profit":0,
|
1009
|
-
# "trade":[],
|
1010
|
-
# "canceled_at":0,
|
1011
|
-
# "fee_asset":"ADA",
|
1012
|
-
# "uid":"123456789",
|
1013
|
-
# "liquidation_type":"0",
|
1014
|
-
# "is_tpsl": 0,
|
1015
|
-
# "real_profit": 0
|
1016
|
-
# }
|
1017
|
-
#
|
1018
|
-
lastTradeTimestamp = self.safe_integer_2(order, 'lastActTime', 'ts')
|
1019
|
-
created = self.safe_integer(order, 'orderCreateTime')
|
1020
|
-
marketId = self.safe_string_2(order, 'contract_code', 'symbol')
|
1021
|
-
market = self.safe_market(marketId, market)
|
1022
|
-
symbol = self.safe_symbol(marketId, market)
|
1023
|
-
amount = self.safe_string_2(order, 'orderSize', 'volume')
|
1024
|
-
status = self.parse_order_status(self.safe_string_2(order, 'orderStatus', 'status'))
|
1025
|
-
id = self.safe_string_2(order, 'orderId', 'order_id')
|
1026
|
-
clientOrderId = self.safe_string_2(order, 'clientOrderId', 'client_order_id')
|
1027
|
-
price = self.safe_string_2(order, 'orderPrice', 'price')
|
1028
|
-
filled = self.safe_string(order, 'execAmt')
|
1029
|
-
typeSide = self.safe_string(order, 'type')
|
1030
|
-
feeCost = self.safe_string(order, 'fee')
|
1031
|
-
fee = None
|
1032
|
-
if feeCost is not None:
|
1033
|
-
feeCurrencyId = self.safe_string(order, 'fee_asset')
|
1034
|
-
fee = {
|
1035
|
-
'cost': feeCost,
|
1036
|
-
'currency': self.safe_currency_code(feeCurrencyId),
|
1037
|
-
}
|
1038
|
-
avgPrice = self.safe_string(order, 'trade_avg_price')
|
1039
|
-
rawTrades = self.safe_value(order, 'trade')
|
1040
|
-
typeSideParts = []
|
1041
|
-
if typeSide is not None:
|
1042
|
-
typeSideParts = typeSide.split('-')
|
1043
|
-
type = self.safe_string_lower(typeSideParts, 1)
|
1044
|
-
if type is None:
|
1045
|
-
type = self.safe_string(order, 'order_price_type')
|
1046
|
-
side = self.safe_string_lower(typeSideParts, 0)
|
1047
|
-
if side is None:
|
1048
|
-
side = self.safe_string(order, 'direction')
|
1049
|
-
cost = self.safe_string(order, 'orderValue')
|
1050
|
-
return self.safe_order({
|
1051
|
-
'info': order,
|
1052
|
-
'id': id,
|
1053
|
-
'clientOrderId': clientOrderId,
|
1054
|
-
'timestamp': created,
|
1055
|
-
'datetime': self.iso8601(created),
|
1056
|
-
'lastTradeTimestamp': lastTradeTimestamp,
|
1057
|
-
'status': status,
|
1058
|
-
'symbol': symbol,
|
1059
|
-
'type': type,
|
1060
|
-
'timeInForce': None,
|
1061
|
-
'postOnly': None,
|
1062
|
-
'side': side,
|
1063
|
-
'price': price,
|
1064
|
-
'amount': amount,
|
1065
|
-
'filled': filled,
|
1066
|
-
'remaining': None,
|
1067
|
-
'cost': cost,
|
1068
|
-
'fee': fee,
|
1069
|
-
'average': avgPrice,
|
1070
|
-
'trades': rawTrades,
|
1071
|
-
}, market)
|
1072
|
-
|
1073
|
-
def parse_order_trade(self, trade, market=None):
|
1074
|
-
# spot private wrapped trade
|
1075
|
-
#
|
1076
|
-
# {
|
1077
|
-
# "tradePrice": "130.01",
|
1078
|
-
# "tradeVolume": "0.0385",
|
1079
|
-
# "tradeTime": 1648714741525,
|
1080
|
-
# "aggressor": True,
|
1081
|
-
# "execAmt": "0.0385",
|
1082
|
-
# "orderSource": "spot-web",
|
1083
|
-
# "orderSize": "0.0385",
|
1084
|
-
# "remainAmt": "0",
|
1085
|
-
# "tradeId": 101541578884,
|
1086
|
-
# "symbol": "ltcusdt",
|
1087
|
-
# "type": "sell-market",
|
1088
|
-
# "eventType": "trade",
|
1089
|
-
# "clientOrderId": '',
|
1090
|
-
# "orderStatus": "filled",
|
1091
|
-
# "orderId": 509835753860328
|
1092
|
-
# }
|
1093
|
-
#
|
1094
|
-
market = self.safe_market(None, market)
|
1095
|
-
symbol = market['symbol']
|
1096
|
-
tradeId = self.safe_string(trade, 'tradeId')
|
1097
|
-
price = self.safe_string(trade, 'tradePrice')
|
1098
|
-
amount = self.safe_string(trade, 'tradeVolume')
|
1099
|
-
order = self.safe_string(trade, 'orderId')
|
1100
|
-
timestamp = self.safe_integer(trade, 'tradeTime')
|
1101
|
-
type = self.safe_string(trade, 'type')
|
1102
|
-
side = None
|
1103
|
-
if type is not None:
|
1104
|
-
typeParts = type.split('-')
|
1105
|
-
side = typeParts[0]
|
1106
|
-
type = typeParts[1]
|
1107
|
-
aggressor = self.safe_value(trade, 'aggressor')
|
1108
|
-
takerOrMaker = None
|
1109
|
-
if aggressor is not None:
|
1110
|
-
takerOrMaker = 'taker' if aggressor else 'maker'
|
1111
|
-
return self.safe_trade({
|
1112
|
-
'info': trade,
|
1113
|
-
'timestamp': timestamp,
|
1114
|
-
'datetime': self.iso8601(timestamp),
|
1115
|
-
'symbol': symbol,
|
1116
|
-
'id': tradeId,
|
1117
|
-
'order': order,
|
1118
|
-
'type': type,
|
1119
|
-
'takerOrMaker': takerOrMaker,
|
1120
|
-
'side': side,
|
1121
|
-
'price': price,
|
1122
|
-
'amount': amount,
|
1123
|
-
'cost': None,
|
1124
|
-
'fee': None,
|
1125
|
-
}, market)
|
1126
|
-
|
1127
|
-
async def watch_positions(self, symbols: Optional[List[str]] = None, since: Optional[int] = None, limit: Optional[int] = None, params={}):
|
1128
|
-
"""
|
1129
|
-
:see: https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7de1c-77b5-11ed-9966-0242ac110003
|
1130
|
-
:see: https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7df0f-77b5-11ed-9966-0242ac110003
|
1131
|
-
:see: https://www.huobi.com/en-in/opend/newApiPages/?id=28c34a7d-77ae-11ed-9966-0242ac110003
|
1132
|
-
:see: https://www.huobi.com/en-in/opend/newApiPages/?id=5d5156b5-77b6-11ed-9966-0242ac110003
|
1133
|
-
watch all open positions. Note: huobi has one channel for each marginMode and type
|
1134
|
-
:param str[]|None symbols: list of unified market symbols
|
1135
|
-
:param dict params: extra parameters specific to the huobi api endpoint
|
1136
|
-
:returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
|
1137
|
-
"""
|
1138
|
-
await self.load_markets()
|
1139
|
-
market = None
|
1140
|
-
messageHash = ''
|
1141
|
-
if not self.is_empty(symbols):
|
1142
|
-
market = self.get_market_from_symbols(symbols)
|
1143
|
-
messageHash = '::' + ','.join(symbols)
|
1144
|
-
type = None
|
1145
|
-
subType = None
|
1146
|
-
if market is not None:
|
1147
|
-
type = market['type']
|
1148
|
-
subType = 'linear' if market['linear'] else 'inverse'
|
1149
|
-
else:
|
1150
|
-
type, params = self.handle_market_type_and_params('watchPositions', market, params)
|
1151
|
-
if type == 'spot':
|
1152
|
-
type = 'future'
|
1153
|
-
subType, params = self.handle_option_and_params(params, 'watchPositions', 'subType', subType)
|
1154
|
-
symbols = self.market_symbols(symbols)
|
1155
|
-
marginMode = None
|
1156
|
-
marginMode, params = self.handle_margin_mode_and_params('watchPositions', params, 'cross')
|
1157
|
-
isLinear = (subType == 'linear')
|
1158
|
-
url = self.get_url_by_market_type(type, isLinear, True)
|
1159
|
-
messageHash = marginMode + ':positions' + messageHash
|
1160
|
-
channel = 'positions_cross.*' if (marginMode == 'cross') else 'positions.*'
|
1161
|
-
newPositions = await self.subscribe_private(channel, messageHash, type, subType, params)
|
1162
|
-
if self.newUpdates:
|
1163
|
-
return newPositions
|
1164
|
-
return self.filter_by_symbols_since_limit(self.positions[url][marginMode], symbols, since, limit, False)
|
1165
|
-
|
1166
|
-
def handle_positions(self, client, message):
|
1167
|
-
#
|
1168
|
-
# {
|
1169
|
-
# op: 'notify',
|
1170
|
-
# topic: 'positions_cross',
|
1171
|
-
# ts: 1696767149650,
|
1172
|
-
# event: 'snapshot',
|
1173
|
-
# data: [
|
1174
|
-
# {
|
1175
|
-
# contract_type: 'swap',
|
1176
|
-
# pair: 'BTC-USDT',
|
1177
|
-
# business_type: 'swap',
|
1178
|
-
# liquidation_price: null,
|
1179
|
-
# symbol: 'BTC',
|
1180
|
-
# contract_code: 'BTC-USDT',
|
1181
|
-
# volume: 1,
|
1182
|
-
# available: 1,
|
1183
|
-
# frozen: 0,
|
1184
|
-
# cost_open: 27802.2,
|
1185
|
-
# cost_hold: 27802.2,
|
1186
|
-
# profit_unreal: 0.0175,
|
1187
|
-
# profit_rate: 0.000629446590557581,
|
1188
|
-
# profit: 0.0175,
|
1189
|
-
# margin_asset: 'USDT',
|
1190
|
-
# position_margin: 27.8197,
|
1191
|
-
# lever_rate: 1,
|
1192
|
-
# direction: 'buy',
|
1193
|
-
# last_price: 27819.7,
|
1194
|
-
# margin_mode: 'cross',
|
1195
|
-
# margin_account: 'USDT',
|
1196
|
-
# trade_partition: 'USDT',
|
1197
|
-
# position_mode: 'dual_side'
|
1198
|
-
# },
|
1199
|
-
# ]
|
1200
|
-
# }
|
1201
|
-
#
|
1202
|
-
url = client.url
|
1203
|
-
topic = self.safe_string(message, 'topic', '')
|
1204
|
-
marginMode = 'cross' if (topic == 'positions_cross') else 'isolated'
|
1205
|
-
if self.positions is None:
|
1206
|
-
self.positions = {}
|
1207
|
-
clientPositions = self.safe_value(self.positions, url)
|
1208
|
-
if clientPositions is None:
|
1209
|
-
self.positions[url] = {}
|
1210
|
-
clientMarginModePositions = self.safe_value(clientPositions, marginMode)
|
1211
|
-
if clientMarginModePositions is None:
|
1212
|
-
self.positions[url][marginMode] = ArrayCacheBySymbolBySide()
|
1213
|
-
cache = self.positions[url][marginMode]
|
1214
|
-
rawPositions = self.safe_value(message, 'data', [])
|
1215
|
-
newPositions = []
|
1216
|
-
timestamp = self.safe_integer(message, 'ts')
|
1217
|
-
for i in range(0, len(rawPositions)):
|
1218
|
-
rawPosition = rawPositions[i]
|
1219
|
-
position = self.parse_position(rawPosition)
|
1220
|
-
position['timestamp'] = timestamp
|
1221
|
-
position['datetime'] = self.iso8601(timestamp)
|
1222
|
-
newPositions.append(position)
|
1223
|
-
cache.append(position)
|
1224
|
-
messageHashes = self.find_message_hashes(client, marginMode + ':positions::')
|
1225
|
-
for i in range(0, len(messageHashes)):
|
1226
|
-
messageHash = messageHashes[i]
|
1227
|
-
parts = messageHash.split('::')
|
1228
|
-
symbolsString = parts[1]
|
1229
|
-
symbols = symbolsString.split(',')
|
1230
|
-
positions = self.filter_by_array(newPositions, 'symbol', symbols, False)
|
1231
|
-
if not self.is_empty(positions):
|
1232
|
-
client.resolve(positions, messageHash)
|
1233
|
-
client.resolve(newPositions, marginMode + ':positions')
|
1234
|
-
|
1235
|
-
async def watch_balance(self, params={}):
|
1236
|
-
"""
|
1237
|
-
watch balance and get the amount of funds available for trading or funds locked in orders
|
1238
|
-
:param dict [params]: extra parameters specific to the huobi api endpoint
|
1239
|
-
:returns dict: a `balance structure <https://github.com/ccxt/ccxt/wiki/Manual#balance-structure>`
|
1240
|
-
"""
|
1241
|
-
type = None
|
1242
|
-
type, params = self.handle_market_type_and_params('watchBalance', None, params)
|
1243
|
-
subType = None
|
1244
|
-
subType, params = self.handle_sub_type_and_params('watchBalance', None, params, 'linear')
|
1245
|
-
isUnifiedAccount = self.safe_value_2(params, 'isUnifiedAccount', 'unified', False)
|
1246
|
-
params = self.omit(params, ['isUnifiedAccount', 'unified'])
|
1247
|
-
await self.load_markets()
|
1248
|
-
messageHash = None
|
1249
|
-
channel = None
|
1250
|
-
marginMode = None
|
1251
|
-
if type == 'spot':
|
1252
|
-
mode = self.safe_string_2(self.options, 'watchBalance', 'mode', '2')
|
1253
|
-
mode = self.safe_string(params, 'mode', mode)
|
1254
|
-
messageHash = 'accounts.update' + '#' + mode
|
1255
|
-
channel = messageHash
|
1256
|
-
else:
|
1257
|
-
symbol = self.safe_string(params, 'symbol')
|
1258
|
-
currency = self.safe_string(params, 'currency')
|
1259
|
-
market = self.market(symbol) if (symbol is not None) else None
|
1260
|
-
currencyCode = self.currency(currency) if (currency is not None) else None
|
1261
|
-
marginMode = self.safe_string(params, 'margin', 'cross')
|
1262
|
-
params = self.omit(params, ['currency', 'symbol', 'margin'])
|
1263
|
-
prefix = 'accounts'
|
1264
|
-
messageHash = prefix
|
1265
|
-
if subType == 'linear':
|
1266
|
-
if isUnifiedAccount:
|
1267
|
-
# usdt contracts account
|
1268
|
-
prefix = 'accounts_unify'
|
1269
|
-
messageHash = prefix
|
1270
|
-
channel = prefix + '.' + 'usdt'
|
1271
|
-
else:
|
1272
|
-
# usdt contracts account
|
1273
|
-
prefix = prefix + '_cross' if (marginMode == 'cross') else prefix
|
1274
|
-
messageHash = prefix
|
1275
|
-
if marginMode == 'isolated':
|
1276
|
-
# isolated margin only allows filtering by symbol3
|
1277
|
-
if symbol is not None:
|
1278
|
-
messageHash += '.' + market['id']
|
1279
|
-
channel = messageHash
|
1280
|
-
else:
|
1281
|
-
# subscribe to all
|
1282
|
-
channel = prefix + '.' + '*'
|
1283
|
-
else:
|
1284
|
-
# cross margin
|
1285
|
-
if currencyCode is not None:
|
1286
|
-
channel = prefix + '.' + currencyCode['id']
|
1287
|
-
messageHash = channel
|
1288
|
-
else:
|
1289
|
-
# subscribe to all
|
1290
|
-
channel = prefix + '.' + '*'
|
1291
|
-
elif type == 'future':
|
1292
|
-
# inverse futures account
|
1293
|
-
if currencyCode is not None:
|
1294
|
-
messageHash += '.' + currencyCode['id']
|
1295
|
-
channel = messageHash
|
1296
|
-
else:
|
1297
|
-
# subscribe to all
|
1298
|
-
channel = prefix + '.' + '*'
|
1299
|
-
else:
|
1300
|
-
# inverse swaps account
|
1301
|
-
if market is not None:
|
1302
|
-
messageHash += '.' + market['id']
|
1303
|
-
channel = messageHash
|
1304
|
-
else:
|
1305
|
-
# subscribe to all
|
1306
|
-
channel = prefix + '.' + '*'
|
1307
|
-
subscriptionParams = {
|
1308
|
-
'type': type,
|
1309
|
-
'subType': subType,
|
1310
|
-
'margin': marginMode,
|
1311
|
-
}
|
1312
|
-
# we are differentiating the channel from the messageHash for global subscriptions(*)
|
1313
|
-
# because huobi returns a different topic than the topic sent. Example: we send
|
1314
|
-
# "accounts.*" and "accounts" is returned so we're setting channel = "accounts.*" and
|
1315
|
-
# messageHash = "accounts" allowing handleBalance to freely resolve the topic in the message
|
1316
|
-
return await self.subscribe_private(channel, messageHash, type, subType, params, subscriptionParams)
|
1317
|
-
|
1318
|
-
def handle_balance(self, client: Client, message):
|
1319
|
-
# spot
|
1320
|
-
#
|
1321
|
-
# {
|
1322
|
-
# "action": "push",
|
1323
|
-
# "ch": "accounts.update#0",
|
1324
|
-
# "data": {
|
1325
|
-
# "currency": "btc",
|
1326
|
-
# "accountId": 123456,
|
1327
|
-
# "balance": "23.111",
|
1328
|
-
# "available": "2028.699426619837209087",
|
1329
|
-
# "changeType": "transfer",
|
1330
|
-
# "accountType":"trade",
|
1331
|
-
# "seqNum": "86872993928",
|
1332
|
-
# "changeTime": 1568601800000
|
1333
|
-
# }
|
1334
|
-
# }
|
1335
|
-
#
|
1336
|
-
# inverse future
|
1337
|
-
#
|
1338
|
-
# {
|
1339
|
-
# "op":"notify",
|
1340
|
-
# "topic":"accounts.ada",
|
1341
|
-
# "ts":1604388667226,
|
1342
|
-
# "event":"order.match",
|
1343
|
-
# "data":[
|
1344
|
-
# {
|
1345
|
-
# "symbol":"ADA",
|
1346
|
-
# "margin_balance":446.417641681222726716,
|
1347
|
-
# "margin_static":445.554085945257745136,
|
1348
|
-
# "margin_position":11.049723756906077348,
|
1349
|
-
# "margin_frozen":0,
|
1350
|
-
# "margin_available":435.367917924316649368,
|
1351
|
-
# "profit_real":21.627049781983019459,
|
1352
|
-
# "profit_unreal":0.86355573596498158,
|
1353
|
-
# "risk_rate":40.000796572150656768,
|
1354
|
-
# "liquidation_price":0.018674308027108984,
|
1355
|
-
# "withdraw_available":423.927036163274725677,
|
1356
|
-
# "lever_rate":20,
|
1357
|
-
# "adjust_factor":0.4
|
1358
|
-
# }
|
1359
|
-
# ],
|
1360
|
-
# "uid":"123456789"
|
1361
|
-
# }
|
1362
|
-
#
|
1363
|
-
# usdt / linear future, swap
|
1364
|
-
#
|
1365
|
-
# {
|
1366
|
-
# "op":"notify",
|
1367
|
-
# "topic":"accounts.btc-usdt", # or "accounts" for global subscriptions
|
1368
|
-
# "ts":1603711370689,
|
1369
|
-
# "event":"order.open",
|
1370
|
-
# "data":[
|
1371
|
-
# {
|
1372
|
-
# "margin_mode":"cross",
|
1373
|
-
# "margin_account":"USDT",
|
1374
|
-
# "margin_asset":"USDT",
|
1375
|
-
# "margin_balance":30.959342395,
|
1376
|
-
# "margin_static":30.959342395,
|
1377
|
-
# "margin_position":0,
|
1378
|
-
# "margin_frozen":10,
|
1379
|
-
# "profit_real":0,
|
1380
|
-
# "profit_unreal":0,
|
1381
|
-
# "withdraw_available":20.959342395,
|
1382
|
-
# "risk_rate":153.796711975,
|
1383
|
-
# "position_mode":"dual_side",
|
1384
|
-
# "contract_detail":[
|
1385
|
-
# {
|
1386
|
-
# "symbol":"LTC",
|
1387
|
-
# "contract_code":"LTC-USDT",
|
1388
|
-
# "margin_position":0,
|
1389
|
-
# "margin_frozen":0,
|
1390
|
-
# "margin_available":20.959342395,
|
1391
|
-
# "profit_unreal":0,
|
1392
|
-
# "liquidation_price":null,
|
1393
|
-
# "lever_rate":1,
|
1394
|
-
# "adjust_factor":0.01,
|
1395
|
-
# "contract_type":"swap",
|
1396
|
-
# "pair":"LTC-USDT",
|
1397
|
-
# "business_type":"swap",
|
1398
|
-
# "trade_partition":"USDT"
|
1399
|
-
# },
|
1400
|
-
# ],
|
1401
|
-
# "futures_contract_detail":[],
|
1402
|
-
# }
|
1403
|
-
# ]
|
1404
|
-
# }
|
1405
|
-
#
|
1406
|
-
# inverse future
|
1407
|
-
#
|
1408
|
-
# {
|
1409
|
-
# "op":"notify",
|
1410
|
-
# "topic":"accounts.ada",
|
1411
|
-
# "ts":1604388667226,
|
1412
|
-
# "event":"order.match",
|
1413
|
-
# "data":[
|
1414
|
-
# {
|
1415
|
-
# "symbol":"ADA",
|
1416
|
-
# "margin_balance":446.417641681222726716,
|
1417
|
-
# "margin_static":445.554085945257745136,
|
1418
|
-
# "margin_position":11.049723756906077348,
|
1419
|
-
# "margin_frozen":0,
|
1420
|
-
# "margin_available":435.367917924316649368,
|
1421
|
-
# "profit_real":21.627049781983019459,
|
1422
|
-
# "profit_unreal":0.86355573596498158,
|
1423
|
-
# "risk_rate":40.000796572150656768,
|
1424
|
-
# "liquidation_price":0.018674308027108984,
|
1425
|
-
# "withdraw_available":423.927036163274725677,
|
1426
|
-
# "lever_rate":20,
|
1427
|
-
# "adjust_factor":0.4
|
1428
|
-
# }
|
1429
|
-
# ],
|
1430
|
-
# "uid":"123456789"
|
1431
|
-
# }
|
1432
|
-
#
|
1433
|
-
channel = self.safe_string(message, 'ch')
|
1434
|
-
data = self.safe_value(message, 'data', [])
|
1435
|
-
timestamp = self.safe_integer(data, 'changeTime', self.safe_integer(message, 'ts'))
|
1436
|
-
self.balance['timestamp'] = timestamp
|
1437
|
-
self.balance['datetime'] = self.iso8601(timestamp)
|
1438
|
-
self.balance['info'] = data
|
1439
|
-
if channel is not None:
|
1440
|
-
# spot balance
|
1441
|
-
currencyId = self.safe_string(data, 'currency')
|
1442
|
-
code = self.safe_currency_code(currencyId)
|
1443
|
-
account = self.account()
|
1444
|
-
account['free'] = self.safe_string(data, 'available')
|
1445
|
-
account['total'] = self.safe_string(data, 'balance')
|
1446
|
-
self.balance[code] = account
|
1447
|
-
self.balance = self.safe_balance(self.balance)
|
1448
|
-
client.resolve(self.balance, channel)
|
1449
|
-
else:
|
1450
|
-
# contract balance
|
1451
|
-
dataLength = len(data)
|
1452
|
-
if dataLength == 0:
|
1453
|
-
return
|
1454
|
-
first = self.safe_value(data, 0, {})
|
1455
|
-
topic = self.safe_string(message, 'topic')
|
1456
|
-
splitTopic = topic.split('.')
|
1457
|
-
messageHash = self.safe_string(splitTopic, 0)
|
1458
|
-
subscription = self.safe_value_2(client.subscriptions, messageHash, messageHash + '.*')
|
1459
|
-
if subscription is None:
|
1460
|
-
# if subscription not found means that we subscribed to a specific currency/symbol
|
1461
|
-
# and we use the first data entry to find it
|
1462
|
-
# Example: topic = 'accounts'
|
1463
|
-
# client.subscription hash = 'accounts.usdt'
|
1464
|
-
# we do 'accounts' + '.' + data[0]]['margin_asset'] to get it
|
1465
|
-
currencyId = self.safe_string_2(first, 'margin_asset', 'symbol')
|
1466
|
-
messageHash += '.' + currencyId.lower()
|
1467
|
-
subscription = self.safe_value(client.subscriptions, messageHash)
|
1468
|
-
type = self.safe_string(subscription, 'type')
|
1469
|
-
subType = self.safe_string(subscription, 'subType')
|
1470
|
-
if topic == 'accounts_unify':
|
1471
|
-
# {
|
1472
|
-
# "margin_asset": "USDT",
|
1473
|
-
# "margin_static": 10,
|
1474
|
-
# "cross_margin_static": 10,
|
1475
|
-
# "margin_balance": 10,
|
1476
|
-
# "cross_profit_unreal": 0,
|
1477
|
-
# "margin_frozen": 0,
|
1478
|
-
# "withdraw_available": 10,
|
1479
|
-
# "cross_risk_rate": null,
|
1480
|
-
# "cross_swap": [],
|
1481
|
-
# "cross_future": [],
|
1482
|
-
# "isolated_swap": []
|
1483
|
-
# }
|
1484
|
-
marginAsset = self.safe_string(first, 'margin_asset')
|
1485
|
-
code = self.safe_currency_code(marginAsset)
|
1486
|
-
marginFrozen = self.safe_string(first, 'margin_frozen')
|
1487
|
-
unifiedAccount = self.account()
|
1488
|
-
unifiedAccount['free'] = self.safe_string(first, 'withdraw_available')
|
1489
|
-
unifiedAccount['used'] = marginFrozen
|
1490
|
-
self.balance[code] = unifiedAccount
|
1491
|
-
self.balance = self.safe_balance(self.balance)
|
1492
|
-
client.resolve(self.balance, 'accounts_unify')
|
1493
|
-
elif subType == 'linear':
|
1494
|
-
margin = self.safe_string(subscription, 'margin')
|
1495
|
-
if margin == 'cross':
|
1496
|
-
fieldName = 'futures_contract_detail' if (type == 'future') else 'contract_detail'
|
1497
|
-
balances = self.safe_value(first, fieldName, [])
|
1498
|
-
balancesLength = len(balances)
|
1499
|
-
if balancesLength > 0:
|
1500
|
-
for i in range(0, len(balances)):
|
1501
|
-
balance = balances[i]
|
1502
|
-
marketId = self.safe_string_2(balance, 'contract_code', 'margin_account')
|
1503
|
-
market = self.safe_market(marketId)
|
1504
|
-
currencyId = self.safe_string(balance, 'margin_asset')
|
1505
|
-
currency = self.safe_currency(currencyId)
|
1506
|
-
code = self.safe_string(market, 'settle', currency['code'])
|
1507
|
-
# the exchange outputs positions for delisted markets
|
1508
|
-
# https://www.huobi.com/support/en-us/detail/74882968522337
|
1509
|
-
# we skip it if the market was delisted
|
1510
|
-
if code is not None:
|
1511
|
-
account = self.account()
|
1512
|
-
account['free'] = self.safe_string_2(balance, 'margin_balance', 'margin_available')
|
1513
|
-
account['used'] = self.safe_string(balance, 'margin_frozen')
|
1514
|
-
accountsByCode = {}
|
1515
|
-
accountsByCode[code] = account
|
1516
|
-
symbol = market['symbol']
|
1517
|
-
self.balance[symbol] = self.safe_balance(accountsByCode)
|
1518
|
-
else:
|
1519
|
-
# isolated margin
|
1520
|
-
for i in range(0, len(data)):
|
1521
|
-
isolatedBalance = data[i]
|
1522
|
-
account = self.account()
|
1523
|
-
account['free'] = self.safe_string(isolatedBalance, 'margin_balance', 'margin_available')
|
1524
|
-
account['used'] = self.safe_string(isolatedBalance, 'margin_frozen')
|
1525
|
-
currencyId = self.safe_string_2(isolatedBalance, 'margin_asset', 'symbol')
|
1526
|
-
code = self.safe_currency_code(currencyId)
|
1527
|
-
self.balance[code] = account
|
1528
|
-
self.balance = self.safe_balance(self.balance)
|
1529
|
-
else:
|
1530
|
-
# inverse branch
|
1531
|
-
for i in range(0, len(data)):
|
1532
|
-
balance = data[i]
|
1533
|
-
currencyId = self.safe_string(balance, 'symbol')
|
1534
|
-
code = self.safe_currency_code(currencyId)
|
1535
|
-
account = self.account()
|
1536
|
-
account['free'] = self.safe_string(balance, 'margin_available')
|
1537
|
-
account['used'] = self.safe_string(balance, 'margin_frozen')
|
1538
|
-
self.balance[code] = account
|
1539
|
-
self.balance = self.safe_balance(self.balance)
|
1540
|
-
client.resolve(self.balance, messageHash)
|
1541
|
-
|
1542
|
-
def handle_subscription_status(self, client: Client, message):
|
1543
|
-
#
|
1544
|
-
# {
|
1545
|
-
# "id": 1583414227,
|
1546
|
-
# "status": "ok",
|
1547
|
-
# "subbed": "market.btcusdt.mbp.150",
|
1548
|
-
# "ts": 1583414229143
|
1549
|
-
# }
|
1550
|
-
#
|
1551
|
-
id = self.safe_string(message, 'id')
|
1552
|
-
subscriptionsById = self.index_by(client.subscriptions, 'id')
|
1553
|
-
subscription = self.safe_value(subscriptionsById, id)
|
1554
|
-
if subscription is not None:
|
1555
|
-
method = self.safe_value(subscription, 'method')
|
1556
|
-
if method is not None:
|
1557
|
-
return method(client, message, subscription)
|
1558
|
-
# clean up
|
1559
|
-
if id in client.subscriptions:
|
1560
|
-
del client.subscriptions[id]
|
1561
|
-
return message
|
1562
|
-
|
1563
|
-
def handle_system_status(self, client: Client, message):
|
1564
|
-
#
|
1565
|
-
# todo: answer the question whether handleSystemStatus should be renamed
|
1566
|
-
# and unified for any usage pattern that
|
1567
|
-
# involves system status and maintenance updates
|
1568
|
-
#
|
1569
|
-
# {
|
1570
|
-
# "id": "1578090234088", # connectId
|
1571
|
-
# "type": "welcome",
|
1572
|
-
# }
|
1573
|
-
#
|
1574
|
-
return message
|
1575
|
-
|
1576
|
-
def handle_subject(self, client: Client, message):
|
1577
|
-
# spot
|
1578
|
-
# {
|
1579
|
-
# "ch": "market.btcusdt.mbp.150",
|
1580
|
-
# "ts": 1583472025885,
|
1581
|
-
# "tick": {
|
1582
|
-
# "seqNum": 104998984994,
|
1583
|
-
# "prevSeqNum": 104998984977,
|
1584
|
-
# "bids": [
|
1585
|
-
# [9058.27, 0],
|
1586
|
-
# [9058.43, 0],
|
1587
|
-
# [9058.99, 0],
|
1588
|
-
# ],
|
1589
|
-
# "asks": [
|
1590
|
-
# [9084.27, 0.2],
|
1591
|
-
# [9085.69, 0],
|
1592
|
-
# [9085.81, 0],
|
1593
|
-
# ]
|
1594
|
-
# }
|
1595
|
-
# }
|
1596
|
-
# non spot
|
1597
|
-
#
|
1598
|
-
# {
|
1599
|
-
# "ch":"market.BTC220218.depth.size_150.high_freq",
|
1600
|
-
# "tick":{
|
1601
|
-
# "asks":[],
|
1602
|
-
# "bids":[
|
1603
|
-
# [43445.74,1],
|
1604
|
-
# [43444.48,0],
|
1605
|
-
# [40593.92,9]
|
1606
|
-
# ],
|
1607
|
-
# "ch":"market.BTC220218.depth.size_150.high_freq",
|
1608
|
-
# "event":"update",
|
1609
|
-
# "id":152727500274,
|
1610
|
-
# "mrid":152727500274,
|
1611
|
-
# "ts":1645023376098,
|
1612
|
-
# "version":37536690
|
1613
|
-
# },
|
1614
|
-
# "ts":1645023376098
|
1615
|
-
# }
|
1616
|
-
#
|
1617
|
-
# spot private trade
|
1618
|
-
#
|
1619
|
-
# {
|
1620
|
-
# "action":"push",
|
1621
|
-
# "ch":"trade.clearing#ltcusdt#1",
|
1622
|
-
# "data":{
|
1623
|
-
# "eventType":"trade",
|
1624
|
-
# "symbol":"ltcusdt",
|
1625
|
-
# # ...
|
1626
|
-
# },
|
1627
|
-
# }
|
1628
|
-
#
|
1629
|
-
# spot order
|
1630
|
-
#
|
1631
|
-
# {
|
1632
|
-
# "action":"push",
|
1633
|
-
# "ch":"orders#btcusdt",
|
1634
|
-
# "data": {
|
1635
|
-
# "orderSide":"buy",
|
1636
|
-
# "lastActTime":1583853365586,
|
1637
|
-
# "clientOrderId":"abc123",
|
1638
|
-
# "orderStatus":"rejected",
|
1639
|
-
# "symbol":"btcusdt",
|
1640
|
-
# "eventType":"trigger",
|
1641
|
-
# "errCode": 2002,
|
1642
|
-
# "errMessage":"invalid.client.order.id(NT)"
|
1643
|
-
# }
|
1644
|
-
# }
|
1645
|
-
#
|
1646
|
-
# contract order
|
1647
|
-
#
|
1648
|
-
# {
|
1649
|
-
# "op":"notify",
|
1650
|
-
# "topic":"orders.ada",
|
1651
|
-
# "ts":1604388667226,
|
1652
|
-
# # ?
|
1653
|
-
# }
|
1654
|
-
#
|
1655
|
-
ch = self.safe_value(message, 'ch', '')
|
1656
|
-
parts = ch.split('.')
|
1657
|
-
type = self.safe_string(parts, 0)
|
1658
|
-
if type == 'market':
|
1659
|
-
methodName = self.safe_string(parts, 2)
|
1660
|
-
methods = {
|
1661
|
-
'depth': self.handle_order_book,
|
1662
|
-
'mbp': self.handle_order_book,
|
1663
|
-
'detail': self.handle_ticker,
|
1664
|
-
'bbo': self.handle_ticker,
|
1665
|
-
'ticker': self.handle_ticker,
|
1666
|
-
'trade': self.handle_trades,
|
1667
|
-
'kline': self.handle_ohlcv,
|
1668
|
-
}
|
1669
|
-
method = self.safe_value(methods, methodName)
|
1670
|
-
if method is None:
|
1671
|
-
return message
|
1672
|
-
else:
|
1673
|
-
return method(client, message)
|
1674
|
-
# private spot subjects
|
1675
|
-
privateParts = ch.split('#')
|
1676
|
-
privateType = self.safe_string(privateParts, 0, '')
|
1677
|
-
if privateType == 'trade.clearing':
|
1678
|
-
self.handle_my_trade(client, message)
|
1679
|
-
return
|
1680
|
-
if privateType.find('accounts.update') >= 0:
|
1681
|
-
self.handle_balance(client, message)
|
1682
|
-
return
|
1683
|
-
if privateType == 'orders':
|
1684
|
-
self.handle_order(client, message)
|
1685
|
-
return
|
1686
|
-
# private contract subjects
|
1687
|
-
op = self.safe_string(message, 'op')
|
1688
|
-
if op == 'notify':
|
1689
|
-
topic = self.safe_string(message, 'topic', '')
|
1690
|
-
if topic.find('orders') >= 0:
|
1691
|
-
self.handle_order(client, message)
|
1692
|
-
if topic.find('account') >= 0:
|
1693
|
-
self.handle_balance(client, message)
|
1694
|
-
if topic.find('positions') >= 0:
|
1695
|
-
self.handle_positions(client, message)
|
1696
|
-
|
1697
|
-
async def pong(self, client, message):
|
1698
|
-
#
|
1699
|
-
# {ping: 1583491673714}
|
1700
|
-
# {action: "ping", data: {ts: 1645108204665}}
|
1701
|
-
# {op: "ping", ts: "1645202800015"}
|
1702
|
-
#
|
1703
|
-
try:
|
1704
|
-
ping = self.safe_integer(message, 'ping')
|
1705
|
-
if ping is not None:
|
1706
|
-
await client.send({'pong': ping})
|
1707
|
-
return
|
1708
|
-
action = self.safe_string(message, 'action')
|
1709
|
-
if action == 'ping':
|
1710
|
-
data = self.safe_value(message, 'data')
|
1711
|
-
pingTs = self.safe_integer(data, 'ts')
|
1712
|
-
await client.send({'action': 'pong', 'data': {'ts': pingTs}})
|
1713
|
-
return
|
1714
|
-
op = self.safe_string(message, 'op')
|
1715
|
-
if op == 'ping':
|
1716
|
-
pingTs = self.safe_integer(message, 'ts')
|
1717
|
-
await client.send({'op': 'pong', 'ts': pingTs})
|
1718
|
-
except Exception as e:
|
1719
|
-
error = NetworkError(self.id + ' pong failed ' + self.json(e))
|
1720
|
-
client.reset(error)
|
1721
|
-
|
1722
|
-
def handle_ping(self, client: Client, message):
|
1723
|
-
self.spawn(self.pong, client, message)
|
1724
|
-
|
1725
|
-
def handle_authenticate(self, client: Client, message):
|
1726
|
-
#
|
1727
|
-
# spot
|
1728
|
-
#
|
1729
|
-
# {
|
1730
|
-
# "action": "req",
|
1731
|
-
# "code": 200,
|
1732
|
-
# "ch": "auth",
|
1733
|
-
# "data": {}
|
1734
|
-
# }
|
1735
|
-
#
|
1736
|
-
# non spot
|
1737
|
-
#
|
1738
|
-
# {
|
1739
|
-
# "op": "auth",
|
1740
|
-
# "type": "api",
|
1741
|
-
# "err-code": 0,
|
1742
|
-
# "ts": 1645200307319,
|
1743
|
-
# "data": {"user-id": "35930539"}
|
1744
|
-
# }
|
1745
|
-
#
|
1746
|
-
promise = client.futures['authenticated']
|
1747
|
-
promise.resolve(message)
|
1748
|
-
|
1749
|
-
def handle_error_message(self, client: Client, message):
|
1750
|
-
#
|
1751
|
-
# {
|
1752
|
-
# "action": "sub",
|
1753
|
-
# "code": 2002,
|
1754
|
-
# "ch": "accounts.update#2",
|
1755
|
-
# "message": "invalid.auth.state"
|
1756
|
-
# }
|
1757
|
-
#
|
1758
|
-
# {
|
1759
|
-
# "ts": 1586323747018,
|
1760
|
-
# "status": "error",
|
1761
|
-
# 'err-code': "bad-request",
|
1762
|
-
# 'err-msg': "invalid mbp.150.symbol linkusdt",
|
1763
|
-
# "id": "2"
|
1764
|
-
# }
|
1765
|
-
#
|
1766
|
-
# {
|
1767
|
-
# "op": "sub",
|
1768
|
-
# "cid": "1",
|
1769
|
-
# "topic": "accounts_unify.USDT",
|
1770
|
-
# "err-code": 4007,
|
1771
|
-
# 'err-msg': "Non - single account user is not available, please check through the cross and isolated account asset interface",
|
1772
|
-
# "ts": 1698419490189
|
1773
|
-
# }
|
1774
|
-
#
|
1775
|
-
status = self.safe_string(message, 'status')
|
1776
|
-
if status == 'error':
|
1777
|
-
id = self.safe_string(message, 'id')
|
1778
|
-
subscriptionsById = self.index_by(client.subscriptions, 'id')
|
1779
|
-
subscription = self.safe_value(subscriptionsById, id)
|
1780
|
-
if subscription is not None:
|
1781
|
-
errorCode = self.safe_string(message, 'err-code')
|
1782
|
-
try:
|
1783
|
-
self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], errorCode, self.json(message))
|
1784
|
-
except Exception as e:
|
1785
|
-
messageHash = self.safe_string(subscription, 'messageHash')
|
1786
|
-
client.reject(e, messageHash)
|
1787
|
-
client.reject(e, id)
|
1788
|
-
if id in client.subscriptions:
|
1789
|
-
del client.subscriptions[id]
|
1790
|
-
return False
|
1791
|
-
code = self.safe_integer_2(message, 'code', 'err-code')
|
1792
|
-
if code is not None and ((code != 200) and (code != 0)):
|
1793
|
-
feedback = self.id + ' ' + self.json(message)
|
1794
|
-
try:
|
1795
|
-
self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, feedback)
|
1796
|
-
except Exception as e:
|
1797
|
-
if isinstance(e, AuthenticationError):
|
1798
|
-
client.reject(e, 'auth')
|
1799
|
-
method = 'auth'
|
1800
|
-
if method in client.subscriptions:
|
1801
|
-
del client.subscriptions[method]
|
1802
|
-
return False
|
1803
|
-
else:
|
1804
|
-
client.reject(e)
|
1805
|
-
return message
|
1806
|
-
|
1807
|
-
def handle_message(self, client: Client, message):
|
1808
|
-
if self.handle_error_message(client, message):
|
1809
|
-
#
|
1810
|
-
# {"id":1583414227,"status":"ok","subbed":"market.btcusdt.mbp.150","ts":1583414229143}
|
1811
|
-
#
|
1812
|
-
# first ping format
|
1813
|
-
#
|
1814
|
-
# {"ping": 1645106821667}
|
1815
|
-
#
|
1816
|
-
# second ping format
|
1817
|
-
#
|
1818
|
-
# {"action":"ping","data":{"ts":1645106821667}}
|
1819
|
-
#
|
1820
|
-
# third pong format
|
1821
|
-
#
|
1822
|
-
#
|
1823
|
-
# auth spot
|
1824
|
-
#
|
1825
|
-
# {
|
1826
|
-
# "action": "req",
|
1827
|
-
# "code": 200,
|
1828
|
-
# "ch": "auth",
|
1829
|
-
# "data": {}
|
1830
|
-
# }
|
1831
|
-
#
|
1832
|
-
# auth non spot
|
1833
|
-
#
|
1834
|
-
# {
|
1835
|
-
# "op": "auth",
|
1836
|
-
# "type": "api",
|
1837
|
-
# "err-code": 0,
|
1838
|
-
# "ts": 1645200307319,
|
1839
|
-
# "data": {"user-id": "35930539"}
|
1840
|
-
# }
|
1841
|
-
#
|
1842
|
-
# trade
|
1843
|
-
#
|
1844
|
-
# {
|
1845
|
-
# "action":"push",
|
1846
|
-
# "ch":"trade.clearing#ltcusdt#1",
|
1847
|
-
# "data":{
|
1848
|
-
# "eventType":"trade",
|
1849
|
-
# # ?
|
1850
|
-
# }
|
1851
|
-
# }
|
1852
|
-
#
|
1853
|
-
if 'id' in message:
|
1854
|
-
self.handle_subscription_status(client, message)
|
1855
|
-
return
|
1856
|
-
if 'action' in message:
|
1857
|
-
action = self.safe_string(message, 'action')
|
1858
|
-
if action == 'ping':
|
1859
|
-
self.handle_ping(client, message)
|
1860
|
-
return
|
1861
|
-
if action == 'sub':
|
1862
|
-
self.handle_subscription_status(client, message)
|
1863
|
-
return
|
1864
|
-
if 'ch' in message:
|
1865
|
-
if message['ch'] == 'auth':
|
1866
|
-
self.handle_authenticate(client, message)
|
1867
|
-
return
|
1868
|
-
else:
|
1869
|
-
# route by channel aka topic aka subject
|
1870
|
-
self.handle_subject(client, message)
|
1871
|
-
return
|
1872
|
-
if 'op' in message:
|
1873
|
-
op = self.safe_string(message, 'op')
|
1874
|
-
if op == 'ping':
|
1875
|
-
self.handle_ping(client, message)
|
1876
|
-
return
|
1877
|
-
if op == 'auth':
|
1878
|
-
self.handle_authenticate(client, message)
|
1879
|
-
return
|
1880
|
-
if op == 'sub':
|
1881
|
-
self.handle_subscription_status(client, message)
|
1882
|
-
return
|
1883
|
-
if op == 'notify':
|
1884
|
-
self.handle_subject(client, message)
|
1885
|
-
return
|
1886
|
-
if 'ping' in message:
|
1887
|
-
self.handle_ping(client, message)
|
1888
|
-
|
1889
|
-
def handle_my_trade(self, client: Client, message, extendParams={}):
|
1890
|
-
#
|
1891
|
-
# spot
|
1892
|
-
#
|
1893
|
-
# {
|
1894
|
-
# "action":"push",
|
1895
|
-
# "ch":"trade.clearing#ltcusdt#1",
|
1896
|
-
# "data":{
|
1897
|
-
# "eventType":"trade",
|
1898
|
-
# "symbol":"ltcusdt",
|
1899
|
-
# "orderId":"478862728954426",
|
1900
|
-
# "orderSide":"buy",
|
1901
|
-
# "orderType":"buy-market",
|
1902
|
-
# "accountId":44234548,
|
1903
|
-
# "source":"spot-web",
|
1904
|
-
# "orderValue":"5.01724137",
|
1905
|
-
# "orderCreateTime":1645124660365,
|
1906
|
-
# "orderStatus":"filled",
|
1907
|
-
# "feeCurrency":"ltc",
|
1908
|
-
# "tradePrice":"118.89",
|
1909
|
-
# "tradeVolume":"0.042200701236437042",
|
1910
|
-
# "aggressor":true,
|
1911
|
-
# "tradeId":101539740584,
|
1912
|
-
# "tradeTime":1645124660368,
|
1913
|
-
# "transactFee":"0.000041778694224073",
|
1914
|
-
# "feeDeduct":"0",
|
1915
|
-
# "feeDeductType":""
|
1916
|
-
# }
|
1917
|
-
# }
|
1918
|
-
#
|
1919
|
-
# contract
|
1920
|
-
#
|
1921
|
-
# {
|
1922
|
-
# "symbol": "ADA/USDT:USDT"
|
1923
|
-
# "ch": "orders_cross.ada-usdt"
|
1924
|
-
# "trades": [
|
1925
|
-
# {
|
1926
|
-
# "trade_fee":-0.022099447513812154,
|
1927
|
-
# "fee_asset":"ADA",
|
1928
|
-
# "trade_id":113913755890,
|
1929
|
-
# "id":"113913755890-773207641127878656-1",
|
1930
|
-
# "trade_volume":1,
|
1931
|
-
# "trade_price":0.0905,
|
1932
|
-
# "trade_turnover":10,
|
1933
|
-
# "created_at":1604388667194,
|
1934
|
-
# "profit":0,
|
1935
|
-
# "real_profit": 0,
|
1936
|
-
# "role":"maker"
|
1937
|
-
# }
|
1938
|
-
# ],
|
1939
|
-
# }
|
1940
|
-
#
|
1941
|
-
if self.myTrades is None:
|
1942
|
-
limit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
1943
|
-
self.myTrades = ArrayCacheBySymbolById(limit)
|
1944
|
-
cachedTrades = self.myTrades
|
1945
|
-
messageHash = self.safe_string(message, 'ch')
|
1946
|
-
if messageHash is not None:
|
1947
|
-
data = self.safe_value(message, 'data')
|
1948
|
-
if data is not None:
|
1949
|
-
parsed = self.parse_ws_trade(data)
|
1950
|
-
symbol = self.safe_string(parsed, 'symbol')
|
1951
|
-
if symbol is not None:
|
1952
|
-
cachedTrades.append(parsed)
|
1953
|
-
client.resolve(self.myTrades, messageHash)
|
1954
|
-
else:
|
1955
|
-
# self trades object is artificially created
|
1956
|
-
# in handleOrder
|
1957
|
-
rawTrades = self.safe_value(message, 'trades', [])
|
1958
|
-
marketId = self.safe_value(message, 'symbol')
|
1959
|
-
market = self.market(marketId)
|
1960
|
-
for i in range(0, len(rawTrades)):
|
1961
|
-
trade = rawTrades[i]
|
1962
|
-
parsedTrade = self.parse_trade(trade, market)
|
1963
|
-
# add extra params(side, type, ...) coming from the order
|
1964
|
-
parsedTrade = self.extend(parsedTrade, extendParams)
|
1965
|
-
cachedTrades.append(parsedTrade)
|
1966
|
-
# messageHash here is the orders one, so
|
1967
|
-
# we have to recreate the trades messageHash = orderMessageHash + ':' + 'trade'
|
1968
|
-
tradesHash = messageHash + ':' + 'trade'
|
1969
|
-
client.resolve(self.myTrades, tradesHash)
|
1970
|
-
# when we make an global order sub we have to send the channel like self
|
1971
|
-
# ch = orders_cross.* and we store messageHash = 'orders_cross'
|
1972
|
-
# however it is returned with the specific order update symbol: ch = orders_cross.btc-usd
|
1973
|
-
# since self is a global sub, our messageHash does not specify any symbol(ex: orders_cross:trade)
|
1974
|
-
# so we must remove it
|
1975
|
-
genericOrderHash = messageHash.replace('.' + market['lowercaseId'], '')
|
1976
|
-
genericOrderHash = genericOrderHash.replace('.' + market['lowercaseBaseId'], '')
|
1977
|
-
genericTradesHash = genericOrderHash + ':' + 'trade'
|
1978
|
-
client.resolve(self.myTrades, genericTradesHash)
|
1979
|
-
|
1980
|
-
def parse_ws_trade(self, trade, market=None):
|
1981
|
-
# spot private
|
1982
|
-
#
|
1983
|
-
# {
|
1984
|
-
# "eventType":"trade",
|
1985
|
-
# "symbol":"ltcusdt",
|
1986
|
-
# "orderId":"478862728954426",
|
1987
|
-
# "orderSide":"buy",
|
1988
|
-
# "orderType":"buy-market",
|
1989
|
-
# "accountId":44234548,
|
1990
|
-
# "source":"spot-web",
|
1991
|
-
# "orderValue":"5.01724137",
|
1992
|
-
# "orderCreateTime":1645124660365,
|
1993
|
-
# "orderStatus":"filled",
|
1994
|
-
# "feeCurrency":"ltc",
|
1995
|
-
# "tradePrice":"118.89",
|
1996
|
-
# "tradeVolume":"0.042200701236437042",
|
1997
|
-
# "aggressor":true,
|
1998
|
-
# "tradeId":101539740584,
|
1999
|
-
# "tradeTime":1645124660368,
|
2000
|
-
# "transactFee":"0.000041778694224073",
|
2001
|
-
# "feeDeduct":"0",
|
2002
|
-
# "feeDeductType":""
|
2003
|
-
# }
|
2004
|
-
#
|
2005
|
-
symbol = self.safe_symbol(self.safe_string(trade, 'symbol'))
|
2006
|
-
side = self.safe_string_2(trade, 'side', 'orderSide')
|
2007
|
-
tradeId = self.safe_string(trade, 'tradeId')
|
2008
|
-
price = self.safe_string(trade, 'tradePrice')
|
2009
|
-
amount = self.safe_string(trade, 'tradeVolume')
|
2010
|
-
order = self.safe_string(trade, 'orderId')
|
2011
|
-
timestamp = self.safe_integer(trade, 'tradeTime')
|
2012
|
-
market = self.market(symbol)
|
2013
|
-
orderType = self.safe_string(trade, 'orderType')
|
2014
|
-
aggressor = self.safe_value(trade, 'aggressor')
|
2015
|
-
takerOrMaker = None
|
2016
|
-
if aggressor is not None:
|
2017
|
-
takerOrMaker = 'taker' if aggressor else 'maker'
|
2018
|
-
type = None
|
2019
|
-
orderTypeParts = []
|
2020
|
-
if orderType is not None:
|
2021
|
-
orderTypeParts = orderType.split('-')
|
2022
|
-
type = self.safe_string(orderTypeParts, 1)
|
2023
|
-
fee = None
|
2024
|
-
feeCurrency = self.safe_currency_code(self.safe_string(trade, 'feeCurrency'))
|
2025
|
-
if feeCurrency is not None:
|
2026
|
-
fee = {
|
2027
|
-
'cost': self.safe_string(trade, 'transactFee'),
|
2028
|
-
'currency': feeCurrency,
|
2029
|
-
}
|
2030
|
-
return self.safe_trade({
|
2031
|
-
'info': trade,
|
2032
|
-
'timestamp': timestamp,
|
2033
|
-
'datetime': self.iso8601(timestamp),
|
2034
|
-
'symbol': symbol,
|
2035
|
-
'id': tradeId,
|
2036
|
-
'order': order,
|
2037
|
-
'type': type,
|
2038
|
-
'takerOrMaker': takerOrMaker,
|
2039
|
-
'side': side,
|
2040
|
-
'price': price,
|
2041
|
-
'amount': amount,
|
2042
|
-
'cost': None,
|
2043
|
-
'fee': fee,
|
2044
|
-
}, market)
|
2045
|
-
|
2046
|
-
def get_url_by_market_type(self, type, isLinear=True, isPrivate=False):
|
2047
|
-
api = self.safe_string(self.options, 'api', 'api')
|
2048
|
-
hostname = {'hostname': self.hostname}
|
2049
|
-
hostnameURL = None
|
2050
|
-
url = None
|
2051
|
-
if type == 'spot':
|
2052
|
-
if isPrivate:
|
2053
|
-
hostnameURL = self.urls['api']['ws'][api]['spot']['private']
|
2054
|
-
else:
|
2055
|
-
hostnameURL = self.urls['api']['ws'][api]['spot']['public']
|
2056
|
-
url = self.implode_params(hostnameURL, hostname)
|
2057
|
-
else:
|
2058
|
-
baseUrl = self.urls['api']['ws'][api][type]
|
2059
|
-
subTypeUrl = baseUrl['linear'] if isLinear else baseUrl['inverse']
|
2060
|
-
url = subTypeUrl['private'] if isPrivate else subTypeUrl['public']
|
2061
|
-
return url
|
2062
|
-
|
2063
|
-
async def subscribe_public(self, url, symbol, messageHash, method=None, params={}):
|
2064
|
-
requestId = self.request_id()
|
2065
|
-
request = {
|
2066
|
-
'sub': messageHash,
|
2067
|
-
'id': requestId,
|
2068
|
-
}
|
2069
|
-
subscription = {
|
2070
|
-
'id': requestId,
|
2071
|
-
'messageHash': messageHash,
|
2072
|
-
'symbol': symbol,
|
2073
|
-
'params': params,
|
2074
|
-
}
|
2075
|
-
if method is not None:
|
2076
|
-
subscription['method'] = method
|
2077
|
-
return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription)
|
2078
|
-
|
2079
|
-
async def subscribe_private(self, channel, messageHash, type, subtype, params={}, subscriptionParams={}):
|
2080
|
-
requestId = self.request_id()
|
2081
|
-
subscription = {
|
2082
|
-
'id': requestId,
|
2083
|
-
'messageHash': messageHash,
|
2084
|
-
'params': params,
|
2085
|
-
}
|
2086
|
-
extendedSubsription = self.extend(subscription, subscriptionParams)
|
2087
|
-
request = None
|
2088
|
-
if type == 'spot':
|
2089
|
-
request = {
|
2090
|
-
'action': 'sub',
|
2091
|
-
'ch': channel,
|
2092
|
-
}
|
2093
|
-
else:
|
2094
|
-
request = {
|
2095
|
-
'op': 'sub',
|
2096
|
-
'topic': channel,
|
2097
|
-
'cid': requestId,
|
2098
|
-
}
|
2099
|
-
isLinear = subtype == 'linear'
|
2100
|
-
url = self.get_url_by_market_type(type, isLinear, True)
|
2101
|
-
hostname = self.urls['hostnames']['spot'] if (type == 'spot') else self.urls['hostnames']['contract']
|
2102
|
-
authParams = {
|
2103
|
-
'type': type,
|
2104
|
-
'url': url,
|
2105
|
-
'hostname': hostname,
|
2106
|
-
}
|
2107
|
-
if type == 'spot':
|
2108
|
-
self.options['ws']['gunzip'] = False
|
2109
|
-
await self.authenticate(authParams)
|
2110
|
-
return await self.watch(url, messageHash, self.extend(request, params), channel, extendedSubsription)
|
2111
|
-
|
2112
|
-
async def authenticate(self, params={}):
|
2113
|
-
url = self.safe_string(params, 'url')
|
2114
|
-
hostname = self.safe_string(params, 'hostname')
|
2115
|
-
type = self.safe_string(params, 'type')
|
2116
|
-
if url is None or hostname is None or type is None:
|
2117
|
-
raise ArgumentsRequired(self.id + ' authenticate requires a url, hostname and type argument')
|
2118
|
-
self.check_required_credentials()
|
2119
|
-
messageHash = 'authenticated'
|
2120
|
-
relativePath = url.replace('wss://' + hostname, '')
|
2121
|
-
client = self.client(url)
|
2122
|
-
future = client.future(messageHash)
|
2123
|
-
authenticated = self.safe_value(client.subscriptions, messageHash)
|
2124
|
-
if authenticated is None:
|
2125
|
-
timestamp = self.ymdhms(self.milliseconds(), 'T')
|
2126
|
-
signatureParams = None
|
2127
|
-
if type == 'spot':
|
2128
|
-
signatureParams = {
|
2129
|
-
'accessKey': self.apiKey,
|
2130
|
-
'signatureMethod': 'HmacSHA256',
|
2131
|
-
'signatureVersion': '2.1',
|
2132
|
-
'timestamp': timestamp,
|
2133
|
-
}
|
2134
|
-
else:
|
2135
|
-
signatureParams = {
|
2136
|
-
'AccessKeyId': self.apiKey,
|
2137
|
-
'SignatureMethod': 'HmacSHA256',
|
2138
|
-
'SignatureVersion': '2',
|
2139
|
-
'Timestamp': timestamp,
|
2140
|
-
}
|
2141
|
-
signatureParams = self.keysort(signatureParams)
|
2142
|
-
auth = self.urlencode(signatureParams)
|
2143
|
-
payload = "\n".join(['GET', hostname, relativePath, auth]) # eslint-disable-line quotes
|
2144
|
-
signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64')
|
2145
|
-
request = None
|
2146
|
-
if type == 'spot':
|
2147
|
-
newParams = {
|
2148
|
-
'authType': 'api',
|
2149
|
-
'accessKey': self.apiKey,
|
2150
|
-
'signatureMethod': 'HmacSHA256',
|
2151
|
-
'signatureVersion': '2.1',
|
2152
|
-
'timestamp': timestamp,
|
2153
|
-
'signature': signature,
|
2154
|
-
}
|
2155
|
-
request = {
|
2156
|
-
'params': newParams,
|
2157
|
-
'action': 'req',
|
2158
|
-
'ch': 'auth',
|
2159
|
-
}
|
2160
|
-
else:
|
2161
|
-
request = {
|
2162
|
-
'op': 'auth',
|
2163
|
-
'type': 'api',
|
2164
|
-
'AccessKeyId': self.apiKey,
|
2165
|
-
'SignatureMethod': 'HmacSHA256',
|
2166
|
-
'SignatureVersion': '2',
|
2167
|
-
'Timestamp': timestamp,
|
2168
|
-
'Signature': signature,
|
2169
|
-
}
|
2170
|
-
requestId = self.request_id()
|
2171
|
-
subscription = {
|
2172
|
-
'id': requestId,
|
2173
|
-
'messageHash': messageHash,
|
2174
|
-
'params': params,
|
2175
|
-
}
|
2176
|
-
self.watch(url, messageHash, request, messageHash, subscription)
|
2177
|
-
return future
|