1579 lines
66 KiB
Python
1579 lines
66 KiB
Python
# -*- coding: utf-8 -*-
|
|
|
|
# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
|
|
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code
|
|
|
|
import ccxt.async_support
|
|
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp
|
|
from ccxt.async_support.base.ws.order_book_side import Asks, Bids
|
|
import hashlib
|
|
from ccxt.base.types import Any, Balances, Bool, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Tickers, Trade
|
|
from ccxt.async_support.base.ws.client import Client
|
|
from typing import List
|
|
from ccxt.base.errors import ExchangeError
|
|
from ccxt.base.errors import AuthenticationError
|
|
from ccxt.base.errors import NotSupported
|
|
|
|
|
|
class bitmart(ccxt.async_support.bitmart):
|
|
|
|
def describe(self) -> Any:
|
|
return self.deep_extend(super(bitmart, self).describe(), {
|
|
'has': {
|
|
'createOrderWs': False,
|
|
'editOrderWs': False,
|
|
'fetchOpenOrdersWs': False,
|
|
'fetchOrderWs': False,
|
|
'cancelOrderWs': False,
|
|
'cancelOrdersWs': False,
|
|
'cancelAllOrdersWs': False,
|
|
'ws': True,
|
|
'watchBalance': True,
|
|
'watchTicker': True,
|
|
'watchTickers': True,
|
|
'watchBidsAsks': True,
|
|
'watchOrderBook': True,
|
|
'watchOrderBookForSymbols': True,
|
|
'watchOrders': True,
|
|
'watchTrades': True,
|
|
'watchTradesForSymbols': True,
|
|
'watchOHLCV': True,
|
|
'watchPosition': 'emulated',
|
|
'watchPositions': True,
|
|
},
|
|
'urls': {
|
|
'api': {
|
|
'ws': {
|
|
'spot': {
|
|
'public': 'wss://ws-manager-compress.{hostname}/api?protocol=1.1',
|
|
'private': 'wss://ws-manager-compress.{hostname}/user?protocol=1.1',
|
|
},
|
|
'swap': {
|
|
'public': 'wss://openapi-ws-v2.{hostname}/api?protocol=1.1',
|
|
'private': 'wss://openapi-ws-v2.{hostname}/user?protocol=1.1',
|
|
},
|
|
},
|
|
},
|
|
},
|
|
'options': {
|
|
'defaultType': 'spot',
|
|
'watchBalance': {
|
|
'fetchBalanceSnapshot': True, # or False
|
|
'awaitBalanceSnapshot': False, # whether to wait for the balance snapshot before providing updates
|
|
},
|
|
#
|
|
# orderbook channels can have:
|
|
# - 'depth5', 'depth20', 'depth50' # these endpoints emit full Orderbooks once in every 500ms
|
|
# - 'depth/increase100' # self endpoint is preferred, because it emits once in 100ms. however, when self value is chosen, it only affects spot-market, but contracts markets automatically `depth50` will be being used
|
|
'watchOrderBook': {
|
|
'depth': 'depth/increase100',
|
|
},
|
|
'watchOrderBookForSymbols': {
|
|
'depth': 'depth/increase100',
|
|
},
|
|
'watchTrades': {
|
|
'ignoreDuplicates': True,
|
|
},
|
|
'ws': {
|
|
'inflate': True,
|
|
},
|
|
'timeframes': {
|
|
'1m': '1m',
|
|
'3m': '3m',
|
|
'5m': '5m',
|
|
'15m': '15m',
|
|
'30m': '30m',
|
|
'45m': '45m',
|
|
'1h': '1H',
|
|
'2h': '2H',
|
|
'3h': '3H',
|
|
'4h': '4H',
|
|
'1d': '1D',
|
|
'1w': '1W',
|
|
'1M': '1M',
|
|
},
|
|
},
|
|
'streaming': {
|
|
'keepAlive': 15000,
|
|
},
|
|
})
|
|
|
|
async def subscribe(self, channel, symbol, type, params={}):
|
|
market = self.market(symbol)
|
|
url = self.implode_hostname(self.urls['api']['ws'][type]['public'])
|
|
request = {}
|
|
messageHash = None
|
|
if type == 'spot':
|
|
messageHash = 'spot/' + channel + ':' + market['id']
|
|
request = {
|
|
'op': 'subscribe',
|
|
'args': [messageHash],
|
|
}
|
|
else:
|
|
messageHash = 'futures/' + channel + ':' + market['id']
|
|
speed = self.safe_string(params, 'speed')
|
|
if speed is not None:
|
|
params = self.omit(params, 'speed')
|
|
messageHash += ':' + speed
|
|
request = {
|
|
'action': 'subscribe',
|
|
'args': [messageHash],
|
|
}
|
|
return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash)
|
|
|
|
async def subscribe_multiple(self, channel: str, type: str, symbols: Strings = None, params={}):
|
|
symbols = self.market_symbols(symbols, type, False, True)
|
|
url = self.implode_hostname(self.urls['api']['ws'][type]['public'])
|
|
channelType = 'spot' if (type == 'spot') else 'futures'
|
|
actionType = 'op' if (type == 'spot') else 'action'
|
|
rawSubscriptions = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
market = self.market(symbols[i])
|
|
message = channelType + '/' + channel + ':' + market['id']
|
|
rawSubscriptions.append(message)
|
|
messageHashes.append(channel + ':' + market['symbol'])
|
|
# exclusion, futures "tickers" need one generic request for all symbols
|
|
# if (type != 'spot') and (channel == 'ticker'):
|
|
# rawSubscriptions = [channelType + '/' + channel]
|
|
# }
|
|
# Exchange update from 2025-02-11 supports subscription by trading pair for swap
|
|
request: dict = {
|
|
'args': rawSubscriptions,
|
|
}
|
|
request[actionType] = 'subscribe'
|
|
return await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), rawSubscriptions)
|
|
|
|
async def watch_balance(self, params={}) -> Balances:
|
|
"""
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#private-balance-change
|
|
https://developer-pro.bitmart.com/en/futuresv2/#private-assets-channel
|
|
|
|
watch balance and get the amount of funds available for trading or funds locked in orders
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
type = 'spot'
|
|
type, params = self.handle_market_type_and_params('watchBalance', None, params)
|
|
await self.authenticate(type, params)
|
|
request = {}
|
|
if type == 'spot':
|
|
request = {
|
|
'op': 'subscribe',
|
|
'args': ['spot/user/balance:BALANCE_UPDATE'],
|
|
}
|
|
else:
|
|
request = {
|
|
'action': 'subscribe',
|
|
'args': ['futures/asset:USDT', 'futures/asset:BTC', 'futures/asset:ETH'],
|
|
}
|
|
messageHash = 'balance:' + type
|
|
url = self.implode_hostname(self.urls['api']['ws'][type]['private'])
|
|
client = self.client(url)
|
|
self.set_balance_cache(client, type, messageHash)
|
|
fetchBalanceSnapshot = None
|
|
awaitBalanceSnapshot = None
|
|
fetchBalanceSnapshot, params = self.handle_option_and_params(self.options, 'watchBalance', 'fetchBalanceSnapshot', True)
|
|
awaitBalanceSnapshot, params = self.handle_option_and_params(self.options, 'watchBalance', 'awaitBalanceSnapshot', False)
|
|
if fetchBalanceSnapshot and awaitBalanceSnapshot:
|
|
await client.future(type + ':fetchBalanceSnapshot')
|
|
return await self.watch(url, messageHash, self.deep_extend(request, params), messageHash)
|
|
|
|
def set_balance_cache(self, client: Client, type, subscribeHash):
|
|
if subscribeHash in client.subscriptions:
|
|
return
|
|
options = self.safe_value(self.options, 'watchBalance')
|
|
snapshot = self.safe_bool(options, 'fetchBalanceSnapshot', True)
|
|
if snapshot:
|
|
messageHash = type + ':' + 'fetchBalanceSnapshot'
|
|
if not (messageHash in client.futures):
|
|
client.future(messageHash)
|
|
self.spawn(self.load_balance_snapshot, client, messageHash, type)
|
|
self.balance[type] = {}
|
|
# without self comment, transpilation breaks for some reason...
|
|
|
|
async def load_balance_snapshot(self, client, messageHash, type):
|
|
response = await self.fetch_balance({'type': type})
|
|
self.balance[type] = self.extend(response, self.safe_value(self.balance, type, {}))
|
|
# don't remove the future from the .futures cache
|
|
future = client.futures[messageHash]
|
|
future.resolve()
|
|
client.resolve(self.balance[type], 'balance:' + type)
|
|
|
|
def handle_balance(self, client: Client, message):
|
|
#
|
|
# spot
|
|
# {
|
|
# "data":[
|
|
# {
|
|
# "balance_details":[
|
|
# {
|
|
# "av_bal":"0.206000000000000000000000000000",
|
|
# "ccy":"LTC",
|
|
# "fz_bal":"0.100000000000000000000000000000"
|
|
# }
|
|
# ],
|
|
# "event_time":"1701632345416",
|
|
# "event_type":"TRANSACTION_COMPLETED"
|
|
# }
|
|
# ],
|
|
# "table":"spot/user/balance"
|
|
# }
|
|
# swap
|
|
# {
|
|
# group: 'futures/asset:USDT',
|
|
# data: {
|
|
# currency: 'USDT',
|
|
# available_balance: '37.19688649135',
|
|
# position_deposit: '0.788687546',
|
|
# frozen_balance: '0'
|
|
# }
|
|
# }
|
|
#
|
|
channel = self.safe_string_2(message, 'table', 'group')
|
|
data = self.safe_value(message, 'data')
|
|
if data is None:
|
|
return
|
|
isSpot = (channel.find('spot') >= 0)
|
|
type = 'spot' if isSpot else 'swap'
|
|
self.balance[type]['info'] = message
|
|
if isSpot:
|
|
if not isinstance(data, list):
|
|
return
|
|
for i in range(0, len(data)):
|
|
timestamp = self.safe_integer(message, 'event_time')
|
|
self.balance[type]['timestamp'] = timestamp
|
|
self.balance[type]['datetime'] = self.iso8601(timestamp)
|
|
balanceDetails = self.safe_value(data[i], 'balance_details', [])
|
|
for ii in range(0, len(balanceDetails)):
|
|
rawBalance = balanceDetails[i]
|
|
account = self.account()
|
|
currencyId = self.safe_string(rawBalance, 'ccy')
|
|
code = self.safe_currency_code(currencyId)
|
|
account['free'] = self.safe_string(rawBalance, 'av_bal')
|
|
account['used'] = self.safe_string(rawBalance, 'fz_bal')
|
|
self.balance[type][code] = account
|
|
else:
|
|
currencyId = self.safe_string(data, 'currency')
|
|
code = self.safe_currency_code(currencyId)
|
|
account = self.account()
|
|
account['free'] = self.safe_string(data, 'available_balance')
|
|
account['used'] = self.safe_string(data, 'frozen_balance')
|
|
self.balance[type][code] = account
|
|
self.balance[type] = self.safe_balance(self.balance[type])
|
|
messageHash = 'balance:' + type
|
|
client.resolve(self.balance[type], messageHash)
|
|
|
|
async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#public-trade-channel
|
|
https://developer-pro.bitmart.com/en/futuresv2/#public-trade-channel
|
|
|
|
get the list of most recent trades for a particular symbol
|
|
:param str symbol: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
return await self.watch_trades_for_symbols([symbol], since, limit, params)
|
|
|
|
async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]:
|
|
"""
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#public-trade-channel
|
|
|
|
get the list of most recent trades for a list of symbols
|
|
:param str[] symbols: unified symbol of the market to fetch trades for
|
|
:param int [since]: timestamp in ms of the earliest trade to fetch
|
|
:param int [limit]: the maximum amount of trades to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
|
|
"""
|
|
await self.load_markets()
|
|
marketType = None
|
|
symbols, marketType, params = self.get_params_for_multiple_sub('watchTradesForSymbols', symbols, limit, params)
|
|
channelName = 'trade'
|
|
trades = await self.subscribe_multiple(channelName, marketType, symbols, params)
|
|
if self.newUpdates:
|
|
first = self.safe_dict(trades, 0)
|
|
tradeSymbol = self.safe_string(first, 'symbol')
|
|
limit = trades.getLimit(tradeSymbol, limit)
|
|
result = self.filter_by_since_limit(trades, since, limit, 'timestamp', True)
|
|
if self.handle_option('watchTrades', 'ignoreDuplicates', True):
|
|
filtered = self.remove_repeated_trades_from_array(result)
|
|
filtered = self.sort_by(filtered, 'timestamp')
|
|
return filtered
|
|
return result
|
|
|
|
def get_params_for_multiple_sub(self, methodName: str, symbols: List[str], limit: Int = None, params={}):
|
|
symbols = self.market_symbols(symbols, None, False, True)
|
|
length = len(symbols)
|
|
if length > 20:
|
|
raise NotSupported(self.id + ' ' + methodName + '() accepts a maximum of 20 symbols in one request')
|
|
market = self.market(symbols[0])
|
|
marketType = None
|
|
marketType, params = self.handle_market_type_and_params(methodName, market, params)
|
|
return [symbols, marketType, params]
|
|
|
|
async def watch_ticker(self, symbol: str, params={}) -> Ticker:
|
|
"""
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#public-ticker-channel
|
|
https://developer-pro.bitmart.com/en/futuresv2/#public-ticker-channel
|
|
|
|
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
|
|
:param str symbol: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbol = self.symbol(symbol)
|
|
tickers = await self.watch_tickers([symbol], params)
|
|
return tickers[symbol]
|
|
|
|
async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#public-ticker-channel
|
|
https://developer-pro.bitmart.com/en/futuresv2/#public-ticker-channel
|
|
|
|
watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
|
|
:param str[] symbols: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = self.get_market_from_symbols(symbols)
|
|
marketType = None
|
|
marketType, params = self.handle_market_type_and_params('watchTickers', market, params)
|
|
ticker = await self.subscribe_multiple('ticker', marketType, symbols, params)
|
|
if self.newUpdates:
|
|
tickers: dict = {}
|
|
tickers[ticker['symbol']] = ticker
|
|
return tickers
|
|
return self.filter_by_array(self.tickers, 'symbol', symbols)
|
|
|
|
async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers:
|
|
"""
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#public-ticker-channel
|
|
https://developer-pro.bitmart.com/en/futuresv2/#public-ticker-channel
|
|
|
|
watches best bid & ask for symbols
|
|
:param str[] symbols: unified symbol of the market to fetch the ticker for
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
symbols = self.market_symbols(symbols, None, False)
|
|
firstMarket = self.get_market_from_symbols(symbols)
|
|
marketType = None
|
|
marketType, params = self.handle_market_type_and_params('watchBidsAsks', firstMarket, params)
|
|
url = self.implode_hostname(self.urls['api']['ws'][marketType]['public'])
|
|
channelType = 'spot' if (marketType == 'spot') else 'futures'
|
|
actionType = 'op' if (marketType == 'spot') else 'action'
|
|
rawSubscriptions = []
|
|
messageHashes = []
|
|
for i in range(0, len(symbols)):
|
|
market = self.market(symbols[i])
|
|
rawSubscriptions.append(channelType + '/ticker:' + market['id'])
|
|
messageHashes.append('bidask:' + symbols[i])
|
|
if marketType != 'spot':
|
|
rawSubscriptions = [channelType + '/ticker']
|
|
request: dict = {
|
|
'args': rawSubscriptions,
|
|
}
|
|
request[actionType] = 'subscribe'
|
|
newTickers = await self.watch_multiple(url, messageHashes, request, rawSubscriptions)
|
|
if self.newUpdates:
|
|
tickers: dict = {}
|
|
tickers[newTickers['symbol']] = newTickers
|
|
return tickers
|
|
return self.filter_by_array(self.bidsasks, 'symbol', symbols)
|
|
|
|
def handle_bid_ask(self, client: Client, message):
|
|
table = self.safe_string(message, 'table')
|
|
isSpot = (table is not None)
|
|
rawTickers = []
|
|
if isSpot:
|
|
rawTickers = self.safe_list(message, 'data', [])
|
|
else:
|
|
rawTickers = [self.safe_value(message, 'data', {})]
|
|
if not len(rawTickers):
|
|
return
|
|
for i in range(0, len(rawTickers)):
|
|
ticker = self.parse_ws_bid_ask(rawTickers[i])
|
|
symbol = ticker['symbol']
|
|
self.bidsasks[symbol] = ticker
|
|
messageHash = 'bidask:' + symbol
|
|
client.resolve(ticker, messageHash)
|
|
|
|
def parse_ws_bid_ask(self, ticker, market=None):
|
|
marketId = self.safe_string(ticker, 'symbol')
|
|
market = self.safe_market(marketId, market)
|
|
symbol = self.safe_string(market, 'symbol')
|
|
timestamp = self.safe_integer(ticker, 'ms_t')
|
|
return self.safe_ticker({
|
|
'symbol': symbol,
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'ask': self.safe_string_2(ticker, 'ask_px', 'ask_price'),
|
|
'askVolume': self.safe_string_2(ticker, 'ask_sz', 'ask_vol'),
|
|
'bid': self.safe_string_2(ticker, 'bid_px', 'bid_price'),
|
|
'bidVolume': self.safe_string_2(ticker, 'bid_sz', 'bid_vol'),
|
|
'info': ticker,
|
|
}, market)
|
|
|
|
async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
|
|
"""
|
|
watches information on multiple orders made by the user
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#private-order-progress
|
|
https://developer-pro.bitmart.com/en/futuresv2/#private-order-channel
|
|
|
|
:param str symbol: unified market symbol of the market orders were made in
|
|
:param int [since]: the earliest time in ms to fetch orders for
|
|
:param int [limit]: the maximum number of order structures to retrieve
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
market = None
|
|
messageHash = 'orders'
|
|
if symbol is not None:
|
|
symbol = self.symbol(symbol)
|
|
market = self.market(symbol)
|
|
messageHash = 'orders::' + symbol
|
|
type = 'spot'
|
|
type, params = self.handle_market_type_and_params('watchOrders', market, params)
|
|
await self.authenticate(type, params)
|
|
request = None
|
|
if type == 'spot':
|
|
argsRequest = 'spot/user/order:'
|
|
if symbol is not None:
|
|
argsRequest += market['id']
|
|
else:
|
|
argsRequest = 'spot/user/orders:ALL_SYMBOLS'
|
|
request = {
|
|
'op': 'subscribe',
|
|
'args': [argsRequest],
|
|
}
|
|
else:
|
|
request = {
|
|
'action': 'subscribe',
|
|
'args': ['futures/order'],
|
|
}
|
|
url = self.implode_hostname(self.urls['api']['ws'][type]['private'])
|
|
newOrders = await self.watch(url, messageHash, self.deep_extend(request, params), messageHash)
|
|
if self.newUpdates:
|
|
return newOrders
|
|
return self.filter_by_symbol_since_limit(self.orders, symbol, since, limit, True)
|
|
|
|
def handle_orders(self, client: Client, message):
|
|
#
|
|
# spot
|
|
# {
|
|
# "data":[
|
|
# {
|
|
# "symbol": "LTC_USDT",
|
|
# "notional": '',
|
|
# "side": "buy",
|
|
# "last_fill_time": "0",
|
|
# "ms_t": "1646216634000",
|
|
# "type": "limit",
|
|
# "filled_notional": "0.000000000000000000000000000000",
|
|
# "last_fill_price": "0",
|
|
# "size": "0.500000000000000000000000000000",
|
|
# "price": "50.000000000000000000000000000000",
|
|
# "last_fill_count": "0",
|
|
# "filled_size": "0.000000000000000000000000000000",
|
|
# "margin_trading": "0",
|
|
# "state": "8",
|
|
# "order_id": "24807076628",
|
|
# "order_type": "0"
|
|
# }
|
|
# ],
|
|
# "table":"spot/user/order"
|
|
# }
|
|
# swap
|
|
# {
|
|
# "group":"futures/order",
|
|
# "data":[
|
|
# {
|
|
# "action":2,
|
|
# "order":{
|
|
# "order_id":"2312045036986775",
|
|
# "client_order_id":"",
|
|
# "price":"71.61707928",
|
|
# "size":"1",
|
|
# "symbol":"LTCUSDT",
|
|
# "state":1,
|
|
# "side":4,
|
|
# "type":"market",
|
|
# "leverage":"1",
|
|
# "open_type":"cross",
|
|
# "deal_avg_price":"0",
|
|
# "deal_size":"0",
|
|
# "create_time":1701625324646,
|
|
# "update_time":1701625324640,
|
|
# "plan_order_id":"",
|
|
# "last_trade":null
|
|
# }
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
orders = self.safe_value(message, 'data')
|
|
if orders is None:
|
|
return
|
|
ordersLength = len(orders)
|
|
newOrders = []
|
|
symbols: dict = {}
|
|
if ordersLength > 0:
|
|
limit = self.safe_integer(self.options, 'ordersLimit', 1000)
|
|
if self.orders is None:
|
|
self.orders = ArrayCacheBySymbolById(limit)
|
|
stored = self.orders
|
|
for i in range(0, len(orders)):
|
|
order = self.parse_ws_order(orders[i])
|
|
stored.append(order)
|
|
newOrders.append(order)
|
|
symbol = order['symbol']
|
|
symbols[symbol] = True
|
|
messageHash = 'orders'
|
|
symbolKeys = list(symbols.keys())
|
|
for i in range(0, len(symbolKeys)):
|
|
symbol = symbolKeys[i]
|
|
symbolSpecificMessageHash = messageHash + '::' + symbol
|
|
client.resolve(newOrders, symbolSpecificMessageHash)
|
|
client.resolve(newOrders, messageHash)
|
|
|
|
def parse_ws_order(self, order: dict, market: Market = None):
|
|
#
|
|
# spot
|
|
# {
|
|
# "symbol": "LTC_USDT",
|
|
# "notional": '',
|
|
# "side": "buy",
|
|
# "last_fill_time": "0",
|
|
# "ms_t": "1646216634000",
|
|
# "type": "limit",
|
|
# "filled_notional": "0.000000000000000000000000000000",
|
|
# "last_fill_price": "0",
|
|
# "size": "0.500000000000000000000000000000",
|
|
# "price": "50.000000000000000000000000000000",
|
|
# "last_fill_count": "0",
|
|
# "filled_size": "0.000000000000000000000000000000",
|
|
# "margin_trading": "0",
|
|
# "state": "8",
|
|
# "order_id": "24807076628",
|
|
# "order_type": "0"
|
|
# }
|
|
# swap
|
|
# {
|
|
# "action":2,
|
|
# "order":{
|
|
# "order_id":"2312045036986775",
|
|
# "client_order_id":"",
|
|
# "price":"71.61707928",
|
|
# "size":"1",
|
|
# "symbol":"LTCUSDT",
|
|
# "state":1,
|
|
# "side":4,
|
|
# "type":"market",
|
|
# "leverage":"1",
|
|
# "open_type":"cross",
|
|
# "deal_avg_price":"0",
|
|
# "deal_size":"0",
|
|
# "create_time":1701625324646,
|
|
# "update_time":1701625324640,
|
|
# "plan_order_id":"",
|
|
# "last_trade":null
|
|
# }
|
|
# }
|
|
#
|
|
action = self.safe_number(order, 'action')
|
|
isSpot = (action is None)
|
|
if isSpot:
|
|
marketId = self.safe_string(order, 'symbol')
|
|
market = self.safe_market(marketId, market, '_', 'spot')
|
|
id = self.safe_string(order, 'order_id')
|
|
clientOrderId = self.safe_string(order, 'clientOid')
|
|
price = self.safe_string(order, 'price')
|
|
filled = self.safe_string(order, 'filled_size')
|
|
amount = self.safe_string(order, 'size')
|
|
type = self.safe_string(order, 'type')
|
|
rawState = self.safe_string(order, 'state')
|
|
status = self.parse_order_status_by_type(market['type'], rawState)
|
|
timestamp = self.safe_integer(order, 'ms_t')
|
|
symbol = market['symbol']
|
|
side = self.safe_string_lower(order, 'side')
|
|
return self.safe_order({
|
|
'info': order,
|
|
'symbol': symbol,
|
|
'id': id,
|
|
'clientOrderId': clientOrderId,
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'lastTradeTimestamp': timestamp,
|
|
'type': type,
|
|
'timeInForce': None,
|
|
'postOnly': None,
|
|
'side': side,
|
|
'price': price,
|
|
'stopPrice': None,
|
|
'triggerPrice': None,
|
|
'amount': amount,
|
|
'cost': None,
|
|
'average': None,
|
|
'filled': filled,
|
|
'remaining': None,
|
|
'status': status,
|
|
'fee': None,
|
|
'trades': None,
|
|
}, market)
|
|
else:
|
|
orderInfo = self.safe_value(order, 'order')
|
|
marketId = self.safe_string(orderInfo, 'symbol')
|
|
symbol = self.safe_symbol(marketId, market, '', 'swap')
|
|
orderId = self.safe_string(orderInfo, 'order_id')
|
|
timestamp = self.safe_integer(orderInfo, 'create_time')
|
|
updatedTimestamp = self.safe_integer(orderInfo, 'update_time')
|
|
lastTrade = self.safe_value(orderInfo, 'last_trade')
|
|
cachedOrders = self.orders
|
|
orders = self.safe_value(cachedOrders.hashmap, symbol, {})
|
|
cachedOrder = self.safe_value(orders, orderId)
|
|
trades = None
|
|
if cachedOrder is not None:
|
|
trades = self.safe_value(order, 'trades')
|
|
if lastTrade is not None:
|
|
if trades is None:
|
|
trades = []
|
|
trades.append(lastTrade)
|
|
return self.safe_order({
|
|
'info': order,
|
|
'symbol': symbol,
|
|
'id': orderId,
|
|
'clientOrderId': self.safe_string(orderInfo, 'client_order_id'),
|
|
'timestamp': timestamp,
|
|
'datetime': self.iso8601(timestamp),
|
|
'lastTradeTimestamp': updatedTimestamp,
|
|
'type': self.safe_string(orderInfo, 'type'),
|
|
'timeInForce': None,
|
|
'postOnly': None,
|
|
'side': self.parse_ws_order_side(self.safe_string(orderInfo, 'side')),
|
|
'price': self.safe_string(orderInfo, 'price'),
|
|
'stopPrice': None,
|
|
'triggerPrice': None,
|
|
'amount': self.safe_string(orderInfo, 'size'),
|
|
'cost': None,
|
|
'average': self.safe_string(orderInfo, 'deal_avg_price'),
|
|
'filled': self.safe_string(orderInfo, 'deal_size'),
|
|
'remaining': None,
|
|
'status': self.parse_ws_order_status(self.safe_string(order, 'action')),
|
|
'fee': None,
|
|
'trades': trades,
|
|
}, market)
|
|
|
|
def parse_ws_order_status(self, statusId):
|
|
statuses: dict = {
|
|
'1': 'closed', # match deal
|
|
'2': 'open', # submit order
|
|
'3': 'canceled', # cancel order
|
|
'4': 'closed', # liquidate cancel order
|
|
'5': 'canceled', # adl cancel order
|
|
'6': 'open', # part liquidate
|
|
'7': 'open', # bankrupty order
|
|
'8': 'closed', # passive adl match deal
|
|
'9': 'closed', # active adl match deal
|
|
}
|
|
return self.safe_string(statuses, statusId, statusId)
|
|
|
|
def parse_ws_order_side(self, sideId):
|
|
sides: dict = {
|
|
'1': 'buy', # buy_open_long
|
|
'2': 'buy', # buy_close_short
|
|
'3': 'sell', # sell_close_long
|
|
'4': 'sell', # sell_open_short
|
|
}
|
|
return self.safe_string(sides, sideId, sideId)
|
|
|
|
async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
|
|
"""
|
|
|
|
https://developer-pro.bitmart.com/en/futures/#private-position-channel
|
|
|
|
watch all open positions
|
|
:param str[]|None symbols: list of unified market symbols
|
|
:param int [since]: the earliest time in ms to fetch positions
|
|
:param int [limit]: the maximum number of positions to retrieve
|
|
:param dict params: extra parameters specific to the exchange API endpoint
|
|
:returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
|
|
"""
|
|
await self.load_markets()
|
|
type = 'swap'
|
|
await self.authenticate(type, params)
|
|
symbols = self.market_symbols(symbols, 'swap', True, True, False)
|
|
messageHash = 'positions'
|
|
if symbols is not None:
|
|
messageHash += '::' + ','.join(symbols)
|
|
subscriptionHash = 'futures/position'
|
|
request: dict = {
|
|
'action': 'subscribe',
|
|
'args': ['futures/position'],
|
|
}
|
|
url = self.implode_hostname(self.urls['api']['ws'][type]['private'])
|
|
newPositions = await self.watch(url, messageHash, self.deep_extend(request, params), subscriptionHash)
|
|
if self.newUpdates:
|
|
return newPositions
|
|
return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit)
|
|
|
|
def handle_positions(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "group":"futures/position",
|
|
# "data":[
|
|
# {
|
|
# "symbol":"LTCUSDT",
|
|
# "hold_volume":"5",
|
|
# "position_type":2,
|
|
# "open_type":2,
|
|
# "frozen_volume":"0",
|
|
# "close_volume":"0",
|
|
# "hold_avg_price":"71.582",
|
|
# "close_avg_price":"0",
|
|
# "open_avg_price":"71.582",
|
|
# "liquidate_price":"0",
|
|
# "create_time":1701623327513,
|
|
# "update_time":1701627620439
|
|
# },
|
|
# {
|
|
# "symbol":"LTCUSDT",
|
|
# "hold_volume":"6",
|
|
# "position_type":1,
|
|
# "open_type":2,
|
|
# "frozen_volume":"0",
|
|
# "close_volume":"0",
|
|
# "hold_avg_price":"71.681666666666666667",
|
|
# "close_avg_price":"0",
|
|
# "open_avg_price":"71.681666666666666667",
|
|
# "liquidate_price":"0",
|
|
# "create_time":1701621167225,
|
|
# "update_time":1701628152614
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_value(message, 'data', [])
|
|
if self.positions is None:
|
|
self.positions = ArrayCacheBySymbolBySide()
|
|
cache = self.positions
|
|
newPositions = []
|
|
for i in range(0, len(data)):
|
|
rawPosition = data[i]
|
|
position = self.parse_ws_position(rawPosition)
|
|
newPositions.append(position)
|
|
cache.append(position)
|
|
messageHashes = self.find_message_hashes(client, 'positions::')
|
|
for i in range(0, len(messageHashes)):
|
|
messageHash = messageHashes[i]
|
|
parts = messageHash.split('::')
|
|
symbolsString = parts[1]
|
|
symbols = symbolsString.split(',')
|
|
positions = self.filter_by_array(newPositions, 'symbol', symbols, False)
|
|
if not self.is_empty(positions):
|
|
client.resolve(positions, messageHash)
|
|
client.resolve(newPositions, 'positions')
|
|
|
|
def parse_ws_position(self, position, market: Market = None):
|
|
#
|
|
# {
|
|
# "symbol":"LTCUSDT",
|
|
# "hold_volume":"6",
|
|
# "position_type":1,
|
|
# "open_type":2,
|
|
# "frozen_volume":"0",
|
|
# "close_volume":"0",
|
|
# "hold_avg_price":"71.681666666666666667",
|
|
# "close_avg_price":"0",
|
|
# "open_avg_price":"71.681666666666666667",
|
|
# "liquidate_price":"0",
|
|
# "create_time":1701621167225,
|
|
# "update_time":1701628152614
|
|
# }
|
|
#
|
|
marketId = self.safe_string(position, 'symbol')
|
|
market = self.safe_market(marketId, market, None, 'swap')
|
|
symbol = market['symbol']
|
|
openTimestamp = self.safe_integer(position, 'create_time')
|
|
timestamp = self.safe_integer(position, 'update_time')
|
|
side = self.safe_integer(position, 'position_type')
|
|
marginModeId = self.safe_integer(position, 'open_type')
|
|
return self.safe_position({
|
|
'info': position,
|
|
'id': None,
|
|
'symbol': symbol,
|
|
'timestamp': openTimestamp,
|
|
'datetime': self.iso8601(openTimestamp),
|
|
'lastUpdateTimestamp': timestamp,
|
|
'hedged': None,
|
|
'side': 'long' if (side == 1) else 'short',
|
|
'contracts': self.safe_number(position, 'hold_volume'),
|
|
'contractSize': self.safe_number(market, 'contractSize'),
|
|
'entryPrice': self.safe_number(position, 'open_avg_price'),
|
|
'markPrice': self.safe_number(position, 'hold_avg_price'),
|
|
'lastPrice': None,
|
|
'notional': None,
|
|
'leverage': None,
|
|
'collateral': None,
|
|
'initialMargin': None,
|
|
'initialMarginPercentage': None,
|
|
'maintenanceMargin': None,
|
|
'maintenanceMarginPercentage': None,
|
|
'unrealizedPnl': None,
|
|
'realizedPnl': None,
|
|
'liquidationPrice': self.safe_number(position, 'liquidate_price'),
|
|
'marginMode': 'isolated' if (marginModeId == 1) else 'cross',
|
|
'percentage': None,
|
|
'marginRatio': None,
|
|
'stopLossPrice': None,
|
|
'takeProfitPrice': None,
|
|
})
|
|
|
|
def handle_trade(self, client: Client, message):
|
|
#
|
|
# spot
|
|
# {
|
|
# "table": "spot/trade",
|
|
# "data": [
|
|
# {
|
|
# "price": "52700.50",
|
|
# "s_t": 1630982050,
|
|
# "side": "buy",
|
|
# "size": "0.00112",
|
|
# "symbol": "BTC_USDT"
|
|
# },
|
|
# ]
|
|
# }
|
|
#
|
|
# swap
|
|
# {
|
|
# "group":"futures/trade:BTCUSDT",
|
|
# "data":[
|
|
# {
|
|
# "trade_id":6798697637,
|
|
# "symbol":"BTCUSDT",
|
|
# "deal_price":"39735.8",
|
|
# "deal_vol":"2",
|
|
# "way":1,
|
|
# "created_at":"2023-12-03T15:48:23.517518538Z",
|
|
# "m": True,
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
data = self.safe_value(message, 'data')
|
|
if data is None:
|
|
return
|
|
symbol = None
|
|
length = len(data)
|
|
isSwap = ('group' in message)
|
|
if isSwap:
|
|
# in swap, chronologically decreasing: 1709536849322, 1709536848954,
|
|
for i in range(0, length):
|
|
index = length - i - 1
|
|
symbol = self.handle_trade_loop(data[index])
|
|
else:
|
|
# in spot, chronologically increasing: 1709536771200, 1709536771226,
|
|
for i in range(0, length):
|
|
symbol = self.handle_trade_loop(data[i])
|
|
client.resolve(self.trades[symbol], 'trade:' + symbol)
|
|
|
|
def handle_trade_loop(self, entry):
|
|
trade = self.parse_ws_trade(entry)
|
|
symbol = trade['symbol']
|
|
tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
|
|
if self.safe_value(self.trades, symbol) is None:
|
|
self.trades[symbol] = ArrayCache(tradesLimit)
|
|
stored = self.trades[symbol]
|
|
stored.append(trade)
|
|
return symbol
|
|
|
|
def parse_ws_trade(self, trade: dict, market: Market = None):
|
|
#
|
|
# spot
|
|
# {
|
|
# "ms_t": 1740320841473,
|
|
# "price": "2806.54",
|
|
# "s_t": 1740320841,
|
|
# "side": "sell",
|
|
# "size": "0.77598",
|
|
# "symbol": "ETH_USDT"
|
|
# }
|
|
#
|
|
# swap
|
|
# {
|
|
# "trade_id": "3000000245258661",
|
|
# "symbol": "ETHUSDT",
|
|
# "deal_price": "2811.1",
|
|
# "deal_vol": "1858",
|
|
# "way": 2,
|
|
# "m": True,
|
|
# "created_at": "2025-02-23T13:59:59.646490751Z"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(trade, 'symbol')
|
|
market = self.safe_market(marketId, market)
|
|
timestamp = self.safe_integer(trade, 'ms_t')
|
|
datetime: Str = None
|
|
if timestamp is None:
|
|
datetime = self.safe_string(trade, 'created_at')
|
|
timestamp = self.parse8601(datetime)
|
|
else:
|
|
datetime = self.iso8601(timestamp)
|
|
takerOrMaker = None # True for public trades
|
|
side = self.safe_string(trade, 'side')
|
|
buyerMaker = self.safe_bool(trade, 'm')
|
|
if buyerMaker is not None:
|
|
if side is None:
|
|
if buyerMaker:
|
|
side = 'sell'
|
|
else:
|
|
side = 'buy'
|
|
takerOrMaker = 'taker'
|
|
return self.safe_trade({
|
|
'info': trade,
|
|
'id': self.safe_string(trade, 'trade_id'),
|
|
'order': None,
|
|
'timestamp': timestamp,
|
|
'datetime': datetime,
|
|
'symbol': market['symbol'],
|
|
'type': None,
|
|
'side': side,
|
|
'price': self.safe_string_2(trade, 'price', 'deal_price'),
|
|
'amount': self.safe_string_2(trade, 'size', 'deal_vol'),
|
|
'cost': None,
|
|
'takerOrMaker': takerOrMaker,
|
|
'fee': None,
|
|
}, market)
|
|
|
|
def handle_ticker(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "base_volume_24h": "78615593.81",
|
|
# "high_24h": "52756.97",
|
|
# "last_price": "52638.31",
|
|
# "low_24h": "50991.35",
|
|
# "open_24h": "51692.03",
|
|
# "s_t": 1630981727,
|
|
# "symbol": "BTC_USDT"
|
|
# }
|
|
# ],
|
|
# "table": "spot/ticker"
|
|
# }
|
|
#
|
|
# {
|
|
# "data": {
|
|
# "symbol": "ETHUSDT",
|
|
# "last_price": "2807.73",
|
|
# "volume_24": "2227011952",
|
|
# "range": "0.0273398194664491",
|
|
# "mark_price": "2807.5",
|
|
# "index_price": "2808.71047619",
|
|
# "ask_price": "2808.04",
|
|
# "ask_vol": "7371",
|
|
# "bid_price": "2807.28",
|
|
# "bid_vol": "3561"
|
|
# },
|
|
# "group": "futures/ticker:ETHUSDT@100ms"
|
|
# }
|
|
#
|
|
self.handle_bid_ask(client, message)
|
|
table = self.safe_string(message, 'table')
|
|
isSpot = (table is not None)
|
|
rawTickers = []
|
|
if isSpot:
|
|
rawTickers = self.safe_list(message, 'data', [])
|
|
else:
|
|
rawTickers = [self.safe_value(message, 'data', {})]
|
|
if not len(rawTickers):
|
|
return
|
|
for i in range(0, len(rawTickers)):
|
|
ticker = self.parse_ticker(rawTickers[i]) if isSpot else self.parse_ws_swap_ticker(rawTickers[i])
|
|
symbol = ticker['symbol']
|
|
self.tickers[symbol] = ticker
|
|
messageHash = 'ticker:' + symbol
|
|
client.resolve(ticker, messageHash)
|
|
|
|
def parse_ws_swap_ticker(self, ticker, market: Market = None):
|
|
#
|
|
# {
|
|
# "symbol": "ETHUSDT",
|
|
# "last_price": "2807.73",
|
|
# "volume_24": "2227011952",
|
|
# "range": "0.0273398194664491",
|
|
# "mark_price": "2807.5",
|
|
# "index_price": "2808.71047619",
|
|
# "ask_price": "2808.04",
|
|
# "ask_vol": "7371",
|
|
# "bid_price": "2807.28",
|
|
# "bid_vol": "3561"
|
|
# }
|
|
#
|
|
marketId = self.safe_string(ticker, 'symbol')
|
|
return self.safe_ticker({
|
|
'symbol': self.safe_symbol(marketId, market, '', 'swap'),
|
|
'timestamp': None,
|
|
'datetime': None,
|
|
'high': None,
|
|
'low': None,
|
|
'bid': self.safe_string(ticker, 'bid_price'),
|
|
'bidVolume': self.safe_string(ticker, 'bid_vol'),
|
|
'ask': self.safe_string(ticker, 'ask_price'),
|
|
'askVolume': self.safe_string(ticker, 'ask_vol'),
|
|
'vwap': None,
|
|
'open': None,
|
|
'close': None,
|
|
'last': self.safe_string(ticker, 'last_price'),
|
|
'previousClose': None,
|
|
'change': None,
|
|
'percentage': None,
|
|
'average': None,
|
|
'baseVolume': None,
|
|
'quoteVolume': self.safe_string(ticker, 'volume_24'),
|
|
'info': ticker,
|
|
'markPrice': self.safe_string(ticker, 'mark_price'),
|
|
'indexPrice': self.safe_string(ticker, 'index_price'),
|
|
}, market)
|
|
|
|
async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
|
|
"""
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#public-kline-channel
|
|
https://developer-pro.bitmart.com/en/futuresv2/#public-klinebin-channel
|
|
|
|
watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
|
|
:param str symbol: unified symbol of the market to fetch OHLCV data for
|
|
:param str timeframe: the length of time each candle represents
|
|
:param int [since]: timestamp in ms of the earliest candle to fetch
|
|
:param int [limit]: the maximum amount of candles to fetch
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:returns int[][]: A list of candles ordered, open, high, low, close, volume
|
|
"""
|
|
await self.load_markets()
|
|
symbol = self.symbol(symbol)
|
|
market = self.market(symbol)
|
|
type = 'spot'
|
|
type, params = self.handle_market_type_and_params('watchOrderBook', market, params)
|
|
timeframes = self.safe_value(self.options, 'timeframes', {})
|
|
interval = self.safe_string(timeframes, timeframe)
|
|
name = None
|
|
if type == 'spot':
|
|
name = 'kline' + interval
|
|
else:
|
|
name = 'klineBin' + interval
|
|
ohlcv = await self.subscribe(name, symbol, type, params)
|
|
if self.newUpdates:
|
|
limit = ohlcv.getLimit(symbol, limit)
|
|
return self.filter_by_since_limit(ohlcv, since, limit, 0, True)
|
|
|
|
def handle_ohlcv(self, client: Client, message):
|
|
#
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "candle": [
|
|
# 1631056350,
|
|
# "46532.83",
|
|
# "46555.71",
|
|
# "46511.41",
|
|
# "46555.71",
|
|
# "0.25"
|
|
# ],
|
|
# "symbol": "BTC_USDT"
|
|
# }
|
|
# ],
|
|
# "table": "spot/kline1m"
|
|
# }
|
|
# swap
|
|
# {
|
|
# "group":"futures/klineBin1m:BTCUSDT",
|
|
# "data":{
|
|
# "symbol":"BTCUSDT",
|
|
# "items":[
|
|
# {
|
|
# "o":"39635.8",
|
|
# "h":"39636",
|
|
# "l":"39614.4",
|
|
# "c":"39629.7",
|
|
# "v":"31852",
|
|
# "ts":1701617761
|
|
# }
|
|
# ]
|
|
# }
|
|
# }
|
|
#
|
|
channel = self.safe_string_2(message, 'table', 'group')
|
|
isSpot = (channel.find('spot') >= 0)
|
|
data = self.safe_value(message, 'data')
|
|
if data is None:
|
|
return
|
|
parts = channel.split('/')
|
|
part1 = self.safe_string(parts, 1, '')
|
|
interval = part1.replace('kline', '')
|
|
interval = interval.replace('Bin', '')
|
|
intervalParts = interval.split(':')
|
|
interval = self.safe_string(intervalParts, 0)
|
|
# use a reverse lookup in a static map instead
|
|
timeframes = self.safe_value(self.options, 'timeframes', {})
|
|
timeframe = self.find_timeframe(interval, timeframes)
|
|
duration = self.parse_timeframe(timeframe)
|
|
durationInMs = duration * 1000
|
|
if isSpot:
|
|
for i in range(0, len(data)):
|
|
marketId = self.safe_string(data[i], 'symbol')
|
|
market = self.safe_market(marketId)
|
|
symbol = market['symbol']
|
|
rawOHLCV = self.safe_value(data[i], 'candle')
|
|
parsed = self.parse_ohlcv(rawOHLCV, market)
|
|
parsed[0] = self.parse_to_int(parsed[0] / durationInMs) * durationInMs
|
|
self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
|
|
stored = self.safe_value(self.ohlcvs[symbol], timeframe)
|
|
if stored is None:
|
|
limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
|
|
stored = ArrayCacheByTimestamp(limit)
|
|
self.ohlcvs[symbol][timeframe] = stored
|
|
stored.append(parsed)
|
|
messageHash = channel + ':' + marketId
|
|
client.resolve(stored, messageHash)
|
|
else:
|
|
marketId = self.safe_string(data, 'symbol')
|
|
market = self.safe_market(marketId, None, None, 'swap')
|
|
symbol = market['symbol']
|
|
items = self.safe_value(data, 'items', [])
|
|
self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
|
|
stored = self.safe_value(self.ohlcvs[symbol], timeframe)
|
|
if stored is None:
|
|
limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
|
|
stored = ArrayCacheByTimestamp(limit)
|
|
self.ohlcvs[symbol][timeframe] = stored
|
|
for i in range(0, len(items)):
|
|
candle = items[i]
|
|
parsed = self.parse_ohlcv(candle, market)
|
|
stored.append(parsed)
|
|
client.resolve(stored, channel)
|
|
|
|
async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#public-depth-all-channel
|
|
https://developer-pro.bitmart.com/en/spot/#public-depth-increase-channel
|
|
https://developer-pro.bitmart.com/en/futuresv2/#public-depth-channel
|
|
|
|
watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
:param str symbol: unified symbol of the market to fetch the order book for
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.speed]: *futures only* '100ms' or '200ms'
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
options = self.safe_value(self.options, 'watchOrderBook', {})
|
|
depth = self.safe_string(options, 'depth', 'depth/increase100')
|
|
symbol = self.symbol(symbol)
|
|
market = self.market(symbol)
|
|
type = 'spot'
|
|
type, params = self.handle_market_type_and_params('watchOrderBook', market, params)
|
|
if type == 'swap' and depth == 'depth/increase100':
|
|
depth = 'depth50'
|
|
orderbook = await self.subscribe(depth, symbol, type, params)
|
|
return orderbook.limit()
|
|
|
|
def handle_delta(self, bookside, delta):
|
|
price = self.safe_float(delta, 0)
|
|
amount = self.safe_float(delta, 1)
|
|
bookside.store(price, amount)
|
|
|
|
def handle_deltas(self, bookside, deltas):
|
|
for i in range(0, len(deltas)):
|
|
self.handle_delta(bookside, deltas[i])
|
|
|
|
def handle_order_book_message(self, client: Client, message, orderbook):
|
|
#
|
|
# {
|
|
# "asks": [
|
|
# ['46828.38', "0.21847"],
|
|
# ['46830.68', "0.08232"],
|
|
# ['46832.08', "0.09285"],
|
|
# ['46837.82', "0.02028"],
|
|
# ['46839.43', "0.15068"]
|
|
# ],
|
|
# "bids": [
|
|
# ['46820.78', "0.00444"],
|
|
# ['46814.33', "0.00234"],
|
|
# ['46813.50', "0.05021"],
|
|
# ['46808.14', "0.00217"],
|
|
# ['46808.04', "0.00013"]
|
|
# ],
|
|
# "ms_t": 1631044962431,
|
|
# "symbol": "BTC_USDT"
|
|
# }
|
|
#
|
|
asks = self.safe_list(message, 'asks', [])
|
|
bids = self.safe_list(message, 'bids', [])
|
|
self.handle_deltas(orderbook['asks'], asks)
|
|
self.handle_deltas(orderbook['bids'], bids)
|
|
timestamp = self.safe_integer(message, 'ms_t')
|
|
marketId = self.safe_string(message, 'symbol')
|
|
symbol = self.safe_symbol(marketId)
|
|
orderbook['symbol'] = symbol
|
|
orderbook['timestamp'] = timestamp
|
|
orderbook['datetime'] = self.iso8601(timestamp)
|
|
return orderbook
|
|
|
|
def handle_order_book(self, client: Client, message):
|
|
#
|
|
# spot depth-all
|
|
#
|
|
# {
|
|
# "data": [
|
|
# {
|
|
# "asks": [
|
|
# ['46828.38', "0.21847"],
|
|
# ['46830.68', "0.08232"],
|
|
# ...
|
|
# ],
|
|
# "bids": [
|
|
# ['46820.78', "0.00444"],
|
|
# ['46814.33', "0.00234"],
|
|
# ...
|
|
# ],
|
|
# "ms_t": 1631044962431,
|
|
# "symbol": "BTC_USDT"
|
|
# }
|
|
# ],
|
|
# "table": "spot/depth5"
|
|
# }
|
|
#
|
|
# spot increse depth snapshot
|
|
#
|
|
# {
|
|
# "data":[
|
|
# {
|
|
# "asks":[
|
|
# ["43652.52", "0.02039"],
|
|
# ...
|
|
# ],
|
|
# "bids":[
|
|
# ["43652.51", "0.00500"],
|
|
# ...
|
|
# ],
|
|
# "ms_t":1703376836487,
|
|
# "symbol":"BTC_USDT",
|
|
# "type":"snapshot", # or update
|
|
# "version":2141731
|
|
# }
|
|
# ],
|
|
# "table":"spot/depth/increase100"
|
|
# }
|
|
#
|
|
# swap
|
|
#
|
|
# {
|
|
# "group":"futures/depth50:BTCUSDT",
|
|
# "data":{
|
|
# "symbol":"BTCUSDT",
|
|
# "way":1,
|
|
# "depths":[
|
|
# {
|
|
# "price":"39509.8",
|
|
# "vol":"2379"
|
|
# },
|
|
# {
|
|
# "price":"39509.6",
|
|
# "vol":"6815"
|
|
# },
|
|
# ...
|
|
# ],
|
|
# "ms_t":1701566021194
|
|
# }
|
|
# }
|
|
#
|
|
isSpot = ('table' in message)
|
|
datas = []
|
|
if isSpot:
|
|
datas = self.safe_list(message, 'data', datas)
|
|
else:
|
|
orderBookEntry = self.safe_dict(message, 'data')
|
|
if orderBookEntry is not None:
|
|
datas.append(orderBookEntry)
|
|
length = len(datas)
|
|
if length <= 0:
|
|
return
|
|
channelName = self.safe_string_2(message, 'table', 'group')
|
|
# find limit subscribed to
|
|
limitsToCheck = ['100', '50', '20', '10', '5']
|
|
limit = 0
|
|
for i in range(0, len(limitsToCheck)):
|
|
limitString = limitsToCheck[i]
|
|
if channelName.find(limitString) >= 0:
|
|
limit = self.parse_to_int(limitString)
|
|
break
|
|
if isSpot:
|
|
channel = channelName.replace('spot/', '')
|
|
for i in range(0, len(datas)):
|
|
update = datas[i]
|
|
marketId = self.safe_string(update, 'symbol')
|
|
symbol = self.safe_symbol(marketId)
|
|
if not (symbol in self.orderbooks):
|
|
ob = self.order_book({}, limit)
|
|
ob['symbol'] = symbol
|
|
self.orderbooks[symbol] = ob
|
|
orderbook = self.orderbooks[symbol]
|
|
type = self.safe_string(update, 'type')
|
|
if (type == 'snapshot') or (not(channelName.find('increase') >= 0)):
|
|
orderbook.reset({})
|
|
self.handle_order_book_message(client, update, orderbook)
|
|
timestamp = self.safe_integer(update, 'ms_t')
|
|
if orderbook['timestamp'] is None:
|
|
orderbook['timestamp'] = timestamp
|
|
orderbook['datetime'] = self.iso8601(timestamp)
|
|
messageHash = channelName + ':' + marketId
|
|
client.resolve(orderbook, messageHash)
|
|
# resolve ForSymbols
|
|
messageHashForMulti = channel + ':' + symbol
|
|
client.resolve(orderbook, messageHashForMulti)
|
|
else:
|
|
tableParts = channelName.split(':')
|
|
channel = tableParts[0].replace('futures/', '')
|
|
data = datas[0] # contract markets always contain only one member
|
|
depths = data['depths']
|
|
marketId = self.safe_string(data, 'symbol')
|
|
symbol = self.safe_symbol(marketId)
|
|
if not (symbol in self.orderbooks):
|
|
ob = self.order_book({}, limit)
|
|
ob['symbol'] = symbol
|
|
self.orderbooks[symbol] = ob
|
|
orderbook = self.orderbooks[symbol]
|
|
way = self.safe_integer(data, 'way')
|
|
side = 'bids' if (way == 1) else 'asks'
|
|
if way == 1:
|
|
orderbook[side] = Bids([], limit)
|
|
else:
|
|
orderbook[side] = Asks([], limit)
|
|
for i in range(0, len(depths)):
|
|
depth = depths[i]
|
|
price = self.safe_number(depth, 'price')
|
|
amount = self.safe_number(depth, 'vol')
|
|
orderbookSide = self.safe_value(orderbook, side)
|
|
orderbookSide.store(price, amount)
|
|
bidsLength = len(orderbook['bids'])
|
|
asksLength = len(orderbook['asks'])
|
|
if (bidsLength == 0) or (asksLength == 0):
|
|
return
|
|
timestamp = self.safe_integer(data, 'ms_t')
|
|
orderbook['timestamp'] = timestamp
|
|
orderbook['datetime'] = self.iso8601(timestamp)
|
|
messageHash = channelName
|
|
client.resolve(orderbook, messageHash)
|
|
# resolve ForSymbols
|
|
messageHashForMulti = channel + ':' + symbol
|
|
client.resolve(orderbook, messageHashForMulti)
|
|
|
|
async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook:
|
|
"""
|
|
watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
|
|
|
|
https://developer-pro.bitmart.com/en/spot/#public-depth-increase-channel
|
|
|
|
:param str[] symbols: unified array of symbols
|
|
:param int [limit]: the maximum amount of order book entries to return
|
|
:param dict [params]: extra parameters specific to the exchange API endpoint
|
|
:param str [params.depth]: the type of order book to subscribe to, default is 'depth/increase100', also accepts 'depth5' or 'depth20' or depth50
|
|
:returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
|
|
"""
|
|
await self.load_markets()
|
|
type = None
|
|
symbols, type, params = self.get_params_for_multiple_sub('watchOrderBookForSymbols', symbols, limit, params)
|
|
channel = None
|
|
channel, params = self.handle_option_and_params(params, 'watchOrderBookForSymbols', 'depth', 'depth/increase100')
|
|
if type == 'swap' and channel == 'depth/increase100':
|
|
channel = 'depth50'
|
|
orderbook = await self.subscribe_multiple(channel, type, symbols, params)
|
|
return orderbook.limit()
|
|
|
|
async def authenticate(self, type, params={}):
|
|
self.check_required_credentials()
|
|
url = self.implode_hostname(self.urls['api']['ws'][type]['private'])
|
|
messageHash = 'authenticated'
|
|
client = self.client(url)
|
|
future = client.reusableFuture(messageHash)
|
|
authenticated = self.safe_value(client.subscriptions, messageHash)
|
|
if authenticated is None:
|
|
timestamp = str(self.milliseconds())
|
|
memo = self.uid
|
|
path = 'bitmart.WebSocket'
|
|
auth = timestamp + '#' + memo + '#' + path
|
|
signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)
|
|
request = None
|
|
if type == 'spot':
|
|
request = {
|
|
'op': 'login',
|
|
'args': [
|
|
self.apiKey,
|
|
timestamp,
|
|
signature,
|
|
],
|
|
}
|
|
else:
|
|
request = {
|
|
'action': 'access',
|
|
'args': [
|
|
self.apiKey,
|
|
timestamp,
|
|
signature,
|
|
'web',
|
|
],
|
|
}
|
|
message = self.extend(request, params)
|
|
self.watch(url, messageHash, message, messageHash)
|
|
return await future
|
|
|
|
def handle_subscription_status(self, client: Client, message):
|
|
#
|
|
# {"event":"subscribe","channel":"spot/depth:BTC-USDT"}
|
|
#
|
|
return message
|
|
|
|
def handle_authenticate(self, client: Client, message):
|
|
#
|
|
# spot
|
|
# {event: "login"}
|
|
# swap
|
|
# {action: 'access', success: True}
|
|
#
|
|
messageHash = 'authenticated'
|
|
future = self.safe_value(client.futures, messageHash)
|
|
future.resolve(True)
|
|
|
|
def handle_error_message(self, client: Client, message) -> Bool:
|
|
#
|
|
# {event: "error", message: "Invalid sign", errorCode: 30013}
|
|
# {"event":"error","message":"Unrecognized request: {\"event\":\"subscribe\",\"channel\":\"spot/depth:BTC-USDT\"}","errorCode":30039}
|
|
# {
|
|
# action: '',
|
|
# group: 'futures/trade:BTCUSDT',
|
|
# success: False,
|
|
# request: {action: '', args: ['futures/trade:BTCUSDT']},
|
|
# error: 'Invalid action [] for group [futures/trade:BTCUSDT]'
|
|
# }
|
|
#
|
|
errorCode = self.safe_string(message, 'errorCode')
|
|
error = self.safe_string(message, 'error')
|
|
try:
|
|
if errorCode is not None or error is not None:
|
|
feedback = self.id + ' ' + self.json(message)
|
|
self.throw_exactly_matched_exception(self.exceptions['exact'], errorCode, feedback)
|
|
messageString = self.safe_value(message, 'message', error)
|
|
self.throw_broadly_matched_exception(self.exceptions['broad'], messageString, feedback)
|
|
action = self.safe_string(message, 'action')
|
|
if action == 'access':
|
|
raise AuthenticationError(feedback)
|
|
raise ExchangeError(feedback)
|
|
return False
|
|
except Exception as e:
|
|
if (isinstance(e, AuthenticationError)):
|
|
messageHash = 'authenticated'
|
|
client.reject(e, messageHash)
|
|
if messageHash in client.subscriptions:
|
|
del client.subscriptions[messageHash]
|
|
client.reject(e)
|
|
return True
|
|
|
|
def handle_message(self, client: Client, message):
|
|
if self.handle_error_message(client, message):
|
|
return
|
|
#
|
|
# {"event":"error","message":"Unrecognized request: {\"event\":\"subscribe\",\"channel\":\"spot/depth:BTC-USDT\"}","errorCode":30039}
|
|
#
|
|
# subscribe events on spot:
|
|
#
|
|
# {"event":"subscribe", "topic":"spot/kline1m:BTC_USDT"}
|
|
#
|
|
# subscribe on contracts:
|
|
#
|
|
# {"action":"subscribe", "group":"futures/klineBin1m:BTCUSDT", "success":true, "request":{"action":"subscribe", "args":["futures/klineBin1m:BTCUSDT"]}}
|
|
#
|
|
# regular updates - spot
|
|
#
|
|
# {
|
|
# "table": "spot/depth",
|
|
# "action": "partial",
|
|
# "data": [
|
|
# {
|
|
# "instrument_id": "BTC-USDT",
|
|
# "asks": [
|
|
# ["5301.8", "0.03763319", "1"],
|
|
# ["5302.4", "0.00305", "2"],
|
|
# ],
|
|
# "bids": [
|
|
# ["5301.7", "0.58911427", "6"],
|
|
# ["5301.6", "0.01222922", "4"],
|
|
# ],
|
|
# "timestamp": "2020-03-16T03:25:00.440Z",
|
|
# "checksum": -2088736623
|
|
# }
|
|
# ]
|
|
# }
|
|
#
|
|
# regular updates - contracts
|
|
#
|
|
# {
|
|
# group: "futures/klineBin1m:BTCUSDT",
|
|
# data: {
|
|
# symbol: "BTCUSDT",
|
|
# items: [{o: "67944.7", "h": ....}],
|
|
# },
|
|
# }
|
|
#
|
|
# {data: '', table: "spot/user/order"}
|
|
#
|
|
# the only realiable way(for both spot & swap) is to check 'data' key
|
|
isDataUpdate = ('data' in message)
|
|
if not isDataUpdate:
|
|
event = self.safe_string_2(message, 'event', 'action')
|
|
if event is not None:
|
|
methods: dict = {
|
|
# 'info': self.handleSystemStatus,
|
|
'login': self.handle_authenticate,
|
|
'access': self.handle_authenticate,
|
|
'subscribe': self.handle_subscription_status,
|
|
}
|
|
method = self.safe_value(methods, event)
|
|
if method is not None:
|
|
method(client, message)
|
|
else:
|
|
channel = self.safe_string_2(message, 'table', 'group')
|
|
methods: dict = {
|
|
'depth': self.handle_order_book,
|
|
'ticker': self.handle_ticker,
|
|
'trade': self.handle_trade,
|
|
'kline': self.handle_ohlcv,
|
|
'order': self.handle_orders,
|
|
'position': self.handle_positions,
|
|
'balance': self.handle_balance,
|
|
'asset': self.handle_balance,
|
|
}
|
|
keys = list(methods.keys())
|
|
for i in range(0, len(keys)):
|
|
key = keys[i]
|
|
if channel.find(key) >= 0:
|
|
method = self.safe_value(methods, key)
|
|
method(client, message)
|