# -*- coding: utf-8 -*-

# PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
# https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code

import ccxt.async_support
from ccxt.async_support.base.ws.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheBySymbolBySide, ArrayCacheByTimestamp
import hashlib
from ccxt.base.types import Any, Balances, Bool, Int, Market, Order, OrderBook, Position, Str, Strings, Ticker, Trade
from ccxt.async_support.base.ws.client import Client
from typing import List
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import BadRequest
from ccxt.base.errors import BadSymbol
from ccxt.base.errors import NetworkError
from ccxt.base.errors import InvalidNonce
from ccxt.base.errors import ChecksumError


class htx(ccxt.async_support.htx):

    def describe(self) -> Any:
        return self.deep_extend(super(htx, self).describe(), {
            'has': {
                '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': {
                'api': {
                    'ws': {
                        'api': {
                            'spot': {
                                'public': 'wss://{hostname}/ws',
                                'private': 'wss://{hostname}/ws/v2',
                                'feed': 'wss://{hostname}/feed',
                            },
                            'future': {
                                'linear': {
                                    'public': 'wss://api.hbdm.vn/linear-swap-ws',
                                    'private': 'wss://api.hbdm.vn/linear-swap-notification',
                                },
                                'inverse': {
                                    'public': 'wss://api.hbdm.vn/ws',
                                    'private': 'wss://api.hbdm.vn/notification',
                                },
                            },
                            'swap': {
                                'inverse': {
                                    'public': 'wss://api.hbdm.vn/swap-ws',
                                    'private': 'wss://api.hbdm.vn/swap-notification',
                                },
                                'linear': {
                                    'public': 'wss://api.hbdm.vn/linear-swap-ws',
                                    'private': 'wss://api.hbdm.vn/linear-swap-notification',
                                },
                            },
                        },
                        # these settings work faster for clients hosted on AWS
                        'api-aws': {
                            'spot': {
                                'public': 'wss://api-aws.huobi.pro/ws',
                                'private': 'wss://api-aws.huobi.pro/ws/v2',
                                'feed': 'wss://{hostname}/feed',
                            },
                            'future': {
                                'linear': {
                                    'public': 'wss://api.hbdm.vn/linear-swap-ws',
                                    'private': 'wss://api.hbdm.vn/linear-swap-notification',
                                },
                                'inverse': {
                                    'public': 'wss://api.hbdm.vn/ws',
                                    'private': 'wss://api.hbdm.vn/notification',
                                },
                            },
                            'swap': {
                                'linear': {
                                    'public': 'wss://api.hbdm.vn/linear-swap-ws',
                                    'private': 'wss://api.hbdm.vn/linear-swap-notification',
                                },
                                'inverse': {
                                    'public': 'wss://api.hbdm.vn/swap-ws',
                                    'private': 'wss://api.hbdm.vn/swap-notification',
                                },
                            },
                        },
                    },
                },
            },
            'options': {
                'tradesLimit': 1000,
                'OHLCVLimit': 1000,
                'api': 'api',  # or api-aws for clients hosted on AWS
                'watchOrderBook': {
                    'maxRetries': 3,
                    'checksum': True,
                    'depth': 150,  # 150 or 20
                },
                'ws': {
                    'gunzip': True,
                },
                'watchTicker': {
                    'name': 'market.{marketId}.detail',  # 'market.{marketId}.bbo' or 'market.{marketId}.ticker'
                },
            },
            'exceptions': {
                'ws': {
                    'exact': {
                        'bad-request': BadRequest,  # { ts: 1586323747018,  status: 'error',    'err-code': 'bad-request',  err-msg': 'invalid mbp.150.symbol linkusdt', id: '2'}
                        '2002': AuthenticationError,  # {action: 'sub', code: 2002, ch: 'accounts.update#2', message: 'invalid.auth.state'}
                        '2021': BadRequest,
                        '2001': BadSymbol,  # {action: 'sub', code: 2001, ch: 'orders#2ltcusdt', message: 'invalid.symbol'}
                        '2011': BadSymbol,  # {op: 'sub', cid: '1649149285', topic: 'orders_cross.ltc-usdt', 'err-code': 2011, 'err-msg': "Contract doesn't exist.", ts: 1649149287637}
                        '2040': BadRequest,  # {op: 'sub', cid: '1649152947', 'err-code': 2040, 'err-msg': 'Missing required parameter.', ts: 1649152948684}
                        '4007': BadRequest,  # {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}
                    },
                },
            },
        })

    def request_id(self):
        self.lock_id()
        requestId = self.sum(self.safe_integer(self.options, 'requestId', 0), 1)
        self.options['requestId'] = requestId
        self.unlock_id()
        return str(requestId)

    async def watch_ticker(self, symbol: str, params={}) -> Ticker:
        """
        watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market

        https://www.htx.com/en-us/opend/newApiPages/?id=7ec53561-7773-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c33ab2-77ae-11ed-9966-0242ac110003

        :param str symbol: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/?id=ticker-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        options = self.safe_dict(self.options, 'watchTicker', {})
        topic = self.safe_string(options, 'name', 'market.{marketId}.detail')
        if topic == 'market.{marketId}.ticker' and market['type'] != 'spot':
            raise BadRequest(self.id + ' watchTicker() with name market.{marketId}.ticker is only allowed for spot markets, use market.{marketId}.detail instead')
        messageHash = self.implode_params(topic, {'marketId': market['id']})
        url = self.get_url_by_market_type(market['type'], market['linear'])
        return await self.subscribe_public(url, symbol, messageHash, None, params)

    async def un_watch_ticker(self, symbol: str, params={}) -> Any:
        """
        unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list

        https://www.htx.com/en-us/opend/newApiPages/?id=7ec53561-7773-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c33ab2-77ae-11ed-9966-0242ac110003

        :param str symbol: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/?id=ticker-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        topic = 'ticker'
        options = self.safe_dict(self.options, 'watchTicker', {})
        channel = self.safe_string(options, 'name', 'market.{marketId}.detail')
        if channel == 'market.{marketId}.ticker' and market['type'] != 'spot':
            raise BadRequest(self.id + ' watchTicker() with name market.{marketId}.ticker is only allowed for spot markets, use market.{marketId}.detail instead')
        subMessageHash = self.implode_params(channel, {'marketId': market['id']})
        return await self.unsubscribe_public(market, subMessageHash, topic, params)

    def handle_ticker(self, 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 = self.safe_value(message, 'tick', {})
        ch = self.safe_string(message, 'ch')
        parts = ch.split('.')
        marketId = self.safe_string(parts, 1)
        market = self.safe_market(marketId)
        ticker = self.parse_ticker(tick, market)
        timestamp = self.safe_value(message, 'ts')
        ticker['timestamp'] = timestamp
        ticker['datetime'] = self.iso8601(timestamp)
        symbol = ticker['symbol']
        self.tickers[symbol] = ticker
        client.resolve(ticker, ch)
        return message

    async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        get the list of most recent trades for a particular symbol

        https://www.htx.com/en-us/opend/newApiPages/?id=7ec53b69-7773-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c33c21-77ae-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c33cfe-77ae-11ed-9966-0242ac110003

        :param str symbol: unified symbol of the market to fetch trades for
        :param int [since]: timestamp in ms of the earliest trade to fetch
        :param int [limit]: the maximum amount of trades to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/?id=public-trades>`
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        messageHash = 'market.' + market['id'] + '.trade.detail'
        url = self.get_url_by_market_type(market['type'], market['linear'])
        trades = await self.subscribe_public(url, symbol, messageHash, None, params)
        if self.newUpdates:
            limit = trades.getLimit(symbol, limit)
        return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)

    async def un_watch_trades(self, symbol: str, params={}) -> Any:
        """
        unWatches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list

        https://www.htx.com/en-us/opend/newApiPages/?id=7ec53b69-7773-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c33c21-77ae-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c33cfe-77ae-11ed-9966-0242ac110003

        :param str symbol: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/?id=ticker-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        topic = 'trades'
        options = self.safe_dict(self.options, 'watchTrades', {})
        channel = self.safe_string(options, 'name', 'market.{marketId}.trade.detail')
        subMessageHash = self.implode_params(channel, {'marketId': market['id']})
        return await self.unsubscribe_public(market, subMessageHash, topic, params)

    def handle_trades(self, client: Client, message):
        #
        #     {
        #         "ch": "market.btcusdt.trade.detail",
        #         "ts": 1583495834011,
        #         "tick": {
        #             "id": 105004645372,
        #             "ts": 1583495833751,
        #             "data": [
        #                 {
        #                     "id": 1.050046453727319e+22,
        #                     "ts": 1583495833751,
        #                     "tradeId": 102090727790,
        #                     "amount": 0.003893,
        #                     "price": 9150.01,
        #                     "direction": "sell"
        #                 }
        #             ]
        #         }
        #     }
        #
        tick = self.safe_value(message, 'tick', {})
        data = self.safe_value(tick, 'data', {})
        ch = self.safe_string(message, 'ch')
        parts = ch.split('.')
        marketId = self.safe_string(parts, 1)
        market = self.safe_market(marketId)
        symbol = market['symbol']
        tradesCache = self.safe_value(self.trades, symbol)
        if tradesCache is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            tradesCache = ArrayCache(limit)
            self.trades[symbol] = tradesCache
        for i in range(0, len(data)):
            trade = self.parse_trade(data[i], market)
            tradesCache.append(trade)
        client.resolve(tradesCache, ch)
        return message

    async def watch_ohlcv(self, symbol: str, timeframe: str = '1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
        """
        watches historical candlestick data containing the open, high, low, and close price, and the volume of a market

        https://www.htx.com/en-us/opend/newApiPages/?id=7ec53241-7773-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c3346a-77ae-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c33563-77ae-11ed-9966-0242ac110003

        :param str symbol: unified symbol of the market to fetch OHLCV data for
        :param str timeframe: the length of time each candle represents
        :param int [since]: timestamp in ms of the earliest candle to fetch
        :param int [limit]: the maximum amount of candles to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns int[][]: A list of candles ordered, open, high, low, close, volume
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        interval = self.safe_string(self.timeframes, timeframe, timeframe)
        messageHash = 'market.' + market['id'] + '.kline.' + interval
        url = self.get_url_by_market_type(market['type'], market['linear'])
        ohlcv = await self.subscribe_public(url, symbol, messageHash, None, params)
        if self.newUpdates:
            limit = ohlcv.getLimit(symbol, limit)
        return self.filter_by_since_limit(ohlcv, since, limit, 0, True)

    async def un_watch_ohlcv(self, symbol: str, timeframe: str = '1m', params={}) -> Any:
        """
        unWatches historical candlestick data containing the open, high, low, and close price, and the volume of a market

        https://www.htx.com/en-us/opend/newApiPages/?id=7ec53241-7773-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c3346a-77ae-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c33563-77ae-11ed-9966-0242ac110003

        :param str symbol: unified symbol of the market
        :param str timeframe: the length of time each candle represents
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param dict [params.timezone]: if provided, kline intervals are interpreted in that timezone instead of UTC, example '+08:00'
        :returns int[][]: A list of candles ordered, open, high, low, close, volume
        """
        await self.load_markets()
        market = self.market(symbol)
        interval = self.safe_string(self.timeframes, timeframe, timeframe)
        subMessageHash = 'market.' + market['id'] + '.kline.' + interval
        topic = 'ohlcv'
        params['symbolsAndTimeframes'] = [[market['symbol'], timeframe]]
        return await self.unsubscribe_public(market, subMessageHash, topic, params)

    def handle_ohlcv(self, 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 = self.safe_string(message, 'ch')
        parts = ch.split('.')
        marketId = self.safe_string(parts, 1)
        market = self.safe_market(marketId)
        symbol = market['symbol']
        interval = self.safe_string(parts, 3)
        timeframe = self.find_timeframe(interval)
        self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
        stored = self.safe_value(self.ohlcvs[symbol], timeframe)
        if stored is None:
            limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
            stored = ArrayCacheByTimestamp(limit)
            self.ohlcvs[symbol][timeframe] = stored
        tick = self.safe_value(message, 'tick')
        parsed = self.parse_ohlcv(tick, market)
        stored.append(parsed)
        client.resolve(stored, ch)

    async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        """

        https://huobiapi.github.io/docs/dm/v1/en/#subscribe-market-depth-data
        https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#subscribe-incremental-market-depth-data
        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 str symbol: unified symbol of the market to fetch the order book for
        :param int [limit]: the maximum amount of order book entries to return
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/?id=order-book-structure>` indexed by market symbols
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        allowedLimits = [5, 20, 150, 400]
        # 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 = self.safe_dict(self.options, 'watchOrderBook', {})
        if limit is None:
            limit = self.safe_integer(options, 'depth', 150)
        if not self.in_array(limit, allowedLimits):
            raise ExchangeError(self.id + ' watchOrderBook market accepts limits of 5, 20, 150 or 400 only')
        messageHash = None
        if market['spot']:
            messageHash = 'market.' + market['id'] + '.mbp.' + self.number_to_string(limit)
        else:
            messageHash = 'market.' + market['id'] + '.depth.size_' + self.number_to_string(limit) + '.high_freq'
        url = self.get_url_by_market_type(market['type'], market['linear'], False, True)
        method = self.handle_order_book_subscription
        if not market['spot']:
            params = self.extend(params)
            params['data_type'] = 'incremental'
            method = None
        orderbook = await self.subscribe_public(url, symbol, messageHash, method, params)
        return orderbook.limit()

    async def un_watch_order_book(self, symbol: str, params={}) -> Any:
        """
        unsubscribe from the orderbook channel

        https://huobiapi.github.io/docs/dm/v1/en/#subscribe-market-depth-data
        https://huobiapi.github.io/docs/coin_margined_swap/v1/en/#subscribe-incremental-market-depth-data
        https://huobiapi.github.io/docs/usdt_swap/v1/en/#general-subscribe-incremental-market-depth-data

        :param str symbol: unified symbol of the market to fetch the order book for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.limit]: orderbook limit, default is None
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/?id=order-book-structure>` indexed by market symbols
        """
        await self.load_markets()
        market = self.market(symbol)
        topic = 'orderbook'
        options = self.safe_dict(self.options, 'watchOrderBook', {})
        depth = self.safe_integer(options, 'depth', 150)
        subMessageHash = None
        if market['spot']:
            subMessageHash = 'market.' + market['id'] + '.mbp.' + self.number_to_string(depth)
        else:
            subMessageHash = 'market.' + market['id'] + '.depth.size_' + self.number_to_string(depth) + '.high_freq'
        if not (market['spot']):
            params['data_type'] = 'incremental'
        return await self.unsubscribe_public(market, subMessageHash, topic, params)

    def handle_order_book_snapshot(self, 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 = self.safe_string(subscription, 'symbol')
        messageHash = self.safe_string(subscription, 'messageHash')
        id = self.safe_string(message, 'id')
        lastTimestamp = self.safe_integer(subscription, 'lastTimestamp')
        try:
            orderbook = self.orderbooks[symbol]
            data = self.safe_value(message, 'data')
            messages = orderbook.cache
            firstMessage = self.safe_value(messages, 0, {})
            snapshot = self.parse_order_book(data, symbol)
            tick = self.safe_value(firstMessage, 'tick')
            sequence = self.safe_integer(tick, 'prevSeqNum')
            nonce = self.safe_integer(data, 'seqNum')
            snapshot['nonce'] = nonce
            snapshotTimestamp = self.safe_integer(message, 'ts')
            subscription['lastTimestamp'] = snapshotTimestamp
            snapshotLimit = self.safe_integer(subscription, 'limit')
            snapshotOrderBook = self.order_book(snapshot, snapshotLimit)
            client.resolve(snapshotOrderBook, id)
            if (sequence is None) or (nonce < sequence):
                maxAttempts = self.handle_option('watchOrderBook', 'maxRetries', 3)
                numAttempts = self.safe_integer(subscription, 'numAttempts', 0)
                # retry to synchronize if we have not reached maxAttempts yet
                if numAttempts < maxAttempts:
                    # safety guard
                    if messageHash in client.subscriptions:
                        numAttempts = self.sum(numAttempts, 1)
                        delayTime = self.sum(1000, lastTimestamp - snapshotTimestamp)
                        subscription['numAttempts'] = numAttempts
                        client.subscriptions[messageHash] = subscription
                        self.delay(delayTime, self.watch_order_book_snapshot, client, message, subscription)
                else:
                    # raise upon failing to synchronize in maxAttempts
                    raise InvalidNonce(self.id + ' failed to synchronize WebSocket feed with the snapshot for symbol ' + symbol + ' in ' + str(maxAttempts) + ' attempts')
            else:
                orderbook.reset(snapshot)
                # unroll the accumulated deltas
                for i in range(0, len(messages)):
                    self.handle_order_book_message(client, messages[i])
                orderbook.cache = []
                self.orderbooks[symbol] = orderbook
                client.resolve(orderbook, messageHash)
        except Exception as e:
            del client.subscriptions[messageHash]
            del self.orderbooks[symbol]
            client.reject(e, messageHash)

    async def watch_order_book_snapshot(self, client, message, subscription):
        messageHash = self.safe_string(subscription, 'messageHash')
        symbol = self.safe_string(subscription, 'symbol')
        limit = self.safe_integer(subscription, 'limit')
        timestamp = self.safe_integer(message, 'ts')
        params = self.safe_value(subscription, 'params')
        attempts = self.safe_integer(subscription, 'numAttempts', 0)
        market = self.market(symbol)
        url = self.get_url_by_market_type(market['type'], market['linear'], False, True)
        requestId = self.request_id()
        request: dict = {
            'req': messageHash,
            'id': requestId,
        }
        # self is a temporary subscription by a specific requestId
        # it has a very short lifetime until the snapshot is received over ws
        snapshotSubscription: dict = {
            'id': requestId,
            'messageHash': messageHash,
            'symbol': symbol,
            'limit': limit,
            'params': params,
            'numAttempts': attempts,
            'lastTimestamp': timestamp,
            'method': self.handle_order_book_snapshot,
        }
        try:
            orderbook = await self.watch(url, requestId, request, requestId, snapshotSubscription)
            return orderbook.limit()
        except Exception as e:
            del client.subscriptions[messageHash]
            client.reject(e, messageHash)
        return None

    def handle_delta(self, bookside, delta):
        price = self.safe_float(delta, 0)
        amount = self.safe_float(delta, 1)
        bookside.store(price, amount)

    def handle_deltas(self, bookside, deltas):
        for i in range(0, len(deltas)):
            self.handle_delta(bookside, deltas[i])

    def handle_order_book_message(self, client: Client, message):
        # 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":{
        #             "asks":[],
        #             "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":{
        #             "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 = self.safe_value(message, 'ch')
        parts = ch.split('.')
        marketId = self.safe_string(parts, 1)
        market = self.safe_market(marketId)
        symbol = market['symbol']
        orderbook = self.orderbooks[symbol]
        tick = self.safe_value(message, 'tick', {})
        seqNum = self.safe_integer(tick, 'seqNum')
        prevSeqNum = self.safe_integer(tick, 'prevSeqNum')
        event = self.safe_string(tick, 'event')
        version = self.safe_integer(tick, 'version')
        timestamp = self.safe_integer(message, 'ts')
        if event == 'snapshot':
            snapshot = self.parse_order_book(tick, symbol, timestamp)
            orderbook.reset(snapshot)
            orderbook['nonce'] = version
        if (prevSeqNum is not None) and prevSeqNum > orderbook['nonce']:
            checksum = self.handle_option('watchOrderBook', 'checksum', True)
            if checksum:
                raise ChecksumError(self.id + ' ' + self.orderbook_checksum_message(symbol))
        spotConditon = market['spot'] and (prevSeqNum == orderbook['nonce'])
        nonSpotCondition = market['contract'] and (version - 1 == orderbook['nonce'])
        if spotConditon or nonSpotCondition:
            asks = self.safe_value(tick, 'asks', [])
            bids = self.safe_value(tick, 'bids', [])
            self.handle_deltas(orderbook['asks'], asks)
            self.handle_deltas(orderbook['bids'], bids)
            orderbook['nonce'] = seqNum if spotConditon else version
            orderbook['timestamp'] = timestamp
            orderbook['datetime'] = self.iso8601(timestamp)

    def handle_order_book(self, 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":{
        #             "asks":[],
        #             "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 = self.safe_string(message, 'ch')
        tick = self.safe_dict(message, 'tick')
        event = self.safe_string(tick, 'event')
        ch = self.safe_string(message, 'ch')
        parts = ch.split('.')
        marketId = self.safe_string(parts, 1)
        symbol = self.safe_symbol(marketId)
        if not (symbol in self.orderbooks):
            size = self.safe_string(parts, 3)
            sizeParts = size.split('_')
            limit = self.safe_integer(sizeParts, 1)
            self.orderbooks[symbol] = self.order_book({}, limit)
        orderbook = self.orderbooks[symbol]
        if (event is None) and (orderbook['nonce'] is None):
            orderbook.cache.append(message)
        else:
            self.handle_order_book_message(client, message)
            client.resolve(orderbook, messageHash)

    def handle_order_book_subscription(self, client: Client, message, subscription):
        symbol = self.safe_string(subscription, 'symbol')
        market = self.market(symbol)
        limit = self.safe_integer(subscription, 'limit')
        self.orderbooks[symbol] = self.order_book({}, limit)
        if market['spot']:
            self.spawn(self.watch_order_book_snapshot, client, message, subscription)

    async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        watches information on multiple trades made by the user

        https://www.htx.com/en-us/opend/newApiPages/?id=7ec53dd5-7773-11ed-9966-0242ac110003

        :param str symbol: unified market symbol of the market trades were made in
        :param int [since]: the earliest time in ms to fetch trades for
        :param int [limit]: the maximum number of trade structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/?id=trade-structure>`
        """
        self.check_required_credentials()
        await self.load_markets()
        type = None
        marketId = '*'  # wildcard
        market = None
        messageHash = None
        channel = None
        trades = None
        subType = None
        if symbol is not None:
            market = self.market(symbol)
            symbol = market['symbol']
            type = market['type']
            subType = 'linear' if market['linear'] else 'inverse'
            marketId = market['lowercaseId']
        else:
            type = self.safe_string(self.options, 'defaultType', 'spot')
            type = self.safe_string(params, 'type', type)
            subType = self.safe_string_2(self.options, 'subType', 'defaultSubType', 'linear')
            subType = self.safe_string(params, 'subType', subType)
            params = self.omit(params, ['type', 'subType'])
        if type == 'spot':
            mode = None
            if mode is None:
                mode = self.safe_string_2(self.options, 'watchMyTrades', 'mode', '0')
                mode = self.safe_string(params, 'mode', mode)
                params = self.omit(params, 'mode')
            messageHash = 'trade.clearing' + '#' + marketId + '#' + mode
            channel = messageHash
        else:
            channelAndMessageHash = self.get_order_channel_and_message_hash(type, subType, market, params)
            channel = self.safe_string(channelAndMessageHash, 0)
            orderMessageHash = self.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 = await self.subscribe_private(channel, messageHash, type, subType, params)
        if self.newUpdates:
            limit = trades.getLimit(symbol, limit)
        return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)

    def get_order_channel_and_message_hash(self, type, subType, market=None, params={}):
        messageHash = None
        channel = None
        orderType = self.safe_string(self.options, 'orderType', 'orders')  # orders or matchOrders
        orderType = self.safe_string(params, 'orderType', orderType)
        params = self.omit(params, 'orderType')
        marketCode = market['lowercaseId'].lower() if (market is not None) else None
        baseId = market['baseId'] if (market is not None) else None
        prefix = orderType
        messageHash = prefix
        if subType == 'linear':
            # USDT Margined Contracts Example: LTC/USDT:USDT
            marginMode = self.safe_string(params, 'margin', 'cross')
            marginPrefix = prefix + '_cross' if (marginMode == 'cross') else prefix
            messageHash = marginPrefix
            if marketCode is not None:
                messageHash += '.' + marketCode
                channel = messageHash
            else:
                channel = marginPrefix + '.' + '*'
        elif type == 'future':
            # inverse futures Example: BCH/USD:BCH-220408
            if baseId is not None:
                channel = prefix + '.' + baseId.lower()
                messageHash = channel
            else:
                channel = prefix + '.' + '*'
        else:
            # inverse swaps: Example: BTC/USD:BTC
            if marketCode is not None:
                channel = prefix + '.' + marketCode
                messageHash = channel
            else:
                channel = prefix + '.' + '*'
        return [channel, messageHash]

    async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        watches information on multiple orders made by the user

        https://www.htx.com/en-us/opend/newApiPages/?id=7ec53c8f-7773-11ed-9966-0242ac110003

        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `order structures <https://docs.ccxt.com/?id=order-structure>`
        """
        await self.load_markets()
        type = None
        subType = None
        market = None
        suffix = '*'  # wildcard
        if symbol is not None:
            market = self.market(symbol)
            symbol = market['symbol']
            type = market['type']
            suffix = market['lowercaseId']
            subType = 'linear' if market['linear'] else 'inverse'
        else:
            type = self.safe_string(self.options, 'defaultType', 'spot')
            type = self.safe_string(params, 'type', type)
            subType = self.safe_string_2(self.options, 'subType', 'defaultSubType', 'linear')
            subType = self.safe_string(params, 'subType', subType)
            params = self.omit(params, ['type', 'subType'])
        messageHash = None
        channel = None
        if type == 'spot':
            messageHash = 'orders' + '#' + suffix
            channel = messageHash
        else:
            channelAndMessageHash = self.get_order_channel_and_message_hash(type, subType, market, params)
            channel = self.safe_string(channelAndMessageHash, 0)
            messageHash = self.safe_string(channelAndMessageHash, 1)
        orders = await self.subscribe_private(channel, messageHash, type, subType, params)
        if self.newUpdates:
            limit = orders.getLimit(symbol, limit)
        return self.filter_by_since_limit(orders, since, limit, 'timestamp', True)

    def handle_order(self, client: Client, message):
        #
        # spot
        #
        #     for new order creation
        #
        #     {
        #         "action":"push",
        #         "ch":"orders#btcusdt",  # or "orders#*" for global subscriptions
        #         "data": {
        #             "orderStatus": "submitted",
        #             "eventType": "creation",
        #             "totalTradeAmount": 0  # for "submitted" order status
        #             "orderCreateTime": 1645116048355,  # only when `submitted` status
        #             "orderSource": "spot-web",
        #             "accountId": 44234548,
        #             "orderPrice": "100",
        #             "orderSize": "0.05",
        #             "symbol": "ethusdt",
        #             "type": "buy-limit",
        #             "orderId": "478861479986886",
        #             "clientOrderId": '',
        #         }
        #     }
        #
        #     for filled order, additional fields are present:
        #
        #             "orderStatus": "filled",
        #             "eventType": "trade",
        #             "totalTradeAmount": "5.9892649859",
        #             "tradePrice": "0.676669",
        #             "tradeVolume": "8.8511",
        #             "tradeTime": 1760427775894,
        #             "aggressor": False,
        #             "execAmt": "8.8511",
        #             "tradeId": 100599712781,
        #             "remainAmt": "0",
        #
        # 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": [
        #       {
        #         "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 = self.safe_string_2(message, 'ch', 'topic')
        data = self.safe_value(message, 'data')
        marketId = self.safe_string(message, 'contract_code')
        if marketId is None:
            marketId = self.safe_string(data, 'symbol')
        market = self.safe_market(marketId)
        parsedOrder = None
        if data is not None:
            # spot updates
            eventType = self.safe_string(data, 'eventType')
            if eventType == 'trade':
                # when a spot order is filled we get an update message
                # with the trade info
                parsedTrade = self.parse_order_trade(data, market)
                # inject trade in existing order by faking an order object
                orderId = self.safe_string(parsedTrade, 'order')
                trades = [parsedTrade]
                status = self.parse_order_status(self.safe_string_2(data, 'orderStatus', 'status', 'closed'))
                filled = self.safe_string(data, 'execAmt')
                remaining = self.safe_string(data, 'remainAmt')
                order: dict = {
                    'id': orderId,
                    'trades': trades,
                    'status': status,
                    'symbol': market['symbol'],
                    'filled': self.parse_number(filled),
                    'remaining': self.parse_number(remaining),
                    'price': self.safe_number(data, 'orderPrice'),
                    'amount': self.safe_number(data, 'orderSize'),
                    'info': data,
                }
                parsedOrder = order
            else:
                parsedOrder = self.parse_ws_order(data, market)
        else:
            # contract branch
            parsedOrder = self.parse_ws_order(message, market)
            rawTrades = self.safe_value(message, 'trade', [])
            tradesLength = len(rawTrades)
            if tradesLength > 0:
                tradesObject: dict = {
                    'trades': rawTrades,
                    'ch': messageHash,
                    'symbol': marketId,
                }
                # inject order params in every trade
                extendTradeParams: dict = {
                    'order': self.safe_string(parsedOrder, 'id'),
                    'type': self.safe_string(parsedOrder, 'type'),
                    'side': self.safe_string(parsedOrder, 'side'),
                }
                # trades arrive inside an order update
                # we're forwarding them to handleMyTrade
                # so they can be properly resolved
                self.handle_my_trade(client, tradesObject, extendTradeParams)
        if self.orders is None:
            limit = self.safe_integer(self.options, 'ordersLimit', 1000)
            self.orders = ArrayCacheBySymbolById(limit)
        cachedOrders = self.orders
        cachedOrders.append(parsedOrder)
        client.resolve(self.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 = messageHash.replace('.' + market['lowercaseId'], '')
        lowerCaseBaseId = self.safe_string_lower(market, 'baseId')
        genericMessageHash = genericMessageHash.replace('.' + lowerCaseBaseId, '')
        client.resolve(self.orders, genericMessageHash)

    def parse_ws_order(self, order, market=None):
        #
        # 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": [
        #             {
        #                 "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":[],
        #         "canceled_at":0,
        #         "fee_asset":"ADA",
        #         "uid":"123456789",
        #         "liquidation_type":"0",
        #         "is_tpsl": 0,
        #         "real_profit": 0
        #     }
        #
        lastTradeTimestamp = self.safe_integer_2(order, 'lastActTime', 'ts')
        created = self.safe_integer(order, 'orderCreateTime')
        marketId = self.safe_string_2(order, 'contract_code', 'symbol')
        market = self.safe_market(marketId, market)
        symbol = self.safe_symbol(marketId, market)
        amount = self.safe_string_2(order, 'orderSize', 'volume')
        status = self.parse_order_status(self.safe_string_2(order, 'orderStatus', 'status'))
        id = self.safe_string_2(order, 'orderId', 'order_id')
        clientOrderId = self.safe_string_2(order, 'clientOrderId', 'client_order_id')
        price = self.safe_string_2(order, 'orderPrice', 'price')
        filled = self.safe_string(order, 'execAmt')
        typeSide = self.safe_string(order, 'type')
        feeCost = self.safe_string(order, 'fee')
        fee = None
        if feeCost is not None:
            feeCurrencyId = self.safe_string(order, 'fee_asset')
            fee = {
                'cost': feeCost,
                'currency': self.safe_currency_code(feeCurrencyId),
            }
        avgPrice = self.safe_string(order, 'trade_avg_price')
        rawTrades = self.safe_value(order, 'trade')
        typeSideParts = []
        if typeSide is not None:
            typeSideParts = typeSide.split('-')
        type = self.safe_string_lower(typeSideParts, 1)
        if type is None:
            type = self.safe_string(order, 'order_price_type')
        side = self.safe_string_lower(typeSideParts, 0)
        if side is None:
            side = self.safe_string(order, 'direction')
        cost = self.safe_string(order, 'orderValue')
        return self.safe_order({
            'info': order,
            'id': id,
            'clientOrderId': clientOrderId,
            'timestamp': created,
            'datetime': self.iso8601(created),
            'lastTradeTimestamp': lastTradeTimestamp,
            'status': status,
            'symbol': symbol,
            'type': type,
            'timeInForce': None,
            'postOnly': None,
            'side': side,
            'price': price,
            'amount': amount,
            'filled': filled,
            'remaining': None,
            'cost': cost,
            'fee': fee,
            'average': avgPrice,
            'trades': rawTrades,
        }, market)

    def parse_order_trade(self, trade, market=None):
        # 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 = self.safe_market(None, market)
        symbol = market['symbol']
        tradeId = self.safe_string(trade, 'tradeId')
        price = self.safe_string(trade, 'tradePrice')
        amount = self.safe_string(trade, 'tradeVolume')
        order = self.safe_string(trade, 'orderId')
        timestamp = self.safe_integer(trade, 'tradeTime')
        type = self.safe_string(trade, 'type')
        side = None
        if type is not None:
            typeParts = type.split('-')
            side = typeParts[0]
            type = typeParts[1]
        aggressor = self.safe_value(trade, 'aggressor')
        takerOrMaker = None
        if aggressor is not None:
            takerOrMaker = 'taker' if aggressor else 'maker'
        return self.safe_trade({
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'id': tradeId,
            'order': order,
            'type': type,
            'takerOrMaker': takerOrMaker,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': None,
            'fee': None,
        }, market)

    async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
        """

        https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7de1c-77b5-11ed-9966-0242ac110003
        https://www.huobi.com/en-in/opend/newApiPages/?id=8cb7df0f-77b5-11ed-9966-0242ac110003
        https://www.huobi.com/en-in/opend/newApiPages/?id=28c34a7d-77ae-11ed-9966-0242ac110003
        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 str[] [symbols]: list of unified market symbols
        :param int [since]: timestamp in ms of the earliest position to fetch
        :param int [limit]: the maximum number of positions to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
        """
        await self.load_markets()
        market = None
        messageHash = ''
        if not self.is_empty(symbols):
            market = self.get_market_from_symbols(symbols)
            messageHash = '::' + ','.join(symbols)
        type = None
        subType = None
        if market is not None:
            type = market['type']
            subType = 'linear' if market['linear'] else 'inverse'
        else:
            type, params = self.handle_market_type_and_params('watchPositions', market, params)
            if type == 'spot':
                type = 'future'
            subType, params = self.handle_option_and_params(params, 'watchPositions', 'subType', subType)
        symbols = self.market_symbols(symbols)
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('watchPositions', params, 'cross')
        isLinear = (subType == 'linear')
        url = self.get_url_by_market_type(type, isLinear, True)
        messageHash = marginMode + ':positions' + messageHash
        channel = 'positions_cross.*' if (marginMode == 'cross') else 'positions.*'
        newPositions = await self.subscribe_private(channel, messageHash, type, subType, params)
        if self.newUpdates:
            return newPositions
        return self.filter_by_symbols_since_limit(self.positions[url][marginMode], symbols, since, limit, False)

    def handle_positions(self, client, message):
        #
        #    {
        #        op: 'notify',
        #        topic: 'positions_cross',
        #        ts: 1696767149650,
        #        event: 'snapshot',
        #        data: [
        #          {
        #            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 = self.safe_string(message, 'topic', '')
        marginMode = 'cross' if (topic == 'positions_cross') else 'isolated'
        if self.positions is None:
            self.positions = {}
        clientPositions = self.safe_value(self.positions, url)
        if clientPositions is None:
            self.positions[url] = {}
        clientMarginModePositions = self.safe_value(clientPositions, marginMode)
        if clientMarginModePositions is None:
            self.positions[url][marginMode] = ArrayCacheBySymbolBySide()
        cache = self.positions[url][marginMode]
        rawPositions = self.safe_value(message, 'data', [])
        newPositions = []
        timestamp = self.safe_integer(message, 'ts')
        for i in range(0, len(rawPositions)):
            rawPosition = rawPositions[i]
            position = self.parse_position(rawPosition)
            position['timestamp'] = timestamp
            position['datetime'] = self.iso8601(timestamp)
            newPositions.append(position)
            cache.append(position)
        messageHashes = self.find_message_hashes(client, marginMode + ':positions::')
        for i in range(0, len(messageHashes)):
            messageHash = messageHashes[i]
            parts = messageHash.split('::')
            symbolsString = parts[1]
            symbols = symbolsString.split(',')
            positions = self.filter_by_array(newPositions, 'symbol', symbols, False)
            if not self.is_empty(positions):
                client.resolve(positions, messageHash)
        client.resolve(newPositions, marginMode + ':positions')

    async def watch_balance(self, params={}) -> Balances:
        """
        watch balance and get the amount of funds available for trading or funds locked in orders

        https://www.htx.com/en-us/opend/newApiPages/?id=7ec52e28-7773-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=10000084-77b7-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=8cb7dcca-77b5-11ed-9966-0242ac110003
        https://www.htx.com/en-us/opend/newApiPages/?id=28c34995-77ae-11ed-9966-0242ac110003

        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `balance structure <https://docs.ccxt.com/?id=balance-structure>`
        """
        type = None
        type, params = self.handle_market_type_and_params('watchBalance', None, params)
        subType = None
        subType, params = self.handle_sub_type_and_params('watchBalance', None, params, 'linear')
        isUnifiedAccount = self.safe_value_2(params, 'isUnifiedAccount', 'unified', False)
        params = self.omit(params, ['isUnifiedAccount', 'unified'])
        await self.load_markets()
        messageHash = None
        channel = None
        marginMode = None
        if type == 'spot':
            mode = self.safe_string_2(self.options, 'watchBalance', 'mode', '2')
            mode = self.safe_string(params, 'mode', mode)
            messageHash = 'accounts.update' + '#' + mode
            channel = messageHash
        else:
            symbol = self.safe_string(params, 'symbol')
            currency = self.safe_string(params, 'currency')
            market = self.market(symbol) if (symbol is not None) else None
            currencyCode = self.currency(currency) if (currency is not None) else None
            marginMode = self.safe_string(params, 'margin', 'cross')
            params = self.omit(params, ['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 = prefix + '_cross' if (marginMode == 'cross') else prefix
                    messageHash = prefix
                    if marginMode == 'isolated':
                        # isolated margin only allows filtering by symbol3
                        if symbol is not None:
                            messageHash += '.' + market['id']
                            channel = messageHash
                        else:
                            # subscribe to all
                            channel = prefix + '.' + '*'
                    else:
                        # cross margin
                        if currencyCode is not None:
                            channel = prefix + '.' + currencyCode['id']
                            messageHash = channel
                        else:
                            # subscribe to all
                            channel = prefix + '.' + '*'
            elif type == 'future':
                # inverse futures account
                if currencyCode is not None:
                    messageHash += '.' + currencyCode['id']
                    channel = messageHash
                else:
                    # subscribe to all
                    channel = prefix + '.' + '*'
            else:
                # inverse swaps account
                if market is not None:
                    messageHash += '.' + market['id']
                    channel = messageHash
                else:
                    # subscribe to all
                    channel = prefix + '.' + '*'
        subscriptionParams: dict = {
            '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 await self.subscribe_private(channel, messageHash, type, subType, params, subscriptionParams)

    def handle_balance(self, 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":[
        #             {
        #                 "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":[
        #             {
        #                 "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":[
        #                     {
        #                         "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":[],
        #             }
        #         ]
        #     }
        #
        # inverse future
        #
        #     {
        #         "op":"notify",
        #         "topic":"accounts.ada",
        #         "ts":1604388667226,
        #         "event":"order.match",
        #         "data":[
        #             {
        #                 "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 = self.safe_string(message, 'ch')
        data = self.safe_value(message, 'data', [])
        timestamp = self.safe_integer(data, 'changeTime', self.safe_integer(message, 'ts'))
        self.balance['timestamp'] = timestamp
        self.balance['datetime'] = self.iso8601(timestamp)
        self.balance['info'] = data
        if channel is not None:
            # spot balance
            currencyId = self.safe_string(data, 'currency')
            code = self.safe_currency_code(currencyId)
            account = self.account()
            account['free'] = self.safe_string(data, 'available')
            account['total'] = self.safe_string(data, 'balance')
            self.balance[code] = account
            self.balance = self.safe_balance(self.balance)
            client.resolve(self.balance, channel)
        else:
            # contract balance
            dataLength = len(data)
            if dataLength == 0:
                return
            first = self.safe_value(data, 0, {})
            topic = self.safe_string(message, 'topic')
            splitTopic = topic.split('.')
            messageHash = self.safe_string(splitTopic, 0)
            subscription = self.safe_value_2(client.subscriptions, messageHash, messageHash + '.*')
            if subscription is None:
                # 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 = self.safe_string_2(first, 'margin_asset', 'symbol')
                messageHash += '.' + currencyId.lower()
                subscription = self.safe_value(client.subscriptions, messageHash)
            type = self.safe_string(subscription, 'type')
            subType = self.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": [],
                #     "cross_future": [],
                #     "isolated_swap": []
                # }
                marginAsset = self.safe_string(first, 'margin_asset')
                code = self.safe_currency_code(marginAsset)
                marginFrozen = self.safe_string(first, 'margin_frozen')
                unifiedAccount = self.account()
                unifiedAccount['free'] = self.safe_string(first, 'withdraw_available')
                unifiedAccount['used'] = marginFrozen
                self.balance[code] = unifiedAccount
                self.balance = self.safe_balance(self.balance)
                client.resolve(self.balance, 'accounts_unify')
            elif subType == 'linear':
                margin = self.safe_string(subscription, 'margin')
                if margin == 'cross':
                    fieldName = 'futures_contract_detail' if (type == 'future') else 'contract_detail'
                    balances = self.safe_value(first, fieldName, [])
                    balancesLength = len(balances)
                    if balancesLength > 0:
                        for i in range(0, len(balances)):
                            balance = balances[i]
                            marketId = self.safe_string_2(balance, 'contract_code', 'margin_account')
                            market = self.safe_market(marketId)
                            currencyId = self.safe_string(balance, 'margin_asset')
                            currency = self.safe_currency(currencyId)
                            code = self.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 is not None:
                                account = self.account()
                                account['free'] = self.safe_string_2(balance, 'margin_balance', 'margin_available')
                                account['used'] = self.safe_string(balance, 'margin_frozen')
                                accountsByCode: dict = {}
                                accountsByCode[code] = account
                                symbol = market['symbol']
                                self.balance[symbol] = self.safe_balance(accountsByCode)
                else:
                    # isolated margin
                    for i in range(0, len(data)):
                        isolatedBalance = data[i]
                        account = self.account()
                        account['free'] = self.safe_string(isolatedBalance, 'margin_balance', 'margin_available')
                        account['used'] = self.safe_string(isolatedBalance, 'margin_frozen')
                        currencyId = self.safe_string_2(isolatedBalance, 'margin_asset', 'symbol')
                        code = self.safe_currency_code(currencyId)
                        self.balance[code] = account
                        self.balance = self.safe_balance(self.balance)
            else:
                # inverse branch
                for i in range(0, len(data)):
                    balance = data[i]
                    currencyId = self.safe_string(balance, 'symbol')
                    code = self.safe_currency_code(currencyId)
                    account = self.account()
                    account['free'] = self.safe_string(balance, 'margin_available')
                    account['used'] = self.safe_string(balance, 'margin_frozen')
                    self.balance[code] = account
                    self.balance = self.safe_balance(self.balance)
            client.resolve(self.balance, messageHash)

    def handle_subscription_status(self, 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 = self.safe_string(message, 'id')
        subscriptionsById = self.index_by(client.subscriptions, 'id')
        subscription = self.safe_dict(subscriptionsById, id)
        if subscription is not None:
            method = self.safe_value(subscription, 'method')
            if method is not None:
                method(client, message, subscription)
                # return; commented out to clean up
            # clean up
            if id in client.subscriptions:
                del client.subscriptions[id]
        if 'unsubbed' in message:
            self.handle_un_subscription(client, subscription)

    def handle_un_subscription(self, client: Client, subscription: dict):
        messageHashes = self.safe_list(subscription, 'messageHashes', [])
        subMessageHashes = self.safe_list(subscription, 'subMessageHashes', [])
        for i in range(0, len(messageHashes)):
            unsubHash = messageHashes[i]
            subHash = subMessageHashes[i]
            self.clean_unsubscription(client, subHash, unsubHash)
        self.clean_cache(subscription)

    def handle_system_status(self, client: Client, message):
        #
        # todo: answer the question whether handleSystemStatus should be renamed
        # and unified for any usage pattern that
        # involves system status and maintenance updates
        #
        #     {
        #         "id": "1578090234088",  # connectId
        #         "type": "welcome",
        #     }
        #
        return message

    def handle_subject(self, 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":{
        #             "asks":[],
        #             "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":{
        #             "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 = self.safe_value(message, 'ch', '')
        parts = ch.split('.')
        type = self.safe_string(parts, 0)
        if type == 'market':
            methodName = self.safe_string(parts, 2)
            methods: dict = {
                'depth': self.handle_order_book,
                'mbp': self.handle_order_book,
                'detail': self.handle_ticker,
                'bbo': self.handle_ticker,
                'ticker': self.handle_ticker,
                'trade': self.handle_trades,
                'kline': self.handle_ohlcv,
            }
            method = self.safe_value(methods, methodName)
            if method is not None:
                method(client, message)
                return
        # private spot subjects
        privateParts = ch.split('#')
        privateType = self.safe_string(privateParts, 0, '')
        if privateType == 'trade.clearing':
            self.handle_my_trade(client, message)
            return
        if privateType.find('accounts.update') >= 0:
            self.handle_balance(client, message)
            return
        if privateType == 'orders':
            self.handle_order(client, message)
            return
        # private contract subjects
        op = self.safe_string(message, 'op')
        if op == 'notify':
            topic = self.safe_string(message, 'topic', '')
            if topic.find('orders') >= 0:
                self.handle_order(client, message)
            if topic.find('account') >= 0:
                self.handle_balance(client, message)
            if topic.find('positions') >= 0:
                self.handle_positions(client, message)

    async def pong(self, client, message):
        #
        #     {ping: 1583491673714}
        #     {action: "ping", data: {ts: 1645108204665}}
        #     {op: "ping", ts: "1645202800015"}
        #
        try:
            ping = self.safe_integer(message, 'ping')
            if ping is not None:
                await client.send({'pong': ping})
                return
            action = self.safe_string(message, 'action')
            if action == 'ping':
                data = self.safe_value(message, 'data')
                pingTs = self.safe_integer(data, 'ts')
                await client.send({'action': 'pong', 'data': {'ts': pingTs}})
                return
            op = self.safe_string(message, 'op')
            if op == 'ping':
                pingTs = self.safe_integer(message, 'ts')
                await client.send({'op': 'pong', 'ts': pingTs})
        except Exception as e:
            error = NetworkError(self.id + ' pong failed ' + self.exception_message(e))
            client.reset(error)

    def handle_ping(self, client: Client, message):
        self.spawn(self.pong, client, message)

    def handle_authenticate(self, client: Client, message):
        #
        # spot
        #
        #     {
        #         "action": "req",
        #         "code": 200,
        #         "ch": "auth",
        #         "data": {}
        #     }
        #
        # non spot
        #
        #    {
        #        "op": "auth",
        #        "type": "api",
        #        "err-code": 0,
        #        "ts": 1645200307319,
        #        "data": {"user-id": "35930539"}
        #    }
        #
        promise = client.futures['auth']
        promise.resolve(message)

    def handle_error_message(self, 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 = self.safe_string(message, 'status')
        if status == 'error':
            id = self.safe_string(message, 'id')
            subscriptionsById = self.index_by(client.subscriptions, 'id')
            subscription = self.safe_value(subscriptionsById, id)
            if subscription is not None:
                errorCode = self.safe_string(message, 'err-code')
                try:
                    self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], errorCode, self.json(message))
                    raise ExchangeError(self.json(message))
                except Exception as e:
                    messageHash = self.safe_string(subscription, 'messageHash')
                    client.reject(e, messageHash)
                    client.reject(e, id)
                    if id in client.subscriptions:
                        del client.subscriptions[id]
            return False
        code = self.safe_string_2(message, 'code', 'err-code')
        if code is not None and ((code != '200') and (code != '0')):
            feedback = self.id + ' ' + self.json(message)
            try:
                self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, feedback)
                raise ExchangeError(feedback)
            except Exception as e:
                if isinstance(e, AuthenticationError):
                    client.reject(e, 'auth')
                    method = 'auth'
                    if method in client.subscriptions:
                        del client.subscriptions[method]
                    return False
                else:
                    client.reject(e)
        return True

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

    def handle_my_trade(self, client: Client, message, extendParams={}):
        #
        # 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": [
        #             {
        #                 "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 self.myTrades is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            self.myTrades = ArrayCacheBySymbolById(limit)
        cachedTrades = self.myTrades
        messageHash = self.safe_string(message, 'ch')
        if messageHash is not None:
            data = self.safe_value(message, 'data')
            if data is not None:
                parsed = self.parse_ws_trade(data)
                symbol = self.safe_string(parsed, 'symbol')
                if symbol is not None:
                    cachedTrades.append(parsed)
                    client.resolve(self.myTrades, messageHash)
            else:
                # self trades object is artificially created
                # in handleOrder
                rawTrades = self.safe_value(message, 'trades', [])
                marketId = self.safe_value(message, 'symbol')
                market = self.market(marketId)
                for i in range(0, len(rawTrades)):
                    trade = rawTrades[i]
                    parsedTrade = self.parse_trade(trade, market)
                    # add extra params(side, type, ...) coming from the order
                    parsedTrade = self.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(self.myTrades, tradesHash)
                # when we make an global order sub we have to send the channel like self
                # 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 self is a global sub, our messageHash does not specify any symbol(ex: orders_cross:trade)
                # so we must remove it
                genericOrderHash = messageHash.replace('.' + market['lowercaseId'], '')
                lowerCaseBaseId = self.safe_string_lower(market, 'baseId')
                genericOrderHash = genericOrderHash.replace('.' + lowerCaseBaseId, '')
                genericTradesHash = genericOrderHash + ':' + 'trade'
                client.resolve(self.myTrades, genericTradesHash)

    def parse_ws_trade(self, trade, market=None):
        # 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 = self.safe_symbol(self.safe_string(trade, 'symbol'))
        side = self.safe_string_2(trade, 'side', 'orderSide')
        tradeId = self.safe_string(trade, 'tradeId')
        price = self.safe_string(trade, 'tradePrice')
        amount = self.safe_string(trade, 'tradeVolume')
        order = self.safe_string(trade, 'orderId')
        timestamp = self.safe_integer(trade, 'tradeTime')
        market = self.market(symbol)
        orderType = self.safe_string(trade, 'orderType')
        aggressor = self.safe_value(trade, 'aggressor')
        takerOrMaker = None
        if aggressor is not None:
            takerOrMaker = 'taker' if aggressor else 'maker'
        type = None
        orderTypeParts = []
        if orderType is not None:
            orderTypeParts = orderType.split('-')
            type = self.safe_string(orderTypeParts, 1)
        fee = None
        feeCurrency = self.safe_currency_code(self.safe_string(trade, 'feeCurrency'))
        if feeCurrency is not None:
            fee = {
                'cost': self.safe_string(trade, 'transactFee'),
                'currency': feeCurrency,
            }
        return self.safe_trade({
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'id': tradeId,
            'order': order,
            'type': type,
            'takerOrMaker': takerOrMaker,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': None,
            'fee': fee,
        }, market)

    def get_url_by_market_type(self, type, isLinear=True, isPrivate=False, isFeed=False):
        api = self.safe_string(self.options, 'api', 'api')
        hostname: dict = {'hostname': self.hostname}
        hostnameURL = None
        url = None
        if type == 'spot':
            if isPrivate:
                hostnameURL = self.urls['api']['ws'][api]['spot']['private']
            else:
                if isFeed:
                    hostnameURL = self.urls['api']['ws'][api]['spot']['feed']
                else:
                    hostnameURL = self.urls['api']['ws'][api]['spot']['public']
            url = self.implode_params(hostnameURL, hostname)
        else:
            baseUrl = self.urls['api']['ws'][api][type]
            subTypeUrl = baseUrl['linear'] if isLinear else baseUrl['inverse']
            url = subTypeUrl['private'] if isPrivate else subTypeUrl['public']
        return url

    async def subscribe_public(self, url, symbol, messageHash, method=None, params={}):
        requestId = self.request_id()
        request: dict = {
            'sub': messageHash,
            'id': requestId,
        }
        subscription: dict = {
            'id': requestId,
            'messageHash': messageHash,
            'symbol': symbol,
            'params': params,
        }
        if method is not None:
            subscription['method'] = method
        return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription)

    async def unsubscribe_public(self, market: Market, subMessageHash: str, topic: str, params={}):
        requestId = self.request_id()
        request: dict = {
            'unsub': subMessageHash,
            'id': requestId,
        }
        messageHash = 'unsubscribe::' + subMessageHash
        isFeed = (topic == 'orderbook')
        url = self.get_url_by_market_type(market['type'], market['linear'], False, isFeed)
        subscription: dict = {
            'unsubscribe': True,
            'id': requestId,
            'subMessageHashes': [subMessageHash],
            'messageHashes': [messageHash],
            'symbols': [market['symbol']],
            'topic': topic,
        }
        symbolsAndTimeframes = self.safe_list(params, 'symbolsAndTimeframes')
        if symbolsAndTimeframes is not None:
            subscription['symbolsAndTimeframes'] = symbolsAndTimeframes
            params = self.omit(params, 'symbolsAndTimeframes')
        return await self.watch(url, messageHash, self.extend(request, params), messageHash, subscription)

    async def subscribe_private(self, channel, messageHash, type, subtype, params={}, subscriptionParams={}):
        requestId = self.request_id()
        subscription: dict = {
            'id': requestId,
            'messageHash': messageHash,
            'params': params,
        }
        extendedSubsription = self.extend(subscription, subscriptionParams)
        request = None
        if type == 'spot':
            request = {
                'action': 'sub',
                'ch': channel,
            }
        else:
            request = {
                'op': 'sub',
                'topic': channel,
                'cid': requestId,
            }
        isLinear = subtype == 'linear'
        url = self.get_url_by_market_type(type, isLinear, True)
        hostname = self.urls['hostnames']['spot'] if (type == 'spot') else self.urls['hostnames']['contract']
        authParams: dict = {
            'type': type,
            'url': url,
            'hostname': hostname,
        }
        await self.authenticate(authParams)
        return await self.watch(url, messageHash, self.extend(request, params), channel, extendedSubsription)

    async def authenticate(self, params={}):
        url = self.safe_string(params, 'url')
        hostname = self.safe_string(params, 'hostname')
        type = self.safe_string(params, 'type')
        if url is None or hostname is None or type is None:
            raise ArgumentsRequired(self.id + ' authenticate requires a url, hostname and type argument')
        self.check_required_credentials()
        messageHash = 'auth'
        relativePath = url.replace('wss://' + hostname, '')
        client = self.client(url)
        future = client.reusableFuture(messageHash)
        authenticated = self.safe_value(client.subscriptions, messageHash)
        if authenticated is None:
            timestamp = self.ymdhms(self.milliseconds(), 'T')
            signatureParams = None
            if type == 'spot':
                signatureParams = {
                    'accessKey': self.apiKey,
                    'signatureMethod': 'HmacSHA256',
                    'signatureVersion': '2.1',
                    'timestamp': timestamp,
                }
            else:
                signatureParams = {
                    'AccessKeyId': self.apiKey,
                    'SignatureMethod': 'HmacSHA256',
                    'SignatureVersion': '2',
                    'Timestamp': timestamp,
                }
            signatureParams = self.keysort(signatureParams)
            auth = self.urlencode(signatureParams, True)  # True required in go
            payload = "\n".join(['GET', hostname, relativePath, auth])  # eslint-disable-line quotes
            signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha256, 'base64')
            request = None
            if type == 'spot':
                newParams: dict = {
                    'authType': 'api',
                    'accessKey': self.apiKey,
                    'signatureMethod': 'HmacSHA256',
                    'signatureVersion': '2.1',
                    'timestamp': timestamp,
                    'signature': signature,
                }
                request = {
                    'params': newParams,
                    'action': 'req',
                    'ch': 'auth',
                }
            else:
                request = {
                    'op': 'auth',
                    'type': 'api',
                    'AccessKeyId': self.apiKey,
                    'SignatureMethod': 'HmacSHA256',
                    'SignatureVersion': '2',
                    'Timestamp': timestamp,
                    'Signature': signature,
                }
            requestId = self.request_id()
            subscription: dict = {
                'id': requestId,
                'messageHash': messageHash,
                'params': params,
            }
            self.watch(url, messageHash, request, messageHash, subscription)
        return await future
