# -*- 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

from ccxt.pro.base.exchange import Exchange
import ccxt.async_support
from ccxt.pro.base.cache import ArrayCache, ArrayCacheBySymbolById, ArrayCacheByTimestamp
import hashlib
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 InvalidNonce


class huobi(Exchange, ccxt.async_support.huobi):

    def describe(self):
        return self.deep_extend(super(huobi, self).describe(), {
            'has': {
                'ws': True,
                'watchOrderBook': True,
                'watchOrders': True,
                'watchTickers': False,  # for now
                'watchTicker': True,
                'watchTrades': True,
                'watchMyTrades': True,
                'watchBalance': True,  # for now
                'watchOHLCV': True,
            },
            'urls': {
                'api': {
                    'ws': {
                        'api': {
                            'spot': {
                                'public': 'wss://{hostname}/ws',
                                'private': 'wss://{hostname}/ws/v2',
                            },
                            'future': {
                                'linear': {
                                    'public': 'wss://api.hbdm.com/linear-swap-ws',
                                    'private': 'wss://api.hbdm.com/linear-swap-notification',
                                },
                                'inverse': {
                                    'public': 'wss://api.hbdm.com/ws',
                                    'private': 'wss://api.hbdm.com/notification',
                                },
                            },
                            'swap': {
                                'inverse': {
                                    'public': 'wss://api.hbdm.com/swap-ws',
                                    'private': 'wss://api.hbdm.com/swap-notification',
                                },
                                'linear': {
                                    'public': 'wss://api.hbdm.com/linear-swap-ws',
                                    'private': 'wss://api.hbdm.com/linear-swap-notification',
                                },
                            },
                        },
                        # these settings work faster for clients hosted on AWS
                        'api-aws': {
                            'spot': {
                                'public': 'wss://api-aws.huobi.pro/ws',
                                'private': 'wss://api-aws.huobi.pro/ws/v2',
                            },
                            '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',
                                },
                            },
                        },
                    },
                },
            },
            'options': {
                'tradesLimit': 1000,
                'OHLCVLimit': 1000,
                'api': 'api',  # or api-aws for clients hosted on AWS
                'maxOrderBookSyncAttempts': 3,
                'ws': {
                    'gunzip': True,
                },
            },
            '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.hereltc-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}
                    },
                },
            },
        })

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

    async def watch_ticker(self, symbol, params={}):
        """
        watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
        :param str symbol: unified symbol of the market to fetch the ticker for
        :param dict params: extra parameters specific to the huobi api endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/en/latest/manual.html#ticker-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        messageHash = 'market.' + market['id'] + '.detail'
        url = self.get_url_by_market_type(market['type'], market['linear'])
        return await self.subscribe_public(url, symbol, messageHash, None, params)

    def handle_ticker(self, client, message):
        #
        #     {
        #         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
        #         }
        #     }
        #
        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, since=None, limit=None, params={}):
        """
        get the list of most recent trades for a particular symbol
        :param str symbol: unified symbol of the market to fetch trades for
        :param int|None since: timestamp in ms of the earliest trade to fetch
        :param int|None limit: the maximum amount of trades to fetch
        :param dict params: extra parameters specific to the huobi api endpoint
        :returns [dict]: a list of `trade structures <https://docs.ccxt.com/en/latest/manual.html?#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)

    def handle_trades(self, 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, timeframe='1m', since=None, limit=None, params={}):
        """
        watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
        :param str symbol: unified symbol of the market to fetch OHLCV data for
        :param str timeframe: the length of time each candle represents
        :param int|None since: timestamp in ms of the earliest candle to fetch
        :param int|None limit: the maximum amount of candles to fetch
        :param dict params: extra parameters specific to the huobi api endpoint
        :returns [[int]]: A list of candles ordered as timestamp, open, high, low, close, volume
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        interval = self.timeframes[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)

    def handle_ohlcv(self, 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, limit=None, params={}):
        """
        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|None limit: the maximum amount of order book entries to return
        :param dict params: extra parameters specific to the huobi api endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/en/latest/manual.html#order-book-structure>` indexed by market symbols
        """
        if (limit is not None) and (limit != 150):
            raise ExchangeError(self.id + ' watchOrderBook accepts limit = 150 only')
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        # only supports a limit of 150 at self time
        limit = 150 if (limit is None) else limit
        messageHash = None
        if market['spot']:
            messageHash = 'market.' + market['id'] + '.mbp.' + str(limit)
        else:
            messageHash = 'market.' + market['id'] + '.depth.size_' + str(limit) + '.high_freq'
        url = self.get_url_by_market_type(market['type'], market['linear'])
        if not market['spot']:
            params['data_type'] = 'incremental'
        orderbook = await self.subscribe_public(url, symbol, messageHash, self.handle_order_book_subscription, params)
        return orderbook.limit()

    def handle_order_book_snapshot(self, client, message, subscription):
        #
        #     {
        #         id: 1583473663565,
        #         rep: 'market.btcusdt.mbp.150',
        #         status: 'ok',
        #         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')
        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, 'seqNum')
            nonce = self.safe_integer(data, 'seqNum')
            snapshot['nonce'] = nonce
            if (sequence is not None) and (nonce < sequence):
                maxAttempts = self.safe_integer(self.options, 'maxOrderBookSyncAttempts', 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)
                        subscription['numAttempts'] = numAttempts
                        client.subscriptions[messageHash] = subscription
                        self.spawn(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)):
                    message = messages[i]
                    self.handle_order_book_message(client, message, orderbook)
                self.orderbooks[symbol] = orderbook
                client.resolve(orderbook, messageHash)
        except Exception as e:
            client.reject(e, messageHash)

    async def watch_order_book_snapshot(self, client, message, subscription):
        symbol = self.safe_string(subscription, 'symbol')
        limit = self.safe_integer(subscription, 'limit')
        params = self.safe_value(subscription, 'params')
        attempts = self.safe_integer(subscription, 'numAttempts', 0)
        messageHash = self.safe_string(subscription, 'messageHash')
        market = self.market(symbol)
        url = self.get_url_by_market_type(market['type'], market['linear'])
        requestId = self.request_id()
        request = {
            '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 = {
            'id': requestId,
            'messageHash': messageHash,
            'symbol': symbol,
            'limit': limit,
            'params': params,
            'numAttempts': attempts,
            'method': self.handle_order_book_snapshot,
        }
        orderbook = await self.watch(url, requestId, request, requestId, snapshotSubscription)
        return orderbook.limit()

    async def fetch_order_book_snapshot(self, client, message, subscription):
        symbol = self.safe_string(subscription, 'symbol')
        limit = self.safe_integer(subscription, 'limit')
        messageHash = self.safe_string(subscription, 'messageHash')
        try:
            snapshot = await self.fetch_order_book(symbol, limit)
            orderbook = self.orderbooks[symbol]
            messages = orderbook.cache
            firstMessage = self.safe_value(messages, 0, {})
            tick = self.safe_value(firstMessage, 'tick')
            sequence = self.safe_integer(tick, 'seqNum')
            nonce = self.safe_integer(snapshot, 'nonce')
            # if the received snapshot is earlier than the first cached delta
            # then we cannot align it with the cached deltas and we need to
            # retry synchronizing in maxAttempts
            if (sequence is not None) and (nonce < sequence):
                maxAttempts = self.safe_integer(self.options, 'maxOrderBookSyncAttempts', 3)
                numAttempts = self.safe_integer(subscription, 'numAttempts', 0)
                # retry to syncrhonize if we haven't reached maxAttempts yet
                if numAttempts < maxAttempts:
                    # safety guard
                    if messageHash in client.subscriptions:
                        numAttempts = self.sum(numAttempts, 1)
                        subscription['numAttempts'] = numAttempts
                        client.subscriptions[messageHash] = subscription
                        self.spawn(self.fetch_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)):
                    message = messages[i]
                    self.handle_order_book_message(client, message, orderbook)
                self.orderbooks[symbol] = orderbook
                client.resolve(orderbook, messageHash)
        except Exception as e:
            client.reject(e, messageHash)

    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, message, orderbook):
        # 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
        #
        #     {
        #         "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
        #     }
        #
        tick = self.safe_value(message, 'tick', {})
        seqNum = self.safe_integer_2(tick, 'seqNum', 'id')
        prevSeqNum = self.safe_integer(tick, 'prevSeqNum')
        if (prevSeqNum is None or prevSeqNum <= orderbook['nonce']) and (seqNum > orderbook['nonce']):
            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
            timestamp = self.safe_integer(message, 'ts')
            orderbook['timestamp'] = timestamp
            orderbook['datetime'] = self.iso8601(timestamp)
        return orderbook

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

    def handle_order_book_subscription(self, client, message, subscription):
        symbol = self.safe_string(subscription, 'symbol')
        limit = self.safe_integer(subscription, 'limit')
        if symbol in self.orderbooks:
            del self.orderbooks[symbol]
        self.orderbooks[symbol] = self.order_book({}, limit)
        if self.markets[symbol]['spot'] is True:
            self.spawn(self.watch_order_book_snapshot, client, message, subscription)
        else:
            self.spawn(self.fetch_order_book_snapshot, client, message, subscription)

    async def watch_my_trades(self, symbol=None, since=None, limit=None, params={}):
        """
        watches information on multiple trades made by the user
        :param str symbol: unified market symbol of the market orders were made in
        :param int|None since: the earliest time in ms to fetch orders for
        :param int|None limit: the maximum number of  orde structures to retrieve
        :param dict params: extra parameters specific to the huobi api endpoint
        :returns [dict]: a list of [order structures]{@link https://docs.ccxt.com/en/latest/manual.html#order-structure
        """
        self.check_required_credentials()
        type = None
        marketId = '*'  # wildcard
        market = None
        messageHash = None
        channel = None
        trades = None
        subType = None
        if symbol is not None:
            await self.load_markets()
            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'] if (market is not None) else None
        baseId = market['lowercaseBaseId'] 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
                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=None, since=None, limit=None, params={}):
        """
        watches information on multiple orders made by the user
        :param str|None symbol: unified market symbol of the market orders were made in
        :param int|None since: the earliest time in ms to fetch orders for
        :param int|None limit: the maximum number of  orde structures to retrieve
        :param dict params: extra parameters specific to the huobi api endpoint
        :returns [dict]: a list of `order structures <https://docs.ccxt.com/en/latest/manual.html#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)

    def handle_order(self, client, message):
        #
        # spot
        #
        #     {
        #         "action":"push",
        #         "ch":"orders#btcusdt",  # or 'orders#*' for global subscriptions
        #         "data": {
        #             orderSource: 'spot-web',
        #             orderCreateTime: 1645116048355,
        #             accountId: 44234548,
        #             orderPrice: '100',
        #             orderSize: '0.05',
        #             symbol: 'ethusdt',
        #             type: 'buy-limit',
        #             orderId: '478861479986886',
        #             eventType: 'creation',
        #             clientOrderId: '',
        #             orderStatus: 'submitted'
        #         }
        #     }
        #
        # spot wrapped trade
        #
        #     {
        #         action: 'push',
        #         ch: 'orders#ltcusdt',
        #         data: {
        #             tradePrice: '130.01',
        #             tradeVolume: '0.0385',
        #             tradeTime: 1648714741525,
        #             aggressor: True,
        #             execAmt: '0.0385',
        #             orderSource: 'spot-web',
        #             orderSize: '0.0385',
        #             remainAmt: '0',
        #             tradeId: 101541578884,
        #             symbol: 'ltcusdt',
        #             type: 'sell-market',
        #             eventType: 'trade',
        #             clientOrderId: '',
        #             orderStatus: 'filled',
        #             orderId: 509835753860328
        #         }
        #     }
        #
        # non spot order
        #
        # {
        #     contract_type: 'swap',
        #     pair: 'LTC-USDT',
        #     business_type: 'swap',
        #     op: 'notify',
        #     topic: 'orders_cross.ltc-usdt',
        #     ts: 1650354508696,
        #     symbol: 'LTC',
        #     contract_code: 'LTC-USDT',
        #     volume: 1,
        #     price: 110.34,
        #     order_price_type: 'lightning',
        #     direction: 'sell',
        #     offset: 'close',
        #     status: 6,
        #     lever_rate: 1,
        #     order_id: '966002354015051776',
        #     order_id_str: '966002354015051776',
        #     client_order_id: null,
        #     order_source: 'web',
        #     order_type: 1,
        #     created_at: 1650354508649,
        #     trade_volume: 1,
        #     trade_turnover: 11.072,
        #     fee: -0.005536,
        #     trade_avg_price: 110.72,
        #     margin_frozen: 0,
        #     profit: -0.045,
        #     trade: [
        #       {
        #         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]
                order = {
                    'id': orderId,
                    'trades': trades,
                    'status': 'closed',
                    'symbol': market['symbol'],
                }
                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 = {
                    'trades': rawTrades,
                    'ch': messageHash,
                    'symbol': marketId,
                }
                # inject order params in every trade
                extendTradeParams = {
                    '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'], '')
        genericMessageHash = genericMessageHash.replace('.' + market['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')
        if typeSide is not None:
            typeSide = typeSide.split('-')
        type = self.safe_string_lower(typeSide, 1)
        if type is None:
            type = self.safe_string(order, 'order_price_type')
        side = self.safe_string_lower(typeSide, 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_balance(self, params={}):
        """
        query for balance and get the amount of funds available for trading or funds locked in orders
        :param dict params: extra parameters specific to the huobi api endpoint
        :returns dict: a `balance structure <https://docs.ccxt.com/en/latest/manual.html?#balance-structure>`
        """
        type = self.safe_string_2(self.options, 'watchBalance', 'defaultType', 'spot')
        type = self.safe_string(params, 'type', type)
        subType = self.safe_string_2(self.options, 'watchBalance', 'subType', 'linear')
        subType = self.safe_string(params, 'subType', subType)
        params = self.omit(params, ['type', 'subType'])
        params = self.omit(params, 'type')
        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':
                # 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 = {
            '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, 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')
        if channel is not None:
            # spot balance
            data = self.safe_value(message, 'data', {})
            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
            data = self.safe_value(message, 'data', [])
            dataLength = len(data)
            if dataLength == 0:
                return
            first = self.safe_value(data, 0, {})
            messageHash = self.safe_string(message, 'topic')
            subscription = self.safe_value(client.subscriptions, 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
                marginAsset = self.safe_string(first, 'margin_asset')
                messageHash += '.' + marginAsset.lower()
                subscription = self.safe_value(client.subscriptions, messageHash)
            type = self.safe_string(subscription, 'type')
            subType = self.safe_string(subscription, 'subType')
            if 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(balance, 'margin_balance')
                                account['used'] = self.safe_string(balance, 'margin_frozen')
                                accountsByCode = {}
                                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, message):
        #
        #     {
        #         "id": 1583414227,
        #         "status": "ok",
        #         "subbed": "market.btcusdt.mbp.150",
        #         "ts": 1583414229143
        #     }
        #
        id = self.safe_string(message, 'id')
        subscriptionsById = self.index_by(client.subscriptions, 'id')
        subscription = self.safe_value(subscriptionsById, id)
        if subscription is not None:
            method = self.safe_value(subscription, 'method')
            if method is not None:
                return method(client, message, subscription)
            # clean up
            if id in client.subscriptions:
                del client.subscriptions[id]
        return message

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

    def handle_subject(self, 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 = {
                'depth': self.handle_order_book,
                'mbp': self.handle_order_book,
                'detail': self.handle_ticker,
                'trade': self.handle_trades,
                'kline': self.handle_ohlcv,
            }
            method = self.safe_value(methods, methodName)
            if method is None:
                return message
            else:
                return method(client, message)
        # 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') != -1:
            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') != -1:
                self.handle_order(client, message)
            if topic.find('account') != -1:
                self.handle_balance(client, message)

    async def pong(self, client, message):
        #
        #     {ping: 1583491673714}
        #     {action: 'ping', data: {ts: 1645108204665}}
        #     {op: 'ping', ts: '1645202800015'}
        #
        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')
            ping = self.safe_integer(data, 'ts')
            await client.send({'action': 'pong', 'data': {'ts': ping}})
            return
        op = self.safe_string(message, 'op')
        if op == 'ping':
            ping = self.safe_integer(message, 'ts')
            await client.send({'op': 'pong', 'ts': ping})

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

    def handle_authenticate(self, 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'}
        #    }
        #
        client.resolve(message, 'auth')
        return message

    def handle_error_message(self, client, message):
        #
        #     {
        #         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'
        #     }
        #
        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))
                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_integer(message, 'code')
        if code is not None and code != 200:
            feedback = self.id + ' ' + self.json(message)
            try:
                self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, 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 message

    def handle_message(self, 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, 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'], '')
                genericOrderHash = genericOrderHash.replace('.' + market['lowercaseBaseId'], '')
                genericTradesHash = genericOrderHash + ':' + 'trade'
                client.resolve(self.myTrades, genericTradesHash)

    def parse_ws_trade(self, trade):
        # 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
        if orderType is not None:
            orderType = orderType.split('-')
            type = self.safe_string(orderType, 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):
        api = self.safe_string(self.options, 'api', 'api')
        hostname = {'hostname': self.hostname}
        hostnameURL = None
        url = None
        if type == 'spot':
            if isPrivate:
                hostnameURL = self.urls['api']['ws'][api]['spot']['private']
            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 = {
            'sub': messageHash,
            'id': requestId,
        }
        subscription = {
            '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 subscribe_private(self, channel, messageHash, type, subtype, params={}, subscriptionParams={}):
        requestId = self.nonce()
        subscription = {
            '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 = {
            'type': type,
            'url': url,
            'hostname': hostname,
        }
        if type == 'spot':
            self.options['ws']['gunzip'] = False
        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 = self.safe_value(client.subscriptions, messageHash)
        if future is None:
            future = client.future(messageHash)
            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)
            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':
                params = {
                    'authType': 'api',
                    'accessKey': self.apiKey,
                    'signatureMethod': 'HmacSHA256',
                    'signatureVersion': '2.1',
                    'timestamp': timestamp,
                    'signature': signature,
                }
                request = {
                    'params': params,
                    'action': 'req',
                    'ch': messageHash,
                }
            else:
                request = {
                    'op': messageHash,
                    'type': 'api',
                    'AccessKeyId': self.apiKey,
                    'SignatureMethod': 'HmacSHA256',
                    'SignatureVersion': '2',
                    'Timestamp': timestamp,
                    'Signature': signature,
                }
            await self.watch(url, messageHash, request, messageHash, future)
        return await future
