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.

Files changed (166) hide show
  1. ccxt/__init__.py +3 -1
  2. ccxt/abstract/binance.py +2 -0
  3. ccxt/abstract/binancecoinm.py +2 -0
  4. ccxt/abstract/binanceus.py +2 -0
  5. ccxt/abstract/binanceusdm.py +2 -0
  6. ccxt/abstract/htx.py +541 -0
  7. ccxt/ace.py +2 -2
  8. ccxt/ascendex.py +2 -2
  9. ccxt/async_support/__init__.py +3 -1
  10. ccxt/async_support/ace.py +2 -2
  11. ccxt/async_support/ascendex.py +2 -2
  12. ccxt/async_support/base/exchange.py +1 -1
  13. ccxt/async_support/bigone.py +2 -2
  14. ccxt/async_support/binance.py +5 -3
  15. ccxt/async_support/bingx.py +3 -3
  16. ccxt/async_support/bitbns.py +2 -2
  17. ccxt/async_support/bitfinex.py +2 -2
  18. ccxt/async_support/bitfinex2.py +2 -2
  19. ccxt/async_support/bitget.py +3 -3
  20. ccxt/async_support/bithumb.py +2 -2
  21. ccxt/async_support/bitmart.py +157 -53
  22. ccxt/async_support/bitmex.py +2 -2
  23. ccxt/async_support/bitopro.py +2 -2
  24. ccxt/async_support/bitpanda.py +2 -2
  25. ccxt/async_support/bitrue.py +2 -2
  26. ccxt/async_support/bitstamp.py +2 -2
  27. ccxt/async_support/bittrex.py +2 -2
  28. ccxt/async_support/bitvavo.py +2 -2
  29. ccxt/async_support/blockchaincom.py +2 -2
  30. ccxt/async_support/btcalpha.py +2 -2
  31. ccxt/async_support/btcturk.py +2 -2
  32. ccxt/async_support/bybit.py +2 -2
  33. ccxt/async_support/cex.py +2 -2
  34. ccxt/async_support/coinbase.py +2 -2
  35. ccxt/async_support/coinbasepro.py +2 -2
  36. ccxt/async_support/coinex.py +63 -20
  37. ccxt/async_support/coinfalcon.py +2 -2
  38. ccxt/async_support/coinlist.py +2 -2
  39. ccxt/async_support/coinone.py +2 -2
  40. ccxt/async_support/coinsph.py +2 -2
  41. ccxt/async_support/coinspot.py +2 -2
  42. ccxt/async_support/cryptocom.py +2 -2
  43. ccxt/async_support/currencycom.py +2 -2
  44. ccxt/async_support/delta.py +2 -2
  45. ccxt/async_support/deribit.py +2 -2
  46. ccxt/async_support/digifinex.py +2 -2
  47. ccxt/async_support/exmo.py +2 -2
  48. ccxt/async_support/gate.py +2 -2
  49. ccxt/async_support/gemini.py +3 -3
  50. ccxt/async_support/hitbtc.py +2 -2
  51. ccxt/async_support/hollaex.py +2 -2
  52. ccxt/async_support/htx.py +7853 -0
  53. ccxt/async_support/huobi.py +3 -7846
  54. ccxt/async_support/huobijp.py +2 -2
  55. ccxt/async_support/idex.py +2 -2
  56. ccxt/async_support/indodax.py +2 -2
  57. ccxt/async_support/kraken.py +2 -6
  58. ccxt/async_support/krakenfutures.py +2 -2
  59. ccxt/async_support/kucoin.py +2 -2
  60. ccxt/async_support/kuna.py +2 -2
  61. ccxt/async_support/latoken.py +2 -2
  62. ccxt/async_support/lbank.py +2 -2
  63. ccxt/async_support/lbank2.py +2 -2
  64. ccxt/async_support/luno.py +2 -2
  65. ccxt/async_support/lykke.py +2 -2
  66. ccxt/async_support/mexc.py +23 -23
  67. ccxt/async_support/novadax.py +2 -2
  68. ccxt/async_support/oceanex.py +2 -2
  69. ccxt/async_support/okcoin.py +3 -3
  70. ccxt/async_support/okx.py +3 -3
  71. ccxt/async_support/phemex.py +2 -2
  72. ccxt/async_support/poloniex.py +2 -2
  73. ccxt/async_support/poloniexfutures.py +2 -2
  74. ccxt/async_support/probit.py +2 -2
  75. ccxt/async_support/tidex.py +2 -2
  76. ccxt/async_support/timex.py +4 -4
  77. ccxt/async_support/tokocrypto.py +2 -2
  78. ccxt/async_support/upbit.py +2 -2
  79. ccxt/async_support/wavesexchange.py +2 -2
  80. ccxt/async_support/wazirx.py +2 -2
  81. ccxt/async_support/whitebit.py +2 -2
  82. ccxt/async_support/yobit.py +2 -2
  83. ccxt/async_support/zonda.py +2 -2
  84. ccxt/base/exchange.py +1 -1
  85. ccxt/base/types.py +43 -4
  86. ccxt/bigone.py +2 -2
  87. ccxt/binance.py +5 -3
  88. ccxt/bingx.py +3 -3
  89. ccxt/bitbns.py +2 -2
  90. ccxt/bitfinex.py +2 -2
  91. ccxt/bitfinex2.py +2 -2
  92. ccxt/bitget.py +3 -3
  93. ccxt/bithumb.py +2 -2
  94. ccxt/bitmart.py +157 -53
  95. ccxt/bitmex.py +2 -2
  96. ccxt/bitopro.py +2 -2
  97. ccxt/bitpanda.py +2 -2
  98. ccxt/bitrue.py +2 -2
  99. ccxt/bitstamp.py +2 -2
  100. ccxt/bittrex.py +2 -2
  101. ccxt/bitvavo.py +2 -2
  102. ccxt/blockchaincom.py +2 -2
  103. ccxt/btcalpha.py +2 -2
  104. ccxt/btcturk.py +2 -2
  105. ccxt/bybit.py +2 -2
  106. ccxt/cex.py +2 -2
  107. ccxt/coinbase.py +2 -2
  108. ccxt/coinbasepro.py +2 -2
  109. ccxt/coinex.py +63 -20
  110. ccxt/coinfalcon.py +2 -2
  111. ccxt/coinlist.py +2 -2
  112. ccxt/coinone.py +2 -2
  113. ccxt/coinsph.py +2 -2
  114. ccxt/coinspot.py +2 -2
  115. ccxt/cryptocom.py +2 -2
  116. ccxt/currencycom.py +2 -2
  117. ccxt/delta.py +2 -2
  118. ccxt/deribit.py +2 -2
  119. ccxt/digifinex.py +2 -2
  120. ccxt/exmo.py +2 -2
  121. ccxt/gate.py +2 -2
  122. ccxt/gemini.py +3 -3
  123. ccxt/hitbtc.py +2 -2
  124. ccxt/hollaex.py +2 -2
  125. ccxt/htx.py +7852 -0
  126. ccxt/huobi.py +3 -7845
  127. ccxt/huobijp.py +2 -2
  128. ccxt/idex.py +2 -2
  129. ccxt/indodax.py +2 -2
  130. ccxt/kraken.py +2 -6
  131. ccxt/krakenfutures.py +2 -2
  132. ccxt/kucoin.py +2 -2
  133. ccxt/kuna.py +2 -2
  134. ccxt/latoken.py +2 -2
  135. ccxt/lbank.py +2 -2
  136. ccxt/lbank2.py +2 -2
  137. ccxt/luno.py +2 -2
  138. ccxt/lykke.py +2 -2
  139. ccxt/mexc.py +23 -23
  140. ccxt/novadax.py +2 -2
  141. ccxt/oceanex.py +2 -2
  142. ccxt/okcoin.py +3 -3
  143. ccxt/okx.py +3 -3
  144. ccxt/phemex.py +2 -2
  145. ccxt/poloniex.py +2 -2
  146. ccxt/poloniexfutures.py +2 -2
  147. ccxt/pro/__init__.py +3 -1
  148. ccxt/pro/htx.py +2177 -0
  149. ccxt/pro/huobi.py +4 -2166
  150. ccxt/probit.py +2 -2
  151. ccxt/test/test_async.py +15 -1
  152. ccxt/test/test_sync.py +15 -1
  153. ccxt/tidex.py +2 -2
  154. ccxt/timex.py +4 -4
  155. ccxt/tokocrypto.py +2 -2
  156. ccxt/upbit.py +2 -2
  157. ccxt/wavesexchange.py +2 -2
  158. ccxt/wazirx.py +2 -2
  159. ccxt/whitebit.py +2 -2
  160. ccxt/yobit.py +2 -2
  161. ccxt/zonda.py +2 -2
  162. ccxt-4.1.49.dist-info/METADATA +624 -0
  163. {ccxt-4.1.47.dist-info → ccxt-4.1.49.dist-info}/RECORD +165 -161
  164. ccxt-4.1.47.dist-info/METADATA +0 -624
  165. {ccxt-4.1.47.dist-info → ccxt-4.1.49.dist-info}/WHEEL +0 -0
  166. {ccxt-4.1.47.dist-info → ccxt-4.1.49.dist-info}/top_level.txt +0 -0
ccxt/pro/htx.py ADDED
@@ -0,0 +1,2177 @@
1
+ # -*- coding: utf-8 -*-
2
+
3
+ # PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
4
+ # https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
5
+
6
+ import ccxt.async_support
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
19
+
20
+
21
+ class htx(ccxt.async_support.htx):
22
+
23
+ def describe(self):
24
+ return self.deep_extend(super(htx, self).describe(), {
25
+ 'has': {
26
+ 'ws': True,
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
+ },
122
+ })
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