<?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 ccxt\ArgumentsRequired;
use ccxt\BadRequest;
use ccxt\NetworkError;
use ccxt\InvalidNonce;
use ccxt\ChecksumError;
use \React\Async;
use \React\Promise\PromiseInterface;

class htx extends \ccxt\async\htx {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'createOrderWs' => false,
                'editOrderWs' => false,
                'fetchOpenOrdersWs' => false,
                'fetchOrderWs' => false,
                'cancelOrderWs' => false,
                'cancelOrdersWs' => false,
                'cancelAllOrdersWs' => false,
                'fetchTradesWs' => false,
                'fetchBalanceWs' => false,
                'watchOrderBook' => true,
                'watchOrders' => true,
                'watchTickers' => false,
                'watchTicker' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => false,
                'watchMyTrades' => true,
                'watchBalance' => true,
                'watchOHLCV' => true,
                'unwatchTicker' => true,
                'unwatchOHLCV' => true,
                'unwatchTrades' => true,
                'unwatchOrderBook' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => array(
                        'api' => array(
                            'spot' => array(
                                'public' => 'wss://{hostname}/ws',
                                'private' => 'wss://{hostname}/ws/v2',
                                'feed' => 'wss://{hostname}/feed',
                            ),
                            'future' => array(
                                'linear' => array(
                                    'public' => 'wss://api.hbdm.com/linear-swap-ws',
                                    'private' => 'wss://api.hbdm.com/linear-swap-notification',
                                ),
                                'inverse' => array(
                                    'public' => 'wss://api.hbdm.com/ws',
                                    'private' => 'wss://api.hbdm.com/notification',
                                ),
                            ),
                            'swap' => array(
                                'inverse' => array(
                                    'public' => 'wss://api.hbdm.com/swap-ws',
                                    'private' => 'wss://api.hbdm.com/swap-notification',
                                ),
                                'linear' => array(
                                    'public' => 'wss://api.hbdm.com/linear-swap-ws',
                                    'private' => 'wss://api.hbdm.com/linear-swap-notification',
                                ),
                            ),
                        ),
                        // these settings work faster for clients hosted on AWS
                        'api-aws' => array(
                            'spot' => array(
                                'public' => 'wss://api-aws.huobi.pro/ws',
                                'private' => 'wss://api-aws.huobi.pro/ws/v2',
                                'feed' => 'wss://{hostname}/feed',
                            ),
                            'future' => array(
                                'linear' => array(
                                    'public' => 'wss://api.hbdm.vn/linear-swap-ws',
                                    'private' => 'wss://api.hbdm.vn/linear-swap-notification',
                                ),
                                'inverse' => array(
                                    'public' => 'wss://api.hbdm.vn/ws',
                                    'private' => 'wss://api.hbdm.vn/notification',
                                ),
                            ),
                            'swap' => array(
                                'linear' => array(
                                    'public' => 'wss://api.hbdm.vn/linear-swap-ws',
                                    'private' => 'wss://api.hbdm.vn/linear-swap-notification',
                                ),
                                'inverse' => array(
                                    'public' => 'wss://api.hbdm.vn/swap-ws',
                                    'private' => 'wss://api.hbdm.vn/swap-notification',
                                ),
                            ),
                        ),
                    ),
                ),
            ),
            'options' => array(
                'tradesLimit' => 1000,
                'OHLCVLimit' => 1000,
                'api' => 'api', // or api-aws for clients hosted on AWS
                'watchOrderBook' => array(
                    'maxRetries' => 3,
                    'checksum' => true,
                    'depth' => 150, // 150 or 20
                ),
                'ws' => array(
                    'gunzip' => true,
                ),
                'watchTicker' => array(
                    'name' => 'market.{marketId}.detail', // 'market.{marketId}.bbo' or 'market.{marketId}.ticker'
                ),
            ),
            'exceptions' => array(
                'ws' => array(
                    'exact' => array(
                        'bad-request' => '\\ccxt\\BadRequest', // array(  ts => 1586323747018,  status => 'error',    'err-code' => 'bad-request',  err-msg' => 'invalid mbp.150.symbol linkusdt', id => '2')
                        '2002' => '\\ccxt\\AuthenticationError', // array( action => 'sub', code => 2002, ch => 'accounts.update#2', message => 'invalid.auth.state' )
                        '2021' => '\\ccxt\\BadRequest',
                        '2001' => '\\ccxt\\BadSymbol', // array( action => 'sub', code => 2001, ch => 'orders#2ltcusdt', message => 'invalid.symbol')
                        '2011' => '\\ccxt\\BadSymbol', // array( op => 'sub', cid => '1649149285', topic => 'orders_cross.ltc-usdt', 'err-code' => 2011, 'err-msg' => "Contract doesn't exist.", ts => 1649149287637 )
                        '2040' => '\\ccxt\\BadRequest', // array( op => 'sub', cid => '1649152947', 'err-code' => 2040, 'err-msg' => 'Missing required parameter.', ts => 1649152948684 )
                        '4007' => '\\ccxt\\BadRequest', // array( op => 'sub', cid => '1', topic => 'accounts_unify.USDT', 'err-code' => 4007, 'err-msg' => 'Non - single account user is not available, please check through the cross and isolated account asset interface', ts => 1698419318540 )
                    ),
                ),
            ),
        ));
    }

    public function request_id() {
        $requestId = $this->sum($this->safe_integer($this->options, 'requestId', 0), 1);
        $this->options['requestId'] = $requestId;
        return (string) $requestId;
    }

    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.htx.com/en-us/opend/newApiPages/?id=7ec53561-7773-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c33ab2-77ae-11ed-9966-0242ac110003
             *
             * @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());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $options = $this->safe_dict($this->options, 'watchTicker', array());
            $topic = $this->safe_string($options, 'name', 'market.{marketId}.detail');
            if ($topic === 'market.{marketId}.ticker' && $market['type'] !== 'spot') {
                throw new BadRequest($this->id . ' watchTicker() with name $market->{marketId}.ticker is only allowed for spot markets, use $market->{marketId}.detail instead');
            }
            $messageHash = $this->implode_params($topic, array( 'marketId' => $market['id'] ));
            $url = $this->get_url_by_market_type($market['type'], $market['linear']);
            return Async\await($this->subscribe_public($url, $symbol, $messageHash, null, $params));
        }) ();
    }

    public function un_watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unWatches 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.htx.com/en-us/opend/newApiPages/?id=7ec53561-7773-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c33ab2-77ae-11ed-9966-0242ac110003
             *
             * @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());
            $market = $this->market($symbol);
            $topic = 'ticker';
            $options = $this->safe_dict($this->options, 'watchTicker', array());
            $channel = $this->safe_string($options, 'name', 'market.{marketId}.detail');
            if ($channel === 'market.{marketId}.ticker' && $market['type'] !== 'spot') {
                throw new BadRequest($this->id . ' watchTicker() with name $market->{marketId}.ticker is only allowed for spot markets, use $market->{marketId}.detail instead');
            }
            $subMessageHash = $this->implode_params($channel, array( 'marketId' => $market['id'] ));
            return Async\await($this->unsubscribe_public($market, $subMessageHash, $topic, $params));
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        // "market.btcusdt.detail"
        //     {
        //         "ch" => "market.btcusdt.detail",
        //         "ts" => 1583494163784,
        //         "tick" => {
        //             "id" => 209988464418,
        //             "low" => 8988,
        //             "high" => 9155.41,
        //             "open" => 9078.91,
        //             "close" => 9136.46,
        //             "vol" => 237813910.5928412,
        //             "amount" => 26184.202558551195,
        //             "version" => 209988464418,
        //             "count" => 265673
        //         }
        //     }
        // "market.btcusdt.bbo"
        //     {
        //         "ch" => "market.btcusdt.bbo",
        //         "ts" => 1671941599613,
        //         "tick" => {
        //             "seqId" => 161499562790,
        //             "ask" => 16829.51,
        //             "askSize" => 0.707776,
        //             "bid" => 16829.5,
        //             "bidSize" => 1.685945,
        //             "quoteTime" => 1671941599612,
        //             "symbol" => "btcusdt"
        //         }
        //     }
        //
        $tick = $this->safe_value($message, 'tick', array());
        $ch = $this->safe_string($message, 'ch');
        $parts = explode('.', $ch);
        $marketId = $this->safe_string($parts, 1);
        $market = $this->safe_market($marketId);
        $ticker = $this->parse_ticker($tick, $market);
        $timestamp = $this->safe_value($message, 'ts');
        $ticker['timestamp'] = $timestamp;
        $ticker['datetime'] = $this->iso8601($timestamp);
        $symbol = $ticker['symbol'];
        $this->tickers[$symbol] = $ticker;
        $client->resolve ($ticker, $ch);
        return $message;
    }

    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.htx.com/en-us/opend/newApiPages/?id=7ec53b69-7773-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c33c21-77ae-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c33cfe-77ae-11ed-9966-0242ac110003
             *
             * @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~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $messageHash = 'market.' . $market['id'] . '.trade.detail';
            $url = $this->get_url_by_market_type($market['type'], $market['linear']);
            $trades = Async\await($this->subscribe_public($url, $symbol, $messageHash, null, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function un_watch_trades(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unWatches 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.htx.com/en-us/opend/newApiPages/?id=7ec53b69-7773-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c33c21-77ae-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c33cfe-77ae-11ed-9966-0242ac110003
             *
             * @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());
            $market = $this->market($symbol);
            $topic = 'trades';
            $options = $this->safe_dict($this->options, 'watchTrades', array());
            $channel = $this->safe_string($options, 'name', 'market.{marketId}.trade.detail');
            $subMessageHash = $this->implode_params($channel, array( 'marketId' => $market['id'] ));
            return Async\await($this->unsubscribe_public($market, $subMessageHash, $topic, $params));
        }) ();
    }

    public function handle_trades(Client $client, $message) {
        //
        //     {
        //         "ch" => "market.btcusdt.trade.detail",
        //         "ts" => 1583495834011,
        //         "tick" => {
        //             "id" => 105004645372,
        //             "ts" => 1583495833751,
        //             "data" => array(
        //                 {
        //                     "id" => 1.050046453727319e+22,
        //                     "ts" => 1583495833751,
        //                     "tradeId" => 102090727790,
        //                     "amount" => 0.003893,
        //                     "price" => 9150.01,
        //                     "direction" => "sell"
        //                 }
        //             )
        //         }
        //     }
        //
        $tick = $this->safe_value($message, 'tick', array());
        $data = $this->safe_value($tick, 'data', array());
        $ch = $this->safe_string($message, 'ch');
        $parts = explode('.', $ch);
        $marketId = $this->safe_string($parts, 1);
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $tradesCache = $this->safe_value($this->trades, $symbol);
        if ($tradesCache === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $tradesCache = new ArrayCache ($limit);
            $this->trades[$symbol] = $tradesCache;
        }
        for ($i = 0; $i < count($data); $i++) {
            $trade = $this->parse_trade($data[$i], $market);
            $tradesCache->append ($trade);
        }
        $client->resolve ($tradesCache, $ch);
        return $message;
    }

    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.htx.com/en-us/opend/newApiPages/?id=7ec53241-7773-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c3346a-77ae-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c33563-77ae-11ed-9966-0242ac110003
             *
             * @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'];
            $interval = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $messageHash = 'market.' . $market['id'] . '.kline.' . $interval;
            $url = $this->get_url_by_market_type($market['type'], $market['linear']);
            $ohlcv = Async\await($this->subscribe_public($url, $symbol, $messageHash, null, $params));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function un_watch_ohlcv(string $symbol, $timeframe = '1m', $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $params) {
            /**
             * unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a $market
             *
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=7ec53241-7773-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c3346a-77ae-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c33563-77ae-11ed-9966-0242ac110003
             *
             * @param {string} $symbol unified $symbol of the $market
             * @param {string} $timeframe the length of time each candle represents
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {array} [$params->timezone] if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00'
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $interval = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $subMessageHash = 'market.' . $market['id'] . '.kline.' . $interval;
            $topic = 'ohlcv';
            $params['symbolsAndTimeframes'] = [ [ $market['symbol'], $timeframe ] ];
            return Async\await($this->unsubscribe_public($market, $subMessageHash, $topic, $params));
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         "ch" => "market.btcusdt.kline.1min",
        //         "ts" => 1583501786794,
        //         "tick" => {
        //             "id" => 1583501760,
        //             "open" => 9094.5,
        //             "close" => 9094.51,
        //             "low" => 9094.5,
        //             "high" => 9094.51,
        //             "amount" => 0.44639786263800907,
        //             "vol" => 4059.76919054,
        //             "count" => 16
        //         }
        //     }
        //
        $ch = $this->safe_string($message, 'ch');
        $parts = explode('.', $ch);
        $marketId = $this->safe_string($parts, 1);
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $interval = $this->safe_string($parts, 3);
        $timeframe = $this->find_timeframe($interval);
        $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;
        }
        $tick = $this->safe_value($message, 'tick');
        $parsed = $this->parse_ohlcv($tick, $market);
        $stored->append ($parsed);
        $client->resolve ($stored, $ch);
    }

    public function watch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             *
             * @see https://huobiapi.github.io/docs/dm/v1/en/#subscribe-$market-$depth-data
             * @see https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#subscribe-incremental-$market-$depth-data
             * @see https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-incremental-$market-$depth-data
             *
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             * @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
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $allowedLimits = array( 20, 150 );
            // 2) 5-level/20-level incremental MBP is a tick by tick feed,
            // which means whenever there is an order book change at that level, it pushes an update;
            // 150-levels/400-level incremental MBP feed is based on the gap
            // between two snapshots at 100ms interval.
            $options = $this->safe_dict($this->options, 'watchOrderBook', array());
            $depth = $this->safe_integer($options, 'depth', 150);
            if (!$this->in_array($depth, $allowedLimits)) {
                throw new ExchangeError($this->id . ' watchOrderBook $market accepts limits of 20 and 150 only');
            }
            $messageHash = null;
            if ($market['spot']) {
                $messageHash = 'market.' . $market['id'] . '.mbp.' . $this->number_to_string($depth);
            } else {
                $messageHash = 'market.' . $market['id'] . '.depth.size_' . $this->number_to_string($depth) . '.high_freq';
            }
            $url = $this->get_url_by_market_type($market['type'], $market['linear'], false, true);
            $method = array($this, 'handle_order_book_subscription');
            if (!$market['spot']) {
                $params = $this->extend($params);
                $params['data_type'] = 'incremental';
                $method = null;
            }
            $orderbook = Async\await($this->subscribe_public($url, $symbol, $messageHash, $method, $params));
            return $orderbook->limit ();
        }) ();
    }

    public function un_watch_order_book(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * unsubscribe from the orderbook channel
             *
             * @see https://huobiapi.github.io/docs/dm/v1/en/#subscribe-$market-$depth-data
             * @see https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#subscribe-incremental-$market-$depth-data
             * @see https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-incremental-$market-$depth-data
             *
             * @param {string} $symbol unified $symbol of the $market to fetch the order book for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int} [$params->limit] orderbook limit, default is null
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by $market symbols
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $topic = 'orderbook';
            $options = $this->safe_dict($this->options, 'watchOrderBook', array());
            $depth = $this->safe_integer($options, 'depth', 150);
            $subMessageHash = null;
            if ($market['spot']) {
                $subMessageHash = 'market.' . $market['id'] . '.mbp.' . $this->number_to_string($depth);
            } else {
                $subMessageHash = 'market.' . $market['id'] . '.depth.size_' . $this->number_to_string($depth) . '.high_freq';
            }
            if (!($market['spot'])) {
                $params['data_type'] = 'incremental';
            }
            return Async\await($this->unsubscribe_public($market, $subMessageHash, $topic, $params));
        }) ();
    }

    public function handle_order_book_snapshot(Client $client, $message, $subscription) {
        //
        //     {
        //         "id" => 1583473663565,
        //         "rep" => "market.btcusdt.mbp.150",
        //         "status" => "ok",
        //         "ts" => 1698359289261,
        //         "data" => {
        //             "seqNum" => 104999417756,
        //             "bids" => [
        //                 [9058.27, 0],
        //                 [9058.43, 0],
        //                 [9058.99, 0],
        //             ],
        //             "asks" => [
        //                 [9084.27, 0.2],
        //                 [9085.69, 0],
        //                 [9085.81, 0],
        //             ]
        //         }
        //     }
        //
        $symbol = $this->safe_string($subscription, 'symbol');
        $messageHash = $this->safe_string($subscription, 'messageHash');
        $id = $this->safe_string($message, 'id');
        $lastTimestamp = $this->safe_integer($subscription, 'lastTimestamp');
        try {
            $orderbook = $this->orderbooks[$symbol];
            $data = $this->safe_value($message, 'data');
            $messages = $orderbook->cache;
            $firstMessage = $this->safe_value($messages, 0, array());
            $snapshot = $this->parse_order_book($data, $symbol);
            $tick = $this->safe_value($firstMessage, 'tick');
            $sequence = $this->safe_integer($tick, 'prevSeqNum');
            $nonce = $this->safe_integer($data, 'seqNum');
            $snapshot['nonce'] = $nonce;
            $snapshotTimestamp = $this->safe_integer($message, 'ts');
            $subscription['lastTimestamp'] = $snapshotTimestamp;
            $snapshotLimit = $this->safe_integer($subscription, 'limit');
            $snapshotOrderBook = $this->order_book($snapshot, $snapshotLimit);
            $client->resolve ($snapshotOrderBook, $id);
            if (($sequence === null) || ($nonce < $sequence)) {
                $maxAttempts = $this->handle_option('watchOrderBook', 'maxRetries', 3);
                $numAttempts = $this->safe_integer($subscription, 'numAttempts', 0);
                // retry to synchronize if we have not reached $maxAttempts yet
                if ($numAttempts < $maxAttempts) {
                    // safety guard
                    if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                        $numAttempts = $this->sum($numAttempts, 1);
                        $delayTime = $this->sum(1000, $lastTimestamp - $snapshotTimestamp);
                        $subscription['numAttempts'] = $numAttempts;
                        $client->subscriptions[$messageHash] = $subscription;
                        $this->delay($delayTime, array($this, 'watch_order_book_snapshot'), $client, $message, $subscription);
                    }
                } else {
                    // throw upon failing to synchronize in $maxAttempts
                    throw new InvalidNonce($this->id . ' failed to synchronize WebSocket feed with the $snapshot for $symbol ' . $symbol . ' in ' . (string) $maxAttempts . ' attempts');
                }
            } else {
                $orderbook->reset ($snapshot);
                // unroll the accumulated deltas
                for ($i = 0; $i < count($messages); $i++) {
                    $this->handle_order_book_message($client, $messages[$i]);
                }
                $orderbook->cache = array();
                $this->orderbooks[$symbol] = $orderbook;
                $client->resolve ($orderbook, $messageHash);
            }
        } catch (Exception $e) {
            unset($client->subscriptions[$messageHash]);
            unset($this->orderbooks[$symbol]);
            $client->reject ($e, $messageHash);
        }
    }

    public function watch_order_book_snapshot($client, $message, $subscription) {
        return Async\async(function () use ($client, $message, $subscription) {
            $messageHash = $this->safe_string($subscription, 'messageHash');
            $symbol = $this->safe_string($subscription, 'symbol');
            $limit = $this->safe_integer($subscription, 'limit');
            $timestamp = $this->safe_integer($message, 'ts');
            $params = $this->safe_value($subscription, 'params');
            $attempts = $this->safe_integer($subscription, 'numAttempts', 0);
            $market = $this->market($symbol);
            $url = $this->get_url_by_market_type($market['type'], $market['linear'], false, true);
            $requestId = $this->request_id();
            $request = array(
                'req' => $messageHash,
                'id' => $requestId,
            );
            // this is a temporary $subscription by a specific $requestId
            // it has a very short lifetime until the snapshot is received over ws
            $snapshotSubscription = array(
                'id' => $requestId,
                'messageHash' => $messageHash,
                'symbol' => $symbol,
                'limit' => $limit,
                'params' => $params,
                'numAttempts' => $attempts,
                'lastTimestamp' => $timestamp,
                'method' => array($this, 'handle_order_book_snapshot'),
            );
            try {
                $orderbook = Async\await($this->watch($url, $requestId, $request, $requestId, $snapshotSubscription));
                return $orderbook->limit ();
            } catch (Exception $e) {
                unset($client->subscriptions[$messageHash]);
                $client->reject ($e, $messageHash);
            }
            return null;
        }) ();
    }

    public function handle_delta($bookside, $delta) {
        $price = $this->safe_float($delta, 0);
        $amount = $this->safe_float($delta, 1);
        $bookside->store ($price, $amount);
    }

    public function handle_deltas($bookside, $deltas) {
        for ($i = 0; $i < count($deltas); $i++) {
            $this->handle_delta($bookside, $deltas[$i]);
        }
    }

    public function handle_order_book_message(Client $client, $message) {
        // spot markets
        //
        //     {
        //         "ch" => "market.btcusdt.mbp.150",
        //         "ts" => 1583472025885,
        //         "tick" => {
        //             "seqNum" => 104998984994,
        //             "prevSeqNum" => 104998984977,
        //             "bids" => [
        //                 [9058.27, 0],
        //                 [9058.43, 0],
        //                 [9058.99, 0],
        //             ],
        //             "asks" => [
        //                 [9084.27, 0.2],
        //                 [9085.69, 0],
        //                 [9085.81, 0],
        //             ]
        //         }
        //     }
        //
        // non-spot $market update
        //
        //     {
        //         "ch":"market.BTC220218.depth.size_150.high_freq",
        //         "tick":array(
        //             "asks":array(),
        //             "bids":[
        //                 [43445.74,1],
        //                 [43444.48,0 ],
        //                 [40593.92,9]
        //             ],
        //             "ch":"market.BTC220218.depth.size_150.high_freq",
        //             "event":"update",
        //             "id":152727500274,
        //             "mrid":152727500274,
        //             "ts":1645023376098,
        //             "version":37536690
        //         ),
        //         "ts":1645023376098
        //     }
        // non-spot $market $snapshot
        //
        //     {
        //         "ch":"market.BTC220218.depth.size_150.high_freq",
        //         "tick":array(
        //             "asks":[
        //                 [43445.74,1],
        //                 [43444.48,0 ],
        //                 [40593.92,9]
        //             ],
        //             "bids":[
        //                 [43445.74,1],
        //                 [43444.48,0 ],
        //                 [40593.92,9]
        //             ],
        //             "ch":"market.BTC220218.depth.size_150.high_freq",
        //             "event":"snapshot",
        //             "id":152727500274,
        //             "mrid":152727500274,
        //             "ts":1645023376098,
        //             "version":37536690
        //         ),
        //         "ts":1645023376098
        //     }
        //
        $ch = $this->safe_value($message, 'ch');
        $parts = explode('.', $ch);
        $marketId = $this->safe_string($parts, 1);
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $orderbook = $this->orderbooks[$symbol];
        $tick = $this->safe_value($message, 'tick', array());
        $seqNum = $this->safe_integer($tick, 'seqNum');
        $prevSeqNum = $this->safe_integer($tick, 'prevSeqNum');
        $event = $this->safe_string($tick, 'event');
        $version = $this->safe_integer($tick, 'version');
        $timestamp = $this->safe_integer($message, 'ts');
        if ($event === 'snapshot') {
            $snapshot = $this->parse_order_book($tick, $symbol, $timestamp);
            $orderbook->reset ($snapshot);
            $orderbook['nonce'] = $version;
        }
        if (($prevSeqNum !== null) && $prevSeqNum > $orderbook['nonce']) {
            $checksum = $this->handle_option('watchOrderBook', 'checksum', true);
            if ($checksum) {
                throw new ChecksumError($this->id . ' ' . $this->orderbook_checksum_message($symbol));
            }
        }
        $spotConditon = $market['spot'] && ($prevSeqNum === $orderbook['nonce']);
        $nonSpotCondition = $market['contract'] && ($version - 1 === $orderbook['nonce']);
        if ($spotConditon || $nonSpotCondition) {
            $asks = $this->safe_value($tick, 'asks', array());
            $bids = $this->safe_value($tick, 'bids', array());
            $this->handle_deltas($orderbook['asks'], $asks);
            $this->handle_deltas($orderbook['bids'], $bids);
            $orderbook['nonce'] = $spotConditon ? $seqNum : $version;
            $orderbook['timestamp'] = $timestamp;
            $orderbook['datetime'] = $this->iso8601($timestamp);
        }
    }

    public function handle_order_book(Client $client, $message) {
        //
        // deltas
        //
        // spot markets
        //
        //     {
        //         "ch" => "market.btcusdt.mbp.150",
        //         "ts" => 1583472025885,
        //         "tick" => {
        //             "seqNum" => 104998984994,
        //             "prevSeqNum" => 104998984977,
        //             "bids" => [
        //                 [9058.27, 0],
        //                 [9058.43, 0],
        //                 [9058.99, 0],
        //             ],
        //             "asks" => [
        //                 [9084.27, 0.2],
        //                 [9085.69, 0],
        //                 [9085.81, 0],
        //             ]
        //         }
        //     }
        //
        // non spot markets
        //
        //     {
        //         "ch":"market.BTC220218.depth.size_150.high_freq",
        //         "tick":array(
        //             "asks":array(),
        //             "bids":[
        //                 [43445.74,1],
        //                 [43444.48,0 ],
        //                 [40593.92,9]
        //             ],
        //             "ch":"market.BTC220218.depth.size_150.high_freq",
        //             "event":"update",
        //             "id":152727500274,
        //             "mrid":152727500274,
        //             "ts":1645023376098,
        //             "version":37536690
        //         ),
        //         "ts":1645023376098
        //     }
        //
        $messageHash = $this->safe_string($message, 'ch');
        $tick = $this->safe_dict($message, 'tick');
        $event = $this->safe_string($tick, 'event');
        $ch = $this->safe_string($message, 'ch');
        $parts = explode('.', $ch);
        $marketId = $this->safe_string($parts, 1);
        $symbol = $this->safe_symbol($marketId);
        if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
            $size = $this->safe_string($parts, 3);
            $sizeParts = explode('_', $size);
            $limit = $this->safe_integer($sizeParts, 1);
            $this->orderbooks[$symbol] = $this->order_book(array(), $limit);
        }
        $orderbook = $this->orderbooks[$symbol];
        if (($event === null) && ($orderbook['nonce'] === null)) {
            $orderbook->cache[] = $message;
        } else {
            $this->handle_order_book_message($client, $message);
            $client->resolve ($orderbook, $messageHash);
        }
    }

    public function handle_order_book_subscription(Client $client, $message, $subscription) {
        $symbol = $this->safe_string($subscription, 'symbol');
        $market = $this->market($symbol);
        $limit = $this->safe_integer($subscription, 'limit');
        $this->orderbooks[$symbol] = $this->order_book(array(), $limit);
        if ($market['spot']) {
            $this->spawn(array($this, 'watch_order_book_snapshot'), $client, $message, $subscription);
        }
    }

    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.htx.com/en-us/opend/newApiPages/?id=7ec53dd5-7773-11ed-9966-0242ac110003
             *
             * @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~
             */
            $this->check_required_credentials();
            Async\await($this->load_markets());
            $type = null;
            $marketId = '*'; // wildcard
            $market = null;
            $messageHash = null;
            $channel = null;
            $trades = null;
            $subType = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
                $type = $market['type'];
                $subType = $market['linear'] ? 'linear' : 'inverse';
                $marketId = $market['lowercaseId'];
            } else {
                $type = $this->safe_string($this->options, 'defaultType', 'spot');
                $type = $this->safe_string($params, 'type', $type);
                $subType = $this->safe_string_2($this->options, 'subType', 'defaultSubType', 'linear');
                $subType = $this->safe_string($params, 'subType', $subType);
                $params = $this->omit($params, array( 'type', 'subType' ));
            }
            if ($type === 'spot') {
                $mode = null;
                if ($mode === null) {
                    $mode = $this->safe_string_2($this->options, 'watchMyTrades', 'mode', '0');
                    $mode = $this->safe_string($params, 'mode', $mode);
                    $params = $this->omit($params, 'mode');
                }
                $messageHash = 'trade.clearing' . '#' . $marketId . '#' . $mode;
                $channel = $messageHash;
            } else {
                $channelAndMessageHash = $this->get_order_channel_and_message_hash($type, $subType, $market, $params);
                $channel = $this->safe_string($channelAndMessageHash, 0);
                $orderMessageHash = $this->safe_string($channelAndMessageHash, 1);
                // we will take advantage of the order $messageHash because already handles stuff
                // like symbol/margin/subtype/type variations
                $messageHash = $orderMessageHash . ':' . 'trade';
            }
            $trades = Async\await($this->subscribe_private($channel, $messageHash, $type, $subType, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($trades, $symbol, $since, $limit, true);
        }) ();
    }

    public function get_order_channel_and_message_hash($type, $subType, $market = null, $params = array ()) {
        $messageHash = null;
        $channel = null;
        $orderType = $this->safe_string($this->options, 'orderType', 'orders'); // orders or matchOrders
        $orderType = $this->safe_string($params, 'orderType', $orderType);
        $params = $this->omit($params, 'orderType');
        $marketCode = ($market !== null) ? strtolower($market['lowercaseId']) : null;
        $baseId = ($market !== null) ? $market['baseId'] : null;
        $prefix = $orderType;
        $messageHash = $prefix;
        if ($subType === 'linear') {
            // USDT Margined Contracts Example => LTC/USDT:USDT
            $marginMode = $this->safe_string($params, 'margin', 'cross');
            $marginPrefix = ($marginMode === 'cross') ? $prefix . '_cross' : $prefix;
            $messageHash = $marginPrefix;
            if ($marketCode !== null) {
                $messageHash .= '.' . $marketCode;
                $channel = $messageHash;
            } else {
                $channel = $marginPrefix . '.' . '*';
            }
        } elseif ($type === 'future') {
            // inverse futures Example => BCH/USD:BCH-220408
            if ($baseId !== null) {
                $channel = $prefix . '.' . strtolower($baseId);
                $messageHash = $channel;
            } else {
                $channel = $prefix . '.' . '*';
            }
        } else {
            // inverse swaps => Example => BTC/USD:BTC
            if ($marketCode !== null) {
                $channel = $prefix . '.' . $marketCode;
                $messageHash = $channel;
            } else {
                $channel = $prefix . '.' . '*';
            }
        }
        return array( $channel, $messageHash );
    }

    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.htx.com/en-us/opend/newApiPages/?id=7ec53c8f-7773-11ed-9966-0242ac110003
             *
             * @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());
            $type = null;
            $subType = null;
            $market = null;
            $suffix = '*'; // wildcard
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
                $type = $market['type'];
                $suffix = $market['lowercaseId'];
                $subType = $market['linear'] ? 'linear' : 'inverse';
            } else {
                $type = $this->safe_string($this->options, 'defaultType', 'spot');
                $type = $this->safe_string($params, 'type', $type);
                $subType = $this->safe_string_2($this->options, 'subType', 'defaultSubType', 'linear');
                $subType = $this->safe_string($params, 'subType', $subType);
                $params = $this->omit($params, array( 'type', 'subType' ));
            }
            $messageHash = null;
            $channel = null;
            if ($type === 'spot') {
                $messageHash = 'orders' . '#' . $suffix;
                $channel = $messageHash;
            } else {
                $channelAndMessageHash = $this->get_order_channel_and_message_hash($type, $subType, $market, $params);
                $channel = $this->safe_string($channelAndMessageHash, 0);
                $messageHash = $this->safe_string($channelAndMessageHash, 1);
            }
            $orders = Async\await($this->subscribe_private($channel, $messageHash, $type, $subType, $params));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($orders, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function handle_order(Client $client, $message) {
        //
        // spot
        //
        //     {
        //         "action":"push",
        //         "ch":"orders#btcusdt", // or "orders#*" for global subscriptions
        //         "data" => {
        //             "orderSource" => "spot-web",
        //             "orderCreateTime" => 1645116048355,
        //             "accountId" => 44234548,
        //             "orderPrice" => "100",
        //             "orderSize" => "0.05",
        //             "symbol" => "ethusdt",
        //             "type" => "buy-$limit",
        //             "orderId" => "478861479986886",
        //             "eventType" => "creation",
        //             "clientOrderId" => '',
        //             "orderStatus" => "submitted"
        //         }
        //     }
        //
        // spot wrapped trade
        //
        //     {
        //         "action" => "push",
        //         "ch" => "orders#ltcusdt",
        //         "data" => {
        //             "tradePrice" => "130.01",
        //             "tradeVolume" => "0.0385",
        //             "tradeTime" => 1648714741525,
        //             "aggressor" => true,
        //             "execAmt" => "0.0385",
        //             "orderSource" => "spot-web",
        //             "orderSize" => "0.0385",
        //             "remainAmt" => "0",
        //             "tradeId" => 101541578884,
        //             "symbol" => "ltcusdt",
        //             "type" => "sell-$market",
        //             "eventType" => "trade",
        //             "clientOrderId" => '',
        //             "orderStatus" => "filled",
        //             "orderId" => 509835753860328
        //         }
        //     }
        //
        // non spot $order
        //
        // {
        //     "contract_type" => "swap",
        //     "pair" => "LTC-USDT",
        //     "business_type" => "swap",
        //     "op" => "notify",
        //     "topic" => "orders_cross.ltc-usdt",
        //     "ts" => 1650354508696,
        //     "symbol" => "LTC",
        //     "contract_code" => "LTC-USDT",
        //     "volume" => 1,
        //     "price" => 110.34,
        //     "order_price_type" => "lightning",
        //     "direction" => "sell",
        //     "offset" => "close",
        //     "status" => 6,
        //     "lever_rate" => 1,
        //     "order_id" => "966002354015051776",
        //     "order_id_str" => "966002354015051776",
        //     "client_order_id" => null,
        //     "order_source" => "web",
        //     "order_type" => 1,
        //     "created_at" => 1650354508649,
        //     "trade_volume" => 1,
        //     "trade_turnover" => 11.072,
        //     "fee" => -0.005536,
        //     "trade_avg_price" => 110.72,
        //     "margin_frozen" => 0,
        //     "profit" => -0.045,
        //     "trade" => array(
        //       {
        //         "trade_fee" => -0.005536,
        //         "fee_asset" => "USDT",
        //         "real_profit" => 0.473,
        //         "profit" => -0.045,
        //         "trade_id" => 86678766507,
        //         "id" => "86678766507-966002354015051776-1",
        //         "trade_volume" => 1,
        //         "trade_price" => 110.72,
        //         "trade_turnover" => 11.072,
        //         "created_at" => 1650354508656,
        //         "role" => "taker"
        //       }
        //     ),
        //     "canceled_at" => 0,
        //     "fee_asset" => "USDT",
        //     "margin_asset" => "USDT",
        //     "uid" => "359305390",
        //     "liquidation_type" => "0",
        //     "margin_mode" => "cross",
        //     "margin_account" => "USDT",
        //     "is_tpsl" => 0,
        //     "real_profit" => 0.473,
        //     "trade_partition" => "USDT",
        //     "reduce_only" => 1
        //   }
        //
        //
        $messageHash = $this->safe_string_2($message, 'ch', 'topic');
        $data = $this->safe_value($message, 'data');
        $marketId = $this->safe_string($message, 'contract_code');
        if ($marketId === null) {
            $marketId = $this->safe_string($data, 'symbol');
        }
        $market = $this->safe_market($marketId);
        $parsedOrder = null;
        if ($data !== null) {
            // spot updates
            $eventType = $this->safe_string($data, 'eventType');
            if ($eventType === 'trade') {
                // when a spot $order is $filled we get an update $message
                // with the trade info
                $parsedTrade = $this->parse_order_trade($data, $market);
                // inject trade in existing $order by faking an $order object
                $orderId = $this->safe_string($parsedTrade, 'order');
                $trades = array( $parsedTrade );
                $status = $this->parse_order_status($this->safe_string_2($data, 'orderStatus', 'status', 'closed'));
                $filled = $this->safe_string($data, 'execAmt');
                $remaining = $this->safe_string($data, 'remainAmt');
                $order = array(
                    'id' => $orderId,
                    'trades' => $trades,
                    'status' => $status,
                    'symbol' => $market['symbol'],
                    'filled' => $this->parse_number($filled),
                    'remaining' => $this->parse_number($remaining),
                );
                $parsedOrder = $order;
            } else {
                $parsedOrder = $this->parse_ws_order($data, $market);
            }
        } else {
            // contract branch
            $parsedOrder = $this->parse_ws_order($message, $market);
            $rawTrades = $this->safe_value($message, 'trade', array());
            $tradesLength = count($rawTrades);
            if ($tradesLength > 0) {
                $tradesObject = array(
                    'trades' => $rawTrades,
                    'ch' => $messageHash,
                    'symbol' => $marketId,
                );
                // inject $order params in every trade
                $extendTradeParams = array(
                    'order' => $this->safe_string($parsedOrder, 'id'),
                    'type' => $this->safe_string($parsedOrder, 'type'),
                    'side' => $this->safe_string($parsedOrder, 'side'),
                );
                // $trades arrive inside an $order update
                // we're forwarding them to handleMyTrade
                // so they can be properly resolved
                $this->handle_my_trade($client, $tradesObject, $extendTradeParams);
            }
        }
        if ($this->orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $cachedOrders = $this->orders;
        $cachedOrders->append ($parsedOrder);
        $client->resolve ($this->orders, $messageHash);
        // when we make a global subscription (for contracts only) our $message hash can't have a symbol/currency attached
        // so we're removing it here
        $genericMessageHash = str_replace('.' . $market['lowercaseId'], '', $messageHash);
        $lowerCaseBaseId = $this->safe_string_lower($market, 'baseId');
        $genericMessageHash = str_replace('.' . $lowerCaseBaseId, '', $genericMessageHash);
        $client->resolve ($this->orders, $genericMessageHash);
    }

    public function parse_ws_order($order, $market = null) {
        //
        // spot
        //
        //     {
        //         "orderSource" => "spot-web",
        //         "orderCreateTime" => 1645116048355, // creating only
        //         "accountId" => 44234548,
        //         "orderPrice" => "100",
        //         "orderSize" => "0.05",
        //         "orderValue" => "3.71676361", // $market-buy only
        //         "symbol" => "ethusdt",
        //         "type" => "buy-limit",
        //         "orderId" => "478861479986886",
        //         "eventType" => "creation",
        //         "clientOrderId" => '',
        //         "orderStatus" => "submitted"
        //         "lastActTime":1645118621810 // except creating
        //         "execAmt":"0"
        //     }
        //
        // swap $order
        //
        //     {
        //         "contract_type" => "swap",
        //         "pair" => "LTC-USDT",
        //         "business_type" => "swap",
        //         "op" => "notify",
        //         "topic" => "orders_cross.ltc-usdt",
        //         "ts" => 1648717911384,
        //         "symbol" => "LTC",
        //         "contract_code" => "LTC-USDT",
        //         "volume" => 1,
        //         "price" => 129.13,
        //         "order_price_type" => "lightning",
        //         "direction" => "sell",
        //         "offset" => "close",
        //         "status" => 6,
        //         "lever_rate" => 5,
        //         "order_id" => "959137967397068800",
        //         "order_id_str" => "959137967397068800",
        //         "client_order_id" => null,
        //         "order_source" => "web",
        //         "order_type" => 1,
        //         "created_at" => 1648717911344,
        //         "trade_volume" => 1,
        //         "trade_turnover" => 12.952,
        //         "fee" => -0.006476,
        //         "trade_avg_price" => 129.52,
        //         "margin_frozen" => 0,
        //         "profit" => -0.005,
        //         "trade" => array(
        //             {
        //                 "trade_fee" => -0.006476,
        //                 "fee_asset" => "USDT",
        //                 "real_profit" => -0.005,
        //                 "profit" => -0.005,
        //                 "trade_id" => 83619995370,
        //                 "id" => "83619995370-959137967397068800-1",
        //                 "trade_volume" => 1,
        //                 "trade_price" => 129.52,
        //                 "trade_turnover" => 12.952,
        //                 "created_at" => 1648717911352,
        //                 "role" => "taker"
        //             }
        //         ),
        //         "canceled_at" => 0,
        //         "fee_asset" => "USDT",
        //         "margin_asset" => "USDT",
        //         "uid" => "359305390",
        //         "liquidation_type" => "0",
        //         "margin_mode" => "cross",
        //         "margin_account" => "USDT",
        //         "is_tpsl" => 0,
        //         "real_profit" => -0.005,
        //         "trade_partition" => "USDT",
        //         "reduce_only" => 1
        //     }
        //
        //     {
        //         "op":"notify",
        //         "topic":"orders.ada",
        //         "ts":1604388667226,
        //         "symbol":"ADA",
        //         "contract_type":"quarter",
        //         "contract_code":"ADA201225",
        //         "volume":1,
        //         "price":0.0905,
        //         "order_price_type":"post_only",
        //         "direction":"sell",
        //         "offset":"open",
        //         "status":6,
        //         "lever_rate":20,
        //         "order_id":773207641127878656,
        //         "order_id_str":"773207641127878656",
        //         "client_order_id":null,
        //         "order_source":"web",
        //         "order_type":1,
        //         "created_at":1604388667146,
        //         "trade_volume":1,
        //         "trade_turnover":10,
        //         "fee":-0.022099447513812154,
        //         "trade_avg_price":0.0905,
        //         "margin_frozen":0,
        //         "profit":0,
        //         "trade":array(),
        //         "canceled_at":0,
        //         "fee_asset":"ADA",
        //         "uid":"123456789",
        //         "liquidation_type":"0",
        //         "is_tpsl" => 0,
        //         "real_profit" => 0
        //     }
        //
        $lastTradeTimestamp = $this->safe_integer_2($order, 'lastActTime', 'ts');
        $created = $this->safe_integer($order, 'orderCreateTime');
        $marketId = $this->safe_string_2($order, 'contract_code', 'symbol');
        $market = $this->safe_market($marketId, $market);
        $symbol = $this->safe_symbol($marketId, $market);
        $amount = $this->safe_string_2($order, 'orderSize', 'volume');
        $status = $this->parse_order_status($this->safe_string_2($order, 'orderStatus', 'status'));
        $id = $this->safe_string_2($order, 'orderId', 'order_id');
        $clientOrderId = $this->safe_string_2($order, 'clientOrderId', 'client_order_id');
        $price = $this->safe_string_2($order, 'orderPrice', 'price');
        $filled = $this->safe_string($order, 'execAmt');
        $typeSide = $this->safe_string($order, 'type');
        $feeCost = $this->safe_string($order, 'fee');
        $fee = null;
        if ($feeCost !== null) {
            $feeCurrencyId = $this->safe_string($order, 'fee_asset');
            $fee = array(
                'cost' => $feeCost,
                'currency' => $this->safe_currency_code($feeCurrencyId),
            );
        }
        $avgPrice = $this->safe_string($order, 'trade_avg_price');
        $rawTrades = $this->safe_value($order, 'trade');
        $typeSideParts = array();
        if ($typeSide !== null) {
            $typeSideParts = explode('-', $typeSide);
        }
        $type = $this->safe_string_lower($typeSideParts, 1);
        if ($type === null) {
            $type = $this->safe_string($order, 'order_price_type');
        }
        $side = $this->safe_string_lower($typeSideParts, 0);
        if ($side === null) {
            $side = $this->safe_string($order, 'direction');
        }
        $cost = $this->safe_string($order, 'orderValue');
        return $this->safe_order(array(
            'info' => $order,
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'timestamp' => $created,
            'datetime' => $this->iso8601($created),
            'lastTradeTimestamp' => $lastTradeTimestamp,
            'status' => $status,
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => null,
            'postOnly' => null,
            'side' => $side,
            'price' => $price,
            'amount' => $amount,
            'filled' => $filled,
            'remaining' => null,
            'cost' => $cost,
            'fee' => $fee,
            'average' => $avgPrice,
            'trades' => $rawTrades,
        ), $market);
    }

    public function parse_order_trade($trade, $market = null) {
        // spot private wrapped $trade
        //
        //     {
        //         "tradePrice" => "130.01",
        //         "tradeVolume" => "0.0385",
        //         "tradeTime" => 1648714741525,
        //         "aggressor" => true,
        //         "execAmt" => "0.0385",
        //         "orderSource" => "spot-web",
        //         "orderSize" => "0.0385",
        //         "remainAmt" => "0",
        //         "tradeId" => 101541578884,
        //         "symbol" => "ltcusdt",
        //         "type" => "sell-$market",
        //         "eventType" => "trade",
        //         "clientOrderId" => '',
        //         "orderStatus" => "filled",
        //         "orderId" => 509835753860328
        //     }
        //
        $market = $this->safe_market(null, $market);
        $symbol = $market['symbol'];
        $tradeId = $this->safe_string($trade, 'tradeId');
        $price = $this->safe_string($trade, 'tradePrice');
        $amount = $this->safe_string($trade, 'tradeVolume');
        $order = $this->safe_string($trade, 'orderId');
        $timestamp = $this->safe_integer($trade, 'tradeTime');
        $type = $this->safe_string($trade, 'type');
        $side = null;
        if ($type !== null) {
            $typeParts = explode('-', $type);
            $side = $typeParts[0];
            $type = $typeParts[1];
        }
        $aggressor = $this->safe_value($trade, 'aggressor');
        $takerOrMaker = null;
        if ($aggressor !== null) {
            $takerOrMaker = $aggressor ? 'taker' : 'maker';
        }
        return $this->safe_trade(array(
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'id' => $tradeId,
            'order' => $order,
            'type' => $type,
            'takerOrMaker' => $takerOrMaker,
            'side' => $side,
            'price' => $price,
            'amount' => $amount,
            'cost' => null,
            'fee' => null,
        ), $market);
    }

    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) {
            /**
             *
             * @see https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7de1c-77b5-11ed-9966-0242ac110003
             * @see https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7df0f-77b5-11ed-9966-0242ac110003
             * @see https://www.huobi.com/en-in/opend/newApiPages/?id=28c34a7d-77ae-11ed-9966-0242ac110003
             * @see https://www.huobi.com/en-in/opend/newApiPages/?id=5d5156b5-77b6-11ed-9966-0242ac110003
             *
             * watch all open positions. Note => huobi has one $channel for each $marginMode and $type
             * @param {string[]|null} $symbols list of unified $market $symbols
             * @param $since
             * @param $limit
             * @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());
            $market = null;
            $messageHash = '';
            if (!$this->is_empty($symbols)) {
                $market = $this->get_market_from_symbols($symbols);
                $messageHash = '::' . implode(',', $symbols);
            }
            $type = null;
            $subType = null;
            if ($market !== null) {
                $type = $market['type'];
                $subType = $market['linear'] ? 'linear' : 'inverse';
            } else {
                list($type, $params) = $this->handle_market_type_and_params('watchPositions', $market, $params);
                if ($type === 'spot') {
                    $type = 'future';
                }
                list($subType, $params) = $this->handle_option_and_params($params, 'watchPositions', 'subType', $subType);
            }
            $symbols = $this->market_symbols($symbols);
            $marginMode = null;
            list($marginMode, $params) = $this->handle_margin_mode_and_params('watchPositions', $params, 'cross');
            $isLinear = ($subType === 'linear');
            $url = $this->get_url_by_market_type($type, $isLinear, true);
            $messageHash = $marginMode . ':positions' . $messageHash;
            $channel = ($marginMode === 'cross') ? 'positions_cross.*' : 'positions.*';
            $newPositions = Async\await($this->subscribe_private($channel, $messageHash, $type, $subType, $params));
            if ($this->newUpdates) {
                return $newPositions;
            }
            return $this->filter_by_symbols_since_limit($this->positions[$url][$marginMode], $symbols, $since, $limit, false);
        }) ();
    }

    public function handle_positions($client, $message) {
        //
        //    {
        //        op => 'notify',
        //        $topic => 'positions_cross',
        //        ts => 1696767149650,
        //        event => 'snapshot',
        //        data => array(
        //          array(
        //            contract_type => 'swap',
        //            pair => 'BTC-USDT',
        //            business_type => 'swap',
        //            liquidation_price => null,
        //            symbol => 'BTC',
        //            contract_code => 'BTC-USDT',
        //            volume => 1,
        //            available => 1,
        //            frozen => 0,
        //            cost_open => 27802.2,
        //            cost_hold => 27802.2,
        //            profit_unreal => 0.0175,
        //            profit_rate => 0.000629446590557581,
        //            profit => 0.0175,
        //            margin_asset => 'USDT',
        //            position_margin => 27.8197,
        //            lever_rate => 1,
        //            direction => 'buy',
        //            last_price => 27819.7,
        //            margin_mode => 'cross',
        //            margin_account => 'USDT',
        //            trade_partition => 'USDT',
        //            position_mode => 'dual_side'
        //          ),
        //        )
        //    }
        //
        $url = $client->url;
        $topic = $this->safe_string($message, 'topic', '');
        $marginMode = ($topic === 'positions_cross') ? 'cross' : 'isolated';
        if ($this->positions === null) {
            $this->positions = array();
        }
        $clientPositions = $this->safe_value($this->positions, $url);
        if ($clientPositions === null) {
            $this->positions[$url] = array();
        }
        $clientMarginModePositions = $this->safe_value($clientPositions, $marginMode);
        if ($clientMarginModePositions === null) {
            $this->positions[$url][$marginMode] = new ArrayCacheBySymbolBySide ();
        }
        $cache = $this->positions[$url][$marginMode];
        $rawPositions = $this->safe_value($message, 'data', array());
        $newPositions = array();
        $timestamp = $this->safe_integer($message, 'ts');
        for ($i = 0; $i < count($rawPositions); $i++) {
            $rawPosition = $rawPositions[$i];
            $position = $this->parse_position($rawPosition);
            $position['timestamp'] = $timestamp;
            $position['datetime'] = $this->iso8601($timestamp);
            $newPositions[] = $position;
            $cache->append ($position);
        }
        $messageHashes = $this->find_message_hashes($client, $marginMode . ':$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, $marginMode . ':positions');
    }

    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.htx.com/en-us/opend/newApiPages/?id=7ec52e28-7773-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=10000084-77b7-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=8cb7dcca-77b5-11ed-9966-0242ac110003
             * @see https://www.htx.com/en-us/opend/newApiPages/?id=28c34995-77ae-11ed-9966-0242ac110003
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            $type = null;
            list($type, $params) = $this->handle_market_type_and_params('watchBalance', null, $params);
            $subType = null;
            list($subType, $params) = $this->handle_sub_type_and_params('watchBalance', null, $params, 'linear');
            $isUnifiedAccount = $this->safe_value_2($params, 'isUnifiedAccount', 'unified', false);
            $params = $this->omit($params, array( 'isUnifiedAccount', 'unified' ));
            Async\await($this->load_markets());
            $messageHash = null;
            $channel = null;
            $marginMode = null;
            if ($type === 'spot') {
                $mode = $this->safe_string_2($this->options, 'watchBalance', 'mode', '2');
                $mode = $this->safe_string($params, 'mode', $mode);
                $messageHash = 'accounts.update' . '#' . $mode;
                $channel = $messageHash;
            } else {
                $symbol = $this->safe_string($params, 'symbol');
                $currency = $this->safe_string($params, 'currency');
                $market = ($symbol !== null) ? $this->market($symbol) : null;
                $currencyCode = ($currency !== null) ? $this->currency($currency) : null;
                $marginMode = $this->safe_string($params, 'margin', 'cross');
                $params = $this->omit($params, array( 'currency', 'symbol', 'margin' ));
                $prefix = 'accounts';
                $messageHash = $prefix;
                if ($subType === 'linear') {
                    if ($isUnifiedAccount) {
                        // usdt contracts account
                        $prefix = 'accounts_unify';
                        $messageHash = $prefix;
                        $channel = $prefix . '.' . 'usdt';
                    } else {
                        // usdt contracts account
                        $prefix = ($marginMode === 'cross') ? $prefix . '_cross' : $prefix;
                        $messageHash = $prefix;
                        if ($marginMode === 'isolated') {
                            // isolated margin only allows filtering by symbol3
                            if ($symbol !== null) {
                                $messageHash .= '.' . $market['id'];
                                $channel = $messageHash;
                            } else {
                                // subscribe to all
                                $channel = $prefix . '.' . '*';
                            }
                        } else {
                            // cross margin
                            if ($currencyCode !== null) {
                                $channel = $prefix . '.' . $currencyCode['id'];
                                $messageHash = $channel;
                            } else {
                                // subscribe to all
                                $channel = $prefix . '.' . '*';
                            }
                        }
                    }
                } elseif ($type === 'future') {
                    // inverse futures account
                    if ($currencyCode !== null) {
                        $messageHash .= '.' . $currencyCode['id'];
                        $channel = $messageHash;
                    } else {
                        // subscribe to all
                        $channel = $prefix . '.' . '*';
                    }
                } else {
                    // inverse swaps account
                    if ($market !== null) {
                        $messageHash .= '.' . $market['id'];
                        $channel = $messageHash;
                    } else {
                        // subscribe to all
                        $channel = $prefix . '.' . '*';
                    }
                }
            }
            $subscriptionParams = array(
                'type' => $type,
                'subType' => $subType,
                'margin' => $marginMode,
            );
            // we are differentiating the $channel from the $messageHash for global subscriptions (*)
            // because huobi returns a different topic than the topic sent. Example => we send
            // "accounts.*" and "accounts" is returned so we're setting $channel = "accounts.*" and
            // $messageHash = "accounts" allowing handleBalance to freely resolve the topic in the message
            return Async\await($this->subscribe_private($channel, $messageHash, $type, $subType, $params, $subscriptionParams));
        }) ();
    }

    public function handle_balance(Client $client, $message) {
        // spot
        //
        //     {
        //         "action" => "push",
        //         "ch" => "accounts.update#0",
        //         "data" => {
        //             "currency" => "btc",
        //             "accountId" => 123456,
        //             "balance" => "23.111",
        //             "available" => "2028.699426619837209087",
        //             "changeType" => "transfer",
        //             "accountType":"trade",
        //             "seqNum" => "86872993928",
        //             "changeTime" => 1568601800000
        //         }
        //     }
        //
        // inverse future
        //
        //     {
        //         "op":"notify",
        //         "topic":"accounts.ada",
        //         "ts":1604388667226,
        //         "event":"order.match",
        //         "data":array(
        //             {
        //                 "symbol":"ADA",
        //                 "margin_balance":446.417641681222726716,
        //                 "margin_static":445.554085945257745136,
        //                 "margin_position":11.049723756906077348,
        //                 "margin_frozen":0,
        //                 "margin_available":435.367917924316649368,
        //                 "profit_real":21.627049781983019459,
        //                 "profit_unreal":0.86355573596498158,
        //                 "risk_rate":40.000796572150656768,
        //                 "liquidation_price":0.018674308027108984,
        //                 "withdraw_available":423.927036163274725677,
        //                 "lever_rate":20,
        //                 "adjust_factor":0.4
        //             }
        //         ),
        //         "uid":"123456789"
        //     }
        //
        // usdt / linear future, swap
        //
        //     {
        //         "op":"notify",
        //         "topic":"accounts.btc-usdt", // or "accounts" for global subscriptions
        //         "ts":1603711370689,
        //         "event":"order.open",
        //         "data":array(
        //             {
        //                 "margin_mode":"cross",
        //                 "margin_account":"USDT",
        //                 "margin_asset":"USDT",
        //                 "margin_balance":30.959342395,
        //                 "margin_static":30.959342395,
        //                 "margin_position":0,
        //                 "margin_frozen":10,
        //                 "profit_real":0,
        //                 "profit_unreal":0,
        //                 "withdraw_available":20.959342395,
        //                 "risk_rate":153.796711975,
        //                 "position_mode":"dual_side",
        //                 "contract_detail":array(
        //                     array(
        //                         "symbol":"LTC",
        //                         "contract_code":"LTC-USDT",
        //                         "margin_position":0,
        //                         "margin_frozen":0,
        //                         "margin_available":20.959342395,
        //                         "profit_unreal":0,
        //                         "liquidation_price":null,
        //                         "lever_rate":1,
        //                         "adjust_factor":0.01,
        //                         "contract_type":"swap",
        //                         "pair":"LTC-USDT",
        //                         "business_type":"swap",
        //                         "trade_partition":"USDT"
        //                     ),
        //                 ),
        //                 "futures_contract_detail":array(),
        //             }
        //         )
        //     }
        //
        // inverse future
        //
        //     {
        //         "op":"notify",
        //         "topic":"accounts.ada",
        //         "ts":1604388667226,
        //         "event":"order.match",
        //         "data":array(
        //             {
        //                 "symbol":"ADA",
        //                 "margin_balance":446.417641681222726716,
        //                 "margin_static":445.554085945257745136,
        //                 "margin_position":11.049723756906077348,
        //                 "margin_frozen":0,
        //                 "margin_available":435.367917924316649368,
        //                 "profit_real":21.627049781983019459,
        //                 "profit_unreal":0.86355573596498158,
        //                 "risk_rate":40.000796572150656768,
        //                 "liquidation_price":0.018674308027108984,
        //                 "withdraw_available":423.927036163274725677,
        //                 "lever_rate":20,
        //                 "adjust_factor":0.4
        //             }
        //         ),
        //         "uid":"123456789"
        //     }
        //
        $channel = $this->safe_string($message, 'ch');
        $data = $this->safe_value($message, 'data', array());
        $timestamp = $this->safe_integer($data, 'changeTime', $this->safe_integer($message, 'ts'));
        $this->balance['timestamp'] = $timestamp;
        $this->balance['datetime'] = $this->iso8601($timestamp);
        $this->balance['info'] = $data;
        if ($channel !== null) {
            // spot $balance
            $currencyId = $this->safe_string($data, 'currency');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['free'] = $this->safe_string($data, 'available');
            $account['total'] = $this->safe_string($data, 'balance');
            $this->balance[$code] = $account;
            $this->balance = $this->safe_balance($this->balance);
            $client->resolve ($this->balance, $channel);
        } else {
            // contract $balance
            $dataLength = count($data);
            if ($dataLength === 0) {
                return;
            }
            $first = $this->safe_value($data, 0, array());
            $topic = $this->safe_string($message, 'topic');
            $splitTopic = explode('.', $topic);
            $messageHash = $this->safe_string($splitTopic, 0);
            $subscription = $this->safe_value_2($client->subscriptions, $messageHash, $messageHash . '.*');
            if ($subscription === null) {
                // if $subscription not found means that we subscribed to a specific currency/symbol
                // and we use the $first $data entry to find it
                // Example => $topic = 'accounts'
                // $client->subscription hash = 'accounts.usdt'
                // we do 'accounts' . '.' . $data[0]]['margin_asset'] to get it
                $currencyId = $this->safe_string_2($first, 'margin_asset', 'symbol');
                $messageHash .= '.' . strtolower($currencyId);
                $subscription = $this->safe_value($client->subscriptions, $messageHash);
            }
            $type = $this->safe_string($subscription, 'type');
            $subType = $this->safe_string($subscription, 'subType');
            if ($topic === 'accounts_unify') {
                // {
                //     "margin_asset" => "USDT",
                //     "margin_static" => 10,
                //     "cross_margin_static" => 10,
                //     "margin_balance" => 10,
                //     "cross_profit_unreal" => 0,
                //     "margin_frozen" => 0,
                //     "withdraw_available" => 10,
                //     "cross_risk_rate" => null,
                //     "cross_swap" => array(),
                //     "cross_future" => array(),
                //     "isolated_swap" => array()
                // }
                $marginAsset = $this->safe_string($first, 'margin_asset');
                $code = $this->safe_currency_code($marginAsset);
                $marginFrozen = $this->safe_string($first, 'margin_frozen');
                $unifiedAccount = $this->account();
                $unifiedAccount['free'] = $this->safe_string($first, 'withdraw_available');
                $unifiedAccount['used'] = $marginFrozen;
                $this->balance[$code] = $unifiedAccount;
                $this->balance = $this->safe_balance($this->balance);
                $client->resolve ($this->balance, 'accounts_unify');
            } elseif ($subType === 'linear') {
                $margin = $this->safe_string($subscription, 'margin');
                if ($margin === 'cross') {
                    $fieldName = ($type === 'future') ? 'futures_contract_detail' : 'contract_detail';
                    $balances = $this->safe_value($first, $fieldName, array());
                    $balancesLength = count($balances);
                    if ($balancesLength > 0) {
                        for ($i = 0; $i < count($balances); $i++) {
                            $balance = $balances[$i];
                            $marketId = $this->safe_string_2($balance, 'contract_code', 'margin_account');
                            $market = $this->safe_market($marketId);
                            $currencyId = $this->safe_string($balance, 'margin_asset');
                            $currency = $this->safe_currency($currencyId);
                            $code = $this->safe_string($market, 'settle', $currency['code']);
                            // the exchange outputs positions for delisted markets
                            // https://www.huobi.com/support/en-us/detail/74882968522337
                            // we skip it if the $market was delisted
                            if ($code !== null) {
                                $account = $this->account();
                                $account['free'] = $this->safe_string_2($balance, 'margin_balance', 'margin_available');
                                $account['used'] = $this->safe_string($balance, 'margin_frozen');
                                $accountsByCode = array();
                                $accountsByCode[$code] = $account;
                                $symbol = $market['symbol'];
                                $this->balance[$symbol] = $this->safe_balance($accountsByCode);
                            }
                        }
                    }
                } else {
                    // isolated $margin
                    for ($i = 0; $i < count($data); $i++) {
                        $isolatedBalance = $data[$i];
                        $account = $this->account();
                        $account['free'] = $this->safe_string($isolatedBalance, 'margin_balance', 'margin_available');
                        $account['used'] = $this->safe_string($isolatedBalance, 'margin_frozen');
                        $currencyId = $this->safe_string_2($isolatedBalance, 'margin_asset', 'symbol');
                        $code = $this->safe_currency_code($currencyId);
                        $this->balance[$code] = $account;
                        $this->balance = $this->safe_balance($this->balance);
                    }
                }
            } else {
                // inverse branch
                for ($i = 0; $i < count($data); $i++) {
                    $balance = $data[$i];
                    $currencyId = $this->safe_string($balance, 'symbol');
                    $code = $this->safe_currency_code($currencyId);
                    $account = $this->account();
                    $account['free'] = $this->safe_string($balance, 'margin_available');
                    $account['used'] = $this->safe_string($balance, 'margin_frozen');
                    $this->balance[$code] = $account;
                    $this->balance = $this->safe_balance($this->balance);
                }
            }
            $client->resolve ($this->balance, $messageHash);
        }
    }

    public function handle_subscription_status(Client $client, $message) {
        //
        //     {
        //         "id" => 1583414227,
        //         "status" => "ok",
        //         "subbed" => "market.btcusdt.mbp.150",
        //         "ts" => 1583414229143
        //     }
        //
        // unsubscribe
        //     {
        //         "id" => "2",
        //         "status" => "ok",
        //         "unsubbed" => "market.BTC-USDT-251003.detail",
        //         "ts" => 1759329276980
        //     }
        //
        $id = $this->safe_string($message, 'id');
        $subscriptionsById = $this->index_by($client->subscriptions, 'id');
        $subscription = $this->safe_dict($subscriptionsById, $id);
        if ($subscription !== null) {
            $method = $this->safe_value($subscription, 'method');
            if ($method !== null) {
                $method($client, $message, $subscription);
                // return; commented out to clean up
            }
            // clean up
            if (is_array($client->subscriptions) && array_key_exists($id, $client->subscriptions)) {
                unset($client->subscriptions[$id]);
            }
        }
        if (is_array($message) && array_key_exists('unsubbed', $message)) {
            $this->handle_un_subscription($client, $subscription);
        }
    }

    public function handle_un_subscription(Client $client, array $subscription) {
        $messageHashes = $this->safe_list($subscription, 'messageHashes', array());
        $subMessageHashes = $this->safe_list($subscription, 'subMessageHashes', array());
        for ($i = 0; $i < count($messageHashes); $i++) {
            $unsubHash = $messageHashes[$i];
            $subHash = $subMessageHashes[$i];
            $this->clean_unsubscription($client, $subHash, $unsubHash);
        }
        $this->clean_cache($subscription);
    }

    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
        //
        //     {
        //         "id" => "1578090234088", // connectId
        //         "type" => "welcome",
        //     }
        //
        return $message;
    }

    public function handle_subject(Client $client, $message) {
        // spot
        //     {
        //         "ch" => "market.btcusdt.mbp.150",
        //         "ts" => 1583472025885,
        //         "tick" => {
        //             "seqNum" => 104998984994,
        //             "prevSeqNum" => 104998984977,
        //             "bids" => [
        //                 [9058.27, 0],
        //                 [9058.43, 0],
        //                 [9058.99, 0],
        //             ],
        //             "asks" => [
        //                 [9084.27, 0.2],
        //                 [9085.69, 0],
        //                 [9085.81, 0],
        //             ]
        //         }
        //     }
        // non spot
        //
        //     {
        //         "ch":"market.BTC220218.depth.size_150.high_freq",
        //         "tick":array(
        //             "asks":array(),
        //             "bids":[
        //                 [43445.74,1],
        //                 [43444.48,0 ],
        //                 [40593.92,9]
        //             ],
        //             "ch":"market.BTC220218.depth.size_150.high_freq",
        //             "event":"update",
        //             "id":152727500274,
        //             "mrid":152727500274,
        //             "ts":1645023376098,
        //             "version":37536690
        //         ),
        //         "ts":1645023376098
        //     }
        //
        // spot private trade
        //
        //     {
        //         "action":"push",
        //         "ch":"trade.clearing#ltcusdt#1",
        //         "data":array(
        //             "eventType":"trade",
        //             "symbol":"ltcusdt",
        //             // ...
        //         ),
        //     }
        //
        // spot order
        //
        //     {
        //         "action":"push",
        //         "ch":"orders#btcusdt",
        //         "data" => {
        //             "orderSide":"buy",
        //             "lastActTime":1583853365586,
        //             "clientOrderId":"abc123",
        //             "orderStatus":"rejected",
        //             "symbol":"btcusdt",
        //             "eventType":"trigger",
        //             "errCode" => 2002,
        //             "errMessage":"invalid.client.order.id (NT)"
        //         }
        //     }
        //
        // contract order
        //
        //     {
        //         "op":"notify",
        //         "topic":"orders.ada",
        //         "ts":1604388667226,
        //         // ?
        //     }
        //
        $ch = $this->safe_value($message, 'ch', '');
        $parts = explode('.', $ch);
        $type = $this->safe_string($parts, 0);
        if ($type === 'market') {
            $methodName = $this->safe_string($parts, 2);
            $methods = array(
                'depth' => array($this, 'handle_order_book'),
                'mbp' => array($this, 'handle_order_book'),
                'detail' => array($this, 'handle_ticker'),
                'bbo' => array($this, 'handle_ticker'),
                'ticker' => array($this, 'handle_ticker'),
                'trade' => array($this, 'handle_trades'),
                'kline' => array($this, 'handle_ohlcv'),
            );
            $method = $this->safe_value($methods, $methodName);
            if ($method !== null) {
                $method($client, $message);
                return;
            }
        }
        // private spot subjects
        $privateParts = explode('#', $ch);
        $privateType = $this->safe_string($privateParts, 0, '');
        if ($privateType === 'trade.clearing') {
            $this->handle_my_trade($client, $message);
            return;
        }
        if (mb_strpos($privateType, 'accounts.update') !== false) {
            $this->handle_balance($client, $message);
            return;
        }
        if ($privateType === 'orders') {
            $this->handle_order($client, $message);
            return;
        }
        // private contract subjects
        $op = $this->safe_string($message, 'op');
        if ($op === 'notify') {
            $topic = $this->safe_string($message, 'topic', '');
            if (mb_strpos($topic, 'orders') !== false) {
                $this->handle_order($client, $message);
            }
            if (mb_strpos($topic, 'account') !== false) {
                $this->handle_balance($client, $message);
            }
            if (mb_strpos($topic, 'positions') !== false) {
                $this->handle_positions($client, $message);
            }
        }
    }

    public function pong($client, $message) {
        return Async\async(function () use ($client, $message) {
            //
            //     array( $ping => 1583491673714 )
            //     array( $action => "ping", $data => array( ts => 1645108204665 ) )
            //     array( $op => "ping", ts => "1645202800015" )
            //
            try {
                $ping = $this->safe_integer($message, 'ping');
                if ($ping !== null) {
                    Async\await($client->send (array( 'pong' => $ping )));
                    return;
                }
                $action = $this->safe_string($message, 'action');
                if ($action === 'ping') {
                    $data = $this->safe_value($message, 'data');
                    $pingTs = $this->safe_integer($data, 'ts');
                    Async\await($client->send (array( 'action' => 'pong', 'data' => array( 'ts' => $pingTs ))));
                    return;
                }
                $op = $this->safe_string($message, 'op');
                if ($op === 'ping') {
                    $pingTs = $this->safe_integer($message, 'ts');
                    Async\await($client->send (array( 'op' => 'pong', 'ts' => $pingTs )));
                }
            } catch (Exception $e) {
                $error = new NetworkError ($this->id . ' pong failed ' . $this->json($e));
                $client->reset ($error);
            }
        }) ();
    }

    public function handle_ping(Client $client, $message) {
        $this->spawn(array($this, 'pong'), $client, $message);
    }

    public function handle_authenticate(Client $client, $message) {
        //
        // spot
        //
        //     {
        //         "action" => "req",
        //         "code" => 200,
        //         "ch" => "auth",
        //         "data" => array()
        //     }
        //
        // non spot
        //
        //    {
        //        "op" => "auth",
        //        "type" => "api",
        //        "err-code" => 0,
        //        "ts" => 1645200307319,
        //        "data" => array( "user-id" => "35930539" )
        //    }
        //
        $promise = $client->futures['auth'];
        $promise->resolve ($message);
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //     {
        //         "action" => "sub",
        //         "code" => 2002,
        //         "ch" => "accounts.update#2",
        //         "message" => "invalid.auth.state"
        //      }
        //
        //     {
        //         "ts" => 1586323747018,
        //         "status" => "error",
        //         'err-code' => "bad-request",
        //         'err-msg' => "invalid mbp.150.symbol linkusdt",
        //         "id" => "2"
        //     }
        //
        //     {
        //         "op" => "sub",
        //         "cid" => "1",
        //         "topic" => "accounts_unify.USDT",
        //         "err-$code" => 4007,
        //         'err-msg' => "Non - single account user is not available, please check through the cross and isolated account asset interface",
        //         "ts" => 1698419490189
        //     }
        //     {
        //         "action":"req",
        //         "code":2002,
        //         "ch":"auth",
        //         "message":"auth.fail"
        //     }
        //
        $status = $this->safe_string($message, 'status');
        if ($status === 'error') {
            $id = $this->safe_string($message, 'id');
            $subscriptionsById = $this->index_by($client->subscriptions, 'id');
            $subscription = $this->safe_value($subscriptionsById, $id);
            if ($subscription !== null) {
                $errorCode = $this->safe_string($message, 'err-code');
                try {
                    $this->throw_exactly_matched_exception($this->exceptions['ws']['exact'], $errorCode, $this->json($message));
                    throw new ExchangeError($this->json($message));
                } catch (Exception $e) {
                    $messageHash = $this->safe_string($subscription, 'messageHash');
                    $client->reject ($e, $messageHash);
                    $client->reject ($e, $id);
                    if (is_array($client->subscriptions) && array_key_exists($id, $client->subscriptions)) {
                        unset($client->subscriptions[$id]);
                    }
                }
            }
            return false;
        }
        $code = $this->safe_string_2($message, 'code', 'err-code');
        if ($code !== null && (($code !== '200') && ($code !== '0'))) {
            $feedback = $this->id . ' ' . $this->json($message);
            try {
                $this->throw_exactly_matched_exception($this->exceptions['ws']['exact'], $code, $feedback);
                throw new ExchangeError($feedback);
            } catch (Exception $e) {
                if ($e instanceof AuthenticationError) {
                    $client->reject ($e, 'auth');
                    $method = 'auth';
                    if (is_array($client->subscriptions) && array_key_exists($method, $client->subscriptions)) {
                        unset($client->subscriptions[$method]);
                    }
                    return false;
                } else {
                    $client->reject ($e);
                }
            }
        }
        return true;
    }

    public function handle_message(Client $client, $message) {
        if ($this->handle_error_message($client, $message)) {
            //
            //     array("id":1583414227,"status":"ok","subbed":"market.btcusdt.mbp.150","ts":1583414229143)
            //
            // first ping format
            //
            //    array("ping" => 1645106821667 )
            //
            // second ping format
            //
            //    array("action":"ping","data":array("ts":1645106821667))
            //
            // third pong format
            //
            //
            // auth spot
            //
            //     {
            //         "action" => "req",
            //         "code" => 200,
            //         "ch" => "auth",
            //         "data" => array()
            //     }
            //
            // auth non spot
            //
            //    {
            //        "op" => "auth",
            //        "type" => "api",
            //        "err-code" => 0,
            //        "ts" => 1645200307319,
            //        "data" => array( "user-id" => "35930539" )
            //    }
            //
            // trade
            //
            //     {
            //         "action":"push",
            //         "ch":"trade.clearing#ltcusdt#1",
            //         "data":{
            //             "eventType":"trade",
            //             // ?
            //         }
            //     }
            //
            if (is_array($message) && array_key_exists('id', $message)) {
                $this->handle_subscription_status($client, $message);
                return;
            }
            if (is_array($message) && array_key_exists('action', $message)) {
                $action = $this->safe_string($message, 'action');
                if ($action === 'ping') {
                    $this->handle_ping($client, $message);
                    return;
                }
                if ($action === 'sub') {
                    $this->handle_subscription_status($client, $message);
                    return;
                }
            }
            if (is_array($message) && array_key_exists('ch', $message)) {
                if ($message['ch'] === 'auth') {
                    $this->handle_authenticate($client, $message);
                    return;
                } else {
                    // route by channel aka topic aka subject
                    $this->handle_subject($client, $message);
                    return;
                }
            }
            if (is_array($message) && array_key_exists('op', $message)) {
                $op = $this->safe_string($message, 'op');
                if ($op === 'ping') {
                    $this->handle_ping($client, $message);
                    return;
                }
                if ($op === 'auth') {
                    $this->handle_authenticate($client, $message);
                    return;
                }
                if ($op === 'sub') {
                    $this->handle_subscription_status($client, $message);
                    return;
                }
                if ($op === 'notify') {
                    $this->handle_subject($client, $message);
                    return;
                }
            }
            if (is_array($message) && array_key_exists('ping', $message)) {
                $this->handle_ping($client, $message);
            }
        }
    }

    public function handle_my_trade(Client $client, $message, $extendParams = array ()) {
        //
        // spot
        //
        //     {
        //         "action":"push",
        //         "ch":"trade.clearing#ltcusdt#1",
        //         "data":{
        //             "eventType":"trade",
        //             "symbol":"ltcusdt",
        //             "orderId":"478862728954426",
        //             "orderSide":"buy",
        //             "orderType":"buy-$market",
        //             "accountId":44234548,
        //             "source":"spot-web",
        //             "orderValue":"5.01724137",
        //             "orderCreateTime":1645124660365,
        //             "orderStatus":"filled",
        //             "feeCurrency":"ltc",
        //             "tradePrice":"118.89",
        //             "tradeVolume":"0.042200701236437042",
        //             "aggressor":true,
        //             "tradeId":101539740584,
        //             "tradeTime":1645124660368,
        //             "transactFee":"0.000041778694224073",
        //             "feeDeduct":"0",
        //             "feeDeductType":""
        //         }
        //     }
        //
        // contract
        //
        //     {
        //         "symbol" => "ADA/USDT:USDT"
        //         "ch" => "orders_cross.ada-usdt"
        //         "trades" => array(
        //             {
        //                 "trade_fee":-0.022099447513812154,
        //                 "fee_asset":"ADA",
        //                 "trade_id":113913755890,
        //                 "id":"113913755890-773207641127878656-1",
        //                 "trade_volume":1,
        //                 "trade_price":0.0905,
        //                 "trade_turnover":10,
        //                 "created_at":1604388667194,
        //                 "profit":0,
        //                 "real_profit" => 0,
        //                 "role":"maker"
        //             }
        //         ),
        //     }
        //
        if ($this->myTrades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $this->myTrades = new ArrayCacheBySymbolById ($limit);
        }
        $cachedTrades = $this->myTrades;
        $messageHash = $this->safe_string($message, 'ch');
        if ($messageHash !== null) {
            $data = $this->safe_value($message, 'data');
            if ($data !== null) {
                $parsed = $this->parse_ws_trade($data);
                $symbol = $this->safe_string($parsed, 'symbol');
                if ($symbol !== null) {
                    $cachedTrades->append ($parsed);
                    $client->resolve ($this->myTrades, $messageHash);
                }
            } else {
                // this trades object is artificially created
                // in handleOrder
                $rawTrades = $this->safe_value($message, 'trades', array());
                $marketId = $this->safe_value($message, 'symbol');
                $market = $this->market($marketId);
                for ($i = 0; $i < count($rawTrades); $i++) {
                    $trade = $rawTrades[$i];
                    $parsedTrade = $this->parse_trade($trade, $market);
                    // add extra params (side, type, ...) coming from the order
                    $parsedTrade = $this->extend($parsedTrade, $extendParams);
                    $cachedTrades->append ($parsedTrade);
                }
                // $messageHash here is the orders one, so
                // we have to recreate the trades $messageHash = orderMessageHash . ':' . 'trade'
                $tradesHash = $messageHash . ':' . 'trade';
                $client->resolve ($this->myTrades, $tradesHash);
                // when we make an global order sub we have to send the channel like this
                // ch = orders_cross.* and we store $messageHash = 'orders_cross'
                // however it is returned with the specific order update $symbol => ch = orders_cross.btc-usd
                // since this is a global sub, our $messageHash does not specify any $symbol (ex => orders_cross:$trade)
                // so we must remove it
                $genericOrderHash = str_replace('.' . $market['lowercaseId'], '', $messageHash);
                $lowerCaseBaseId = $this->safe_string_lower($market, 'baseId');
                $genericOrderHash = str_replace('.' . $lowerCaseBaseId, '', $genericOrderHash);
                $genericTradesHash = $genericOrderHash . ':' . 'trade';
                $client->resolve ($this->myTrades, $genericTradesHash);
            }
        }
    }

    public function parse_ws_trade($trade, $market = null) {
        // spot private
        //
        //     {
        //         "eventType":"trade",
        //         "symbol":"ltcusdt",
        //         "orderId":"478862728954426",
        //         "orderSide":"buy",
        //         "orderType":"buy-$market",
        //         "accountId":44234548,
        //         "source":"spot-web",
        //         "orderValue":"5.01724137",
        //         "orderCreateTime":1645124660365,
        //         "orderStatus":"filled",
        //         "feeCurrency":"ltc",
        //         "tradePrice":"118.89",
        //         "tradeVolume":"0.042200701236437042",
        //         "aggressor":true,
        //         "tradeId":101539740584,
        //         "tradeTime":1645124660368,
        //         "transactFee":"0.000041778694224073",
        //         "feeDeduct":"0",
        //         "feeDeductType":""
        //     }
        //
        $symbol = $this->safe_symbol($this->safe_string($trade, 'symbol'));
        $side = $this->safe_string_2($trade, 'side', 'orderSide');
        $tradeId = $this->safe_string($trade, 'tradeId');
        $price = $this->safe_string($trade, 'tradePrice');
        $amount = $this->safe_string($trade, 'tradeVolume');
        $order = $this->safe_string($trade, 'orderId');
        $timestamp = $this->safe_integer($trade, 'tradeTime');
        $market = $this->market($symbol);
        $orderType = $this->safe_string($trade, 'orderType');
        $aggressor = $this->safe_value($trade, 'aggressor');
        $takerOrMaker = null;
        if ($aggressor !== null) {
            $takerOrMaker = $aggressor ? 'taker' : 'maker';
        }
        $type = null;
        $orderTypeParts = array();
        if ($orderType !== null) {
            $orderTypeParts = explode('-', $orderType);
            $type = $this->safe_string($orderTypeParts, 1);
        }
        $fee = null;
        $feeCurrency = $this->safe_currency_code($this->safe_string($trade, 'feeCurrency'));
        if ($feeCurrency !== null) {
            $fee = array(
                'cost' => $this->safe_string($trade, 'transactFee'),
                'currency' => $feeCurrency,
            );
        }
        return $this->safe_trade(array(
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'id' => $tradeId,
            'order' => $order,
            'type' => $type,
            'takerOrMaker' => $takerOrMaker,
            'side' => $side,
            'price' => $price,
            'amount' => $amount,
            'cost' => null,
            'fee' => $fee,
        ), $market);
    }

    public function get_url_by_market_type($type, $isLinear = true, $isPrivate = false, $isFeed = false) {
        $api = $this->safe_string($this->options, 'api', 'api');
        $hostname = array( 'hostname' => $this->hostname );
        $hostnameURL = null;
        $url = null;
        if ($type === 'spot') {
            if ($isPrivate) {
                $hostnameURL = $this->urls['api']['ws'][$api]['spot']['private'];
            } else {
                if ($isFeed) {
                    $hostnameURL = $this->urls['api']['ws'][$api]['spot']['feed'];
                } else {
                    $hostnameURL = $this->urls['api']['ws'][$api]['spot']['public'];
                }
            }
            $url = $this->implode_params($hostnameURL, $hostname);
        } else {
            $baseUrl = $this->urls['api']['ws'][$api][$type];
            $subTypeUrl = $isLinear ? $baseUrl['linear'] : $baseUrl['inverse'];
            $url = $isPrivate ? $subTypeUrl['private'] : $subTypeUrl['public'];
        }
        return $url;
    }

    public function subscribe_public($url, $symbol, $messageHash, $method = null, $params = array ()) {
        return Async\async(function () use ($url, $symbol, $messageHash, $method, $params) {
            $requestId = $this->request_id();
            $request = array(
                'sub' => $messageHash,
                'id' => $requestId,
            );
            $subscription = array(
                'id' => $requestId,
                'messageHash' => $messageHash,
                'symbol' => $symbol,
                'params' => $params,
            );
            if ($method !== null) {
                $subscription['method'] = $method;
            }
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash, $subscription));
        }) ();
    }

    public function unsubscribe_public(array $market, string $subMessageHash, string $topic, $params = array ()) {
        return Async\async(function () use ($market, $subMessageHash, $topic, $params) {
            $requestId = $this->request_id();
            $request = array(
                'unsub' => $subMessageHash,
                'id' => $requestId,
            );
            $messageHash = 'unsubscribe::' . $subMessageHash;
            $isFeed = ($topic === 'orderbook');
            $url = $this->get_url_by_market_type($market['type'], $market['linear'], false, $isFeed);
            $subscription = array(
                'unsubscribe' => true,
                'id' => $requestId,
                'subMessageHashes' => array( $subMessageHash ),
                'messageHashes' => array( $messageHash ),
                'symbols' => [ $market['symbol'] ],
                'topic' => $topic,
            );
            $symbolsAndTimeframes = $this->safe_list($params, 'symbolsAndTimeframes');
            if ($symbolsAndTimeframes !== null) {
                $subscription['symbolsAndTimeframes'] = $symbolsAndTimeframes;
                $params = $this->omit($params, 'symbolsAndTimeframes');
            }
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash, $subscription));
        }) ();
    }

    public function subscribe_private($channel, $messageHash, $type, $subtype, $params = array (), $subscriptionParams = array ()) {
        return Async\async(function () use ($channel, $messageHash, $type, $subtype, $params, $subscriptionParams) {
            $requestId = $this->request_id();
            $subscription = array(
                'id' => $requestId,
                'messageHash' => $messageHash,
                'params' => $params,
            );
            $extendedSubsription = $this->extend($subscription, $subscriptionParams);
            $request = null;
            if ($type === 'spot') {
                $request = array(
                    'action' => 'sub',
                    'ch' => $channel,
                );
            } else {
                $request = array(
                    'op' => 'sub',
                    'topic' => $channel,
                    'cid' => $requestId,
                );
            }
            $isLinear = $subtype === 'linear';
            $url = $this->get_url_by_market_type($type, $isLinear, true);
            $hostname = ($type === 'spot') ? $this->urls['hostnames']['spot'] : $this->urls['hostnames']['contract'];
            $authParams = array(
                'type' => $type,
                'url' => $url,
                'hostname' => $hostname,
            );
            Async\await($this->authenticate($authParams));
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $channel, $extendedSubsription));
        }) ();
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $url = $this->safe_string($params, 'url');
            $hostname = $this->safe_string($params, 'hostname');
            $type = $this->safe_string($params, 'type');
            if ($url === null || $hostname === null || $type === null) {
                throw new ArgumentsRequired($this->id . ' authenticate requires a $url, $hostname and $type argument');
            }
            $this->check_required_credentials();
            $messageHash = 'auth';
            $relativePath = str_replace('wss://' . $hostname, '', $url);
            $client = $this->client($url);
            $future = $client->reusableFuture ($messageHash);
            $authenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                $timestamp = $this->ymdhms($this->milliseconds(), 'T');
                $signatureParams = null;
                if ($type === 'spot') {
                    $signatureParams = array(
                        'accessKey' => $this->apiKey,
                        'signatureMethod' => 'HmacSHA256',
                        'signatureVersion' => '2.1',
                        'timestamp' => $timestamp,
                    );
                } else {
                    $signatureParams = array(
                        'AccessKeyId' => $this->apiKey,
                        'SignatureMethod' => 'HmacSHA256',
                        'SignatureVersion' => '2',
                        'Timestamp' => $timestamp,
                    );
                }
                $signatureParams = $this->keysort($signatureParams);
                $auth = $this->urlencode($signatureParams, true); // true required in go
                $payload = implode("\n", array('GET', $hostname, $relativePath, $auth)); // eslint-disable-line quotes
                $signature = $this->hmac($this->encode($payload), $this->encode($this->secret), 'sha256', 'base64');
                $request = null;
                if ($type === 'spot') {
                    $newParams = array(
                        'authType' => 'api',
                        'accessKey' => $this->apiKey,
                        'signatureMethod' => 'HmacSHA256',
                        'signatureVersion' => '2.1',
                        'timestamp' => $timestamp,
                        'signature' => $signature,
                    );
                    $request = array(
                        'params' => $newParams,
                        'action' => 'req',
                        'ch' => 'auth',
                    );
                } else {
                    $request = array(
                        'op' => 'auth',
                        'type' => 'api',
                        'AccessKeyId' => $this->apiKey,
                        'SignatureMethod' => 'HmacSHA256',
                        'SignatureVersion' => '2',
                        'Timestamp' => $timestamp,
                        'Signature' => $signature,
                    );
                }
                $requestId = $this->request_id();
                $subscription = array(
                    'id' => $requestId,
                    'messageHash' => $messageHash,
                    'params' => $params,
                );
                $this->watch($url, $messageHash, $request, $messageHash, $subscription);
            }
            return Async\await($future);
        }) ();
    }
}
