ccxt 4.4.21__py2.py3-none-any.whl → 4.4.23__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.
Files changed (76) hide show
  1. ccxt/__init__.py +3 -1
  2. ccxt/abstract/binance.py +64 -43
  3. ccxt/abstract/binancecoinm.py +64 -43
  4. ccxt/abstract/binanceus.py +64 -43
  5. ccxt/abstract/binanceusdm.py +64 -43
  6. ccxt/abstract/bitflyer.py +1 -0
  7. ccxt/abstract/bitget.py +3 -0
  8. ccxt/abstract/cex.py +28 -29
  9. ccxt/abstract/coincatch.py +94 -0
  10. ccxt/abstract/gate.py +5 -0
  11. ccxt/abstract/gateio.py +5 -0
  12. ccxt/abstract/kucoin.py +1 -0
  13. ccxt/abstract/kucoinfutures.py +1 -0
  14. ccxt/abstract/okx.py +1 -0
  15. ccxt/alpaca.py +1 -0
  16. ccxt/async_support/__init__.py +3 -1
  17. ccxt/async_support/alpaca.py +1 -0
  18. ccxt/async_support/base/exchange.py +7 -1
  19. ccxt/async_support/bigone.py +3 -0
  20. ccxt/async_support/binance.py +183 -63
  21. ccxt/async_support/bitfinex.py +4 -0
  22. ccxt/async_support/bitflyer.py +57 -1
  23. ccxt/async_support/bitget.py +73 -1
  24. ccxt/async_support/bitrue.py +3 -0
  25. ccxt/async_support/bybit.py +76 -3
  26. ccxt/async_support/cex.py +1247 -1322
  27. ccxt/async_support/coinbase.py +1 -1
  28. ccxt/async_support/coinbaseexchange.py +3 -0
  29. ccxt/async_support/coincatch.py +4955 -0
  30. ccxt/async_support/coinex.py +60 -1
  31. ccxt/async_support/cryptocom.py +1 -1
  32. ccxt/async_support/gate.py +97 -2
  33. ccxt/async_support/htx.py +1 -5
  34. ccxt/async_support/hyperliquid.py +10 -8
  35. ccxt/async_support/kucoin.py +27 -57
  36. ccxt/async_support/latoken.py +6 -0
  37. ccxt/async_support/mexc.py +1 -1
  38. ccxt/async_support/oceanex.py +2 -0
  39. ccxt/async_support/okcoin.py +1 -0
  40. ccxt/async_support/okx.py +67 -1
  41. ccxt/async_support/poloniex.py +5 -0
  42. ccxt/base/exchange.py +21 -1
  43. ccxt/base/types.py +9 -0
  44. ccxt/bigone.py +3 -0
  45. ccxt/binance.py +183 -63
  46. ccxt/bitfinex.py +4 -0
  47. ccxt/bitflyer.py +57 -1
  48. ccxt/bitget.py +73 -1
  49. ccxt/bitrue.py +3 -0
  50. ccxt/bybit.py +76 -3
  51. ccxt/cex.py +1246 -1322
  52. ccxt/coinbase.py +1 -1
  53. ccxt/coinbaseexchange.py +3 -0
  54. ccxt/coincatch.py +4955 -0
  55. ccxt/coinex.py +60 -1
  56. ccxt/cryptocom.py +1 -1
  57. ccxt/gate.py +97 -2
  58. ccxt/htx.py +1 -5
  59. ccxt/hyperliquid.py +10 -8
  60. ccxt/kucoin.py +27 -57
  61. ccxt/latoken.py +6 -0
  62. ccxt/mexc.py +1 -1
  63. ccxt/oceanex.py +2 -0
  64. ccxt/okcoin.py +1 -0
  65. ccxt/okx.py +67 -1
  66. ccxt/poloniex.py +5 -0
  67. ccxt/pro/__init__.py +3 -1
  68. ccxt/pro/coincatch.py +1429 -0
  69. ccxt/test/tests_async.py +19 -5
  70. ccxt/test/tests_sync.py +19 -5
  71. ccxt-4.4.23.dist-info/METADATA +636 -0
  72. {ccxt-4.4.21.dist-info → ccxt-4.4.23.dist-info}/RECORD +75 -71
  73. ccxt-4.4.21.dist-info/METADATA +0 -635
  74. {ccxt-4.4.21.dist-info → ccxt-4.4.23.dist-info}/LICENSE.txt +0 -0
  75. {ccxt-4.4.21.dist-info → ccxt-4.4.23.dist-info}/WHEEL +0 -0
  76. {ccxt-4.4.21.dist-info → ccxt-4.4.23.dist-info}/top_level.txt +0 -0
ccxt/pro/coincatch.py ADDED
@@ -0,0 +1,1429 @@
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.base.types import Balances, Bool, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade
10
+ from ccxt.async_support.base.ws.client import Client
11
+ from typing import List
12
+ from typing import Any
13
+ from ccxt.base.errors import ExchangeError
14
+ from ccxt.base.errors import AuthenticationError
15
+ from ccxt.base.errors import ArgumentsRequired
16
+ from ccxt.base.errors import BadRequest
17
+ from ccxt.base.errors import NotSupported
18
+ from ccxt.base.errors import RateLimitExceeded
19
+ from ccxt.base.errors import ChecksumError
20
+ from ccxt.base.errors import UnsubscribeError
21
+ from ccxt.base.precise import Precise
22
+
23
+
24
+ class coincatch(ccxt.async_support.coincatch):
25
+
26
+ def describe(self):
27
+ return self.deep_extend(super(coincatch, self).describe(), {
28
+ 'has': {
29
+ 'ws': True,
30
+ 'watchTrades': True,
31
+ 'watchTradesForSymbols': True,
32
+ 'watchOrderBook': True,
33
+ 'watchOrderBookForSymbols': True,
34
+ 'watchOHLCV': True,
35
+ 'watchOHLCVForSymbols': False, # todo
36
+ 'watchOrders': True,
37
+ 'watchMyTrades': False,
38
+ 'watchTicker': True,
39
+ 'watchTickers': True,
40
+ 'watchBalance': True,
41
+ 'watchPositions': True,
42
+ },
43
+ 'urls': {
44
+ 'api': {
45
+ 'ws': {
46
+ 'public': 'wss://ws.coincatch.com/public/v1/stream',
47
+ 'private': 'wss://ws.coincatch.com/private/v1/stream',
48
+ },
49
+ },
50
+ },
51
+ 'options': {
52
+ 'tradesLimit': 1000,
53
+ 'OHLCVLimit': 200,
54
+ 'timeframesForWs': {
55
+ '1m': '1m',
56
+ '5m': '5m',
57
+ '15m': '15m',
58
+ '30m': '30m',
59
+ '1h': '1H',
60
+ '4h': '4H',
61
+ '12h': '12H',
62
+ '1d': '1D',
63
+ '1w': '1W',
64
+ },
65
+ 'watchOrderBook': {
66
+ 'checksum': True,
67
+ },
68
+ },
69
+ 'streaming': {
70
+ 'ping': self.ping,
71
+ },
72
+ 'exceptions': {
73
+ 'ws': {
74
+ 'exact': {
75
+ '30001': BadRequest, # Channel does not exist
76
+ '30002': AuthenticationError, # illegal request
77
+ '30003': BadRequest, # invalid op
78
+ '30004': AuthenticationError, # User needs to log in
79
+ '30005': AuthenticationError, # login failed
80
+ '30006': RateLimitExceeded, # request too many
81
+ '30007': RateLimitExceeded, # request over limit,connection close
82
+ '30011': AuthenticationError, # invalid ACCESS_KEY
83
+ '30012': AuthenticationError, # invalid ACCESS_PASSPHRASE
84
+ '30013': AuthenticationError, # invalid ACCESS_TIMESTAMP
85
+ '30014': BadRequest, # Request timestamp expired
86
+ '30015': AuthenticationError, # {event: 'error', code: 30015, msg: 'Invalid sign'}
87
+ },
88
+ 'broad': {},
89
+ },
90
+ },
91
+ })
92
+
93
+ def get_market_from_arg(self, entry):
94
+ instId = self.safe_string(entry, 'instId')
95
+ instType = self.safe_string(entry, 'instType')
96
+ baseAndQuote = self.parseSpotMarketId(instId)
97
+ baseId = baseAndQuote['baseId']
98
+ quoteId = baseAndQuote['quoteId']
99
+ suffix = '_SPBL' # spot suffix
100
+ if instType == 'mc':
101
+ if quoteId == 'USD':
102
+ suffix = '_DMCBL'
103
+ else:
104
+ suffix = '_UMCBL'
105
+ marketId = self.safe_currency_code(baseId) + self.safe_currency_code(quoteId) + suffix
106
+ return self.safeMarketCustom(marketId)
107
+
108
+ async def authenticate(self, params={}):
109
+ self.check_required_credentials()
110
+ url = self.urls['api']['ws']['private']
111
+ client = self.client(url)
112
+ messageHash = 'authenticated'
113
+ future = client.future(messageHash)
114
+ authenticated = self.safe_value(client.subscriptions, messageHash)
115
+ if authenticated is None:
116
+ timestamp = str(self.seconds())
117
+ auth = timestamp + 'GET' + '/user/verify'
118
+ signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'base64')
119
+ operation = 'login'
120
+ request: dict = {
121
+ 'op': operation,
122
+ 'args': [
123
+ {
124
+ 'apiKey': self.apiKey,
125
+ 'passphrase': self.password,
126
+ 'timestamp': timestamp,
127
+ 'sign': signature,
128
+ },
129
+ ],
130
+ }
131
+ message = self.extend(request, params)
132
+ self.watch(url, messageHash, message, messageHash)
133
+ return await future
134
+
135
+ async def watch_public(self, messageHash, subscribeHash, args, params={}):
136
+ url = self.urls['api']['ws']['public']
137
+ request: dict = {
138
+ 'op': 'subscribe',
139
+ 'args': [args],
140
+ }
141
+ message = self.extend(request, params)
142
+ return await self.watch(url, messageHash, message, subscribeHash)
143
+
144
+ async def un_watch_public(self, messageHash, args, params={}):
145
+ url = self.urls['api']['ws']['public']
146
+ request: dict = {
147
+ 'op': 'unsubscribe',
148
+ 'args': [args],
149
+ }
150
+ message = self.extend(request, params)
151
+ return await self.watch(url, messageHash, message, messageHash)
152
+
153
+ async def watch_private(self, messageHash, subscribeHash, args, params={}):
154
+ await self.authenticate()
155
+ url = self.urls['api']['ws']['private']
156
+ request: dict = {
157
+ 'op': 'subscribe',
158
+ 'args': [args],
159
+ }
160
+ message = self.extend(request, params)
161
+ return await self.watch(url, messageHash, message, subscribeHash)
162
+
163
+ async def watch_private_multiple(self, messageHashes, subscribeHashes, args, params={}):
164
+ await self.authenticate()
165
+ url = self.urls['api']['ws']['private']
166
+ request: dict = {
167
+ 'op': 'subscribe',
168
+ 'args': args,
169
+ }
170
+ message = self.extend(request, params)
171
+ return await self.watch_multiple(url, messageHashes, message, subscribeHashes)
172
+
173
+ def handle_authenticate(self, client: Client, message):
174
+ #
175
+ # {event: "login", code: 0}
176
+ #
177
+ messageHash = 'authenticated'
178
+ future = self.safe_value(client.futures, messageHash)
179
+ future.resolve(True)
180
+
181
+ async def watch_public_multiple(self, messageHashes, subscribeHashes, argsArray, params={}):
182
+ url = self.urls['api']['ws']['public']
183
+ request: dict = {
184
+ 'op': 'subscribe',
185
+ 'args': argsArray,
186
+ }
187
+ message = self.extend(request, params)
188
+ return await self.watch_multiple(url, messageHashes, message, subscribeHashes)
189
+
190
+ async def un_watch_channel(self, symbol: str, channel: str, messageHashTopic: str, params={}) -> Any:
191
+ await self.load_markets()
192
+ market = self.market(symbol)
193
+ instType, instId = self.get_public_inst_type_and_id(market)
194
+ messageHash = 'unsubscribe:' + messageHashTopic + ':' + symbol
195
+ args: dict = {
196
+ 'instType': instType,
197
+ 'channel': channel,
198
+ 'instId': instId,
199
+ }
200
+ return await self.un_watch_public(messageHash, args, params)
201
+
202
+ def get_public_inst_type_and_id(self, market: Market):
203
+ instId = market['baseId'] + market['quoteId']
204
+ instType = None
205
+ if market['spot']:
206
+ instType = 'SP'
207
+ elif market['swap']:
208
+ instType = 'MC'
209
+ else:
210
+ raise NotSupported(self.id + ' supports only spot and swap markets')
211
+ return [instType, instId]
212
+
213
+ def handle_dmcbl_market_by_message_hashes(self, market: Market, hash: str, client: Client, timeframe: Str = None):
214
+ marketId = market['id']
215
+ messageHashes = self.find_message_hashes(client, hash)
216
+ # the exchange counts DMCBL markets same market with different quote currencies
217
+ # for example symbols ETHUSD:ETH and ETH/USD:BTC both have the same marketId ETHUSD_DMCBL
218
+ # we need to check all markets with the same marketId to find the correct market that is in messageHashes
219
+ marketsWithCurrentId = self.safe_list(self.markets_by_id, marketId, [])
220
+ suffix = ''
221
+ if timeframe is not None:
222
+ suffix = ':' + timeframe
223
+ for i in range(0, len(marketsWithCurrentId)):
224
+ market = marketsWithCurrentId[i]
225
+ symbol = market['symbol']
226
+ messageHash = hash + symbol + suffix
227
+ if self.in_array(messageHash, messageHashes):
228
+ return market
229
+ return market
230
+
231
+ async def watch_ticker(self, symbol: str, params={}) -> Ticker:
232
+ """
233
+ watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
234
+ :see: https://coincatch.github.io/github.io/en/spot/#tickers-channel
235
+ :param str symbol: unified symbol of the market to fetch the ticker for
236
+ :param dict [params]: extra parameters specific to the exchange API endpoint
237
+ :param str [params.instType]: the type of the instrument to fetch the ticker for, 'SP' for spot markets, 'MC' for futures markets(default is 'SP')
238
+ :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
239
+ """
240
+ await self.load_markets()
241
+ market = self.market(symbol)
242
+ instType, instId = self.get_public_inst_type_and_id(market)
243
+ channel = 'ticker'
244
+ messageHash = channel + ':' + symbol
245
+ args: dict = {
246
+ 'instType': instType,
247
+ 'channel': channel,
248
+ 'instId': instId,
249
+ }
250
+ return await self.watch_public(messageHash, messageHash, args, params)
251
+
252
+ async def un_watch_ticker(self, symbol: str, params={}) -> Any:
253
+ """
254
+ unsubscribe from the ticker channel
255
+ :see: https://coincatch.github.io/github.io/en/mix/#tickers-channel
256
+ :param str symbol: unified symbol of the market to unwatch the ticker for
257
+ :returns any: status of the unwatch request
258
+ """
259
+ await self.load_markets()
260
+ return await self.un_watch_channel(symbol, 'ticker', 'ticker', params)
261
+
262
+ async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
263
+ """
264
+ watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
265
+ :see: https://coincatch.github.io/github.io/en/mix/#tickers-channel
266
+ :param str[] symbols: unified symbol of the market to watch the tickers for
267
+ :param dict [params]: extra parameters specific to the exchange API endpoint
268
+ :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
269
+ """
270
+ await self.load_markets()
271
+ if symbols is None:
272
+ symbols = self.symbols
273
+ topics = []
274
+ messageHashes = []
275
+ for i in range(0, len(symbols)):
276
+ symbol = symbols[i]
277
+ market = self.market(symbol)
278
+ instType, instId = self.get_public_inst_type_and_id(market)
279
+ args: dict = {
280
+ 'instType': instType,
281
+ 'channel': 'ticker',
282
+ 'instId': instId,
283
+ }
284
+ topics.append(args)
285
+ messageHashes.append('ticker:' + symbol)
286
+ tickers = await self.watch_public_multiple(messageHashes, messageHashes, topics, params)
287
+ if self.newUpdates:
288
+ result: dict = {}
289
+ result[tickers['symbol']] = tickers
290
+ return result
291
+ return self.filter_by_array(self.tickers, 'symbol', symbols)
292
+
293
+ def handle_ticker(self, client: Client, message):
294
+ #
295
+ # action: 'snapshot',
296
+ # arg: {instType: 'sp', channel: 'ticker', instId: 'ETHUSDT'},
297
+ # data: [
298
+ # {
299
+ # instId: 'ETHUSDT',
300
+ # last: '2421.06',
301
+ # open24h: '2416.93',
302
+ # high24h: '2441.47',
303
+ # low24h: '2352.99',
304
+ # bestBid: '2421.03',
305
+ # bestAsk: '2421.06',
306
+ # baseVolume: '9445.2043',
307
+ # quoteVolume: '22807159.1148',
308
+ # ts: 1728131730687,
309
+ # labeId: 0,
310
+ # openUtc: '2414.50',
311
+ # chgUTC: '0.00272',
312
+ # bidSz: '3.866',
313
+ # askSz: '0.124'
314
+ # }
315
+ # ],
316
+ # ts: 1728131730688
317
+ #
318
+ arg = self.safe_dict(message, 'arg', {})
319
+ market = self.get_market_from_arg(arg)
320
+ marketId = market['id']
321
+ hash = 'ticker:'
322
+ if marketId.find('_DMCBL') >= 0:
323
+ market = self.handle_dmcbl_market_by_message_hashes(market, hash, client)
324
+ data = self.safe_list(message, 'data', [])
325
+ ticker = self.parse_ws_ticker(self.safe_dict(data, 0, {}), market)
326
+ symbol = market['symbol']
327
+ self.tickers[symbol] = ticker
328
+ messageHash = hash + symbol
329
+ client.resolve(self.tickers[symbol], messageHash)
330
+
331
+ def parse_ws_ticker(self, ticker, market=None):
332
+ #
333
+ # spot
334
+ # {
335
+ # instId: 'ETHUSDT',
336
+ # last: '2421.06',
337
+ # open24h: '2416.93',
338
+ # high24h: '2441.47',
339
+ # low24h: '2352.99',
340
+ # bestBid: '2421.03',
341
+ # bestAsk: '2421.06',
342
+ # baseVolume: '9445.2043',
343
+ # quoteVolume: '22807159.1148',
344
+ # ts: 1728131730687,
345
+ # labeId: 0,
346
+ # openUtc: '2414.50',
347
+ # chgUTC: '0.00272',
348
+ # bidSz: '3.866',
349
+ # askSz: '0.124'
350
+ # }
351
+ #
352
+ # swap
353
+ # {
354
+ # instId: 'ETHUSDT',
355
+ # last: '2434.47',
356
+ # bestAsk: '2434.48',
357
+ # bestBid: '2434.47',
358
+ # high24h: '2471.68',
359
+ # low24h: '2400.01',
360
+ # priceChangePercent: '0.00674',
361
+ # capitalRate: '0.000082',
362
+ # nextSettleTime: 1728489600000,
363
+ # systemTime: 1728471993602,
364
+ # markPrice: '2434.46',
365
+ # indexPrice: '2435.44',
366
+ # holding: '171450.25',
367
+ # baseVolume: '1699298.91',
368
+ # quoteVolume: '4144522832.32',
369
+ # openUtc: '2439.67',
370
+ # chgUTC: '-0.00213',
371
+ # symbolType: 1,
372
+ # symbolId: 'ETHUSDT_UMCBL',
373
+ # deliveryPrice: '0',
374
+ # bidSz: '26.12',
375
+ # askSz: '49.6'
376
+ # }
377
+ #
378
+ last = self.safe_string(ticker, 'last')
379
+ timestamp = self.safe_integer_2(ticker, 'ts', 'systemTime')
380
+ return self.safe_ticker({
381
+ 'symbol': market['symbol'],
382
+ 'timestamp': timestamp,
383
+ 'datetime': self.iso8601(timestamp),
384
+ 'high': self.safe_string(ticker, 'high24h'),
385
+ 'low': self.safe_string(ticker, 'low24h'),
386
+ 'bid': self.safe_string(ticker, 'bestBid'),
387
+ 'bidVolume': self.safe_string(ticker, 'bidSz'),
388
+ 'ask': self.safe_string(ticker, 'bestAsk'),
389
+ 'askVolume': self.safe_string(ticker, 'askSz'),
390
+ 'vwap': None,
391
+ 'open': self.safe_string_2(ticker, 'open24h', 'openUtc'),
392
+ 'close': last,
393
+ 'last': last,
394
+ 'previousClose': None,
395
+ 'change': None,
396
+ 'percentage': Precise.string_mul(self.safe_string(ticker, 'chgUTC'), '100'),
397
+ 'average': None,
398
+ 'baseVolume': self.safe_number(ticker, 'baseVolume'),
399
+ 'quoteVolume': self.safe_number(ticker, 'quoteVolume'),
400
+ 'indexPrice': self.safe_string(ticker, 'indexPrice'),
401
+ 'markPrice': self.safe_string(ticker, 'markPrice'),
402
+ 'info': ticker,
403
+ }, market)
404
+
405
+ async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
406
+ """
407
+ watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
408
+ :see: https://coincatch.github.io/github.io/en/spot/#candlesticks-channel
409
+ :param str symbol: unified symbol of the market to fetch OHLCV data for
410
+ :param str timeframe: the length of time each candle represents
411
+ :param int [since]: timestamp in ms of the earliest candle to fetch(not including)
412
+ :param int [limit]: the maximum amount of candles to fetch(not including)
413
+ :param dict [params]: extra parameters specific to the exchange API endpoint
414
+ :param bool [params.instType]: the type of the instrument to fetch the OHLCV data for, 'SP' for spot markets, 'MC' for futures markets(default is 'SP')
415
+ :returns int[][]: A list of candles ordered, open, high, low, close, volume
416
+ """
417
+ await self.load_markets()
418
+ market = self.market(symbol)
419
+ timeframes = self.options['timeframesForWs']
420
+ channel = 'candle' + self.safe_string(timeframes, timeframe)
421
+ instType, instId = self.get_public_inst_type_and_id(market)
422
+ args: dict = {
423
+ 'instType': instType,
424
+ 'channel': channel,
425
+ 'instId': instId,
426
+ }
427
+ messageHash = 'ohlcv:' + symbol + ':' + timeframe
428
+ ohlcv = await self.watch_public(messageHash, messageHash, args, params)
429
+ if self.newUpdates:
430
+ limit = ohlcv.getLimit(symbol, limit)
431
+ return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
432
+
433
+ async def un_watch_ohlcv(self, symbol: str, timeframe='1m', params={}) -> Any:
434
+ """
435
+ unsubscribe from the ohlcv channel
436
+ :see: https://www.bitget.com/api-doc/spot/websocket/public/Candlesticks-Channel
437
+ :param str symbol: unified symbol of the market to unwatch the ohlcv for
438
+ :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
439
+ """
440
+ await self.load_markets()
441
+ timeframes = self.options['timeframesForWs']
442
+ interval = self.safe_string(timeframes, timeframe)
443
+ channel = 'candle' + interval
444
+ return await self.un_watch_channel(symbol, channel, 'ohlcv:' + interval, params)
445
+
446
+ def handle_ohlcv(self, client: Client, message):
447
+ #
448
+ # {
449
+ # action: 'update',
450
+ # arg: {instType: 'sp', channel: 'candle1D', instId: 'ETHUSDT'},
451
+ # data: [
452
+ # [
453
+ # '1728316800000',
454
+ # '2474.5',
455
+ # '2478.21',
456
+ # '2459.8',
457
+ # '2463.51',
458
+ # '86.0551'
459
+ # ]
460
+ # ],
461
+ # ts: 1728317607657
462
+ # }
463
+ #
464
+ arg = self.safe_dict(message, 'arg', {})
465
+ market = self.get_market_from_arg(arg)
466
+ marketId = market['id']
467
+ hash = 'ohlcv:'
468
+ data = self.safe_list(message, 'data', [])
469
+ channel = self.safe_string(arg, 'channel')
470
+ klineType = channel[6:]
471
+ timeframe = self.find_timeframe(klineType)
472
+ if marketId.find('_DMCBL') >= 0:
473
+ market = self.handle_dmcbl_market_by_message_hashes(market, hash, client, timeframe)
474
+ symbol = market['symbol']
475
+ if not (symbol in self.ohlcvs):
476
+ self.ohlcvs[symbol] = {}
477
+ if not (timeframe in self.ohlcvs[symbol]):
478
+ limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
479
+ self.ohlcvs[symbol][timeframe] = ArrayCacheByTimestamp(limit)
480
+ stored = self.ohlcvs[symbol][timeframe]
481
+ for i in range(0, len(data)):
482
+ candle = self.safe_list(data, i, [])
483
+ parsed = self.parse_ws_ohlcv(candle, market)
484
+ stored.append(parsed)
485
+ messageHash = hash + symbol + ':' + timeframe
486
+ client.resolve(stored, messageHash)
487
+
488
+ def parse_ws_ohlcv(self, ohlcv, market: Market = None) -> list:
489
+ #
490
+ # [
491
+ # '1728316800000',
492
+ # '2474.5',
493
+ # '2478.21',
494
+ # '2459.8',
495
+ # '2463.51',
496
+ # '86.0551'
497
+ # ]
498
+ #
499
+ return [
500
+ self.safe_integer(ohlcv, 0),
501
+ self.safe_number(ohlcv, 1),
502
+ self.safe_number(ohlcv, 2),
503
+ self.safe_number(ohlcv, 3),
504
+ self.safe_number(ohlcv, 4),
505
+ self.safe_number(ohlcv, 5),
506
+ ]
507
+
508
+ async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
509
+ """
510
+ watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
511
+ :see: https://coincatch.github.io/github.io/en/spot/#depth-channel
512
+ :param str symbol: unified symbol of the market to fetch the order book for
513
+ :param int [limit]: the maximum amount of order book entries to return
514
+ :param dict [params]: extra parameters specific to the exchange API endpoint
515
+ :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
516
+ """
517
+ return await self.watch_order_book_for_symbols([symbol], limit, params)
518
+
519
+ async def un_watch_order_book(self, symbol: str, params={}) -> Any:
520
+ """
521
+ unsubscribe from the orderbook channel
522
+ :see: https://coincatch.github.io/github.io/en/spot/#depth-channel
523
+ :param str symbol: unified symbol of the market to fetch the order book for
524
+ :param int [params.limit]: orderbook limit, default is None
525
+ :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
526
+ """
527
+ await self.load_markets()
528
+ channel = 'books'
529
+ limit = self.safe_integer(params, 'limit')
530
+ if (limit == 5) or (limit == 15):
531
+ params = self.omit(params, 'limit')
532
+ channel += str(limit)
533
+ return await self.un_watch_channel(symbol, channel, channel, params)
534
+
535
+ async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook:
536
+ """
537
+ watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
538
+ :see: https://coincatch.github.io/github.io/en/spot/#depth-channel
539
+ :param str symbol: unified symbol of the market to fetch the order book for
540
+ :param int [limit]: the maximum amount of order book entries to return
541
+ :param dict [params]: extra parameters specific to the exchange API endpoint
542
+ :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
543
+ """
544
+ await self.load_markets()
545
+ symbols = self.market_symbols(symbols)
546
+ channel = 'books'
547
+ topics = []
548
+ messageHashes = []
549
+ for i in range(0, len(symbols)):
550
+ symbol = symbols[i]
551
+ market = self.market(symbol)
552
+ instType, instId = self.get_public_inst_type_and_id(market)
553
+ args: dict = {
554
+ 'instType': instType,
555
+ 'channel': channel,
556
+ 'instId': instId,
557
+ }
558
+ topics.append(args)
559
+ messageHashes.append(channel + ':' + symbol)
560
+ orderbook = await self.watch_public_multiple(messageHashes, messageHashes, topics, params)
561
+ return orderbook.limit()
562
+
563
+ def handle_order_book(self, client: Client, message):
564
+ #
565
+ # {
566
+ # action: 'update',
567
+ # arg: {instType: 'sp', channel: 'books', instId: 'ETHUSDT'},
568
+ # data: [
569
+ # {
570
+ # asks: [[2507.07, 0.4248]],
571
+ # bids: [[2507.05, 0.1198]],
572
+ # checksum: -1400923312,
573
+ # ts: '1728339446908'
574
+ # }
575
+ # ],
576
+ # ts: 1728339446908
577
+ # }
578
+ #
579
+ arg = self.safe_dict(message, 'arg', {})
580
+ market = self.get_market_from_arg(arg)
581
+ marketId = market['id']
582
+ hash = 'books:'
583
+ if marketId.find('_DMCBL') >= 0:
584
+ market = self.handle_dmcbl_market_by_message_hashes(market, hash, client)
585
+ symbol = market['symbol']
586
+ channel = self.safe_string(arg, 'channel')
587
+ messageHash = hash + symbol
588
+ data = self.safe_list(message, 'data', [])
589
+ rawOrderBook = self.safe_dict(data, 0)
590
+ timestamp = self.safe_integer(rawOrderBook, 'ts')
591
+ incrementalBook = channel
592
+ if incrementalBook:
593
+ if not (symbol in self.orderbooks):
594
+ ob = self.counted_order_book({})
595
+ ob['symbol'] = symbol
596
+ self.orderbooks[symbol] = ob
597
+ storedOrderBook = self.orderbooks[symbol]
598
+ asks = self.safe_list(rawOrderBook, 'asks', [])
599
+ bids = self.safe_list(rawOrderBook, 'bids', [])
600
+ self.handle_deltas(storedOrderBook['asks'], asks)
601
+ self.handle_deltas(storedOrderBook['bids'], bids)
602
+ storedOrderBook['timestamp'] = timestamp
603
+ storedOrderBook['datetime'] = self.iso8601(timestamp)
604
+ checksum = self.safe_bool(self.options, 'checksum', True)
605
+ isSnapshot = self.safe_string(message, 'action') == 'snapshot'
606
+ if not isSnapshot and checksum:
607
+ storedAsks = storedOrderBook['asks']
608
+ storedBids = storedOrderBook['bids']
609
+ asksLength = len(storedAsks)
610
+ bidsLength = len(storedBids)
611
+ payloadArray = []
612
+ for i in range(0, 25):
613
+ if i < bidsLength:
614
+ payloadArray.append(storedBids[i][2][0])
615
+ payloadArray.append(storedBids[i][2][1])
616
+ if i < asksLength:
617
+ payloadArray.append(storedAsks[i][2][0])
618
+ payloadArray.append(storedAsks[i][2][1])
619
+ payload = ':'.join(payloadArray)
620
+ calculatedChecksum = self.crc32(payload, True)
621
+ responseChecksum = self.safe_integer(rawOrderBook, 'checksum')
622
+ if calculatedChecksum != responseChecksum:
623
+ self.spawn(self.handle_check_sum_error, client, symbol, messageHash)
624
+ return
625
+ else:
626
+ orderbook = self.order_book({})
627
+ parsedOrderbook = self.parse_order_book(rawOrderBook, symbol, timestamp)
628
+ orderbook.reset(parsedOrderbook)
629
+ self.orderbooks[symbol] = orderbook
630
+ client.resolve(self.orderbooks[symbol], messageHash)
631
+
632
+ async def handle_check_sum_error(self, client: Client, symbol: str, messageHash: str):
633
+ await self.un_watch_order_book(symbol)
634
+ error = ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol))
635
+ client.reject(error, messageHash)
636
+
637
+ def handle_delta(self, bookside, delta):
638
+ bidAsk = self.parse_bid_ask(delta, 0, 1)
639
+ bidAsk.append(delta)
640
+ bookside.storeArray(bidAsk)
641
+
642
+ def handle_deltas(self, bookside, deltas):
643
+ for i in range(0, len(deltas)):
644
+ self.handle_delta(bookside, deltas[i])
645
+
646
+ async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
647
+ """
648
+ get the list of most recent trades for a particular symbol
649
+ :see: https://coincatch.github.io/github.io/en/spot/#trades-channel
650
+ :param str symbol: unified symbol of the market to fetch trades for
651
+ :param int [since]: timestamp in ms of the earliest trade to fetch
652
+ :param int [limit]: the maximum amount of trades to fetch
653
+ :param dict [params]: extra parameters specific to the exchange API endpoint
654
+ :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
655
+ """
656
+ return await self.watch_trades_for_symbols([symbol], since, limit, params)
657
+
658
+ async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]:
659
+ """
660
+ watches information on multiple trades made in a market
661
+ :see: https://coincatch.github.io/github.io/en/spot/#trades-channel
662
+ :param str symbol: unified market symbol of the market trades were made in
663
+ :param int [since]: the earliest time in ms to fetch orders for
664
+ :param int [limit]: the maximum number of trade structures to retrieve
665
+ :param dict [params]: extra parameters specific to the exchange API endpoint
666
+ :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
667
+ """
668
+ symbolsLength = len(symbols)
669
+ if symbolsLength == 0:
670
+ raise ArgumentsRequired(self.id + ' watchTradesForSymbols() requires a non-empty array of symbols')
671
+ await self.load_markets()
672
+ symbols = self.market_symbols(symbols)
673
+ topics = []
674
+ messageHashes = []
675
+ for i in range(0, len(symbols)):
676
+ symbol = symbols[i]
677
+ market = self.market(symbol)
678
+ instType, instId = self.get_public_inst_type_and_id(market)
679
+ args: dict = {
680
+ 'instType': instType,
681
+ 'channel': 'trade',
682
+ 'instId': instId,
683
+ }
684
+ topics.append(args)
685
+ messageHashes.append('trade:' + symbol)
686
+ trades = await self.watch_public_multiple(messageHashes, messageHashes, topics, params)
687
+ if self.newUpdates:
688
+ first = self.safe_dict(trades, 0)
689
+ tradeSymbol = self.safe_string(first, 'symbol')
690
+ limit = trades.getLimit(tradeSymbol, limit)
691
+ return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
692
+
693
+ async def un_watch_trades(self, symbol: str, params={}) -> Any:
694
+ """
695
+ unsubscribe from the trades channel
696
+ :see: https://coincatch.github.io/github.io/en/spot/#trades-channel
697
+ :param str symbol: unified symbol of the market to unwatch the trades for
698
+ :returns any: status of the unwatch request
699
+ """
700
+ await self.load_markets()
701
+ return await self.un_watch_channel(symbol, 'trade', 'trade', params)
702
+
703
+ def handle_trades(self, client: Client, message):
704
+ #
705
+ # {
706
+ # action: 'update',
707
+ # arg: {instType: 'sp', channel: 'trade', instId: 'ETHUSDT'},
708
+ # data: [['1728341807469', '2421.41', '0.478', 'sell']],
709
+ # ts: 1728341807482
710
+ # }
711
+ #
712
+ arg = self.safe_dict(message, 'arg', {})
713
+ market = self.get_market_from_arg(arg)
714
+ marketId = market['id']
715
+ hash = 'trade:'
716
+ if marketId.find('_DMCBL') >= 0:
717
+ market = self.handle_dmcbl_market_by_message_hashes(market, hash, client)
718
+ symbol = market['symbol']
719
+ if not (symbol in self.trades):
720
+ limit = self.safe_integer(self.options, 'tradesLimit', 1000)
721
+ self.trades[symbol] = ArrayCache(limit)
722
+ stored = self.trades[symbol]
723
+ data = self.safe_list(message, 'data', [])
724
+ if data is not None:
725
+ data = self.sort_by(data, 0)
726
+ for i in range(0, len(data)):
727
+ trade = self.safe_list(data, i)
728
+ parsed = self.parse_ws_trade(trade, market)
729
+ stored.append(parsed)
730
+ messageHash = hash + symbol
731
+ client.resolve(stored, messageHash)
732
+
733
+ def parse_ws_trade(self, trade, market=None) -> Trade:
734
+ #
735
+ # [
736
+ # '1728341807469',
737
+ # '2421.41',
738
+ # '0.478',
739
+ # 'sell'
740
+ # ]
741
+ #
742
+ timestamp = self.safe_integer(trade, 0)
743
+ return self.safe_trade({
744
+ 'id': None,
745
+ 'timestamp': timestamp,
746
+ 'datetime': self.iso8601(timestamp),
747
+ 'symbol': market['symbol'],
748
+ 'side': self.safe_string_lower(trade, 3),
749
+ 'price': self.safe_string(trade, 1),
750
+ 'amount': self.safe_string(trade, 2),
751
+ 'cost': None,
752
+ 'takerOrMaker': None,
753
+ 'type': None,
754
+ 'order': None,
755
+ 'fee': None,
756
+ 'info': trade,
757
+ }, market)
758
+
759
+ async def watch_balance(self, params={}) -> Balances:
760
+ """
761
+ watch balance and get the amount of funds available for trading or funds locked in orders
762
+ :see: https://coincatch.github.io/github.io/en/spot/#account-channel
763
+ :see: https://coincatch.github.io/github.io/en/mix/#account-channel
764
+ :param dict [params]: extra parameters specific to the exchange API endpoint
765
+ :param str [params.type]: 'spot' or 'swap'(default is 'spot')
766
+ :param str [params.instType]: *swap only* 'umcbl' or 'dmcbl'(default is 'umcbl')
767
+ :returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
768
+ """
769
+ type = None
770
+ type, params = self.handle_market_type_and_params('watchBalance', None, params)
771
+ instType = 'spbl' # must be lower case for spot
772
+ if type == 'swap':
773
+ instType = 'umcbl'
774
+ channel = 'account'
775
+ instType, params = self.handle_option_and_params(params, 'watchBalance', 'instType', instType)
776
+ args: dict = {
777
+ 'instType': instType,
778
+ 'channel': channel,
779
+ 'instId': 'default',
780
+ }
781
+ messageHash = 'balance:' + instType.lower()
782
+ return await self.watch_private(messageHash, messageHash, args, params)
783
+
784
+ def handle_balance(self, client: Client, message):
785
+ #
786
+ # spot
787
+ # {
788
+ # action: 'snapshot',
789
+ # arg: {instType: 'spbl', channel: 'account', instId: 'default'},
790
+ # data: [
791
+ # {
792
+ # coinId: '3',
793
+ # coinName: 'ETH',
794
+ # available: '0.0000832',
795
+ # frozen: '0',
796
+ # lock: '0'
797
+ # }
798
+ # ],
799
+ # ts: 1728464548725
800
+ # }
801
+ #
802
+ # # swap
803
+ # {
804
+ # action: 'snapshot',
805
+ # arg: {instType: 'dmcbl', channel: 'account', instId: 'default'},
806
+ # data: [
807
+ # {
808
+ # marginCoin: 'ETH',
809
+ # locked: '0.00000000',
810
+ # available: '0.00001203',
811
+ # maxOpenPosAvailable: '0.00001203',
812
+ # maxTransferOut: '0.00001203',
813
+ # equity: '0.00001203',
814
+ # usdtEquity: '0.029092328738',
815
+ # coinDisplayName: 'ETH'
816
+ # }
817
+ # ],
818
+ # ts: 1728650777643
819
+ # }
820
+ #
821
+ data = self.safe_list(message, 'data', [])
822
+ for i in range(0, len(data)):
823
+ rawBalance = data[i]
824
+ currencyId = self.safe_string_2(rawBalance, 'coinName', 'marginCoin')
825
+ code = self.safe_currency_code(currencyId)
826
+ account = self.balance[code] if (code in self.balance) else self.account()
827
+ freeQuery = 'maxTransferOut' if ('maxTransferOut' in rawBalance) else 'available'
828
+ account['free'] = self.safe_string(rawBalance, freeQuery)
829
+ account['total'] = self.safe_string(rawBalance, 'equity')
830
+ account['used'] = self.safe_string(rawBalance, 'frozen')
831
+ self.balance[code] = account
832
+ self.balance = self.safe_balance(self.balance)
833
+ arg = self.safe_dict(message, 'arg')
834
+ instType = self.safe_string_lower(arg, 'instType')
835
+ messageHash = 'balance:' + instType
836
+ client.resolve(self.balance, messageHash)
837
+
838
+ async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
839
+ """
840
+ watches information on multiple orders made by the user
841
+ :see: https://coincatch.github.io/github.io/en/spot/#order-channel
842
+ :see: https://coincatch.github.io/github.io/en/mix/#order-channel
843
+ :see: https://coincatch.github.io/github.io/en/mix/#plan-order-channel
844
+ :param str symbol: unified market symbol of the market orders were made in
845
+ :param int [since]: the earliest time in ms to fetch orders for
846
+ :param int [limit]: the maximum number of order structures to retrieve
847
+ :param dict [params]: extra parameters specific to the exchange API endpoint
848
+ :param str [params.type]: 'spot' or 'swap'
849
+ :param str [params.instType]: *swap only* 'umcbl' or 'dmcbl'(default is 'umcbl')
850
+ :param bool [params.trigger]: *swap only* whether to watch trigger orders(default is False)
851
+ :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
852
+ """
853
+ methodName = 'watchOrders'
854
+ await self.load_markets()
855
+ market = None
856
+ marketId = None
857
+ if symbol is not None:
858
+ market = self.market(symbol)
859
+ symbol = market['symbol']
860
+ marketId = market['id']
861
+ marketType = None
862
+ marketType, params = self.handle_market_type_and_params(methodName, market, params)
863
+ instType = 'spbl'
864
+ instId = marketId
865
+ if marketType == 'spot':
866
+ if symbol is None:
867
+ raise ArgumentsRequired(self.id + ' ' + methodName + '() requires a symbol argument for ' + marketType + ' markets.')
868
+ else:
869
+ instId = 'default'
870
+ instType = 'umcbl'
871
+ if symbol is None:
872
+ instType, params = self.handle_option_and_params(params, methodName, 'instType', instType)
873
+ else:
874
+ if marketId.find('_DMCBL') >= 0:
875
+ instType = 'dmcbl'
876
+ channel = 'orders'
877
+ isTrigger = self.safe_bool(params, 'trigger')
878
+ if isTrigger:
879
+ channel = 'ordersAlgo' # channel does not return any data
880
+ params = self.omit(params, 'trigger')
881
+ args: dict = {
882
+ 'instType': instType,
883
+ 'channel': channel,
884
+ 'instId': instId,
885
+ }
886
+ messageHash = 'orders'
887
+ if symbol is not None:
888
+ messageHash += ':' + symbol
889
+ orders = await self.watch_private(messageHash, messageHash, args, params)
890
+ if self.newUpdates:
891
+ limit = orders.getLimit(symbol, limit)
892
+ return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)
893
+
894
+ def handle_order(self, client: Client, message):
895
+ #
896
+ # spot
897
+ #
898
+ # {
899
+ # action: 'snapshot',
900
+ # arg: {instType: 'spbl', channel: 'orders', instId: 'ETHUSDT_SPBL'},
901
+ # data: [
902
+ # {
903
+ # instId: 'ETHUSDT_SPBL',
904
+ # ordId: '1228627925964996608',
905
+ # clOrdId: 'f0cccf74-c535-4523-a53d-dbe3b9958559',
906
+ # px: '2000',
907
+ # sz: '0.001',
908
+ # notional: '2',
909
+ # ordType: 'limit',
910
+ # force: 'normal',
911
+ # side: 'buy',
912
+ # accFillSz: '0',
913
+ # avgPx: '0',
914
+ # status: 'new',
915
+ # cTime: 1728653645030,
916
+ # uTime: 1728653645030,
917
+ # orderFee: [],
918
+ # eps: 'API'
919
+ # }
920
+ # ],
921
+ # ts: 1728653645046
922
+ # }
923
+ #
924
+ # swap
925
+ #
926
+ # {
927
+ # action: 'snapshot',
928
+ # arg: {instType: 'umcbl', channel: 'orders', instId: 'default'},
929
+ # data: [
930
+ # {
931
+ # accFillSz: '0',
932
+ # cTime: 1728653796976,
933
+ # clOrdId: '1228628563272753152',
934
+ # eps: 'API',
935
+ # force: 'normal',
936
+ # hM: 'single_hold',
937
+ # instId: 'ETHUSDT_UMCBL',
938
+ # lever: '5',
939
+ # low: False,
940
+ # notionalUsd: '20',
941
+ # ordId: '1228628563188867072',
942
+ # ordType: 'limit',
943
+ # orderFee: [],
944
+ # posSide: 'net',
945
+ # px: '2000',
946
+ # side: 'buy',
947
+ # status: 'new',
948
+ # sz: '0.01',
949
+ # tS: 'buy_single',
950
+ # tdMode: 'cross',
951
+ # tgtCcy: 'USDT',
952
+ # uTime: 1728653796976
953
+ # }
954
+ # ],
955
+ # ts: 1728653797002
956
+ # }
957
+ #
958
+ #
959
+ arg = self.safe_dict(message, 'arg', {})
960
+ instType = self.safe_string(arg, 'instType')
961
+ argInstId = self.safe_string(arg, 'instId')
962
+ marketType = None
963
+ if instType == 'spbl':
964
+ marketType = 'spot'
965
+ else:
966
+ marketType = 'swap'
967
+ data = self.safe_list(message, 'data', [])
968
+ if self.orders is None:
969
+ limit = self.safe_integer(self.options, 'ordersLimit', 1000)
970
+ self.orders = ArrayCacheBySymbolById(limit)
971
+ hash = 'orders'
972
+ stored = self.orders
973
+ symbol: Str = None
974
+ for i in range(0, len(data)):
975
+ order = data[i]
976
+ marketId = self.safe_string(order, 'instId', argInstId)
977
+ market = self.safe_market(marketId, None, None, marketType)
978
+ parsed = self.parse_ws_order(order, market)
979
+ stored.append(parsed)
980
+ symbol = parsed['symbol']
981
+ messageHash = 'orders:' + symbol
982
+ client.resolve(stored, messageHash)
983
+ client.resolve(stored, hash)
984
+
985
+ def parse_ws_order(self, order: dict, market: Market = None) -> Order:
986
+ #
987
+ # spot
988
+ # {
989
+ # instId: 'ETHUSDT_SPBL',
990
+ # ordId: '1228627925964996608',
991
+ # clOrdId: 'f0cccf74-c535-4523-a53d-dbe3b9958559',
992
+ # px: '2000',
993
+ # sz: '0.001',
994
+ # notional: '2',
995
+ # ordType: 'limit',
996
+ # force: 'normal',
997
+ # side: 'buy',
998
+ # accFillSz: '0',
999
+ # avgPx: '0',
1000
+ # status: 'new',
1001
+ # cTime: 1728653645030,
1002
+ # uTime: 1728653645030,
1003
+ # orderFee: orderFee: [{fee: '0', feeCcy: 'USDT'}],
1004
+ # eps: 'API'
1005
+ # }
1006
+ #
1007
+ # swap
1008
+ # {
1009
+ # accFillSz: '0',
1010
+ # cTime: 1728653796976,
1011
+ # clOrdId: '1228628563272753152',
1012
+ # eps: 'API',
1013
+ # force: 'normal',
1014
+ # hM: 'single_hold',
1015
+ # instId: 'ETHUSDT_UMCBL',
1016
+ # lever: '5',
1017
+ # low: False,
1018
+ # notionalUsd: '20',
1019
+ # ordId: '1228628563188867072',
1020
+ # ordType: 'limit',
1021
+ # orderFee: [{fee: '0', feeCcy: 'USDT'}],
1022
+ # posSide: 'net',
1023
+ # px: '2000',
1024
+ # side: 'buy',
1025
+ # status: 'new',
1026
+ # sz: '0.01',
1027
+ # tS: 'buy_single',
1028
+ # tdMode: 'cross',
1029
+ # tgtCcy: 'USDT',
1030
+ # uTime: 1728653796976
1031
+ # }
1032
+ #
1033
+ marketId = self.safe_string(order, 'instId')
1034
+ settleId = self.safe_string(order, 'tgtCcy')
1035
+ market = self.safeMarketCustom(marketId, market, settleId)
1036
+ timestamp = self.safe_integer(order, 'cTime')
1037
+ symbol = market['symbol']
1038
+ rawStatus = self.safe_string(order, 'status')
1039
+ orderFee = self.safe_list(order, 'orderFee', [])
1040
+ fee = self.safe_dict(orderFee, 0)
1041
+ feeCost = Precise.string_mul(self.safe_string(fee, 'fee'), '-1')
1042
+ feeCurrency = self.safe_string(fee, 'feeCcy')
1043
+ price = self.omit_zero(self.safe_string(order, 'px'))
1044
+ priceAvg = self.omit_zero(self.safe_string(order, 'avgPx'))
1045
+ if price is None:
1046
+ price = priceAvg
1047
+ type = self.safe_string_lower(order, 'ordType')
1048
+ return self.safe_order({
1049
+ 'id': self.safe_string(order, 'ordId'),
1050
+ 'clientOrderId': self.safe_string(order, 'clOrdId'),
1051
+ 'timestamp': timestamp,
1052
+ 'datetime': self.iso8601(timestamp),
1053
+ 'lastTradeTimestamp': None,
1054
+ 'lastUpdateTimestamp': self.safe_integer(order, 'uTime'),
1055
+ 'status': self.parse_order_status(rawStatus),
1056
+ 'symbol': symbol,
1057
+ 'type': type,
1058
+ 'timeInForce': self.parseOrderTimeInForce(self.safe_string_lower(order, 'force')),
1059
+ 'side': self.safe_string_lower(order, 'side'),
1060
+ 'price': price,
1061
+ 'average': self.safe_string(order, 'avgPx'),
1062
+ 'amount': self.safe_string(order, 'sz'),
1063
+ 'filled': self.safe_string(order, 'accFillSz'),
1064
+ 'remaining': None,
1065
+ 'triggerPrice': None,
1066
+ 'takeProfitPrice': None,
1067
+ 'stopLossPrice': None,
1068
+ 'cost': self.safe_string(order, 'notional'),
1069
+ 'trades': None,
1070
+ 'fee': {
1071
+ 'currency': feeCurrency,
1072
+ 'cost': feeCost,
1073
+ },
1074
+ 'reduceOnly': self.safe_bool(order, 'low'),
1075
+ 'postOnly': None,
1076
+ 'info': order,
1077
+ }, market)
1078
+
1079
+ async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
1080
+ """
1081
+ watch all open positions
1082
+ :see: https://coincatch.github.io/github.io/en/mix/#positions-channel
1083
+ :param str[]|None symbols: list of unified market symbols
1084
+ :param dict params: extra parameters specific to the exchange API endpoint
1085
+ :returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
1086
+ """
1087
+ await self.load_markets()
1088
+ symbols = self.market_symbols(symbols, 'swap')
1089
+ messageHashes = []
1090
+ hash = 'positions'
1091
+ instTypes = []
1092
+ if symbols is not None:
1093
+ for i in range(0, len(symbols)):
1094
+ symbol = symbols[i]
1095
+ market = self.market(symbol)
1096
+ instType = self.get_private_inst_type(market)
1097
+ if not self.in_array(instType, instTypes):
1098
+ instTypes.append(instType)
1099
+ messageHashes.append(hash + '::' + symbol)
1100
+ else:
1101
+ instTypes = ['umcbl', 'dmcbl']
1102
+ messageHashes.append(hash)
1103
+ args = []
1104
+ subscribeHashes = []
1105
+ for i in range(0, len(instTypes)):
1106
+ instType = instTypes[i]
1107
+ arg: dict = {
1108
+ 'instType': instType,
1109
+ 'channel': hash,
1110
+ 'instId': 'default',
1111
+ }
1112
+ subscribeHashes.append(hash + '::' + instType)
1113
+ args.append(arg)
1114
+ newPositions = await self.watch_private_multiple(messageHashes, subscribeHashes, args, params)
1115
+ if self.newUpdates:
1116
+ return newPositions
1117
+ return self.filter_by_symbols_since_limit(newPositions, symbols, since, limit, True)
1118
+
1119
+ def get_private_inst_type(self, market: Market):
1120
+ marketId = market['id']
1121
+ if marketId.find('_DMCBL') >= 0:
1122
+ return 'dmcbl'
1123
+ return 'umcbl'
1124
+
1125
+ def handle_positions(self, client: Client, message):
1126
+ #
1127
+ # {
1128
+ # action: 'snapshot',
1129
+ # arg: {instType: 'umcbl', channel: 'positions', instId: 'default'},
1130
+ # data: [
1131
+ # {
1132
+ # posId: '1221355728745619456',
1133
+ # instId: 'ETHUSDT_UMCBL',
1134
+ # instName: 'ETHUSDT',
1135
+ # marginCoin: 'USDT',
1136
+ # margin: '5.27182',
1137
+ # marginMode: 'crossed',
1138
+ # holdSide: 'long',
1139
+ # holdMode: 'single_hold',
1140
+ # total: '0.01',
1141
+ # available: '0.01',
1142
+ # locked: '0',
1143
+ # averageOpenPrice: '2635.91',
1144
+ # leverage: 5,
1145
+ # achievedProfits: '0',
1146
+ # upl: '-0.0267',
1147
+ # uplRate: '-0.005064664576',
1148
+ # liqPx: '-3110.66866033',
1149
+ # keepMarginRate: '0.0033',
1150
+ # marginRate: '0.002460827254',
1151
+ # cTime: '1726919818102',
1152
+ # uTime: '1728919604312',
1153
+ # markPrice: '2633.24',
1154
+ # autoMargin: 'off'
1155
+ # }
1156
+ # ],
1157
+ # ts: 1728919604329
1158
+ # }
1159
+ #
1160
+ if self.positions is None:
1161
+ self.positions = ArrayCacheBySymbolBySide()
1162
+ cache = self.positions
1163
+ rawPositions = self.safe_value(message, 'data', [])
1164
+ dataLength = len(rawPositions)
1165
+ if dataLength == 0:
1166
+ return
1167
+ newPositions = []
1168
+ symbols = []
1169
+ for i in range(0, len(rawPositions)):
1170
+ rawPosition = rawPositions[i]
1171
+ position = self.parse_ws_position(rawPosition)
1172
+ symbols.append(position['symbol'])
1173
+ newPositions.append(position)
1174
+ cache.append(position)
1175
+ hash = 'positions'
1176
+ messageHashes = self.find_message_hashes(client, hash)
1177
+ for i in range(0, len(messageHashes)):
1178
+ messageHash = messageHashes[i]
1179
+ parts = messageHash.split('::')
1180
+ symbol = parts[1]
1181
+ if self.in_array(symbol, symbols):
1182
+ positionsForSymbol = []
1183
+ for j in range(0, len(newPositions)):
1184
+ position = newPositions[j]
1185
+ if position['symbol'] == symbol:
1186
+ positionsForSymbol.append(position)
1187
+ client.resolve(positionsForSymbol, messageHash)
1188
+ client.resolve(newPositions, hash)
1189
+
1190
+ def parse_ws_position(self, position, market=None):
1191
+ #
1192
+ # {
1193
+ # posId: '1221355728745619456',
1194
+ # instId: 'ETHUSDT_UMCBL',
1195
+ # instName: 'ETHUSDT',
1196
+ # marginCoin: 'USDT',
1197
+ # margin: '5.27182',
1198
+ # marginMode: 'crossed',
1199
+ # holdSide: 'long',
1200
+ # holdMode: 'single_hold',
1201
+ # total: '0.01',
1202
+ # available: '0.01',
1203
+ # locked: '0',
1204
+ # averageOpenPrice: '2635.91',
1205
+ # leverage: 5,
1206
+ # achievedProfits: '0',
1207
+ # upl: '-0.0267',
1208
+ # uplRate: '-0.005064664576',
1209
+ # liqPx: '-3110.66866033',
1210
+ # keepMarginRate: '0.0033',
1211
+ # marginRate: '0.002460827254',
1212
+ # cTime: '1726919818102',
1213
+ # uTime: '1728919604312',
1214
+ # markPrice: '2633.24',
1215
+ # autoMargin: 'off'
1216
+ # }
1217
+ #
1218
+ marketId = self.safe_string(position, 'symbol')
1219
+ settleId = self.safe_string(position, 'marginCoin')
1220
+ market = self.safeMarketCustom(marketId, market, settleId)
1221
+ timestamp = self.safe_integer(position, 'cTime')
1222
+ marginModeId = self.safe_string(position, 'marginMode')
1223
+ marginMode = self.get_supported_mapping(marginModeId, {
1224
+ 'crossed': 'cross',
1225
+ 'isolated': 'isolated',
1226
+ })
1227
+ isHedged: Bool = None
1228
+ holdMode = self.safe_string(position, 'holdMode')
1229
+ if holdMode == 'double_hold':
1230
+ isHedged = True
1231
+ elif holdMode == 'single_hold':
1232
+ isHedged = False
1233
+ percentageDecimal = self.safe_string(position, 'uplRate')
1234
+ percentage = Precise.string_mul(percentageDecimal, '100')
1235
+ margin = self.safe_number(position, 'margin')
1236
+ return self.safe_position({
1237
+ 'symbol': market['symbol'],
1238
+ 'id': None,
1239
+ 'timestamp': timestamp,
1240
+ 'datetime': self.iso8601(timestamp),
1241
+ 'contracts': self.safe_number(position, 'total'),
1242
+ 'contractSize': None,
1243
+ 'side': self.safe_string_lower(position, 'holdSide'),
1244
+ 'notional': margin, # todo check
1245
+ 'leverage': self.safe_integer(position, 'leverage'),
1246
+ 'unrealizedPnl': self.safe_number(position, 'upl'),
1247
+ 'realizedPnl': self.safe_number(position, 'achievedProfits'),
1248
+ 'collateral': None, # todo check
1249
+ 'entryPrice': self.safe_number(position, 'averageOpenPrice'),
1250
+ 'markPrice': self.safe_number(position, 'markPrice'),
1251
+ 'liquidationPrice': self.safe_number(position, 'liqPx'),
1252
+ 'marginMode': marginMode,
1253
+ 'hedged': isHedged,
1254
+ 'maintenanceMargin': None, # todo check
1255
+ 'maintenanceMarginPercentage': self.safe_number(position, 'keepMarginRate'),
1256
+ 'initialMargin': margin, # todo check
1257
+ 'initialMarginPercentage': None,
1258
+ 'marginRatio': self.safe_number(position, 'marginRate'),
1259
+ 'lastUpdateTimestamp': self.safe_integer(position, 'uTime'),
1260
+ 'lastPrice': None,
1261
+ 'stopLossPrice': None,
1262
+ 'takeProfitPrice': None,
1263
+ 'percentage': percentage,
1264
+ 'info': position,
1265
+ })
1266
+
1267
+ def handle_error_message(self, client: Client, message):
1268
+ #
1269
+ # {event: "error", code: 30001, msg: "Channel does not exist"}
1270
+ #
1271
+ event = self.safe_string(message, 'event')
1272
+ try:
1273
+ if event == 'error':
1274
+ code = self.safe_string(message, 'code')
1275
+ feedback = self.id + ' ' + self.json(message)
1276
+ self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, feedback)
1277
+ msg = self.safe_string(message, 'msg', '')
1278
+ self.throw_broadly_matched_exception(self.exceptions['ws']['broad'], msg, feedback)
1279
+ raise ExchangeError(feedback)
1280
+ return False
1281
+ except Exception as e:
1282
+ if isinstance(e, AuthenticationError):
1283
+ messageHash = 'authenticated'
1284
+ client.reject(e, messageHash)
1285
+ if messageHash in client.subscriptions:
1286
+ del client.subscriptions[messageHash]
1287
+ else:
1288
+ client.reject(e)
1289
+ return True
1290
+
1291
+ def handle_message(self, client: Client, message):
1292
+ # todo handle with subscribe and unsubscribe
1293
+ if self.handle_error_message(client, message):
1294
+ return
1295
+ content = self.safe_string(message, 'message')
1296
+ if content == 'pong':
1297
+ self.handle_pong(client, message)
1298
+ return
1299
+ if message == 'pong':
1300
+ self.handle_pong(client, message)
1301
+ return
1302
+ event = self.safe_string(message, 'event')
1303
+ if event == 'login':
1304
+ self.handle_authenticate(client, message)
1305
+ return
1306
+ if event == 'subscribe':
1307
+ self.handle_subscription_status(client, message)
1308
+ return
1309
+ if event == 'unsubscribe':
1310
+ self.handle_un_subscription_status(client, message)
1311
+ return
1312
+ data = self.safe_dict(message, 'arg', {})
1313
+ channel = self.safe_string(data, 'channel')
1314
+ if channel == 'ticker':
1315
+ self.handle_ticker(client, message)
1316
+ if channel.find('candle') >= 0:
1317
+ self.handle_ohlcv(client, message)
1318
+ if channel.find('books') >= 0:
1319
+ self.handle_order_book(client, message)
1320
+ if channel == 'trade':
1321
+ self.handle_trades(client, message)
1322
+ if channel == 'account':
1323
+ self.handle_balance(client, message)
1324
+ if (channel == 'orders') or (channel == 'ordersAlgo'):
1325
+ self.handle_order(client, message)
1326
+ if channel == 'positions':
1327
+ self.handle_positions(client, message)
1328
+
1329
+ def ping(self, client: Client):
1330
+ return 'ping'
1331
+
1332
+ def handle_pong(self, client: Client, message):
1333
+ client.lastPong = self.milliseconds()
1334
+ return message
1335
+
1336
+ def handle_subscription_status(self, client: Client, message):
1337
+ return message
1338
+
1339
+ def handle_un_subscription_status(self, client: Client, message):
1340
+ argsList = self.safe_list(message, 'args')
1341
+ if argsList is None:
1342
+ argsList = [self.safe_dict(message, 'arg', {})]
1343
+ for i in range(0, len(argsList)):
1344
+ arg = argsList[i]
1345
+ channel = self.safe_string(arg, 'channel')
1346
+ if channel == 'books':
1347
+ self.handle_order_book_un_subscription(client, message)
1348
+ elif channel == 'trade':
1349
+ self.handle_trades_un_subscription(client, message)
1350
+ elif channel == 'ticker':
1351
+ self.handle_ticker_un_subscription(client, message)
1352
+ elif channel.startswith('candle'):
1353
+ self.handle_ohlcv_un_subscription(client, message)
1354
+ return message
1355
+
1356
+ def handle_order_book_un_subscription(self, client: Client, message):
1357
+ arg = self.safe_dict(message, 'arg', {})
1358
+ instType = self.safe_string_lower(arg, 'instType')
1359
+ type = 'spot' if (instType == 'sp') else 'swap'
1360
+ instId = self.safe_string(arg, 'instId')
1361
+ market = self.safe_market(instId, None, None, type)
1362
+ symbol = market['symbol']
1363
+ messageHash = 'unsubscribe:orderbook:' + market['symbol']
1364
+ subMessageHash = 'orderbook:' + symbol
1365
+ if symbol in self.orderbooks:
1366
+ del self.orderbooks[symbol]
1367
+ if subMessageHash in client.subscriptions:
1368
+ del client.subscriptions[subMessageHash]
1369
+ if messageHash in client.subscriptions:
1370
+ del client.subscriptions[messageHash]
1371
+ error = UnsubscribeError(self.id + 'orderbook ' + symbol)
1372
+ client.reject(error, subMessageHash)
1373
+ client.resolve(True, messageHash)
1374
+
1375
+ def handle_trades_un_subscription(self, client: Client, message):
1376
+ arg = self.safe_dict(message, 'arg', {})
1377
+ instType = self.safe_string_lower(arg, 'instType')
1378
+ type = 'spot' if (instType == 'sp') else 'swap'
1379
+ instId = self.safe_string(arg, 'instId')
1380
+ market = self.safe_market(instId, None, None, type)
1381
+ symbol = market['symbol']
1382
+ messageHash = 'unsubscribe:trade:' + market['symbol']
1383
+ subMessageHash = 'trade:' + symbol
1384
+ if symbol in self.trades:
1385
+ del self.trades[symbol]
1386
+ if subMessageHash in client.subscriptions:
1387
+ del client.subscriptions[subMessageHash]
1388
+ if messageHash in client.subscriptions:
1389
+ del client.subscriptions[messageHash]
1390
+ error = UnsubscribeError(self.id + 'trades ' + symbol)
1391
+ client.reject(error, subMessageHash)
1392
+ client.resolve(True, messageHash)
1393
+
1394
+ def handle_ticker_un_subscription(self, client: Client, message):
1395
+ arg = self.safe_dict(message, 'arg', {})
1396
+ instType = self.safe_string_lower(arg, 'instType')
1397
+ type = 'spot' if (instType == 'sp') else 'swap'
1398
+ instId = self.safe_string(arg, 'instId')
1399
+ market = self.safe_market(instId, None, None, type)
1400
+ symbol = market['symbol']
1401
+ messageHash = 'unsubscribe:ticker:' + market['symbol']
1402
+ subMessageHash = 'ticker:' + symbol
1403
+ if symbol in self.tickers:
1404
+ del self.tickers[symbol]
1405
+ if subMessageHash in client.subscriptions:
1406
+ del client.subscriptions[subMessageHash]
1407
+ if messageHash in client.subscriptions:
1408
+ del client.subscriptions[messageHash]
1409
+ error = UnsubscribeError(self.id + 'ticker ' + symbol)
1410
+ client.reject(error, subMessageHash)
1411
+ client.resolve(True, messageHash)
1412
+
1413
+ def handle_ohlcv_un_subscription(self, client: Client, message):
1414
+ arg = self.safe_dict(message, 'arg', {})
1415
+ instType = self.safe_string_lower(arg, 'instType')
1416
+ type = 'spot' if (instType == 'sp') else 'swap'
1417
+ instId = self.safe_string(arg, 'instId')
1418
+ channel = self.safe_string(arg, 'channel')
1419
+ interval = channel.replace('candle', '')
1420
+ timeframes = self.safe_dict(self.options, 'timeframesForWs')
1421
+ timeframe = self.find_timeframe(interval, timeframes)
1422
+ market = self.safe_market(instId, None, None, type)
1423
+ symbol = market['symbol']
1424
+ messageHash = 'unsubscribe:ohlcv:' + timeframe + ':' + market['symbol']
1425
+ subMessageHash = 'ohlcv:' + symbol + ':' + timeframe
1426
+ if symbol in self.ohlcvs:
1427
+ if timeframe in self.ohlcvs[symbol]:
1428
+ del self.ohlcvs[symbol][timeframe]
1429
+ self.clean_unsubscription(client, subMessageHash, messageHash)