# -*- 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
import hashlib
from ccxt.base.types import Balances, Int, Liquidation, 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 RateLimitExceeded


class bitmex(ccxt.async_support.bitmex):

    def describe(self):
        return self.deep_extend(super(bitmex, self).describe(), {
            'has': {
                'ws': True,
                'watchBalance': True,
                'watchLiquidations': True,
                'watchLiquidationsForSymbols': True,
                'watchMyLiquidations': None,
                'watchMyLiquidationsForSymbols': None,
                'watchMyTrades': True,
                'watchOHLCV': True,
                'watchOrderBook': True,
                'watchOrderBookForSymbols': True,
                'watchOrders': True,
                'watchPostions': True,
                'watchTicker': True,
                'watchTickers': True,
                'watchTrades': True,
                'watchTradesForSymbols': True,
            },
            'urls': {
                'test': {
                    'ws': 'wss://ws.testnet.bitmex.com/realtime',
                },
                'api': {
                    'ws': 'wss://ws.bitmex.com/realtime',
                },
            },
            # 'versions': {
            #     'ws': '0.2.0',
            # },
            'options': {
                'watchOrderBookLevel': 'orderBookL2',  # 'orderBookL2' = L2 full order book, 'orderBookL2_25' = L2 top 25, 'orderBook10' L3 top 10
                'tradesLimit': 1000,
                'OHLCVLimit': 1000,
            },
            'exceptions': {
                'ws': {
                    'exact': {
                    },
                    'broad': {
                        'Rate limit exceeded': RateLimitExceeded,
                    },
                },
            },
        })

    async def watch_ticker(self, symbol: str, params={}) -> Ticker:
        """
        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:
        """
        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()
        symbols = self.market_symbols(symbols, None, True)
        name = 'instrument'
        url = self.urls['api']['ws']
        messageHashes = []
        rawSubscriptions = []
        if symbols is not None:
            for i in range(0, len(symbols)):
                symbol = symbols[i]
                market = self.market(symbol)
                subscription = name + ':' + market['id']
                rawSubscriptions.append(subscription)
                messageHash = 'ticker:' + symbol
                messageHashes.append(messageHash)
        else:
            rawSubscriptions.append(name)
            messageHashes.append('alltickers')
        request: dict = {
            'op': 'subscribe',
            'args': rawSubscriptions,
        }
        ticker = await self.watch_multiple(url, messageHashes, self.extend(request, params), rawSubscriptions)
        if self.newUpdates:
            result: dict = {}
            result[ticker['symbol']] = ticker
            return result
        return self.filter_by_array(self.tickers, 'symbol', symbols)

    def handle_ticker(self, client: Client, message):
        #
        #     {
        #         "table": "instrument",
        #         "action": "partial",
        #         "keys": ["symbol"],
        #         "types": {
        #             "symbol": "symbol",
        #             "rootSymbol": "symbol",
        #             "state": "symbol",
        #             "typ": "symbol",
        #             "listing": "timestamp",
        #             "front": "timestamp",
        #             "expiry": "timestamp",
        #             "settle": "timestamp",
        #             "relistInterval": "timespan",
        #             "inverseLeg": "symbol",
        #             "sellLeg": "symbol",
        #             "buyLeg": "symbol",
        #             "optionStrikePcnt": "float",
        #             "optionStrikeRound": "float",
        #             "optionStrikePrice": "float",
        #             "optionMultiplier": "float",
        #             "positionCurrency": "symbol",
        #             "underlying": "symbol",
        #             "quoteCurrency": "symbol",
        #             "underlyingSymbol": "symbol",
        #             "reference": "symbol",
        #             "referenceSymbol": "symbol",
        #             "calcInterval": "timespan",
        #             "publishInterval": "timespan",
        #             "publishTime": "timespan",
        #             "maxOrderQty": "long",
        #             "maxPrice": "float",
        #             "lotSize": "long",
        #             "tickSize": "float",
        #             "multiplier": "long",
        #             "settlCurrency": "symbol",
        #             "underlyingToPositionMultiplier": "long",
        #             "underlyingToSettleMultiplier": "long",
        #             "quoteToSettleMultiplier": "long",
        #             "isQuanto": "boolean",
        #             "isInverse": "boolean",
        #             "initMargin": "float",
        #             "maintMargin": "float",
        #             "riskLimit": "long",
        #             "riskStep": "long",
        #             "limit": "float",
        #             "capped": "boolean",
        #             "taxed": "boolean",
        #             "deleverage": "boolean",
        #             "makerFee": "float",
        #             "takerFee": "float",
        #             "settlementFee": "float",
        #             "insuranceFee": "float",
        #             "fundingBaseSymbol": "symbol",
        #             "fundingQuoteSymbol": "symbol",
        #             "fundingPremiumSymbol": "symbol",
        #             "fundingTimestamp": "timestamp",
        #             "fundingInterval": "timespan",
        #             "fundingRate": "float",
        #             "indicativeFundingRate": "float",
        #             "rebalanceTimestamp": "timestamp",
        #             "rebalanceInterval": "timespan",
        #             "openingTimestamp": "timestamp",
        #             "closingTimestamp": "timestamp",
        #             "sessionInterval": "timespan",
        #             "prevClosePrice": "float",
        #             "limitDownPrice": "float",
        #             "limitUpPrice": "float",
        #             "bankruptLimitDownPrice": "float",
        #             "bankruptLimitUpPrice": "float",
        #             "prevTotalVolume": "long",
        #             "totalVolume": "long",
        #             "volume": "long",
        #             "volume24h": "long",
        #             "prevTotalTurnover": "long",
        #             "totalTurnover": "long",
        #             "turnover": "long",
        #             "turnover24h": "long",
        #             "homeNotional24h": "float",
        #             "foreignNotional24h": "float",
        #             "prevPrice24h": "float",
        #             "vwap": "float",
        #             "highPrice": "float",
        #             "lowPrice": "float",
        #             "lastPrice": "float",
        #             "lastPriceProtected": "float",
        #             "lastTickDirection": "symbol",
        #             "lastChangePcnt": "float",
        #             "bidPrice": "float",
        #             "midPrice": "float",
        #             "askPrice": "float",
        #             "impactBidPrice": "float",
        #             "impactMidPrice": "float",
        #             "impactAskPrice": "float",
        #             "hasLiquidity": "boolean",
        #             "openInterest": "long",
        #             "openValue": "long",
        #             "fairMethod": "symbol",
        #             "fairBasisRate": "float",
        #             "fairBasis": "float",
        #             "fairPrice": "float",
        #             "markMethod": "symbol",
        #             "markPrice": "float",
        #             "indicativeTaxRate": "float",
        #             "indicativeSettlePrice": "float",
        #             "optionUnderlyingPrice": "float",
        #             "settledPrice": "float",
        #             "timestamp": "timestamp"
        #         },
        #         "foreignKeys": {
        #             "inverseLeg": "instrument",
        #             "sellLeg": "instrument",
        #             "buyLeg": "instrument"
        #         },
        #         "attributes": {symbol: "unique"},
        #         "filter": {symbol: "XBTUSD"},
        #         "data": [
        #             {
        #                 "symbol": "XBTUSD",
        #                 "rootSymbol": "XBT",
        #                 "state": "Open",
        #                 "typ": "FFWCSX",
        #                 "listing": "2016-05-13T12:00:00.000Z",
        #                 "front": "2016-05-13T12:00:00.000Z",
        #                 "expiry": null,
        #                 "settle": null,
        #                 "relistInterval": null,
        #                 "inverseLeg": '',
        #                 "sellLeg": '',
        #                 "buyLeg": '',
        #                 "optionStrikePcnt": null,
        #                 "optionStrikeRound": null,
        #                 "optionStrikePrice": null,
        #                 "optionMultiplier": null,
        #                 "positionCurrency": "USD",
        #                 "underlying": "XBT",
        #                 "quoteCurrency": "USD",
        #                 "underlyingSymbol": "XBT=",
        #                 "reference": "BMEX",
        #                 "referenceSymbol": ".BXBT",
        #                 "calcInterval": null,
        #                 "publishInterval": null,
        #                 "publishTime": null,
        #                 "maxOrderQty": 10000000,
        #                 "maxPrice": 1000000,
        #                 "lotSize": 1,
        #                 "tickSize": 0.5,
        #                 "multiplier": -100000000,
        #                 "settlCurrency": "XBt",
        #                 "underlyingToPositionMultiplier": null,
        #                 "underlyingToSettleMultiplier": -100000000,
        #                 "quoteToSettleMultiplier": null,
        #                 "isQuanto": False,
        #                 "isInverse": True,
        #                 "initMargin": 0.01,
        #                 "maintMargin": 0.005,
        #                 "riskLimit": 20000000000,
        #                 "riskStep": 10000000000,
        #                 "limit": null,
        #                 "capped": False,
        #                 "taxed": True,
        #                 "deleverage": True,
        #                 "makerFee": -0.00025,
        #                 "takerFee": 0.00075,
        #                 "settlementFee": 0,
        #                 "insuranceFee": 0,
        #                 "fundingBaseSymbol": ".XBTBON8H",
        #                 "fundingQuoteSymbol": ".USDBON8H",
        #                 "fundingPremiumSymbol": ".XBTUSDPI8H",
        #                 "fundingTimestamp": "2020-01-29T12:00:00.000Z",
        #                 "fundingInterval": "2000-01-01T08:00:00.000Z",
        #                 "fundingRate": 0.000597,
        #                 "indicativeFundingRate": 0.000652,
        #                 "rebalanceTimestamp": null,
        #                 "rebalanceInterval": null,
        #                 "openingTimestamp": "2020-01-29T11:00:00.000Z",
        #                 "closingTimestamp": "2020-01-29T12:00:00.000Z",
        #                 "sessionInterval": "2000-01-01T01:00:00.000Z",
        #                 "prevClosePrice": 9063.96,
        #                 "limitDownPrice": null,
        #                 "limitUpPrice": null,
        #                 "bankruptLimitDownPrice": null,
        #                 "bankruptLimitUpPrice": null,
        #                 "prevTotalVolume": 1989881049026,
        #                 "totalVolume": 1990196740950,
        #                 "volume": 315691924,
        #                 "volume24h": 4491824765,
        #                 "prevTotalTurnover": 27865497128425564,
        #                 "totalTurnover": 27868891594857150,
        #                 "turnover": 3394466431587,
        #                 "turnover24h": 48863390064843,
        #                 "homeNotional24h": 488633.9006484273,
        #                 "foreignNotional24h": 4491824765,
        #                 "prevPrice24h": 9091,
        #                 "vwap": 9192.8663,
        #                 "highPrice": 9440,
        #                 "lowPrice": 8886,
        #                 "lastPrice": 9287,
        #                 "lastPriceProtected": 9287,
        #                 "lastTickDirection": "PlusTick",
        #                 "lastChangePcnt": 0.0216,
        #                 "bidPrice": 9286,
        #                 "midPrice": 9286.25,
        #                 "askPrice": 9286.5,
        #                 "impactBidPrice": 9285.9133,
        #                 "impactMidPrice": 9286.75,
        #                 "impactAskPrice": 9287.6382,
        #                 "hasLiquidity": True,
        #                 "openInterest": 967826984,
        #                 "openValue": 10432207060536,
        #                 "fairMethod": "FundingRate",
        #                 "fairBasisRate": 0.6537149999999999,
        #                 "fairBasis": 0.33,
        #                 "fairPrice": 9277.2,
        #                 "markMethod": "FairPrice",
        #                 "markPrice": 9277.2,
        #                 "indicativeTaxRate": 0,
        #                 "indicativeSettlePrice": 9276.87,
        #                 "optionUnderlyingPrice": null,
        #                 "settledPrice": null,
        #                 "timestamp": "2020-01-29T11:31:37.114Z"
        #             }
        #         ]
        #     }
        #
        data = self.safe_list(message, 'data', [])
        tickers: dict = {}
        for i in range(0, len(data)):
            update = data[i]
            marketId = self.safe_string(update, 'symbol')
            symbol = self.safe_symbol(marketId)
            if not (symbol in self.tickers):
                self.tickers[symbol] = self.parse_ticker({})
            updatedTicker = self.parse_ticker(update)
            fullParsedTicker = self.deep_extend(self.tickers[symbol], updatedTicker)
            tickers[symbol] = fullParsedTicker
            self.tickers[symbol] = fullParsedTicker
            messageHash = 'ticker:' + symbol
            client.resolve(fullParsedTicker, messageHash)
            client.resolve(fullParsedTicker, 'alltickers')
        return message

    async def watch_liquidations(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]:
        """
        watch the public liquidations of a trading pair
        :see: https://www.bitmex.com/app/wsAPI#Liquidation
        :param str symbol: unified CCXT market symbol
        :param int [since]: the earliest time in ms to fetch liquidations for
        :param int [limit]: the maximum number of liquidation structures to retrieve
        :param dict [params]: exchange specific parameters for the bitmex api endpoint
        :returns dict: an array of `liquidation structures <https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure>`
        """
        return self.watch_liquidations_for_symbols([symbol], since, limit, params)

    async def watch_liquidations_for_symbols(self, symbols: List[str] = None, since: Int = None, limit: Int = None, params={}) -> List[Liquidation]:
        """
        watch the public liquidations of a trading pair
        :see: https://www.bitmex.com/app/wsAPI#Liquidation
        :param str symbol: unified CCXT market symbol
        :param int [since]: the earliest time in ms to fetch liquidations for
        :param int [limit]: the maximum number of liquidation structures to retrieve
        :param dict [params]: exchange specific parameters for the bitmex api endpoint
        :returns dict: an array of `liquidation structures <https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure>`
        """
        await self.load_markets()
        symbols = self.market_symbols(symbols, None, True, True)
        messageHashes = []
        subscriptionHashes = []
        if self.is_empty(symbols):
            subscriptionHashes.append('liquidation')
            messageHashes.append('liquidations')
        else:
            for i in range(0, len(symbols)):
                symbol = symbols[i]
                market = self.market(symbol)
                subscriptionHashes.append('liquidation:' + market['id'])
                messageHashes.append('liquidations::' + symbol)
        url = self.urls['api']['ws']
        request = {
            'op': 'subscribe',
            'args': subscriptionHashes,
        }
        newLiquidations = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), subscriptionHashes)
        if self.newUpdates:
            return newLiquidations
        return self.filter_by_symbols_since_limit(self.liquidations, symbols, since, limit, True)

    def handle_liquidation(self, client: Client, message):
        #
        #    {
        #        "table":"liquidation",
        #        "action":"partial",
        #        "keys":[
        #           "orderID"
        #        ],
        #        "types":{
        #           "orderID":"guid",
        #           "symbol":"symbol",
        #           "side":"symbol",
        #           "price":"float",
        #           "leavesQty":"long"
        #        },
        #        "filter":{},
        #        "data":[
        #           {
        #              "orderID":"e0a568ee-7830-4428-92c3-73e82b9576ce",
        #              "symbol":"XPLAUSDT",
        #              "side":"Sell",
        #              "price":0.206,
        #              "leavesQty":340
        #           }
        #        ]
        #    }
        #
        rawLiquidations = self.safe_value(message, 'data', [])
        newLiquidations = []
        for i in range(0, len(rawLiquidations)):
            rawLiquidation = rawLiquidations[i]
            liquidation = self.parse_liquidation(rawLiquidation)
            symbol = liquidation['symbol']
            liquidations = self.safe_value(self.liquidations, symbol)
            if liquidations is None:
                limit = self.safe_integer(self.options, 'liquidationsLimit', 1000)
                liquidations = ArrayCache(limit)
            liquidations.append(liquidation)
            self.liquidations[symbol] = liquidations
            newLiquidations.append(liquidation)
        client.resolve(newLiquidations, 'liquidations')
        liquidationsBySymbol = self.index_by(newLiquidations, 'symbol')
        symbols = list(liquidationsBySymbol.keys())
        for i in range(0, len(symbols)):
            symbol = symbols[i]
            client.resolve(liquidationsBySymbol[symbol], 'liquidations::' + symbol)

    async def watch_balance(self, params={}) -> Balances:
        """
        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()
        await self.authenticate()
        messageHash = 'margin'
        url = self.urls['api']['ws']
        request: dict = {
            'op': 'subscribe',
            'args': [
                messageHash,
            ],
        }
        return await self.watch(url, messageHash, self.extend(request, params), messageHash)

    def handle_balance(self, client: Client, message):
        #
        #     {
        #         "table": "margin",
        #         "action": "partial",
        #         "keys": ["account"],
        #         "types": {
        #             "account": "long",
        #             "currency": "symbol",
        #             "riskLimit": "long",
        #             "prevState": "symbol",
        #             "state": "symbol",
        #             "action": "symbol",
        #             "amount": "long",
        #             "pendingCredit": "long",
        #             "pendingDebit": "long",
        #             "confirmedDebit": "long",
        #             "prevRealisedPnl": "long",
        #             "prevUnrealisedPnl": "long",
        #             "grossComm": "long",
        #             "grossOpenCost": "long",
        #             "grossOpenPremium": "long",
        #             "grossExecCost": "long",
        #             "grossMarkValue": "long",
        #             "riskValue": "long",
        #             "taxableMargin": "long",
        #             "initMargin": "long",
        #             "maintMargin": "long",
        #             "sessionMargin": "long",
        #             "targetExcessMargin": "long",
        #             "varMargin": "long",
        #             "realisedPnl": "long",
        #             "unrealisedPnl": "long",
        #             "indicativeTax": "long",
        #             "unrealisedProfit": "long",
        #             "syntheticMargin": "long",
        #             "walletBalance": "long",
        #             "marginBalance": "long",
        #             "marginBalancePcnt": "float",
        #             "marginLeverage": "float",
        #             "marginUsedPcnt": "float",
        #             "excessMargin": "long",
        #             "excessMarginPcnt": "float",
        #             "availableMargin": "long",
        #             "withdrawableMargin": "long",
        #             "timestamp": "timestamp",
        #             "grossLastValue": "long",
        #             "commission": "float"
        #         },
        #         "foreignKeys": {},
        #         "attributes": {account: "sorted"},
        #         "filter": {account: 1455728},
        #         "data": [
        #             {
        #                 "account": 1455728,
        #                 "currency": "XBt",
        #                 "riskLimit": 1000000000000,
        #                 "prevState": '',
        #                 "state": '',
        #                 "action": '',
        #                 "amount": 263542,
        #                 "pendingCredit": 0,
        #                 "pendingDebit": 0,
        #                 "confirmedDebit": 0,
        #                 "prevRealisedPnl": 0,
        #                 "prevUnrealisedPnl": 0,
        #                 "grossComm": 0,
        #                 "grossOpenCost": 0,
        #                 "grossOpenPremium": 0,
        #                 "grossExecCost": 0,
        #                 "grossMarkValue": 0,
        #                 "riskValue": 0,
        #                 "taxableMargin": 0,
        #                 "initMargin": 0,
        #                 "maintMargin": 0,
        #                 "sessionMargin": 0,
        #                 "targetExcessMargin": 0,
        #                 "varMargin": 0,
        #                 "realisedPnl": 0,
        #                 "unrealisedPnl": 0,
        #                 "indicativeTax": 0,
        #                 "unrealisedProfit": 0,
        #                 "syntheticMargin": null,
        #                 "walletBalance": 263542,
        #                 "marginBalance": 263542,
        #                 "marginBalancePcnt": 1,
        #                 "marginLeverage": 0,
        #                 "marginUsedPcnt": 0,
        #                 "excessMargin": 263542,
        #                 "excessMarginPcnt": 1,
        #                 "availableMargin": 263542,
        #                 "withdrawableMargin": 263542,
        #                 "timestamp": "2020-08-03T12:01:01.246Z",
        #                 "grossLastValue": 0,
        #                 "commission": null
        #             }
        #         ]
        #     }
        #
        data = self.safe_value(message, 'data')
        balance = self.parse_balance(data)
        self.balance = self.extend(self.balance, balance)
        messageHash = self.safe_string(message, 'table')
        client.resolve(self.balance, messageHash)

    def handle_trades(self, client: Client, message):
        #
        # initial snapshot
        #
        #     {
        #         "table": "trade",
        #         "action": "partial",
        #         "keys": [],
        #         "types": {
        #             "timestamp": "timestamp",
        #             "symbol": "symbol",
        #             "side": "symbol",
        #             "size": "long",
        #             "price": "float",
        #             "tickDirection": "symbol",
        #             "trdMatchID": "guid",
        #             "grossValue": "long",
        #             "homeNotional": "float",
        #             "foreignNotional": "float"
        #         },
        #         "foreignKeys": {symbol: "instrument", side: "side"},
        #         "attributes": {timestamp: "sorted", symbol: "grouped"},
        #         "filter": {symbol: "XBTUSD"},
        #         "data": [
        #             {
        #                 "timestamp": "2020-01-30T17:03:07.854Z",
        #                 "symbol": "XBTUSD",
        #                 "side": "Buy",
        #                 "size": 15000,
        #                 "price": 9378,
        #                 "tickDirection": "ZeroPlusTick",
        #                 "trdMatchID": "5b426e7f-83d1-2c80-295d-ee995b8ceb4a",
        #                 "grossValue": 159945000,
        #                 "homeNotional": 1.59945,
        #                 "foreignNotional": 15000
        #             }
        #         ]
        #     }
        #
        # updates
        #
        #     {
        #         "table": "trade",
        #         "action": "insert",
        #         "data": [
        #             {
        #                 "timestamp": "2020-01-30T17:31:40.160Z",
        #                 "symbol": "XBTUSD",
        #                 "side": "Sell",
        #                 "size": 37412,
        #                 "price": 9521.5,
        #                 "tickDirection": "ZeroMinusTick",
        #                 "trdMatchID": "a4bfc6bc-6cf1-1a11-622e-270eef8ca5c7",
        #                 "grossValue": 392938236,
        #                 "homeNotional": 3.92938236,
        #                 "foreignNotional": 37412
        #             }
        #         ]
        #     }
        #
        table = 'trade'
        data = self.safe_value(message, 'data', [])
        dataByMarketIds = self.group_by(data, 'symbol')
        marketIds = list(dataByMarketIds.keys())
        for i in range(0, len(marketIds)):
            marketId = marketIds[i]
            market = self.safe_market(marketId)
            symbol = market['symbol']
            messageHash = table + ':' + symbol
            trades = self.parse_trades(dataByMarketIds[marketId], market)
            stored = self.safe_value(self.trades, symbol)
            if stored is None:
                limit = self.safe_integer(self.options, 'tradesLimit', 1000)
                stored = ArrayCache(limit)
                self.trades[symbol] = stored
            for j in range(0, len(trades)):
                stored.append(trades[j])
            client.resolve(stored, messageHash)

    async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        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 authenticate(self, params={}):
        url = self.urls['api']['ws']
        client = self.client(url)
        messageHash = 'authenticated'
        future = client.future(messageHash)
        authenticated = self.safe_value(client.subscriptions, messageHash)
        if authenticated is None:
            self.check_required_credentials()
            timestamp = self.milliseconds()
            payload = 'GET' + '/realtime' + str(timestamp)
            signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256)
            request: dict = {
                'op': 'authKeyExpires',
                'args': [
                    self.apiKey,
                    timestamp,
                    signature,
                ],
            }
            message = self.extend(request, params)
            self.watch(url, messageHash, message, messageHash)
        return await future

    def handle_authentication_message(self, client: Client, message):
        authenticated = self.safe_bool(message, 'success', False)
        messageHash = 'authenticated'
        if authenticated:
            # we resolve the future here permanently so authentication only happens once
            future = self.safe_value(client.futures, messageHash)
            future.resolve(True)
        else:
            error = AuthenticationError(self.json(message))
            client.reject(error, messageHash)
            if messageHash in client.subscriptions:
                del client.subscriptions[messageHash]

    async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
        """
        :see: https://www.bitmex.com/app/wsAPI
        watch all open positions
        :param str[]|None symbols: list of unified market symbols
        :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()
        await self.authenticate()
        subscriptionHash = 'position'
        messageHash = 'positions'
        if not self.is_empty(symbols):
            messageHash = '::' + ','.join(symbols)
        url = self.urls['api']['ws']
        request: dict = {
            'op': 'subscribe',
            'args': [
                subscriptionHash,
            ],
        }
        newPositions = await self.watch(url, messageHash, request, subscriptionHash)
        if self.newUpdates:
            return newPositions
        return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True)

    def handle_positions(self, client, message):
        #
        # partial
        #    {
        #        table: 'position',
        #        action: 'partial',
        #        keys: ['account', 'symbol'],
        #        types: {
        #            account: 'long',
        #            symbol: 'symbol',
        #            currency: 'symbol',
        #            underlying: 'symbol',
        #            quoteCurrency: 'symbol',
        #            commission: 'float',
        #            initMarginReq: 'float',
        #            maintMarginReq: 'float',
        #            riskLimit: 'long',
        #            leverage: 'float',
        #            crossMargin: 'boolean',
        #            deleveragePercentile: 'float',
        #            rebalancedPnl: 'long',
        #            prevRealisedPnl: 'long',
        #            prevUnrealisedPnl: 'long',
        #            openingQty: 'long',
        #            openOrderBuyQty: 'long',
        #            openOrderBuyCost: 'long',
        #            openOrderBuyPremium: 'long',
        #            openOrderSellQty: 'long',
        #            openOrderSellCost: 'long',
        #            openOrderSellPremium: 'long',
        #            currentQty: 'long',
        #            currentCost: 'long',
        #            currentComm: 'long',
        #            realisedCost: 'long',
        #            unrealisedCost: 'long',
        #            grossOpenPremium: 'long',
        #            isOpen: 'boolean',
        #            markPrice: 'float',
        #            markValue: 'long',
        #            riskValue: 'long',
        #            homeNotional: 'float',
        #            foreignNotional: 'float',
        #            posState: 'symbol',
        #            posCost: 'long',
        #            posCross: 'long',
        #            posComm: 'long',
        #            posLoss: 'long',
        #            posMargin: 'long',
        #            posMaint: 'long',
        #            initMargin: 'long',
        #            maintMargin: 'long',
        #            realisedPnl: 'long',
        #            unrealisedPnl: 'long',
        #            unrealisedPnlPcnt: 'float',
        #            unrealisedRoePcnt: 'float',
        #            avgCostPrice: 'float',
        #            avgEntryPrice: 'float',
        #            breakEvenPrice: 'float',
        #            marginCallPrice: 'float',
        #            liquidationPrice: 'float',
        #            bankruptPrice: 'float',
        #            timestamp: 'timestamp'
        #        },
        #        filter: {account: 412475},
        #        data: [
        #            {
        #                account: 412475,
        #                symbol: 'XBTUSD',
        #                currency: 'XBt',
        #                underlying: 'XBT',
        #                quoteCurrency: 'USD',
        #                commission: 0.00075,
        #                initMarginReq: 0.01,
        #                maintMarginReq: 0.0035,
        #                riskLimit: 20000000000,
        #                leverage: 100,
        #                crossMargin: True,
        #                deleveragePercentile: 1,
        #                rebalancedPnl: 0,
        #                prevRealisedPnl: 0,
        #                prevUnrealisedPnl: 0,
        #                openingQty: 400,
        #                openOrderBuyQty: 0,
        #                openOrderBuyCost: 0,
        #                openOrderBuyPremium: 0,
        #                openOrderSellQty: 0,
        #                openOrderSellCost: 0,
        #                openOrderSellPremium: 0,
        #                currentQty: 400,
        #                currentCost: -912269,
        #                currentComm: 684,
        #                realisedCost: 0,
        #                unrealisedCost: -912269,
        #                grossOpenPremium: 0,
        #                isOpen: True,
        #                markPrice: 43772,
        #                markValue: -913828,
        #                riskValue: 913828,
        #                homeNotional: 0.00913828,
        #                foreignNotional: -400,
        #                posCost: -912269,
        #                posCross: 1559,
        #                posComm: 694,
        #                posLoss: 0,
        #                posMargin: 11376,
        #                posMaint: 3887,
        #                initMargin: 0,
        #                maintMargin: 9817,
        #                realisedPnl: -684,
        #                unrealisedPnl: -1559,
        #                unrealisedPnlPcnt: -0.0017,
        #                unrealisedRoePcnt: -0.1709,
        #                avgCostPrice: 43846.7643,
        #                avgEntryPrice: 43846.7643,
        #                breakEvenPrice: 43880,
        #                marginCallPrice: 20976,
        #                liquidationPrice: 20976,
        #                bankruptPrice: 20941,
        #                timestamp: '2023-12-07T00:09:00.709Z'
        #            }
        #        ]
        #    }
        # update
        #    {
        #        table: 'position',
        #        action: 'update',
        #        data: [
        #            {
        #                account: 412475,
        #                symbol: 'XBTUSD',
        #                currency: 'XBt',
        #                currentQty: 400,
        #                markPrice: 43772.75,
        #                markValue: -913812,
        #                riskValue: 913812,
        #                homeNotional: 0.00913812,
        #                posCross: 1543,
        #                posComm: 693,
        #                posMargin: 11359,
        #                posMaint: 3886,
        #                maintMargin: 9816,
        #                unrealisedPnl: -1543,
        #                unrealisedRoePcnt: -0.1691,
        #                liquidationPrice: 20976,
        #                timestamp: '2023-12-07T00:09:10.760Z'
        #            }
        #        ]
        #    }
        #
        if self.positions is None:
            self.positions = ArrayCacheBySymbolBySide()
        cache = self.positions
        rawPositions = self.safe_value(message, 'data', [])
        newPositions = []
        for i in range(0, len(rawPositions)):
            rawPosition = rawPositions[i]
            position = self.parse_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')

    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
        :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()
        await self.authenticate()
        name = 'order'
        subscriptionHash = name
        messageHash = name
        if symbol is not None:
            symbol = self.symbol(symbol)
            messageHash += ':' + symbol
        url = self.urls['api']['ws']
        request: dict = {
            'op': 'subscribe',
            'args': [
                subscriptionHash,
            ],
        }
        orders = await self.watch(url, messageHash, request, subscriptionHash)
        if self.newUpdates:
            limit = orders.getLimit(symbol, limit)
        return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)

    def handle_orders(self, client: Client, message):
        #
        #     {
        #         "table": "order",
        #         "action": "partial",
        #         "keys": ["orderID"],
        #         "types": {
        #             "orderID": "guid",
        #             "clOrdID": "string",
        #             "clOrdLinkID": "symbol",
        #             "account": "long",
        #             "symbol": "symbol",
        #             "side": "symbol",
        #             "simpleOrderQty": "float",
        #             "orderQty": "long",
        #             "price": "float",
        #             "displayQty": "long",
        #             "stopPx": "float",
        #             "pegOffsetValue": "float",
        #             "pegPriceType": "symbol",
        #             "currency": "symbol",
        #             "settlCurrency": "symbol",
        #             "ordType": "symbol",
        #             "timeInForce": "symbol",
        #             "execInst": "symbol",
        #             "contingencyType": "symbol",
        #             "exDestination": "symbol",
        #             "ordStatus": "symbol",
        #             "triggered": "symbol",
        #             "workingIndicator": "boolean",
        #             "ordRejReason": "symbol",
        #             "simpleLeavesQty": "float",
        #             "leavesQty": "long",
        #             "simpleCumQty": "float",
        #             "cumQty": "long",
        #             "avgPx": "float",
        #             "multiLegReportingType": "symbol",
        #             "text": "string",
        #             "transactTime": "timestamp",
        #             "timestamp": "timestamp"
        #         },
        #         "foreignKeys": {symbol: 'instrument', side: "side", ordStatus: "ordStatus"},
        #         "attributes": {
        #             "orderID": "grouped",
        #             "account": "grouped",
        #             "ordStatus": "grouped",
        #             "workingIndicator": "grouped"
        #         },
        #         "filter": {account: 1455728},
        #         "data": [
        #             {
        #                 "orderID": "56222c7a-9956-413a-82cf-99f4812c214b",
        #                 "clOrdID": '',
        #                 "clOrdLinkID": '',
        #                 "account": 1455728,
        #                 "symbol": "XBTUSD",
        #                 "side": "Sell",
        #                 "simpleOrderQty": null,
        #                 "orderQty": 1,
        #                 "price": 40000,
        #                 "displayQty": null,
        #                 "stopPx": null,
        #                 "pegOffsetValue": null,
        #                 "pegPriceType": '',
        #                 "currency": "USD",
        #                 "settlCurrency": "XBt",
        #                 "ordType": "Limit",
        #                 "timeInForce": "GoodTillCancel",
        #                 "execInst": '',
        #                 "contingencyType": '',
        #                 "exDestination": "XBME",
        #                 "ordStatus": "New",
        #                 "triggered": '',
        #                 "workingIndicator": True,
        #                 "ordRejReason": '',
        #                 "simpleLeavesQty": null,
        #                 "leavesQty": 1,
        #                 "simpleCumQty": null,
        #                 "cumQty": 0,
        #                 "avgPx": null,
        #                 "multiLegReportingType": "SingleSecurity",
        #                 "text": "Submitted via API.",
        #                 "transactTime": "2021-01-02T21:38:49.246Z",
        #                 "timestamp": "2021-01-02T21:38:49.246Z"
        #             }
        #         ]
        #     }
        #
        #     {
        #         "table": "order",
        #         "action": "insert",
        #         "data": [
        #             {
        #                 "orderID": "fa993d8e-f7e4-46ed-8097-04f8e9393585",
        #                 "clOrdID": '',
        #                 "clOrdLinkID": '',
        #                 "account": 1455728,
        #                 "symbol": "XBTUSD",
        #                 "side": "Sell",
        #                 "simpleOrderQty": null,
        #                 "orderQty": 1,
        #                 "price": 40000,
        #                 "displayQty": null,
        #                 "stopPx": null,
        #                 "pegOffsetValue": null,
        #                 "pegPriceType": '',
        #                 "currency": "USD",
        #                 "settlCurrency": "XBt",
        #                 "ordType": "Limit",
        #                 "timeInForce": "GoodTillCancel",
        #                 "execInst": '',
        #                 "contingencyType": '',
        #                 "exDestination": "XBME",
        #                 "ordStatus": "New",
        #                 "triggered": '',
        #                 "workingIndicator": True,
        #                 "ordRejReason": '',
        #                 "simpleLeavesQty": null,
        #                 "leavesQty": 1,
        #                 "simpleCumQty": null,
        #                 "cumQty": 0,
        #                 "avgPx": null,
        #                 "multiLegReportingType": "SingleSecurity",
        #                 "text": "Submitted via API.",
        #                 "transactTime": "2021-01-02T23:49:02.286Z",
        #                 "timestamp": "2021-01-02T23:49:02.286Z"
        #             }
        #         ]
        #     }
        #
        #
        #
        #     {
        #         "table": "order",
        #         "action": "update",
        #         "data": [
        #             {
        #                 "orderID": "fa993d8e-f7e4-46ed-8097-04f8e9393585",
        #                 "ordStatus": "Canceled",
        #                 "workingIndicator": False,
        #                 "leavesQty": 0,
        #                 "text": "Canceled: Canceled via API.\nSubmitted via API.",
        #                 "timestamp": "2021-01-02T23:50:51.272Z",
        #                 "clOrdID": '',
        #                 "account": 1455728,
        #                 "symbol": "XBTUSD"
        #             }
        #         ]
        #     }
        #
        data = self.safe_value(message, 'data', [])
        messageHash = 'order'
        # initial subscription response with multiple orders
        dataLength = len(data)
        if dataLength > 0:
            if self.orders is None:
                limit = self.safe_integer(self.options, 'ordersLimit', 1000)
                self.orders = ArrayCacheBySymbolById(limit)
            stored = self.orders
            symbols: dict = {}
            for i in range(0, dataLength):
                currentOrder = data[i]
                orderId = self.safe_string(currentOrder, 'orderID')
                previousOrder = self.safe_value(stored.hashmap, orderId)
                rawOrder = currentOrder
                if previousOrder is not None:
                    rawOrder = self.extend(previousOrder['info'], currentOrder)
                order = self.parse_order(rawOrder)
                stored.append(order)
                symbol = order['symbol']
                symbols[symbol] = True
            client.resolve(self.orders, messageHash)
            keys = list(symbols.keys())
            for i in range(0, len(keys)):
                symbol = keys[i]
                client.resolve(self.orders, messageHash + ':' + symbol)

    async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        watches information on multiple trades made by the user
        :param str symbol: unified market symbol of the market trades were made in
        :param int [since]: the earliest time in ms to fetch trades for
        :param int [limit]: the maximum number of trade structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure
        """
        await self.load_markets()
        await self.authenticate()
        name = 'execution'
        subscriptionHash = name
        messageHash = name
        if symbol is not None:
            symbol = self.symbol(symbol)
            messageHash += ':' + symbol
        url = self.urls['api']['ws']
        request: dict = {
            'op': 'subscribe',
            'args': [
                subscriptionHash,
            ],
        }
        trades = await self.watch(url, messageHash, request, subscriptionHash)
        if self.newUpdates:
            limit = trades.getLimit(symbol, limit)
        return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)

    def handle_my_trades(self, client: Client, message):
        #
        #     {
        #         "table":"execution",
        #         "action":"insert",
        #         "data":[
        #             {
        #                 "execID":"0193e879-cb6f-2891-d099-2c4eb40fee21",
        #                 "orderID":"00000000-0000-0000-0000-000000000000",
        #                 "clOrdID":"",
        #                 "clOrdLinkID":"",
        #                 "account":2,
        #                 "symbol":"XBTUSD",
        #                 "side":"Sell",
        #                 "lastQty":1,
        #                 "lastPx":1134.37,
        #                 "underlyingLastPx":null,
        #                 "lastMkt":"XBME",
        #                 "lastLiquidityInd":"RemovedLiquidity",
        #                 "simpleOrderQty":null,
        #                 "orderQty":1,
        #                 "price":1134.37,
        #                 "displayQty":null,
        #                 "stopPx":null,
        #                 "pegOffsetValue":null,
        #                 "pegPriceType":"",
        #                 "currency":"USD",
        #                 "settlCurrency":"XBt",
        #                 "execType":"Trade",
        #                 "ordType":"Limit",
        #                 "timeInForce":"ImmediateOrCancel",
        #                 "execInst":"",
        #                 "contingencyType":"",
        #                 "exDestination":"XBME",
        #                 "ordStatus":"Filled",
        #                 "triggered":"",
        #                 "workingIndicator":false,
        #                 "ordRejReason":"",
        #                 "simpleLeavesQty":0,
        #                 "leavesQty":0,
        #                 "simpleCumQty":0.001,
        #                 "cumQty":1,
        #                 "avgPx":1134.37,
        #                 "commission":0.00075,
        #                 "tradePublishIndicator":"DoNotPublishTrade",
        #                 "multiLegReportingType":"SingleSecurity",
        #                 "text":"Liquidation",
        #                 "trdMatchID":"7f4ab7f6-0006-3234-76f4-ae1385aad00f",
        #                 "execCost":88155,
        #                 "execComm":66,
        #                 "homeNotional":-0.00088155,
        #                 "foreignNotional":1,
        #                 "transactTime":"2017-04-04T22:07:46.035Z",
        #                 "timestamp":"2017-04-04T22:07:46.035Z"
        #             }
        #         ]
        #     }
        #
        messageHash = self.safe_string(message, 'table')
        data = self.safe_value(message, 'data', [])
        dataByExecType = self.group_by(data, 'execType')
        rawTrades = self.safe_value(dataByExecType, 'Trade', [])
        trades = self.parse_trades(rawTrades)
        if self.myTrades is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            self.myTrades = ArrayCacheBySymbolById(limit)
        stored = self.myTrades
        symbols: dict = {}
        for j in range(0, len(trades)):
            trade = trades[j]
            symbol = trade['symbol']
            stored.append(trade)
            symbols[symbol] = trade
        numTrades = len(trades)
        if numTrades > 0:
            client.resolve(stored, messageHash)
        keys = list(symbols.keys())
        for i in range(0, len(keys)):
            client.resolve(stored, messageHash + ':' + keys[i])

    async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        """
        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
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
        """
        return await self.watch_order_book_for_symbols([symbol], limit, params)

    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
        :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
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
        """
        table = None
        if limit is None:
            table = self.safe_string(self.options, 'watchOrderBookLevel', 'orderBookL2')
        elif limit == 25:
            table = 'orderBookL2_25'
        elif limit == 10:
            table = 'orderBookL10'
        else:
            raise ExchangeError(self.id + ' watchOrderBookForSymbols limit argument must be None(L2), 25(L2) or 10(L3)')
        await self.load_markets()
        symbols = self.market_symbols(symbols)
        topics = []
        messageHashes = []
        for i in range(0, len(symbols)):
            symbol = symbols[i]
            market = self.market(symbol)
            topic = table + ':' + market['id']
            topics.append(topic)
            messageHash = table + ':' + symbol
            messageHashes.append(messageHash)
        url = self.urls['api']['ws']
        request: dict = {
            'op': 'subscribe',
            'args': topics,
        }
        orderbook = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), topics)
        return orderbook.limit()

    async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        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()
        symbols = self.market_symbols(symbols, None, False)
        table = 'trade'
        topics = []
        messageHashes = []
        for i in range(0, len(symbols)):
            symbol = symbols[i]
            market = self.market(symbol)
            topic = table + ':' + market['id']
            topics.append(topic)
            messageHash = table + ':' + symbol
            messageHashes.append(messageHash)
        url = self.urls['api']['ws']
        request: dict = {
            'op': 'subscribe',
            'args': topics,
        }
        trades = await self.watch_multiple(url, messageHashes, self.deep_extend(request, params), topics)
        if self.newUpdates:
            first = self.safe_value(trades, 0)
            tradeSymbol = self.safe_string(first, 'symbol')
            limit = trades.getLimit(tradeSymbol, limit)
        return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)

    async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
        """
        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()
        market = self.market(symbol)
        symbol = market['symbol']
        table = 'tradeBin' + self.safe_string(self.timeframes, timeframe, timeframe)
        messageHash = table + ':' + market['id']
        url = self.urls['api']['ws']
        request: dict = {
            'op': 'subscribe',
            'args': [
                messageHash,
            ],
        }
        ohlcv = await self.watch(url, messageHash, self.extend(request, params), messageHash)
        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):
        #
        #     {
        #         "table": "tradeBin1m",
        #         "action": "partial",
        #         "keys": [],
        #         "types": {
        #             "timestamp": "timestamp",
        #             "symbol": "symbol",
        #             "open": "float",
        #             "high": "float",
        #             "low": "float",
        #             "close": "float",
        #             "trades": "long",
        #             "volume": "long",
        #             "vwap": "float",
        #             "lastSize": "long",
        #             "turnover": "long",
        #             "homeNotional": "float",
        #             "foreignNotional": "float"
        #         },
        #         "foreignKeys": {symbol: "instrument"},
        #         "attributes": {timestamp: "sorted", symbol: "grouped"},
        #         "filter": {symbol: "XBTUSD"},
        #         "data": [
        #             {
        #                 "timestamp": "2020-02-03T01:13:00.000Z",
        #                 "symbol": "XBTUSD",
        #                 "open": 9395,
        #                 "high": 9395.5,
        #                 "low": 9394.5,
        #                 "close": 9395,
        #                 "trades": 221,
        #                 "volume": 839204,
        #                 "vwap": 9394.9643,
        #                 "lastSize": 1874,
        #                 "turnover": 8932641535,
        #                 "homeNotional": 89.32641534999999,
        #                 "foreignNotional": 839204
        #             }
        #         ]
        #     }
        #
        #
        #     {
        #         "table": "tradeBin1m",
        #         "action": "insert",
        #         "data": [
        #             {
        #                 "timestamp": "2020-02-03T18:28:00.000Z",
        #                 "symbol": "XBTUSD",
        #                 "open": 9256,
        #                 "high": 9256.5,
        #                 "low": 9256,
        #                 "close": 9256,
        #                 "trades": 29,
        #                 "volume": 79057,
        #                 "vwap": 9256.688,
        #                 "lastSize": 100,
        #                 "turnover": 854077082,
        #                 "homeNotional": 8.540770820000002,
        #                 "foreignNotional": 79057
        #             }
        #         ]
        #     }
        #
        table = self.safe_string(message, 'table')
        interval = table.replace('tradeBin', '')
        timeframe = self.find_timeframe(interval)
        duration = self.parse_timeframe(timeframe)
        candles = self.safe_value(message, 'data', [])
        results: dict = {}
        for i in range(0, len(candles)):
            candle = candles[i]
            marketId = self.safe_string(candle, 'symbol')
            market = self.safe_market(marketId)
            symbol = market['symbol']
            messageHash = table + ':' + market['id']
            result = [
                self.parse8601(self.safe_string(candle, 'timestamp')) - duration * 1000,
                None,  # set open price to None, see: https://github.com/ccxt/ccxt/pull/21356#issuecomment-1969565862
                self.safe_float(candle, 'high'),
                self.safe_float(candle, 'low'),
                self.safe_float(candle, 'close'),
                self.safe_float(candle, 'volume'),
            ]
            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(result)
            results[messageHash] = stored
        messageHashes = list(results.keys())
        for i in range(0, len(messageHashes)):
            messageHash = messageHashes[i]
            client.resolve(results[messageHash], messageHash)

    async def watch_heartbeat(self, params={}):
        await self.load_markets()
        event = 'heartbeat'
        url = self.urls['api']['ws']
        return await self.watch(url, event)

    def handle_order_book(self, client: Client, message):
        #
        # first snapshot
        #
        #     {
        #         "table": "orderBookL2",
        #         "action": "partial",
        #         "keys": ['symbol', "id", "side"],
        #         "types": {
        #             "symbol": "symbol",
        #             "id": "long",
        #             "side": "symbol",
        #             "size": "long",
        #             "price": "float"
        #         },
        #         "foreignKeys": {symbol: "instrument", side: "side"},
        #         "attributes": {symbol: "parted", id: "sorted"},
        #         "filter": {symbol: "XBTUSD"},
        #         "data": [
        #             {symbol: "XBTUSD", id: 8700000100, side: "Sell", size: 1, price: 999999},
        #             {symbol: "XBTUSD", id: 8700000200, side: "Sell", size: 3, price: 999998},
        #             {symbol: "XBTUSD", id: 8716991250, side: "Sell", size: 26, price: 830087.5},
        #             {symbol: "XBTUSD", id: 8728701950, side: "Sell", size: 1720, price: 712980.5},
        #         ]
        #     }
        #
        # subsequent updates
        #
        #     {
        #         "table": "orderBookL2",
        #         "action": "update",
        #         "data": [
        #             {
        #               "table": "orderBookL2",
        #               "action": "insert",
        #               "data": [
        #                 {
        #                   "symbol": "ETH_USDT",
        #                   "id": 85499965912,
        #                   "side": "Buy",
        #                   "size": 83000000,
        #                   "price": 1704.4,
        #                   "timestamp": "2023-03-26T22:29:00.299Z"
        #                 }
        #               ]
        #             }
        #             ...
        #         ]
        #     }
        #
        action = self.safe_string(message, 'action')
        table = self.safe_string(message, 'table')
        if table is None:
            return  # protecting from weird updates
        data = self.safe_value(message, 'data', [])
        # if it's an initial snapshot
        if action == 'partial':
            filter = self.safe_dict(message, 'filter', {})
            marketId = self.safe_value(filter, 'symbol')
            if marketId is None:
                return  # protecting from weird update
            market = self.safe_market(marketId)
            symbol = market['symbol']
            if table == 'orderBookL2':
                self.orderbooks[symbol] = self.indexed_order_book()
            elif table == 'orderBookL2_25':
                self.orderbooks[symbol] = self.indexed_order_book({}, 25)
            elif table == 'orderBook10':
                self.orderbooks[symbol] = self.indexed_order_book({}, 10)
            orderbook = self.orderbooks[symbol]
            orderbook['symbol'] = symbol
            for i in range(0, len(data)):
                price = self.safe_float(data[i], 'price')
                size = self.convertFromRawQuantity(symbol, self.safe_string(data[i], 'size'))
                id = self.safe_string(data[i], 'id')
                side = self.safe_string(data[i], 'side')
                side = 'bids' if (side == 'Buy') else 'asks'
                bookside = orderbook[side]
                bookside.storeArray([price, size, id])
                datetime = self.safe_string(data[i], 'timestamp')
                orderbook['timestamp'] = self.parse8601(datetime)
                orderbook['datetime'] = datetime
            messageHash = table + ':' + symbol
            client.resolve(orderbook, messageHash)
        else:
            numUpdatesByMarketId: dict = {}
            for i in range(0, len(data)):
                marketId = self.safe_value(data[i], 'symbol')
                if marketId is None:
                    return  # protecting from weird update
                if not (marketId in numUpdatesByMarketId):
                    numUpdatesByMarketId[marketId] = 0
                numUpdatesByMarketId[marketId] = self.sum(numUpdatesByMarketId, 1)
                market = self.safe_market(marketId)
                symbol = market['symbol']
                orderbook = self.orderbooks[symbol]
                price = self.safe_number(data[i], 'price')
                size = 0 if (action == 'delete') else self.convertFromRawQuantity(symbol, self.safe_string(data[i], 'size', '0'))
                id = self.safe_string(data[i], 'id')
                side = self.safe_string(data[i], 'side')
                side = 'bids' if (side == 'Buy') else 'asks'
                bookside = orderbook[side]
                bookside.storeArray([price, size, id])
                datetime = self.safe_string(data[i], 'timestamp')
                orderbook['timestamp'] = self.parse8601(datetime)
                orderbook['datetime'] = datetime
            marketIds = list(numUpdatesByMarketId.keys())
            for i in range(0, len(marketIds)):
                marketId = marketIds[i]
                market = self.safe_market(marketId)
                symbol = market['symbol']
                messageHash = table + ':' + symbol
                orderbook = self.orderbooks[symbol]
                client.resolve(orderbook, messageHash)

    def handle_system_status(self, client: Client, message):
        #
        # todo answer the question whether handleSystemStatus should be renamed
        # and unified for any usage pattern that
        # involves system status and maintenance updates
        #
        #     {
        #         "info": "Welcome to the BitMEX Realtime API.",
        #         "version": "2019-11-22T00:24:37.000Z",
        #         "timestamp": "2019-11-23T09:02:27.771Z",
        #         "docs": "https://www.bitmex.com/app/wsAPI",
        #         "limit": {remaining: 39}
        #     }
        #
        return message

    def handle_subscription_status(self, client: Client, message):
        #
        #     {
        #         "success": True,
        #         "subscribe": "orderBookL2:XBTUSD",
        #         "request": {op: "subscribe", args: ["orderBookL2:XBTUSD"]}
        #     }
        #
        return message

    def handle_error_message(self, client: Client, message):
        #
        # generic error format
        #
        #     {"error": errorMessage}
        #
        # examples
        #
        #     {
        #         "status": 429,
        #         "error": "Rate limit exceeded, retry in 1 seconds.",
        #         "meta": {"retryAfter": 1},
        #         "request": {"op": "subscribe", "args": "orderBook"},
        #     }
        #
        #     {"error": "Rate limit exceeded, retry in 29 seconds."}
        #
        error = self.safe_string(message, 'error')
        if error is not None:
            request = self.safe_value(message, 'request', {})
            args = self.safe_value(request, 'args', [])
            numArgs = len(args)
            if numArgs > 0:
                messageHash = args[0]
                broad = self.exceptions['ws']['broad']
                broadKey = self.find_broadly_matched_key(broad, error)
                exception = None
                if broadKey is None:
                    exception = ExchangeError(error)  # c# requirement for now
                else:
                    exception = broad[broadKey](error)
                client.reject(exception, messageHash)
                return False
        return True

    def handle_message(self, client: Client, message):
        #
        #     {
        #         "info": "Welcome to the BitMEX Realtime API.",
        #         "version": "2019-11-22T00:24:37.000Z",
        #         "timestamp": "2019-11-23T09:04:42.569Z",
        #         "docs": "https://www.bitmex.com/app/wsAPI",
        #         "limit": {remaining: 38}
        #     }
        #
        #     {
        #         "success": True,
        #         "subscribe": "orderBookL2:XBTUSD",
        #         "request": {op: "subscribe", args: ["orderBookL2:XBTUSD"]}
        #     }
        #
        #     {
        #         "table": "orderBookL2",
        #         "action": "update",
        #         "data": [
        #             {symbol: "XBTUSD", id: 8799284800, side: "Sell", size: 721000},
        #             {symbol: "XBTUSD", id: 8799285100, side: "Sell", size: 70590},
        #             {symbol: "XBTUSD", id: 8799285550, side: "Sell", size: 217652},
        #             {symbol: "XBTUSD", id: 8799285850, side: "Sell", size: 105578},
        #             {symbol: "XBTUSD", id: 8799286350, side: "Sell", size: 172093},
        #             {symbol: "XBTUSD", id: 8799286650, side: "Sell", size: 201125},
        #             {symbol: "XBTUSD", id: 8799288950, side: "Buy", size: 47552},
        #             {symbol: "XBTUSD", id: 8799289250, side: "Buy", size: 78217},
        #             {symbol: "XBTUSD", id: 8799289700, side: "Buy", size: 193677},
        #             {symbol: "XBTUSD", id: 8799290000, side: "Buy", size: 818161},
        #             {symbol: "XBTUSD", id: 8799290500, side: "Buy", size: 218806},
        #             {symbol: "XBTUSD", id: 8799290800, side: "Buy", size: 102946}
        #         ]
        #     }
        #
        if self.handle_error_message(client, message):
            table = self.safe_string(message, 'table')
            methods: dict = {
                'orderBookL2': self.handle_order_book,
                'orderBookL2_25': self.handle_order_book,
                'orderBook10': self.handle_order_book,
                'instrument': self.handle_ticker,
                'trade': self.handle_trades,
                'tradeBin1m': self.handle_ohlcv,
                'tradeBin5m': self.handle_ohlcv,
                'tradeBin1h': self.handle_ohlcv,
                'tradeBin1d': self.handle_ohlcv,
                'order': self.handle_orders,
                'execution': self.handle_my_trades,
                'margin': self.handle_balance,
                'liquidation': self.handle_liquidation,
                'position': self.handle_positions,
            }
            method = self.safe_value(methods, table)
            if method is None:
                request = self.safe_value(message, 'request', {})
                op = self.safe_value(request, 'op')
                if op == 'authKeyExpires':
                    self.handle_authentication_message(client, message)
            else:
                method(client, message)
