<?php

namespace ccxt\pro;

// 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

use Exception; // a common import
use ccxt\ExchangeError;
use ccxt\AuthenticationError;
use \React\Async;
use \React\Promise\PromiseInterface;

class bitmex extends \ccxt\async\bitmex {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchBalance' => true,
                'watchLiquidations' => true,
                'watchLiquidationsForSymbols' => true,
                'watchMyLiquidations' => null,
                'watchMyLiquidationsForSymbols' => null,
                'watchMyTrades' => true,
                'watchOHLCV' => true,
                'watchOrderBook' => true,
                'watchOrderBookForSymbols' => true,
                'watchOrders' => true,
                'watchPostions' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => true,
            ),
            'urls' => array(
                'test' => array(
                    'ws' => 'wss://ws.testnet.bitmex.com/realtime',
                ),
                'api' => array(
                    'ws' => 'wss://ws.bitmex.com/realtime',
                ),
            ),
            // 'versions' => array(
            //     'ws' => '0.2.0',
            // ),
            'options' => array(
                'watchOrderBookLevel' => 'orderBookL2', // 'orderBookL2' = L2 full order book, 'orderBookL2_25' = L2 top 25, 'orderBook10' L3 top 10
                'tradesLimit' => 1000,
                'OHLCVLimit' => 1000,
            ),
            'exceptions' => array(
                'ws' => array(
                    'exact' => array(
                    ),
                    'broad' => array(
                        'Rate limit exceeded' => '\\ccxt\\RateLimitExceeded',
                    ),
                ),
            ),
        ));
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
             *
             * @see https://www.bitmex.com/app/wsAPI#Subscriptions
             *
             * @param {string} $symbol unified $symbol of the market to fetch the ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            Async\await($this->load_markets());
            $symbol = $this->symbol($symbol);
            $tickers = Async\await($this->watch_tickers(array( $symbol ), $params));
            return $tickers[$symbol];
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * watches a price $ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
             *
             * @see https://www.bitmex.com/app/wsAPI#Subscriptions
             *
             * @param {string[]} $symbols unified $symbol of the $market to fetch the $ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=$ticker-structure $ticker structure~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, true);
            $name = 'instrument';
            $url = $this->urls['api']['ws'];
            $messageHashes = array();
            $rawSubscriptions = array();
            if ($symbols !== null) {
                for ($i = 0; $i < count($symbols); $i++) {
                    $symbol = $symbols[$i];
                    $market = $this->market($symbol);
                    $subscription = $name . ':' . $market['id'];
                    $rawSubscriptions[] = $subscription;
                    $messageHash = 'ticker:' . $symbol;
                    $messageHashes[] = $messageHash;
                }
            } else {
                $rawSubscriptions[] = $name;
                $messageHashes[] = 'alltickers';
            }
            $request = array(
                'op' => 'subscribe',
                'args' => $rawSubscriptions,
            );
            $ticker = Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $rawSubscriptions));
            if ($this->newUpdates) {
                $result = array();
                $result[$ticker['symbol']] = $ticker;
                return $result;
            }
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //     {
        //         "table" => "instrument",
        //         "action" => "partial",
        //         "keys" => array( "symbol" ),
        //         "types" => array(
        //             "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" => array(
        //             "inverseLeg" => "instrument",
        //             "sellLeg" => "instrument",
        //             "buyLeg" => "instrument"
        //         ),
        //         "attributes" => array( $symbol => "unique" ),
        //         "filter" => array( $symbol => "XBTUSD" ),
        //         "data" => array(
        //             {
        //                 "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 = $this->safe_list($message, 'data', array());
        $tickers = array();
        for ($i = 0; $i < count($data); $i++) {
            $update = $data[$i];
            $marketId = $this->safe_string($update, 'symbol');
            $symbol = $this->safe_symbol($marketId);
            if (!(is_array($this->tickers) && array_key_exists($symbol, $this->tickers))) {
                $this->tickers[$symbol] = $this->parse_ticker(array());
            }
            $updatedTicker = $this->parse_ticker($update);
            $fullParsedTicker = $this->deep_extend($this->tickers[$symbol], $updatedTicker);
            $tickers[$symbol] = $fullParsedTicker;
            $this->tickers[$symbol] = $fullParsedTicker;
            $messageHash = 'ticker:' . $symbol;
            $client->resolve ($fullParsedTicker, $messageHash);
            $client->resolve ($fullParsedTicker, 'alltickers');
        }
        return $message;
    }

    public function watch_liquidations(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        /**
         * watch the public liquidations of a trading pair
         *
         * @see https://www.bitmex.com/app/wsAPI#Liquidation
         *
         * @param {string} $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 {array} [$params] exchange specific parameters for the bitmex api endpoint
         * @return {array} an array of {@link https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure liquidation structures}
         */
        return $this->watch_liquidations_for_symbols(array( $symbol ), $since, $limit, $params);
    }

    public function watch_liquidations_for_symbols(array $symbols, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * watch the public liquidations of a trading pair
             *
             * @see https://www.bitmex.com/app/wsAPI#Liquidation
             *
             * @param {string[]} $symbols
             * @param {int} [$since] the earliest time in ms to fetch liquidations for
             * @param {int} [$limit] the maximum number of liquidation structures to retrieve
             * @param {array} [$params] exchange specific parameters for the bitmex api endpoint
             * @return {array} an array of {@link https://github.com/ccxt/ccxt/wiki/Manual#liquidation-structure liquidation structures}
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, true, true);
            $messageHashes = array();
            $subscriptionHashes = array();
            if ($this->is_empty($symbols)) {
                $subscriptionHashes[] = 'liquidation';
                $messageHashes[] = 'liquidations';
            } else {
                for ($i = 0; $i < count($symbols); $i++) {
                    $symbol = $symbols[$i];
                    $market = $this->market($symbol);
                    $subscriptionHashes[] = 'liquidation:' . $market['id'];
                    $messageHashes[] = 'liquidations::' . $symbol;
                }
            }
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => $subscriptionHashes,
            );
            $newLiquidations = Async\await($this->watch_multiple($url, $messageHashes, $this->deep_extend($request, $params), $subscriptionHashes));
            if ($this->newUpdates) {
                return $newLiquidations;
            }
            return $this->filter_by_symbols_since_limit($this->liquidations, $symbols, $since, $limit, true);
        }) ();
    }

    public function handle_liquidation(Client $client, $message) {
        //
        //    {
        //        "table":"liquidation",
        //        "action":"partial",
        //        "keys":array(
        //           "orderID"
        //        ),
        //        "types":array(
        //           "orderID":"guid",
        //           "symbol":"symbol",
        //           "side":"symbol",
        //           "price":"float",
        //           "leavesQty":"long"
        //        ),
        //        "filter":array(),
        //        "data":array(
        //           {
        //              "orderID":"e0a568ee-7830-4428-92c3-73e82b9576ce",
        //              "symbol":"XPLAUSDT",
        //              "side":"Sell",
        //              "price":0.206,
        //              "leavesQty":340
        //           }
        //        )
        //    }
        //
        $rawLiquidations = $this->safe_value($message, 'data', array());
        $newLiquidations = array();
        for ($i = 0; $i < count($rawLiquidations); $i++) {
            $rawLiquidation = $rawLiquidations[$i];
            $liquidation = $this->parse_liquidation($rawLiquidation);
            $symbol = $liquidation['symbol'];
            $liquidations = $this->safe_value($this->liquidations, $symbol);
            if ($liquidations === null) {
                $limit = $this->safe_integer($this->options, 'liquidationsLimit', 1000);
                $liquidations = new ArrayCache ($limit);
            }
            $liquidations->append ($liquidation);
            $this->liquidations[$symbol] = $liquidations;
            $newLiquidations[] = $liquidation;
        }
        $client->resolve ($newLiquidations, 'liquidations');
        $liquidationsBySymbol = $this->index_by($newLiquidations, 'symbol');
        $symbols = is_array($liquidationsBySymbol) ? array_keys($liquidationsBySymbol) : array();
        for ($i = 0; $i < count($symbols); $i++) {
            $symbol = $symbols[$i];
            $client->resolve ($liquidationsBySymbol[$symbol], 'liquidations::' . $symbol);
        }
    }

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * watch balance and get the amount of funds available for trading or funds locked in orders
             *
             * @see https://www.bitmex.com/app/wsAPI#Subscriptions
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $messageHash = 'margin';
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => array(
                    $messageHash,
                ),
            );
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
        }) ();
    }

    public function handle_balance(Client $client, $message) {
        //
        //     {
        //         "table" => "margin",
        //         "action" => "partial",
        //         "keys" => array( "account" ),
        //         "types" => array(
        //             "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" => array(),
        //         "attributes" => array( account => "sorted" ),
        //         "filter" => array( account => 1455728 ),
        //         "data" => array(
        //             {
        //                 "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 = $this->safe_value($message, 'data');
        $balance = $this->parse_balance($data);
        $this->balance = $this->extend($this->balance, $balance);
        $messageHash = $this->safe_string($message, 'table');
        $client->resolve ($this->balance, $messageHash);
    }

    public function handle_trades(Client $client, $message) {
        //
        // initial snapshot
        //
        //     {
        //         "table" => "trade",
        //         "action" => "partial",
        //         "keys" => array(),
        //         "types" => array(
        //             "timestamp" => "timestamp",
        //             "symbol" => "symbol",
        //             "side" => "symbol",
        //             "size" => "long",
        //             "price" => "float",
        //             "tickDirection" => "symbol",
        //             "trdMatchID" => "guid",
        //             "grossValue" => "long",
        //             "homeNotional" => "float",
        //             "foreignNotional" => "float"
        //         ),
        //         "foreignKeys" => array( $symbol => "instrument", side => "side" ),
        //         "attributes" => array( timestamp => "sorted", $symbol => "grouped" ),
        //         "filter" => array( $symbol => "XBTUSD" ),
        //         "data" => array(
        //             {
        //                 "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" => array(
        //             {
        //                 "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 = $this->safe_value($message, 'data', array());
        $dataByMarketIds = $this->group_by($data, 'symbol');
        $marketIds = is_array($dataByMarketIds) ? array_keys($dataByMarketIds) : array();
        for ($i = 0; $i < count($marketIds); $i++) {
            $marketId = $marketIds[$i];
            $market = $this->safe_market($marketId);
            $symbol = $market['symbol'];
            $messageHash = $table . ':' . $symbol;
            $trades = $this->parse_trades($dataByMarketIds[$marketId], $market);
            $stored = $this->safe_value($this->trades, $symbol);
            if ($stored === null) {
                $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
                $stored = new ArrayCache ($limit);
                $this->trades[$symbol] = $stored;
            }
            for ($j = 0; $j < count($trades); $j++) {
                $stored->append ($trades[$j]);
            }
            $client->resolve ($stored, $messageHash);
        }
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * get the list of most recent trades for a particular $symbol
             *
             * @see https://www.bitmex.com/app/wsAPI#Subscriptions
             *
             * @param {string} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            return Async\await($this->watch_trades_for_symbols(array( $symbol ), $since, $limit, $params));
        }) ();
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $url = $this->urls['api']['ws'];
            $client = $this->client($url);
            $messageHash = 'authenticated';
            $future = $client->reusableFuture ($messageHash);
            $authenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                $this->check_required_credentials();
                $timestamp = $this->milliseconds();
                $payload = 'GET' . '/realtime' . (string) $timestamp;
                $signature = $this->hmac($this->encode($payload), $this->encode($this->secret), 'sha256');
                $request = array(
                    'op' => 'authKeyExpires',
                    'args' => array(
                        $this->apiKey,
                        $timestamp,
                        $signature,
                    ),
                );
                $message = $this->extend($request, $params);
                $this->watch($url, $messageHash, $message, $messageHash);
            }
            return Async\await($future);
        }) ();
    }

    public function handle_authentication_message(Client $client, $message) {
        $authenticated = $this->safe_bool($message, 'success', false);
        $messageHash = 'authenticated';
        if ($authenticated) {
            // we resolve the $future here permanently so authentication only happens once
            $future = $this->safe_value($client->futures, $messageHash);
            $future->resolve (true);
        } else {
            $error = new AuthenticationError ($this->json($message));
            $client->reject ($error, $messageHash);
            if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                unset($client->subscriptions[$messageHash]);
            }
        }
    }

    public function watch_positions(?array $symbols = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * watch all open positions
             *
             * @see https://www.bitmex.com/app/wsAPI#Subscriptions
             *
             * @param {string[]|null} $symbols list of unified market $symbols
             * @param {int} [$since] the earliest time in ms to watch positions for
             * @param {int} [$limit] the maximum number of positions to retrieve
             * @param {array} $params extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of {@link https://docs.ccxt.com/en/latest/manual.html#position-structure position structure}
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $subscriptionHash = 'position';
            $messageHash = 'positions';
            if (!$this->is_empty($symbols)) {
                $messageHash = '::' . implode(',', $symbols);
            }
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => array(
                    $subscriptionHash,
                ),
            );
            $newPositions = Async\await($this->watch($url, $messageHash, $request, $subscriptionHash));
            if ($this->newUpdates) {
                return $newPositions;
            }
            return $this->filter_by_symbols_since_limit($this->positions, $symbols, $since, $limit, true);
        }) ();
    }

    public function handle_positions($client, $message) {
        //
        // partial
        //    {
        //        table => 'position',
        //        action => 'partial',
        //        keys => array( 'account', 'symbol' ),
        //        types => array(
        //            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 => array( account => 412475 ),
        //        data => array(
        //            {
        //                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 => array(
        //            {
        //                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 ($this->positions === null) {
            $this->positions = new ArrayCacheBySymbolBySide ();
        }
        $cache = $this->positions;
        $rawPositions = $this->safe_value($message, 'data', array());
        $newPositions = array();
        for ($i = 0; $i < count($rawPositions); $i++) {
            $rawPosition = $rawPositions[$i];
            $position = $this->parse_position($rawPosition);
            $newPositions[] = $position;
            $cache->append ($position);
        }
        $messageHashes = $this->find_message_hashes($client, 'positions::');
        for ($i = 0; $i < count($messageHashes); $i++) {
            $messageHash = $messageHashes[$i];
            $parts = explode('::', $messageHash);
            $symbolsString = $parts[1];
            $symbols = explode(',', $symbolsString);
            $positions = $this->filter_by_array($newPositions, 'symbol', $symbols, false);
            if (!$this->is_empty($positions)) {
                $client->resolve ($positions, $messageHash);
            }
        }
        $client->resolve ($newPositions, 'positions');
    }

    public function watch_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $orders made by the user
             *
             * @see https://www.bitmex.com/app/wsAPI#Subscriptions
             *
             * @param {string} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $name = 'order';
            $subscriptionHash = $name;
            $messageHash = $name;
            if ($symbol !== null) {
                $symbol = $this->symbol($symbol);
                $messageHash .= ':' . $symbol;
            }
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => array(
                    $subscriptionHash,
                ),
            );
            $orders = Async\await($this->watch($url, $messageHash, $request, $subscriptionHash));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_orders(Client $client, $message) {
        //
        //     {
        //         "table" => "order",
        //         "action" => "partial",
        //         "keys" => array( "orderID" ),
        //         "types" => array(
        //             "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" => array( $symbol => 'instrument', side => "side", ordStatus => "ordStatus" ),
        //         "attributes" => array(
        //             "orderID" => "grouped",
        //             "account" => "grouped",
        //             "ordStatus" => "grouped",
        //             "workingIndicator" => "grouped"
        //         ),
        //         "filter" => array( account => 1455728 ),
        //         "data" => array(
        //             {
        //                 "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" => array(
        //             {
        //                 "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" => array(
        //             {
        //                 "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 = $this->safe_value($message, 'data', array());
        $messageHash = 'order';
        // initial subscription response with multiple orders
        $dataLength = count($data);
        if ($dataLength > 0) {
            if ($this->orders === null) {
                $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
                $this->orders = new ArrayCacheBySymbolById ($limit);
            }
            $stored = $this->orders;
            $symbols = array();
            for ($i = 0; $i < $dataLength; $i++) {
                $currentOrder = $data[$i];
                $orderId = $this->safe_string($currentOrder, 'orderID');
                $previousOrder = $this->safe_value($stored->hashmap, $orderId);
                $rawOrder = $currentOrder;
                if ($previousOrder !== null) {
                    $rawOrder = $this->extend($previousOrder['info'], $currentOrder);
                }
                $order = $this->parse_order($rawOrder);
                $stored->append ($order);
                $symbol = $order['symbol'];
                $symbols[$symbol] = true;
            }
            $client->resolve ($this->orders, $messageHash);
            $keys = is_array($symbols) ? array_keys($symbols) : array();
            for ($i = 0; $i < count($keys); $i++) {
                $symbol = $keys[$i];
                $client->resolve ($this->orders, $messageHash . ':' . $symbol);
            }
        }
    }

    public function watch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $trades made by the user
             *
             * @see https://www.bitmex.com/app/wsAPI#Subscriptions
             *
             * @param {string} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $name = 'execution';
            $subscriptionHash = $name;
            $messageHash = $name;
            if ($symbol !== null) {
                $symbol = $this->symbol($symbol);
                $messageHash .= ':' . $symbol;
            }
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => array(
                    $subscriptionHash,
                ),
            );
            $trades = Async\await($this->watch($url, $messageHash, $request, $subscriptionHash));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($trades, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_my_trades(Client $client, $message) {
        //
        //     {
        //         "table":"execution",
        //         "action":"insert",
        //         "data":array(
        //             {
        //                 "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 = $this->safe_string($message, 'table');
        $data = $this->safe_value($message, 'data', array());
        $dataByExecType = $this->group_by($data, 'execType');
        $rawTrades = $this->safe_value($dataByExecType, 'Trade', array());
        $trades = $this->parse_trades($rawTrades);
        if ($this->myTrades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $this->myTrades = new ArrayCacheBySymbolById ($limit);
        }
        $stored = $this->myTrades;
        $symbols = array();
        for ($j = 0; $j < count($trades); $j++) {
            $trade = $trades[$j];
            $symbol = $trade['symbol'];
            $stored->append ($trade);
            $symbols[$symbol] = $trade;
        }
        $numTrades = count($trades);
        if ($numTrades > 0) {
            $client->resolve ($stored, $messageHash);
        }
        $keys = is_array($symbols) ? array_keys($symbols) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $client->resolve ($stored, $messageHash . ':' . $keys[$i]);
        }
    }

    public function watch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://www.bitmex.com/app/wsAPI#OrderBookL2
             *
             * @param {string} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market symbols
             */
            return Async\await($this->watch_order_book_for_symbols(array( $symbol ), $limit, $params));
        }) ();
    }

    public function watch_order_book_for_symbols(array $symbols, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://www.bitmex.com/app/wsAPI#OrderBookL2
             *
             * @param {string[]} $symbols unified array of $symbols
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by $market $symbols
             */
            $table = null;
            if ($limit === null) {
                $table = $this->safe_string($this->options, 'watchOrderBookLevel', 'orderBookL2');
            } elseif ($limit === 25) {
                $table = 'orderBookL2_25';
            } elseif ($limit === 10) {
                $table = 'orderBookL10';
            } else {
                throw new ExchangeError($this->id . ' watchOrderBookForSymbols $limit argument must be null (L2), 25 (L2) or 10 (L3)');
            }
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $topic = $table . ':' . $market['id'];
                $topics[] = $topic;
                $messageHash = $table . ':' . $symbol;
                $messageHashes[] = $messageHash;
            }
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => $topics,
            );
            $orderbook = Async\await($this->watch_multiple($url, $messageHashes, $this->deep_extend($request, $params), $topics));
            return $orderbook->limit ();
        }) ();
    }

    public function watch_trades_for_symbols(array $symbols, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * get the list of most recent $trades for a list of $symbols
             *
             * @see https://www.bitmex.com/app/wsAPI#Subscriptions
             *
             * @param {string[]} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-$trades trade structures~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $table = 'trade';
            $topics = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $market = $this->market($symbol);
                $topic = $table . ':' . $market['id'];
                $topics[] = $topic;
                $messageHash = $table . ':' . $symbol;
                $messageHashes[] = $messageHash;
            }
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => $topics,
            );
            $trades = Async\await($this->watch_multiple($url, $messageHashes, $this->deep_extend($request, $params), $topics));
            if ($this->newUpdates) {
                $first = $this->safe_value($trades, 0);
                $tradeSymbol = $this->safe_string($first, 'symbol');
                $limit = $trades->getLimit ($tradeSymbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function watch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, and close price, and the volume of a $market
             *
             * @see https://www.bitmex.com/app/wsAPI#Subscriptions
             *
             * @param {string} $symbol unified $symbol of the $market to fetch OHLCV data for
             * @param {string} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $table = 'tradeBin' . $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $messageHash = $table . ':' . $market['id'];
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => array(
                    $messageHash,
                ),
            );
            $ohlcv = Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         "table" => "tradeBin1m",
        //         "action" => "partial",
        //         "keys" => array(),
        //         "types" => array(
        //             "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" => array( $symbol => "instrument" ),
        //         "attributes" => array( timestamp => "sorted", $symbol => "grouped" ),
        //         "filter" => array( $symbol => "XBTUSD" ),
        //         "data" => array(
        //             {
        //                 "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" => array(
        //             {
        //                 "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 = $this->safe_string($message, 'table');
        $interval = str_replace('tradeBin', '', $table);
        $timeframe = $this->find_timeframe($interval);
        $duration = $this->parse_timeframe($timeframe);
        $candles = $this->safe_value($message, 'data', array());
        $results = array();
        for ($i = 0; $i < count($candles); $i++) {
            $candle = $candles[$i];
            $marketId = $this->safe_string($candle, 'symbol');
            $market = $this->safe_market($marketId);
            $symbol = $market['symbol'];
            $messageHash = $table . ':' . $market['id'];
            $result = array(
                $this->parse8601($this->safe_string($candle, 'timestamp')) - $duration * 1000,
                null, // set open price to null, see => https://github.com/ccxt/ccxt/pull/21356#issuecomment-1969565862
                $this->safe_float($candle, 'high'),
                $this->safe_float($candle, 'low'),
                $this->safe_float($candle, 'close'),
                $this->safe_float($candle, 'volume'),
            );
            $this->ohlcvs[$symbol] = $this->safe_value($this->ohlcvs, $symbol, array());
            $stored = $this->safe_value($this->ohlcvs[$symbol], $timeframe);
            if ($stored === null) {
                $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
                $stored = new ArrayCacheByTimestamp ($limit);
                $this->ohlcvs[$symbol][$timeframe] = $stored;
            }
            $stored->append ($result);
            $results[$messageHash] = $stored;
        }
        $messageHashes = is_array($results) ? array_keys($results) : array();
        for ($i = 0; $i < count($messageHashes); $i++) {
            $messageHash = $messageHashes[$i];
            $client->resolve ($results[$messageHash], $messageHash);
        }
    }

    public function watch_heartbeat($params = array ()) {
        return Async\async(function () use ($params) {
            Async\await($this->load_markets());
            $event = 'heartbeat';
            $url = $this->urls['api']['ws'];
            return Async\await($this->watch($url, $event));
        }) ();
    }

    public function handle_order_book(Client $client, $message) {
        //
        // first snapshot
        //
        //     {
        //         "table" => "orderBookL2",
        //         "action" => "partial",
        //         "keys" => array( 'symbol', "id", "side" ),
        //         "types" => array(
        //             "symbol" => "symbol",
        //             "id" => "long",
        //             "side" => "symbol",
        //             "size" => "long",
        //             "price" => "float"
        //         ),
        //         "foreignKeys" => array( $symbol => "instrument", $side => "side" ),
        //         "attributes" => array( $symbol => "parted", $id => "sorted" ),
        //         "filter" => array( $symbol => "XBTUSD" ),
        //         "data" => array(
        //             array( $symbol => "XBTUSD", $id => 8700000100, $side => "Sell", $size => 1, $price => 999999 ),
        //             array( $symbol => "XBTUSD", $id => 8700000200, $side => "Sell", $size => 3, $price => 999998 ),
        //             array( $symbol => "XBTUSD", $id => 8716991250, $side => "Sell", $size => 26, $price => 830087.5 ),
        //             array( $symbol => "XBTUSD", $id => 8728701950, $side => "Sell", $size => 1720, $price => 712980.5 ),
        //         )
        //     }
        //
        // subsequent updates
        //
        //     {
        //         "table" => "orderBookL2",
        //         "action" => "update",
        //         "data" => array(
        //             {
        //               "table" => "orderBookL2",
        //               "action" => "insert",
        //               "data" => array(
        //                 {
        //                   "symbol" => "ETH_USDT",
        //                   "id" => 85499965912,
        //                   "side" => "Buy",
        //                   "size" => 83000000,
        //                   "price" => 1704.4,
        //                   "timestamp" => "2023-03-26T22:29:00.299Z"
        //                 }
        //               )
        //             }
        //             ...
        //         )
        //     }
        //
        $action = $this->safe_string($message, 'action');
        $table = $this->safe_string($message, 'table');
        if ($table === null) {
            return; // protecting from weird updates
        }
        $data = $this->safe_value($message, 'data', array());
        // if it's an initial snapshot
        if ($action === 'partial') {
            $filter = $this->safe_dict($message, 'filter', array());
            $marketId = $this->safe_value($filter, 'symbol');
            if ($marketId === null) {
                return; // protecting from weird update
            }
            $market = $this->safe_market($marketId);
            $symbol = $market['symbol'];
            if ($table === 'orderBookL2') {
                $this->orderbooks[$symbol] = $this->indexed_order_book();
            } elseif ($table === 'orderBookL2_25') {
                $this->orderbooks[$symbol] = $this->indexed_order_book(array(), 25);
            } elseif ($table === 'orderBook10') {
                $this->orderbooks[$symbol] = $this->indexed_order_book(array(), 10);
            }
            $orderbook = $this->orderbooks[$symbol];
            $orderbook['symbol'] = $symbol;
            for ($i = 0; $i < count($data); $i++) {
                $price = $this->safe_float($data[$i], 'price');
                $size = $this->convertFromRawQuantity ($symbol, $this->safe_string($data[$i], 'size'));
                $id = $this->safe_string($data[$i], 'id');
                $side = $this->safe_string($data[$i], 'side');
                $side = ($side === 'Buy') ? 'bids' : 'asks';
                $bookside = $orderbook[$side];
                $bookside->storeArray (array( $price, $size, $id ));
                $datetime = $this->safe_string($data[$i], 'timestamp');
                $orderbook['timestamp'] = $this->parse8601($datetime);
                $orderbook['datetime'] = $datetime;
            }
            $messageHash = $table . ':' . $symbol;
            $client->resolve ($orderbook, $messageHash);
        } else {
            $numUpdatesByMarketId = array();
            for ($i = 0; $i < count($data); $i++) {
                $marketId = $this->safe_value($data[$i], 'symbol');
                if ($marketId === null) {
                    return; // protecting from weird update
                }
                if (!(is_array($numUpdatesByMarketId) && array_key_exists($marketId, $numUpdatesByMarketId))) {
                    $numUpdatesByMarketId[$marketId] = 0;
                }
                $numUpdatesByMarketId[$marketId] = $this->sum($numUpdatesByMarketId, 1);
                $market = $this->safe_market($marketId);
                $symbol = $market['symbol'];
                $orderbook = $this->orderbooks[$symbol];
                $price = $this->safe_number($data[$i], 'price');
                $size = ($action === 'delete') ? 0 : $this->convertFromRawQuantity ($symbol, $this->safe_string($data[$i], 'size', '0'));
                $id = $this->safe_string($data[$i], 'id');
                $side = $this->safe_string($data[$i], 'side');
                $side = ($side === 'Buy') ? 'bids' : 'asks';
                $bookside = $orderbook[$side];
                $bookside->storeArray (array( $price, $size, $id ));
                $datetime = $this->safe_string($data[$i], 'timestamp');
                $orderbook['timestamp'] = $this->parse8601($datetime);
                $orderbook['datetime'] = $datetime;
            }
            $marketIds = is_array($numUpdatesByMarketId) ? array_keys($numUpdatesByMarketId) : array();
            for ($i = 0; $i < count($marketIds); $i++) {
                $marketId = $marketIds[$i];
                $market = $this->safe_market($marketId);
                $symbol = $market['symbol'];
                $messageHash = $table . ':' . $symbol;
                $orderbook = $this->orderbooks[$symbol];
                $client->resolve ($orderbook, $messageHash);
            }
        }
    }

    public function handle_system_status(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" => array( remaining => 39 )
        //     }
        //
        return $message;
    }

    public function handle_subscription_status(Client $client, $message) {
        //
        //     {
        //         "success" => true,
        //         "subscribe" => "orderBookL2:XBTUSD",
        //         "request" => array( op => "subscribe", args => array( "orderBookL2:XBTUSD" ) )
        //     }
        //
        return $message;
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        // generic $error format
        //
        //     array( "error" => errorMessage )
        //
        // examples
        //
        //     {
        //         "status" => 429,
        //         "error" => "Rate limit exceeded, retry in 1 seconds.",
        //         "meta" => array( "retryAfter" => 1 ),
        //         "request" => array( "op" => "subscribe", "args" => "orderBook" ),
        //     }
        //
        //     array( "error" => "Rate limit exceeded, retry in 29 seconds." )
        //
        $error = $this->safe_string($message, 'error');
        if ($error !== null) {
            $request = $this->safe_value($message, 'request', array());
            $args = $this->safe_value($request, 'args', array());
            $numArgs = count($args);
            if ($numArgs > 0) {
                $messageHash = $args[0];
                $broad = $this->exceptions['ws']['broad'];
                $broadKey = $this->find_broadly_matched_key($broad, $error);
                $exception = null;
                if ($broadKey === null) {
                    $exception = new ExchangeError ($error); // c# requirement for now
                } else {
                    $exception = new $broad[$broadKey] ($error);
                }
                $client->reject ($exception, $messageHash);
                return false;
            }
        }
        return true;
    }

    public function handle_message(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" => array( remaining => 38 )
        //     }
        //
        //     {
        //         "success" => true,
        //         "subscribe" => "orderBookL2:XBTUSD",
        //         "request" => array( $op => "subscribe", args => array( "orderBookL2:XBTUSD" ) )
        //     }
        //
        //     {
        //         "table" => "orderBookL2",
        //         "action" => "update",
        //         "data" => array(
        //             array( symbol => "XBTUSD", id => 8799284800, side => "Sell", size => 721000 ),
        //             array( symbol => "XBTUSD", id => 8799285100, side => "Sell", size => 70590 ),
        //             array( symbol => "XBTUSD", id => 8799285550, side => "Sell", size => 217652 ),
        //             array( symbol => "XBTUSD", id => 8799285850, side => "Sell", size => 105578 ),
        //             array( symbol => "XBTUSD", id => 8799286350, side => "Sell", size => 172093 ),
        //             array( symbol => "XBTUSD", id => 8799286650, side => "Sell", size => 201125 ),
        //             array( symbol => "XBTUSD", id => 8799288950, side => "Buy", size => 47552 ),
        //             array( symbol => "XBTUSD", id => 8799289250, side => "Buy", size => 78217 ),
        //             array( symbol => "XBTUSD", id => 8799289700, side => "Buy", size => 193677 ),
        //             array( symbol => "XBTUSD", id => 8799290000, side => "Buy", size => 818161 ),
        //             array( symbol => "XBTUSD", id => 8799290500, side => "Buy", size => 218806 ),
        //             array( symbol => "XBTUSD", id => 8799290800, side => "Buy", size => 102946 )
        //         )
        //     }
        //
        if ($this->handle_error_message($client, $message)) {
            $table = $this->safe_string($message, 'table');
            $methods = array(
                'orderBookL2' => array($this, 'handle_order_book'),
                'orderBookL2_25' => array($this, 'handle_order_book'),
                'orderBook10' => array($this, 'handle_order_book'),
                'instrument' => array($this, 'handle_ticker'),
                'trade' => array($this, 'handle_trades'),
                'tradeBin1m' => array($this, 'handle_ohlcv'),
                'tradeBin5m' => array($this, 'handle_ohlcv'),
                'tradeBin1h' => array($this, 'handle_ohlcv'),
                'tradeBin1d' => array($this, 'handle_ohlcv'),
                'order' => array($this, 'handle_orders'),
                'execution' => array($this, 'handle_my_trades'),
                'margin' => array($this, 'handle_balance'),
                'liquidation' => array($this, 'handle_liquidation'),
                'position' => array($this, 'handle_positions'),
            );
            $method = $this->safe_value($methods, $table);
            if ($method === null) {
                $request = $this->safe_value($message, 'request', array());
                $op = $this->safe_value($request, 'op');
                if ($op === 'authKeyExpires') {
                    $this->handle_authentication_message($client, $message);
                }
            } else {
                $method($client, $message);
            }
        }
    }
}
