ccxt 4.1.83__py2.py3-none-any.whl → 4.1.85__py2.py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of ccxt might be problematic. Click here for more details.

ccxt/pro/bitmart.py CHANGED
@@ -4,11 +4,14 @@
4
4
  # https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
5
5
 
6
6
  import ccxt.async_support
7
- from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp
7
+ from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp
8
+ from ccxt.async_support.base.ws.order_book_side import Asks, Bids
8
9
  import hashlib
9
- from ccxt.base.types import Int, Str
10
+ from ccxt.base.types import Int, Market, Str, Strings
10
11
  from ccxt.async_support.base.ws.client import Client
12
+ from ccxt.base.errors import ExchangeError
11
13
  from ccxt.base.errors import ArgumentsRequired
14
+ from ccxt.base.errors import NotSupported
12
15
  from ccxt.base.errors import AuthenticationError
13
16
 
14
17
 
@@ -25,24 +28,38 @@ class bitmart(ccxt.async_support.bitmart):
25
28
  'cancelOrdersWs': False,
26
29
  'cancelAllOrdersWs': False,
27
30
  'ws': True,
31
+ 'watchBalance': True,
28
32
  'watchTicker': True,
33
+ 'watchTickers': True,
29
34
  'watchOrderBook': True,
30
35
  'watchOrders': True,
31
36
  'watchTrades': True,
32
37
  'watchOHLCV': True,
38
+ 'watchPosition': 'emulated',
39
+ 'watchPositions': True,
33
40
  },
34
41
  'urls': {
35
42
  'api': {
36
43
  'ws': {
37
- 'public': 'wss://ws-manager-compress.{hostname}/api?protocol=1.1',
38
- 'private': 'wss://ws-manager-compress.{hostname}/user?protocol=1.1',
44
+ 'spot': {
45
+ 'public': 'wss://ws-manager-compress.{hostname}/api?protocol=1.1',
46
+ 'private': 'wss://ws-manager-compress.{hostname}/user?protocol=1.1',
47
+ },
48
+ 'swap': {
49
+ 'public': 'wss://openapi-ws.{hostname}/api?protocol=1.1',
50
+ 'private': 'wss://openapi-ws.{hostname}/user?protocol=1.1',
51
+ },
39
52
  },
40
53
  },
41
54
  },
42
55
  'options': {
43
56
  'defaultType': 'spot',
57
+ 'watchBalance': {
58
+ 'fetchBalanceSnapshot': True, # or False
59
+ 'awaitBalanceSnapshot': True, # whether to wait for the balance snapshot before providing updates
60
+ },
44
61
  'watchOrderBook': {
45
- 'depth': 'depth5', # depth5, depth20, depth50
62
+ 'depth': 'depth50', # depth5, depth20, depth50
46
63
  },
47
64
  'ws': {
48
65
  'inflate': True,
@@ -68,31 +85,147 @@ class bitmart(ccxt.async_support.bitmart):
68
85
  },
69
86
  })
70
87
 
71
- async def subscribe(self, channel, symbol, params={}):
72
- await self.load_markets()
88
+ async def subscribe(self, channel, symbol, type, params={}):
73
89
  market = self.market(symbol)
74
- url = self.implode_hostname(self.urls['api']['ws']['public'])
75
- messageHash = market['type'] + '/' + channel + ':' + market['id']
76
- request = {
77
- 'op': 'subscribe',
78
- 'args': [messageHash],
79
- }
90
+ url = self.implode_hostname(self.urls['api']['ws'][type]['public'])
91
+ request = {}
92
+ messageHash = None
93
+ if type == 'spot':
94
+ messageHash = 'spot/' + channel + ':' + market['id']
95
+ request = {
96
+ 'op': 'subscribe',
97
+ 'args': [messageHash],
98
+ }
99
+ else:
100
+ messageHash = 'futures/' + channel + ':' + market['id']
101
+ request = {
102
+ 'action': 'subscribe',
103
+ 'args': [messageHash],
104
+ }
80
105
  return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash)
81
106
 
82
- async def subscribe_private(self, channel, symbol, params={}):
107
+ async def watch_balance(self, params={}):
108
+ """
109
+ :see: https://developer-pro.bitmart.com/en/spot/#private-balance-change
110
+ :see: https://developer-pro.bitmart.com/en/futures/#private-assets-channel
111
+ watch balance and get the amount of funds available for trading or funds locked in orders
112
+ :param dict [params]: extra parameters specific to the exchange API endpoint
113
+ :returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
114
+ """
83
115
  await self.load_markets()
84
- market = self.market(symbol)
85
- url = self.implode_hostname(self.urls['api']['ws']['private'])
86
- messageHash = channel + ':' + market['id']
87
- await self.authenticate()
88
- request = {
89
- 'op': 'subscribe',
90
- 'args': [messageHash],
91
- }
116
+ type = 'spot'
117
+ type, params = self.handle_market_type_and_params('watchBalance', None, params)
118
+ await self.authenticate(type, params)
119
+ request = {}
120
+ if type == 'spot':
121
+ request = {
122
+ 'op': 'subscribe',
123
+ 'args': ['spot/user/balance:BALANCE_UPDATE'],
124
+ }
125
+ else:
126
+ request = {
127
+ 'action': 'subscribe',
128
+ 'args': ['futures/asset:USDT', 'futures/asset:BTC', 'futures/asset:ETH'],
129
+ }
130
+ messageHash = 'balance:' + type
131
+ url = self.implode_hostname(self.urls['api']['ws'][type]['private'])
132
+ client = self.client(url)
133
+ self.set_balance_cache(client, type)
134
+ fetchBalanceSnapshot = self.handle_option_and_params(self.options, 'watchBalance', 'fetchBalanceSnapshot', True)
135
+ awaitBalanceSnapshot = self.handle_option_and_params(self.options, 'watchBalance', 'awaitBalanceSnapshot', False)
136
+ if fetchBalanceSnapshot and awaitBalanceSnapshot:
137
+ await client.future(type + ':fetchBalanceSnapshot')
92
138
  return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash)
93
139
 
140
+ def set_balance_cache(self, client: Client, type):
141
+ if type in client.subscriptions:
142
+ return None
143
+ options = self.safe_value(self.options, 'watchBalance')
144
+ fetchBalanceSnapshot = self.handle_option_and_params(options, 'watchBalance', 'fetchBalanceSnapshot', True)
145
+ if fetchBalanceSnapshot:
146
+ messageHash = type + ':fetchBalanceSnapshot'
147
+ if not (messageHash in client.futures):
148
+ client.future(messageHash)
149
+ self.spawn(self.load_balance_snapshot, client, messageHash, type)
150
+ else:
151
+ self.balance[type] = {}
152
+
153
+ async def load_balance_snapshot(self, client, messageHash, type):
154
+ response = await self.fetch_balance({'type': type})
155
+ self.balance[type] = self.extend(response, self.safe_value(self.balance, type, {}))
156
+ # don't remove the future from the .futures cache
157
+ future = client.futures[messageHash]
158
+ future.resolve()
159
+ client.resolve(self.balance[type], 'balance:' + type)
160
+
161
+ def handle_balance(self, client: Client, message):
162
+ #
163
+ # spot
164
+ # {
165
+ # "data":[
166
+ # {
167
+ # "balance_details":[
168
+ # {
169
+ # "av_bal":"0.206000000000000000000000000000",
170
+ # "ccy":"LTC",
171
+ # "fz_bal":"0.100000000000000000000000000000"
172
+ # }
173
+ # ],
174
+ # "event_time":"1701632345415",
175
+ # "event_type":"TRANSACTION_COMPLETED"
176
+ # }
177
+ # ],
178
+ # "table":"spot/user/balance"
179
+ # }
180
+ # swap
181
+ # {
182
+ # group: 'futures/asset:USDT',
183
+ # data: {
184
+ # currency: 'USDT',
185
+ # available_balance: '37.19688649135',
186
+ # position_deposit: '0.788687546',
187
+ # frozen_balance: '0'
188
+ # }
189
+ # }
190
+ #
191
+ channel = self.safe_string_2(message, 'table', 'group')
192
+ data = self.safe_value(message, 'data')
193
+ if data is None:
194
+ return
195
+ isSpot = (channel.find('spot') >= 0)
196
+ type = 'spot' if isSpot else 'swap'
197
+ self.balance['info'] = message
198
+ if isSpot:
199
+ if not isinstance(data, list):
200
+ return
201
+ for i in range(0, len(data)):
202
+ timestamp = self.safe_integer(message, 'event_time')
203
+ self.balance['timestamp'] = timestamp
204
+ self.balance['datetime'] = self.iso8601(timestamp)
205
+ balanceDetails = self.safe_value(data[i], 'balance_details', [])
206
+ for ii in range(0, len(balanceDetails)):
207
+ rawBalance = balanceDetails[i]
208
+ account = self.account()
209
+ currencyId = self.safe_string(rawBalance, 'ccy')
210
+ code = self.safe_currency_code(currencyId)
211
+ account['free'] = self.safe_string(rawBalance, 'av_bal')
212
+ account['total'] = self.safe_string(rawBalance, 'fz_bal')
213
+ self.balance[code] = account
214
+ else:
215
+ currencyId = self.safe_string(data, 'currency')
216
+ code = self.safe_currency_code(currencyId)
217
+ account = self.account()
218
+ account['free'] = self.safe_string(data, 'available_balance')
219
+ account['used'] = self.safe_string(data, 'frozen_balance')
220
+ self.balance[type][code] = account
221
+ self.balance[type] = self.safe_balance(self.balance[type])
222
+ messageHash = 'balance:' + type
223
+ client.resolve(self.balance[type], messageHash)
224
+
94
225
  async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}):
95
226
  """
227
+ :see: https://developer-pro.bitmart.com/en/spot/#public-trade-channel
228
+ :see: https://developer-pro.bitmart.com/en/futures/#public-trade-channel
96
229
  get the list of most recent trades for a particular symbol
97
230
  :param str symbol: unified symbol of the market to fetch trades for
98
231
  :param int [since]: timestamp in ms of the earliest trade to fetch
@@ -102,22 +235,65 @@ class bitmart(ccxt.async_support.bitmart):
102
235
  """
103
236
  await self.load_markets()
104
237
  symbol = self.symbol(symbol)
105
- trades = await self.subscribe('trade', symbol, params)
238
+ market = self.market(symbol)
239
+ type = 'spot'
240
+ type, params = self.handle_market_type_and_params('watchTrades', market, params)
241
+ trades = await self.subscribe('trade', symbol, type, params)
106
242
  if self.newUpdates:
107
243
  limit = trades.getLimit(symbol, limit)
108
244
  return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
109
245
 
110
246
  async def watch_ticker(self, symbol: str, params={}):
111
247
  """
248
+ :see: https://developer-pro.bitmart.com/en/spot/#public-ticker-channel
112
249
  watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
113
250
  :param str symbol: unified symbol of the market to fetch the ticker for
114
251
  :param dict [params]: extra parameters specific to the exchange API endpoint
115
252
  :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
116
253
  """
117
- return await self.subscribe('ticker', symbol, params)
254
+ await self.load_markets()
255
+ symbol = self.symbol(symbol)
256
+ market = self.market(symbol)
257
+ type = 'spot'
258
+ type, params = self.handle_market_type_and_params('watchTicker', market, params)
259
+ if type == 'swap':
260
+ raise NotSupported(self.id + ' watchTicker() does not support ' + type + ' markets. Use watchTickers() instead')
261
+ return await self.subscribe('ticker', symbol, type, params)
262
+
263
+ async def watch_tickers(self, symbols: Strings = None, params={}):
264
+ """
265
+ :see: https://developer-pro.bitmart.com/en/futures/#overview
266
+ watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
267
+ :param str[] symbols: unified symbol of the market to fetch the ticker for
268
+ :param dict [params]: extra parameters specific to the exchange API endpoint
269
+ :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
270
+ """
271
+ await self.load_markets()
272
+ market = self.get_market_from_symbols(symbols)
273
+ type = 'spot'
274
+ type, params = self.handle_market_type_and_params('watchTickers', market, params)
275
+ symbols = self.market_symbols(symbols)
276
+ if type == 'spot':
277
+ raise NotSupported(self.id + ' watchTickers() does not support ' + type + ' markets. Use watchTicker() instead')
278
+ url = self.implode_hostname(self.urls['api']['ws'][type]['public'])
279
+ if type == 'swap':
280
+ type = 'futures'
281
+ messageHash = 'tickers'
282
+ if symbols is not None:
283
+ messageHash += '::' + ','.join(symbols)
284
+ request = {
285
+ 'action': 'subscribe',
286
+ 'args': ['futures/ticker'],
287
+ }
288
+ newTickers = await self.watch(url, messageHash, self.deep_extend(request, params), messageHash)
289
+ if self.newUpdates:
290
+ return newTickers
291
+ return self.filter_by_array(self.tickers, 'symbol', symbols)
118
292
 
119
293
  async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
120
294
  """
295
+ :see: https://developer-pro.bitmart.com/en/spot/#private-order-channel
296
+ :see: https://developer-pro.bitmart.com/en/futures/#private-order-channel
121
297
  watches information on multiple orders made by the user
122
298
  :param str symbol: unified market symbol of the market orders were made in
123
299
  :param int [since]: the earliest time in ms to fetch orders for
@@ -125,184 +301,593 @@ class bitmart(ccxt.async_support.bitmart):
125
301
  :param dict [params]: extra parameters specific to the exchange API endpoint
126
302
  :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
127
303
  """
128
- if symbol is None:
129
- raise ArgumentsRequired(self.id + ' watchOrders() requires a symbol argument')
130
304
  await self.load_markets()
131
- market = self.market(symbol)
132
- symbol = market['symbol']
133
- if market['type'] != 'spot':
134
- raise ArgumentsRequired(self.id + ' watchOrders supports spot markets only')
135
- channel = 'spot/user/order'
136
- orders = await self.subscribe_private(channel, symbol, params)
305
+ market = None
306
+ messageHash = 'orders'
307
+ if symbol is not None:
308
+ symbol = self.symbol(symbol)
309
+ market = self.market(symbol)
310
+ messageHash = 'orders::' + symbol
311
+ type = 'spot'
312
+ type, params = self.handle_market_type_and_params('watchOrders', market, params)
313
+ await self.authenticate(type, params)
314
+ request = None
315
+ if type == 'spot':
316
+ if symbol is None:
317
+ raise ArgumentsRequired(self.id + ' watchOrders() requires a symbol argument for spot markets')
318
+ request = {
319
+ 'op': 'subscribe',
320
+ 'args': ['spot/user/order:' + market['id']],
321
+ }
322
+ else:
323
+ request = {
324
+ 'action': 'subscribe',
325
+ 'args': ['futures/order'],
326
+ }
327
+ url = self.implode_hostname(self.urls['api']['ws'][type]['private'])
328
+ newOrders = await self.watch(url, messageHash, self.deep_extend(request, params), messageHash)
137
329
  if self.newUpdates:
138
- limit = orders.getLimit(symbol, limit)
139
- return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)
330
+ return newOrders
331
+ return self.filter_by_symbol_since_limit(self.orders, symbol, since, limit, True)
140
332
 
141
333
  def handle_orders(self, client: Client, message):
142
334
  #
143
- # {
144
- # "data":[
145
- # {
146
- # "symbol": "LTC_USDT",
147
- # "notional": '',
148
- # "side": "buy",
149
- # "last_fill_time": "0",
150
- # "ms_t": "1646216634000",
151
- # "type": "limit",
152
- # "filled_notional": "0.000000000000000000000000000000",
153
- # "last_fill_price": "0",
154
- # "size": "0.500000000000000000000000000000",
155
- # "price": "50.000000000000000000000000000000",
156
- # "last_fill_count": "0",
157
- # "filled_size": "0.000000000000000000000000000000",
158
- # "margin_trading": "0",
159
- # "state": "8",
160
- # "order_id": "24807076628",
161
- # "order_type": "0"
335
+ # spot
336
+ # {
337
+ # "data":[
338
+ # {
339
+ # "symbol": "LTC_USDT",
340
+ # "notional": '',
341
+ # "side": "buy",
342
+ # "last_fill_time": "0",
343
+ # "ms_t": "1646216634000",
344
+ # "type": "limit",
345
+ # "filled_notional": "0.000000000000000000000000000000",
346
+ # "last_fill_price": "0",
347
+ # "size": "0.500000000000000000000000000000",
348
+ # "price": "50.000000000000000000000000000000",
349
+ # "last_fill_count": "0",
350
+ # "filled_size": "0.000000000000000000000000000000",
351
+ # "margin_trading": "0",
352
+ # "state": "8",
353
+ # "order_id": "24807076628",
354
+ # "order_type": "0"
355
+ # }
356
+ # ],
357
+ # "table":"spot/user/order"
358
+ # }
359
+ # swap
360
+ # {
361
+ # "group":"futures/order",
362
+ # "data":[
363
+ # {
364
+ # "action":2,
365
+ # "order":{
366
+ # "order_id":"2312045036986775",
367
+ # "client_order_id":"",
368
+ # "price":"71.61707928",
369
+ # "size":"1",
370
+ # "symbol":"LTCUSDT",
371
+ # "state":1,
372
+ # "side":4,
373
+ # "type":"market",
374
+ # "leverage":"1",
375
+ # "open_type":"cross",
376
+ # "deal_avg_price":"0",
377
+ # "deal_size":"0",
378
+ # "create_time":1701625324646,
379
+ # "update_time":1701625324640,
380
+ # "plan_order_id":"",
381
+ # "last_trade":null
382
+ # }
162
383
  # }
163
- # ],
164
- # "table":"spot/user/order"
165
- # }
384
+ # ]
385
+ # }
166
386
  #
167
- channel = self.safe_string(message, 'table')
168
- orders = self.safe_value(message, 'data', [])
387
+ orders = self.safe_value(message, 'data')
388
+ if orders is None:
389
+ return
169
390
  ordersLength = len(orders)
391
+ newOrders = []
392
+ symbols = {}
170
393
  if ordersLength > 0:
171
394
  limit = self.safe_integer(self.options, 'ordersLimit', 1000)
172
395
  if self.orders is None:
173
396
  self.orders = ArrayCacheBySymbolById(limit)
174
397
  stored = self.orders
175
- marketIds = []
176
398
  for i in range(0, len(orders)):
177
399
  order = self.parse_ws_order(orders[i])
178
400
  stored.append(order)
401
+ newOrders.append(order)
179
402
  symbol = order['symbol']
180
- market = self.market(symbol)
181
- marketIds.append(market['id'])
182
- for i in range(0, len(marketIds)):
183
- messageHash = channel + ':' + marketIds[i]
184
- client.resolve(self.orders, messageHash)
185
-
186
- def parse_ws_order(self, order, market=None):
187
- #
188
- # {
189
- # "symbol": "LTC_USDT",
190
- # "notional": '',
191
- # "side": "buy",
192
- # "last_fill_time": "0",
193
- # "ms_t": "1646216634000",
194
- # "type": "limit",
195
- # "filled_notional": "0.000000000000000000000000000000",
196
- # "last_fill_price": "0",
197
- # "size": "0.500000000000000000000000000000",
198
- # "price": "50.000000000000000000000000000000",
199
- # "last_fill_count": "0",
200
- # "filled_size": "0.000000000000000000000000000000",
201
- # "margin_trading": "0",
202
- # "state": "8",
203
- # "order_id": "24807076628",
204
- # "order_type": "0"
205
- # }
206
- #
207
- marketId = self.safe_string(order, 'symbol')
208
- market = self.safe_market(marketId, market)
209
- id = self.safe_string(order, 'order_id')
210
- clientOrderId = self.safe_string(order, 'clientOid')
211
- price = self.safe_string(order, 'price')
212
- filled = self.safe_string(order, 'filled_size')
213
- amount = self.safe_string(order, 'size')
214
- type = self.safe_string(order, 'type')
215
- rawState = self.safe_string(order, 'state')
216
- status = self.parseOrderStatusByType(market['type'], rawState)
217
- timestamp = self.safe_integer(order, 'ms_t')
403
+ symbols[symbol] = True
404
+ newOrderSymbols = list(symbols.keys())
405
+ for i in range(0, len(newOrderSymbols)):
406
+ symbol = newOrderSymbols[i]
407
+ self.resolve_promise_if_messagehash_matches(client, 'orders::', symbol, newOrders)
408
+ client.resolve(newOrders, 'orders')
409
+
410
+ def parse_ws_order(self, order, market: Market = None):
411
+ #
412
+ # spot
413
+ # {
414
+ # "symbol": "LTC_USDT",
415
+ # "notional": '',
416
+ # "side": "buy",
417
+ # "last_fill_time": "0",
418
+ # "ms_t": "1646216634000",
419
+ # "type": "limit",
420
+ # "filled_notional": "0.000000000000000000000000000000",
421
+ # "last_fill_price": "0",
422
+ # "size": "0.500000000000000000000000000000",
423
+ # "price": "50.000000000000000000000000000000",
424
+ # "last_fill_count": "0",
425
+ # "filled_size": "0.000000000000000000000000000000",
426
+ # "margin_trading": "0",
427
+ # "state": "8",
428
+ # "order_id": "24807076628",
429
+ # "order_type": "0"
430
+ # }
431
+ # swap
432
+ # {
433
+ # "action":2,
434
+ # "order":{
435
+ # "order_id":"2312045036986775",
436
+ # "client_order_id":"",
437
+ # "price":"71.61707928",
438
+ # "size":"1",
439
+ # "symbol":"LTCUSDT",
440
+ # "state":1,
441
+ # "side":4,
442
+ # "type":"market",
443
+ # "leverage":"1",
444
+ # "open_type":"cross",
445
+ # "deal_avg_price":"0",
446
+ # "deal_size":"0",
447
+ # "create_time":1701625324646,
448
+ # "update_time":1701625324640,
449
+ # "plan_order_id":"",
450
+ # "last_trade":null
451
+ # }
452
+ # }
453
+ #
454
+ action = self.safe_number(order, 'action')
455
+ isSpot = (action is None)
456
+ if isSpot:
457
+ marketId = self.safe_string(order, 'symbol')
458
+ market = self.safe_market(marketId, market, '_', 'spot')
459
+ id = self.safe_string(order, 'order_id')
460
+ clientOrderId = self.safe_string(order, 'clientOid')
461
+ price = self.safe_string(order, 'price')
462
+ filled = self.safe_string(order, 'filled_size')
463
+ amount = self.safe_string(order, 'size')
464
+ type = self.safe_string(order, 'type')
465
+ rawState = self.safe_string(order, 'state')
466
+ status = self.parseOrderStatusByType(market['type'], rawState)
467
+ timestamp = self.safe_integer(order, 'ms_t')
468
+ symbol = market['symbol']
469
+ side = self.safe_string_lower(order, 'side')
470
+ return self.safe_order({
471
+ 'info': order,
472
+ 'symbol': symbol,
473
+ 'id': id,
474
+ 'clientOrderId': clientOrderId,
475
+ 'timestamp': None,
476
+ 'datetime': None,
477
+ 'lastTradeTimestamp': timestamp,
478
+ 'type': type,
479
+ 'timeInForce': None,
480
+ 'postOnly': None,
481
+ 'side': side,
482
+ 'price': price,
483
+ 'stopPrice': None,
484
+ 'triggerPrice': None,
485
+ 'amount': amount,
486
+ 'cost': None,
487
+ 'average': None,
488
+ 'filled': filled,
489
+ 'remaining': None,
490
+ 'status': status,
491
+ 'fee': None,
492
+ 'trades': None,
493
+ }, market)
494
+ else:
495
+ orderInfo = self.safe_value(order, 'order')
496
+ marketId = self.safe_string(orderInfo, 'symbol')
497
+ symbol = self.safe_symbol(marketId, market, '', 'swap')
498
+ orderId = self.safe_string(orderInfo, 'order_id')
499
+ timestamp = self.safe_integer(orderInfo, 'create_time')
500
+ updatedTimestamp = self.safe_integer(orderInfo, 'update_time')
501
+ lastTrade = self.safe_value(orderInfo, 'last_trade')
502
+ cachedOrders = self.orders
503
+ orders = self.safe_value(cachedOrders.hashmap, symbol, {})
504
+ cachedOrder = self.safe_value(orders, orderId)
505
+ trades = None
506
+ if cachedOrder is not None:
507
+ trades = self.safe_value(order, 'trades')
508
+ if lastTrade is not None:
509
+ if trades is None:
510
+ trades = []
511
+ trades.append(lastTrade)
512
+ return self.safe_order({
513
+ 'info': order,
514
+ 'symbol': symbol,
515
+ 'id': orderId,
516
+ 'clientOrderId': self.safe_string(orderInfo, 'client_order_id'),
517
+ 'timestamp': timestamp,
518
+ 'datetime': self.iso8601(timestamp),
519
+ 'lastTradeTimestamp': updatedTimestamp,
520
+ 'type': self.safe_string(orderInfo, 'type'),
521
+ 'timeInForce': None,
522
+ 'postOnly': None,
523
+ 'side': self.parse_ws_order_side(self.safe_string(orderInfo, 'side')),
524
+ 'price': self.safe_string(orderInfo, 'price'),
525
+ 'stopPrice': None,
526
+ 'triggerPrice': None,
527
+ 'amount': self.safe_string(orderInfo, 'size'),
528
+ 'cost': None,
529
+ 'average': self.safe_string(orderInfo, 'deal_avg_price'),
530
+ 'filled': self.safe_string(orderInfo, 'deal_size'),
531
+ 'remaining': None,
532
+ 'status': self.parse_ws_order_status(self.safe_string(order, 'action')),
533
+ 'fee': None,
534
+ 'trades': trades,
535
+ }, market)
536
+
537
+ def parse_ws_order_status(self, statusId):
538
+ statuses = {
539
+ '1': 'closed', # match deal
540
+ '2': 'open', # submit order
541
+ '3': 'canceled', # cancel order
542
+ '4': 'closed', # liquidate cancel order
543
+ '5': 'canceled', # adl cancel order
544
+ '6': 'open', # part liquidate
545
+ '7': 'open', # bankrupty order
546
+ '8': 'closed', # passive adl match deal
547
+ '9': 'closed', # active adl match deal
548
+ }
549
+ return self.safe_string(statuses, statusId, statusId)
550
+
551
+ def parse_ws_order_side(self, sideId):
552
+ sides = {
553
+ '1': 'buy', # buy_open_long
554
+ '2': 'buy', # buy_close_short
555
+ '3': 'sell', # sell_close_long
556
+ '4': 'sell', # sell_open_short
557
+ }
558
+ return self.safe_string(sides, sideId, sideId)
559
+
560
+ async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}):
561
+ """
562
+ :see: https://developer-pro.bitmart.com/en/futures/#private-position-channel
563
+ watch all open positions
564
+ :param str[]|None symbols: list of unified market symbols
565
+ :param dict params: extra parameters specific to the exchange API endpoint
566
+ :returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
567
+ """
568
+ await self.load_markets()
569
+ type = 'swap'
570
+ await self.authenticate(type, params)
571
+ symbols = self.market_symbols(symbols, 'swap', True, True, False)
572
+ messageHash = 'positions'
573
+ if symbols is not None:
574
+ messageHash += '::' + ','.join(symbols)
575
+ subscriptionHash = 'futures/position'
576
+ request = {
577
+ 'action': 'subscribe',
578
+ 'args': ['futures/position'],
579
+ }
580
+ url = self.implode_hostname(self.urls['api']['ws'][type]['private'])
581
+ newPositions = await self.watch(url, messageHash, self.deep_extend(request, params), subscriptionHash)
582
+ if self.newUpdates:
583
+ return newPositions
584
+ return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit)
585
+
586
+ def handle_positions(self, client: Client, message):
587
+ #
588
+ # {
589
+ # "group":"futures/position",
590
+ # "data":[
591
+ # {
592
+ # "symbol":"LTCUSDT",
593
+ # "hold_volume":"5",
594
+ # "position_type":2,
595
+ # "open_type":2,
596
+ # "frozen_volume":"0",
597
+ # "close_volume":"0",
598
+ # "hold_avg_price":"71.582",
599
+ # "close_avg_price":"0",
600
+ # "open_avg_price":"71.582",
601
+ # "liquidate_price":"0",
602
+ # "create_time":1701623327513,
603
+ # "update_time":1701627620439
604
+ # },
605
+ # {
606
+ # "symbol":"LTCUSDT",
607
+ # "hold_volume":"6",
608
+ # "position_type":1,
609
+ # "open_type":2,
610
+ # "frozen_volume":"0",
611
+ # "close_volume":"0",
612
+ # "hold_avg_price":"71.681666666666666667",
613
+ # "close_avg_price":"0",
614
+ # "open_avg_price":"71.681666666666666667",
615
+ # "liquidate_price":"0",
616
+ # "create_time":1701621167225,
617
+ # "update_time":1701628152614
618
+ # }
619
+ # ]
620
+ # }
621
+ #
622
+ data = self.safe_value(message, 'data', [])
623
+ cache = self.positions
624
+ if self.positions is None:
625
+ self.positions = ArrayCacheBySymbolBySide()
626
+ newPositions = []
627
+ for i in range(0, len(data)):
628
+ rawPosition = data[i]
629
+ position = self.parse_ws_position(rawPosition)
630
+ newPositions.append(position)
631
+ cache.append(position)
632
+ messageHashes = self.find_message_hashes(client, 'positions::')
633
+ for i in range(0, len(messageHashes)):
634
+ messageHash = messageHashes[i]
635
+ parts = messageHash.split('::')
636
+ symbolsString = parts[1]
637
+ symbols = symbolsString.split(',')
638
+ positions = self.filter_by_array(newPositions, 'symbol', symbols, False)
639
+ if not self.is_empty(positions):
640
+ client.resolve(positions, messageHash)
641
+ client.resolve(newPositions, 'positions')
642
+
643
+ def parse_ws_position(self, position, market: Market = None):
644
+ #
645
+ # {
646
+ # "symbol":"LTCUSDT",
647
+ # "hold_volume":"6",
648
+ # "position_type":1,
649
+ # "open_type":2,
650
+ # "frozen_volume":"0",
651
+ # "close_volume":"0",
652
+ # "hold_avg_price":"71.681666666666666667",
653
+ # "close_avg_price":"0",
654
+ # "open_avg_price":"71.681666666666666667",
655
+ # "liquidate_price":"0",
656
+ # "create_time":1701621167225,
657
+ # "update_time":1701628152614
658
+ # }
659
+ #
660
+ marketId = self.safe_string(position, 'symbol')
661
+ market = self.safe_market(marketId, market, '', 'swap')
218
662
  symbol = market['symbol']
219
- side = self.safe_string_lower(order, 'side')
220
- return self.safe_order({
221
- 'info': order,
663
+ openTimestamp = self.safe_integer(position, 'create_time')
664
+ timestamp = self.safe_integer(position, 'update_time')
665
+ side = self.safe_number(position, 'position_type')
666
+ marginModeId = self.safe_number(position, 'open_type')
667
+ return self.safe_position({
668
+ 'info': position,
669
+ 'id': None,
222
670
  'symbol': symbol,
223
- 'id': id,
224
- 'clientOrderId': clientOrderId,
225
- 'timestamp': None,
226
- 'datetime': None,
227
- 'lastTradeTimestamp': timestamp,
228
- 'type': type,
229
- 'timeInForce': None,
230
- 'postOnly': None,
231
- 'side': side,
232
- 'price': price,
233
- 'stopPrice': None,
234
- 'triggerPrice': None,
235
- 'amount': amount,
236
- 'cost': None,
237
- 'average': None,
238
- 'filled': filled,
239
- 'remaining': None,
240
- 'status': status,
241
- 'fee': None,
242
- 'trades': None,
243
- }, market)
671
+ 'timestamp': openTimestamp,
672
+ 'datetime': self.iso8601(openTimestamp),
673
+ 'lastUpdateTimestamp': timestamp,
674
+ 'hedged': None,
675
+ 'side': 'long' if (side == 1) else 'short',
676
+ 'contracts': self.safe_number(position, 'hold_volume'),
677
+ 'contractSize': self.safe_number(market, 'contractSize'),
678
+ 'entryPrice': self.safe_number(position, 'open_avg_price'),
679
+ 'markPrice': self.safe_number(position, 'hold_avg_price'),
680
+ 'lastPrice': None,
681
+ 'notional': None,
682
+ 'leverage': None,
683
+ 'collateral': None,
684
+ 'initialMargin': None,
685
+ 'initialMarginPercentage': None,
686
+ 'maintenanceMargin': None,
687
+ 'maintenanceMarginPercentage': None,
688
+ 'unrealizedPnl': None,
689
+ 'realizedPnl': None,
690
+ 'liquidationPrice': self.safe_number(position, 'liquidate_price'),
691
+ 'marginMode': 'isolated' if (marginModeId == 1) else 'cross',
692
+ 'percentage': None,
693
+ 'marginRatio': None,
694
+ 'stopLossPrice': None,
695
+ 'takeProfitPrice': None,
696
+ })
244
697
 
245
698
  def handle_trade(self, client: Client, message):
246
699
  #
247
- # {
248
- # "table": "spot/trade",
249
- # "data": [
250
- # {
251
- # "price": "52700.50",
252
- # "s_t": 1630982050,
253
- # "side": "buy",
254
- # "size": "0.00112",
255
- # "symbol": "BTC_USDT"
256
- # },
257
- # ]
258
- # }
700
+ # spot
701
+ # {
702
+ # "table": "spot/trade",
703
+ # "data": [
704
+ # {
705
+ # "price": "52700.50",
706
+ # "s_t": 1630982050,
707
+ # "side": "buy",
708
+ # "size": "0.00112",
709
+ # "symbol": "BTC_USDT"
710
+ # },
711
+ # ]
712
+ # }
259
713
  #
260
- table = self.safe_string(message, 'table')
261
- data = self.safe_value(message, 'data', [])
262
- tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
714
+ # swap
715
+ # {
716
+ # "group":"futures/trade:BTCUSDT",
717
+ # "data":[
718
+ # {
719
+ # "trade_id":6798697637,
720
+ # "contract_id":1,
721
+ # "symbol":"BTCUSDT",
722
+ # "deal_price":"39735.8",
723
+ # "deal_vol":"2",
724
+ # "type":0,
725
+ # "way":1,
726
+ # "create_time":1701618503,
727
+ # "create_time_mill":1701618503517,
728
+ # "created_at":"2023-12-03T15:48:23.517518538Z"
729
+ # }
730
+ # ]
731
+ # }
732
+ #
733
+ channel = self.safe_string_2(message, 'table', 'group')
734
+ isSpot = (channel.find('spot') >= 0)
735
+ data = self.safe_value(message, 'data')
736
+ if data is None:
737
+ return
738
+ stored = None
263
739
  for i in range(0, len(data)):
264
- trade = self.parse_trade(data[i])
740
+ trade = self.parse_ws_trade(data[i])
265
741
  symbol = trade['symbol']
266
- marketId = self.safe_string(trade['info'], 'symbol')
267
- messageHash = table + ':' + marketId
742
+ tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
268
743
  stored = self.safe_value(self.trades, symbol)
269
744
  if stored is None:
270
745
  stored = ArrayCache(tradesLimit)
271
746
  self.trades[symbol] = stored
272
747
  stored.append(trade)
273
- client.resolve(stored, messageHash)
748
+ messageHash = channel
749
+ if isSpot:
750
+ messageHash += ':' + self.safe_string(data[0], 'symbol')
751
+ client.resolve(stored, messageHash)
274
752
  return message
275
753
 
754
+ def parse_ws_trade(self, trade, market: Market = None):
755
+ # spot
756
+ # {
757
+ # "price": "52700.50",
758
+ # "s_t": 1630982050,
759
+ # "side": "buy",
760
+ # "size": "0.00112",
761
+ # "symbol": "BTC_USDT"
762
+ # }
763
+ # swap
764
+ # {
765
+ # "trade_id":6798697637,
766
+ # "contract_id":1,
767
+ # "symbol":"BTCUSDT",
768
+ # "deal_price":"39735.8",
769
+ # "deal_vol":"2",
770
+ # "type":0,
771
+ # "way":1,
772
+ # "create_time":1701618503,
773
+ # "create_time_mill":1701618503517,
774
+ # "created_at":"2023-12-03T15:48:23.517518538Z"
775
+ # }
776
+ #
777
+ contractId = self.safe_string(trade, 'contract_id')
778
+ marketType = 'spot' if (contractId is None) else 'swap'
779
+ marketDelimiter = '_' if (marketType == 'spot') else ''
780
+ timestamp = self.safe_integer(trade, 'create_time_mill', self.safe_timestamp(trade, 's_t'))
781
+ marketId = self.safe_string(trade, 'symbol')
782
+ return self.safe_trade({
783
+ 'info': trade,
784
+ 'id': self.safe_string(trade, 'trade_id'),
785
+ 'order': None,
786
+ 'timestamp': timestamp,
787
+ 'datetime': self.iso8601(timestamp),
788
+ 'symbol': self.safe_symbol(marketId, market, marketDelimiter, marketType),
789
+ 'type': None,
790
+ 'side': self.safe_string(trade, 'side'),
791
+ 'price': self.safe_string_2(trade, 'price', 'deal_price'),
792
+ 'amount': self.safe_string_2(trade, 'size', 'deal_vol'),
793
+ 'cost': None,
794
+ 'takerOrMaker': None,
795
+ 'fee': None,
796
+ }, market)
797
+
276
798
  def handle_ticker(self, client: Client, message):
277
799
  #
278
- # {
279
- # "data": [
280
- # {
281
- # "base_volume_24h": "78615593.81",
282
- # "high_24h": "52756.97",
283
- # "last_price": "52638.31",
284
- # "low_24h": "50991.35",
285
- # "open_24h": "51692.03",
286
- # "s_t": 1630981727,
287
- # "symbol": "BTC_USDT"
288
- # }
289
- # ],
290
- # "table": "spot/ticker"
291
- # }
800
+ # {
801
+ # "data": [
802
+ # {
803
+ # "base_volume_24h": "78615593.81",
804
+ # "high_24h": "52756.97",
805
+ # "last_price": "52638.31",
806
+ # "low_24h": "50991.35",
807
+ # "open_24h": "51692.03",
808
+ # "s_t": 1630981727,
809
+ # "symbol": "BTC_USDT"
810
+ # }
811
+ # ],
812
+ # "table": "spot/ticker"
813
+ # }
814
+ # {
815
+ # "group":"futures/ticker",
816
+ # "data":{
817
+ # "symbol":"BTCUSDT",
818
+ # "volume_24":"117387.58",
819
+ # "fair_price":"146.24",
820
+ # "last_price":"146.24",
821
+ # "range":"147.17",
822
+ # "ask_price": "147.11",
823
+ # "ask_vol": "1",
824
+ # "bid_price": "142.11",
825
+ # "bid_vol": "1"
826
+ # }
827
+ # }
292
828
  #
293
829
  table = self.safe_string(message, 'table')
294
- data = self.safe_value(message, 'data', [])
295
- for i in range(0, len(data)):
296
- ticker = self.parse_ticker(data[i])
297
- symbol = ticker['symbol']
298
- marketId = self.safe_string(ticker['info'], 'symbol')
299
- messageHash = table + ':' + marketId
830
+ isSpot = (table is not None)
831
+ data = self.safe_value(message, 'data')
832
+ if data is None:
833
+ return
834
+ if isSpot:
835
+ for i in range(0, len(data)):
836
+ ticker = self.parse_ticker(data[i])
837
+ symbol = ticker['symbol']
838
+ marketId = self.safe_string(ticker['info'], 'symbol')
839
+ messageHash = table + ':' + marketId
840
+ self.tickers[symbol] = ticker
841
+ client.resolve(ticker, messageHash)
842
+ else:
843
+ ticker = self.parse_ws_swap_ticker(data)
844
+ symbol = self.safe_string(ticker, 'symbol')
300
845
  self.tickers[symbol] = ticker
301
- client.resolve(ticker, messageHash)
846
+ client.resolve(ticker, 'tickers')
847
+ self.resolve_promise_if_messagehash_matches(client, 'tickers::', symbol, ticker)
302
848
  return message
303
849
 
850
+ def parse_ws_swap_ticker(self, ticker, market: Market = None):
851
+ #
852
+ # {
853
+ # "symbol":"BTCUSDT",
854
+ # "volume_24":"117387.58",
855
+ # "fair_price":"146.24",
856
+ # "last_price":"146.24",
857
+ # "range":"147.17",
858
+ # "ask_price": "147.11",
859
+ # "ask_vol": "1",
860
+ # "bid_price": "142.11",
861
+ # "bid_vol": "1"
862
+ # }
863
+ marketId = self.safe_string(ticker, 'symbol')
864
+ return self.safe_ticker({
865
+ 'symbol': self.safe_symbol(marketId, market, '', 'swap'),
866
+ 'timestamp': None,
867
+ 'datetime': None,
868
+ 'high': None,
869
+ 'low': None,
870
+ 'bid': self.safe_string(ticker, 'bid_price'),
871
+ 'bidVolume': self.safe_string(ticker, 'bid_vol'),
872
+ 'ask': self.safe_string(ticker, 'ask_price'),
873
+ 'askVolume': self.safe_string(ticker, 'ask_vol'),
874
+ 'vwap': None,
875
+ 'open': None,
876
+ 'close': None,
877
+ 'last': self.safe_string(ticker, 'last_price'),
878
+ 'previousClose': None,
879
+ 'change': None,
880
+ 'percentage': None,
881
+ 'average': self.safe_string(ticker, 'fair_price'),
882
+ 'baseVolume': None,
883
+ 'quoteVolume': self.safe_string(ticker, 'volume_24'),
884
+ 'info': ticker,
885
+ }, market)
886
+
304
887
  async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}):
305
888
  """
889
+ :see: https://developer-pro.bitmart.com/en/spot/#public-kline-channel
890
+ :see: https://developer-pro.bitmart.com/en/futures/#public-klinebin-channel
306
891
  watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
307
892
  :param str symbol: unified symbol of the market to fetch OHLCV data for
308
893
  :param str timeframe: the length of time each candle represents
@@ -313,71 +898,125 @@ class bitmart(ccxt.async_support.bitmart):
313
898
  """
314
899
  await self.load_markets()
315
900
  symbol = self.symbol(symbol)
901
+ market = self.market(symbol)
902
+ type = 'spot'
903
+ type, params = self.handle_market_type_and_params('watchOrderBook', market, params)
316
904
  timeframes = self.safe_value(self.options, 'timeframes', {})
317
905
  interval = self.safe_string(timeframes, timeframe)
318
- name = 'kline' + interval
319
- ohlcv = await self.subscribe(name, symbol, params)
906
+ name = None
907
+ if type == 'spot':
908
+ name = 'kline' + interval
909
+ else:
910
+ name = 'klineBin' + interval
911
+ ohlcv = await self.subscribe(name, symbol, type, params)
320
912
  if self.newUpdates:
321
913
  limit = ohlcv.getLimit(symbol, limit)
322
914
  return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
323
915
 
324
916
  def handle_ohlcv(self, client: Client, message):
325
917
  #
326
- # {
327
- # "data": [
328
- # {
329
- # "candle": [
330
- # 1631056350,
331
- # "46532.83",
332
- # "46555.71",
333
- # "46511.41",
334
- # "46555.71",
335
- # "0.25"
336
- # ],
337
- # "symbol": "BTC_USDT"
338
- # }
339
- # ],
340
- # "table": "spot/kline1m"
341
- # }
918
+ # {
919
+ # "data": [
920
+ # {
921
+ # "candle": [
922
+ # 1631056350,
923
+ # "46532.83",
924
+ # "46555.71",
925
+ # "46511.41",
926
+ # "46555.71",
927
+ # "0.25"
928
+ # ],
929
+ # "symbol": "BTC_USDT"
930
+ # }
931
+ # ],
932
+ # "table": "spot/kline1m"
933
+ # }
934
+ # swap
935
+ # {
936
+ # "group":"futures/klineBin1m:BTCUSDT",
937
+ # "data":{
938
+ # "symbol":"BTCUSDT",
939
+ # "items":[
940
+ # {
941
+ # "o":"39635.8",
942
+ # "h":"39636",
943
+ # "l":"39614.4",
944
+ # "c":"39629.7",
945
+ # "v":"31852",
946
+ # "ts":1701617761
947
+ # }
948
+ # ]
949
+ # }
950
+ # }
342
951
  #
343
- table = self.safe_string(message, 'table')
344
- data = self.safe_value(message, 'data', [])
345
- parts = table.split('/')
346
- part1 = self.safe_string(parts, 1)
952
+ channel = self.safe_string_2(message, 'table', 'group')
953
+ isSpot = (channel.find('spot') >= 0)
954
+ data = self.safe_value(message, 'data')
955
+ if data is None:
956
+ return
957
+ parts = channel.split('/')
958
+ part1 = self.safe_string(parts, 1, '')
347
959
  interval = part1.replace('kline', '')
960
+ interval = interval.replace('Bin', '')
961
+ intervalParts = interval.split(':')
962
+ interval = self.safe_string(intervalParts, 0)
348
963
  # use a reverse lookup in a static map instead
349
964
  timeframes = self.safe_value(self.options, 'timeframes', {})
350
965
  timeframe = self.find_timeframe(interval, timeframes)
351
966
  duration = self.parse_timeframe(timeframe)
352
967
  durationInMs = duration * 1000
353
- for i in range(0, len(data)):
354
- marketId = self.safe_string(data[i], 'symbol')
355
- candle = self.safe_value(data[i], 'candle')
356
- market = self.safe_market(marketId)
968
+ if isSpot:
969
+ for i in range(0, len(data)):
970
+ marketId = self.safe_string(data[i], 'symbol')
971
+ market = self.safe_market(marketId)
972
+ symbol = market['symbol']
973
+ rawOHLCV = self.safe_value(data[i], 'candle')
974
+ parsed = self.parse_ohlcv(rawOHLCV, market)
975
+ parsed[0] = self.parse_to_int(parsed[0] / durationInMs) * durationInMs
976
+ self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
977
+ stored = self.safe_value(self.ohlcvs[symbol], timeframe)
978
+ if stored is None:
979
+ limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
980
+ stored = ArrayCacheByTimestamp(limit)
981
+ self.ohlcvs[symbol][timeframe] = stored
982
+ stored.append(parsed)
983
+ messageHash = channel + ':' + marketId
984
+ client.resolve(stored, messageHash)
985
+ else:
986
+ marketId = self.safe_string(data, 'symbol')
987
+ market = self.safe_market(marketId, None, '', 'swap')
357
988
  symbol = market['symbol']
358
- parsed = self.parse_ohlcv(candle, market)
359
- parsed[0] = self.parse_to_int(parsed[0] / durationInMs) * durationInMs
989
+ items = self.safe_value(data, 'items', [])
360
990
  self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
361
991
  stored = self.safe_value(self.ohlcvs[symbol], timeframe)
362
992
  if stored is None:
363
993
  limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
364
994
  stored = ArrayCacheByTimestamp(limit)
365
995
  self.ohlcvs[symbol][timeframe] = stored
366
- stored.append(parsed)
367
- messageHash = table + ':' + marketId
368
- client.resolve(stored, messageHash)
996
+ for i in range(0, len(items)):
997
+ candle = items[i]
998
+ parsed = self.parse_ohlcv(candle, market)
999
+ stored.append(parsed)
1000
+ client.resolve(stored, channel)
369
1001
 
370
1002
  async def watch_order_book(self, symbol: str, limit: Int = None, params={}):
371
1003
  """
1004
+ :see: https://developer-pro.bitmart.com/en/spot/#public-depth-all-channel
1005
+ :see: https://developer-pro.bitmart.com/en/futures/#public-depth-channel
372
1006
  watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
373
1007
  :param str symbol: unified symbol of the market to fetch the order book for
374
1008
  :param int [limit]: the maximum amount of order book entries to return
375
1009
  :param dict [params]: extra parameters specific to the exchange API endpoint
376
1010
  :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
377
1011
  """
1012
+ await self.load_markets()
378
1013
  options = self.safe_value(self.options, 'watchOrderBook', {})
379
1014
  depth = self.safe_string(options, 'depth', 'depth50')
380
- orderbook = await self.subscribe(depth, symbol, params)
1015
+ symbol = self.symbol(symbol)
1016
+ market = self.market(symbol)
1017
+ type = 'spot'
1018
+ type, params = self.handle_market_type_and_params('watchOrderBook', market, params)
1019
+ orderbook = await self.subscribe(depth, symbol, type, params)
381
1020
  return orderbook.limit()
382
1021
 
383
1022
  def handle_delta(self, bookside, delta):
@@ -424,22 +1063,19 @@ class bitmart(ccxt.async_support.bitmart):
424
1063
 
425
1064
  def handle_order_book(self, client: Client, message):
426
1065
  #
1066
+ # spot
427
1067
  # {
428
1068
  # "data": [
429
1069
  # {
430
1070
  # "asks": [
431
1071
  # ['46828.38', "0.21847"],
432
1072
  # ['46830.68', "0.08232"],
433
- # ['46832.08', "0.09285"],
434
- # ['46837.82', "0.02028"],
435
- # ['46839.43', "0.15068"]
1073
+ # ...
436
1074
  # ],
437
1075
  # "bids": [
438
1076
  # ['46820.78', "0.00444"],
439
1077
  # ['46814.33', "0.00234"],
440
- # ['46813.50', "0.05021"],
441
- # ['46808.14', "0.00217"],
442
- # ['46808.04', "0.00013"]
1078
+ # ...
443
1079
  # ],
444
1080
  # "ms_t": 1631044962431,
445
1081
  # "symbol": "BTC_USDT"
@@ -447,30 +1083,89 @@ class bitmart(ccxt.async_support.bitmart):
447
1083
  # ],
448
1084
  # "table": "spot/depth5"
449
1085
  # }
1086
+ # swap
1087
+ # {
1088
+ # "group":"futures/depth50:BTCUSDT",
1089
+ # "data":{
1090
+ # "symbol":"BTCUSDT",
1091
+ # "way":1,
1092
+ # "depths":[
1093
+ # {
1094
+ # "price":"39509.8",
1095
+ # "vol":"2379"
1096
+ # },
1097
+ # {
1098
+ # "price":"39509.6",
1099
+ # "vol":"6815"
1100
+ # },
1101
+ # ...
1102
+ # ],
1103
+ # "ms_t":1701566021194
1104
+ # }
1105
+ # }
450
1106
  #
451
- data = self.safe_value(message, 'data', [])
452
- table = self.safe_string(message, 'table')
1107
+ data = self.safe_value(message, 'data')
1108
+ if data is None:
1109
+ return
1110
+ depths = self.safe_value(data, 'depths')
1111
+ isSpot = (depths is None)
1112
+ table = self.safe_string_2(message, 'table', 'group')
453
1113
  parts = table.split('/')
454
1114
  lastPart = self.safe_string(parts, 1)
455
1115
  limitString = lastPart.replace('depth', '')
456
- limit = int(limitString)
457
- for i in range(0, len(data)):
458
- update = data[i]
459
- marketId = self.safe_string(update, 'symbol')
1116
+ dotsIndex = limitString.find(':')
1117
+ limitString = limitString[0:dotsIndex]
1118
+ limit = self.parse_to_int(limitString)
1119
+ if isSpot:
1120
+ for i in range(0, len(data)):
1121
+ update = data[i]
1122
+ marketId = self.safe_string(update, 'symbol')
1123
+ symbol = self.safe_symbol(marketId)
1124
+ orderbook = self.safe_value(self.orderbooks, symbol)
1125
+ if orderbook is None:
1126
+ orderbook = self.order_book({}, limit)
1127
+ orderbook['symbol'] = symbol
1128
+ self.orderbooks[symbol] = orderbook
1129
+ orderbook.reset({})
1130
+ self.handle_order_book_message(client, update, orderbook)
1131
+ timestamp = self.safe_integer(update, 'ms_t')
1132
+ orderbook['timestamp'] = timestamp
1133
+ orderbook['datetime'] = self.iso8601(timestamp)
1134
+ messageHash = table + ':' + marketId
1135
+ client.resolve(orderbook, messageHash)
1136
+ else:
1137
+ marketId = self.safe_string(data, 'symbol')
460
1138
  symbol = self.safe_symbol(marketId)
461
1139
  orderbook = self.safe_value(self.orderbooks, symbol)
462
1140
  if orderbook is None:
463
1141
  orderbook = self.order_book({}, limit)
1142
+ orderbook['symbol'] = symbol
464
1143
  self.orderbooks[symbol] = orderbook
465
- orderbook.reset({})
466
- self.handle_order_book_message(client, update, orderbook)
467
- messageHash = table + ':' + marketId
1144
+ way = self.safe_number(data, 'way')
1145
+ side = 'bids' if (way == 1) else 'asks'
1146
+ if way == 1:
1147
+ orderbook[side] = Bids([], limit)
1148
+ else:
1149
+ orderbook[side] = Asks([], limit)
1150
+ for i in range(0, len(depths)):
1151
+ depth = depths[i]
1152
+ price = self.safe_number(depth, 'price')
1153
+ amount = self.safe_number(depth, 'vol')
1154
+ orderbookSide = self.safe_value(orderbook, side)
1155
+ orderbookSide.store(price, amount)
1156
+ bidsLength = len(orderbook['bids'])
1157
+ asksLength = len(orderbook['asks'])
1158
+ if (bidsLength == 0) or (asksLength == 0):
1159
+ return
1160
+ timestamp = self.safe_integer(data, 'ms_t')
1161
+ orderbook['timestamp'] = timestamp
1162
+ orderbook['datetime'] = self.iso8601(timestamp)
1163
+ messageHash = table
468
1164
  client.resolve(orderbook, messageHash)
469
- return message
470
1165
 
471
- async def authenticate(self, params={}):
1166
+ async def authenticate(self, type, params={}):
472
1167
  self.check_required_credentials()
473
- url = self.implode_hostname(self.urls['api']['ws']['private'])
1168
+ url = self.implode_hostname(self.urls['api']['ws'][type]['private'])
474
1169
  messageHash = 'authenticated'
475
1170
  client = self.client(url)
476
1171
  future = client.future(messageHash)
@@ -481,28 +1176,42 @@ class bitmart(ccxt.async_support.bitmart):
481
1176
  path = 'bitmart.WebSocket'
482
1177
  auth = timestamp + '#' + memo + '#' + path
483
1178
  signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)
484
- operation = 'login'
485
- request = {
486
- 'op': operation,
487
- 'args': [
488
- self.apiKey,
489
- timestamp,
490
- signature,
491
- ],
492
- }
1179
+ request = None
1180
+ if type == 'spot':
1181
+ request = {
1182
+ 'op': 'login',
1183
+ 'args': [
1184
+ self.apiKey,
1185
+ timestamp,
1186
+ signature,
1187
+ ],
1188
+ }
1189
+ else:
1190
+ request = {
1191
+ 'action': 'access',
1192
+ 'args': [
1193
+ self.apiKey,
1194
+ timestamp,
1195
+ signature,
1196
+ 'web',
1197
+ ],
1198
+ }
493
1199
  message = self.extend(request, params)
494
1200
  self.watch(url, messageHash, message, messageHash)
495
1201
  return future
496
1202
 
497
1203
  def handle_subscription_status(self, client: Client, message):
498
1204
  #
499
- # {"event":"subscribe","channel":"spot/depth:BTC-USDT"}
1205
+ # {"event":"subscribe","channel":"spot/depth:BTC-USDT"}
500
1206
  #
501
1207
  return message
502
1208
 
503
1209
  def handle_authenticate(self, client: Client, message):
504
1210
  #
505
- # {event: "login"}
1211
+ # spot
1212
+ # {event: "login"}
1213
+ # swap
1214
+ # {action: 'access', success: True}
506
1215
  #
507
1216
  messageHash = 'authenticated'
508
1217
  future = self.safe_value(client.futures, messageHash)
@@ -510,24 +1219,36 @@ class bitmart(ccxt.async_support.bitmart):
510
1219
 
511
1220
  def handle_error_message(self, client: Client, message):
512
1221
  #
513
- # {event: "error", message: "Invalid sign", errorCode: 30013}
514
- # {"event":"error","message":"Unrecognized request: {\"event\":\"subscribe\",\"channel\":\"spot/depth:BTC-USDT\"}","errorCode":30039}
1222
+ # {event: "error", message: "Invalid sign", errorCode: 30013}
1223
+ # {"event":"error","message":"Unrecognized request: {\"event\":\"subscribe\",\"channel\":\"spot/depth:BTC-USDT\"}","errorCode":30039}
1224
+ # {
1225
+ # action: '',
1226
+ # group: 'futures/trade:BTCUSDT',
1227
+ # success: False,
1228
+ # request: {action: '', args: ['futures/trade:BTCUSDT']},
1229
+ # error: 'Invalid action [] for group [futures/trade:BTCUSDT]'
1230
+ # }
515
1231
  #
516
1232
  errorCode = self.safe_string(message, 'errorCode')
1233
+ error = self.safe_string(message, 'error')
517
1234
  try:
518
- if errorCode is not None:
1235
+ if errorCode is not None or error is not None:
519
1236
  feedback = self.id + ' ' + self.json(message)
520
1237
  self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
521
- messageString = self.safe_value(message, 'message')
522
- if messageString is not None:
523
- self.throw_broadly_matched_exception(self.exceptions['broad'], messageString, feedback)
1238
+ messageString = self.safe_value(message, 'message', error)
1239
+ self.throw_broadly_matched_exception(self.exceptions['broad'], messageString, feedback)
1240
+ action = self.safe_string(message, 'action')
1241
+ if action == 'access':
1242
+ raise AuthenticationError(feedback)
1243
+ raise ExchangeError(feedback)
524
1244
  return False
525
1245
  except Exception as e:
526
- if isinstance(e, AuthenticationError):
1246
+ if (isinstance(e, AuthenticationError)):
527
1247
  messageHash = 'authenticated'
528
1248
  client.reject(e, messageHash)
529
1249
  if messageHash in client.subscriptions:
530
1250
  del client.subscriptions[messageHash]
1251
+ client.reject(e)
531
1252
  return True
532
1253
 
533
1254
  def handle_message(self, client: Client, message):
@@ -558,14 +1279,14 @@ class bitmart(ccxt.async_support.bitmart):
558
1279
  #
559
1280
  # {data: '', table: "spot/user/order"}
560
1281
  #
561
- table = self.safe_string(message, 'table')
562
- if table is None:
563
- event = self.safe_string(message, 'event')
1282
+ channel = self.safe_string_2(message, 'table', 'group')
1283
+ if channel is None:
1284
+ event = self.safe_string_2(message, 'event', 'action')
564
1285
  if event is not None:
565
1286
  methods = {
566
1287
  # 'info': self.handleSystemStatus,
567
- # 'book': 'handleOrderBook',
568
1288
  'login': self.handle_authenticate,
1289
+ 'access': self.handle_authenticate,
569
1290
  'subscribe': self.handle_subscription_status,
570
1291
  }
571
1292
  method = self.safe_value(methods, event)
@@ -574,24 +1295,21 @@ class bitmart(ccxt.async_support.bitmart):
574
1295
  else:
575
1296
  return method(client, message)
576
1297
  else:
577
- parts = table.split('/')
578
- name = self.safe_string(parts, 1)
579
1298
  methods = {
580
- 'depth': self.handle_order_book,
581
1299
  'depth5': self.handle_order_book,
582
1300
  'depth20': self.handle_order_book,
583
1301
  'depth50': self.handle_order_book,
584
1302
  'ticker': self.handle_ticker,
585
1303
  'trade': self.handle_trade,
586
- # ...
1304
+ 'kline': self.handle_ohlcv,
1305
+ 'order': self.handle_orders,
1306
+ 'position': self.handle_positions,
1307
+ 'balance': self.handle_balance,
1308
+ 'asset': self.handle_balance,
587
1309
  }
588
- method = self.safe_value(methods, name)
589
- if name.find('kline') >= 0:
590
- method = self.handle_ohlcv
591
- privateName = self.safe_string(parts, 2)
592
- if privateName == 'order':
593
- method = self.handle_orders
594
- if method is None:
595
- return message
596
- else:
597
- return method(client, message)
1310
+ keys = list(methods.keys())
1311
+ for i in range(0, len(keys)):
1312
+ key = keys[i]
1313
+ if channel.find(key) >= 0:
1314
+ method = self.safe_value(methods, key)
1315
+ return method(client, message)