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

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

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


class gate(ccxt.async_support.gate):

    def describe(self):
        return self.deep_extend(super(gate, self).describe(), {
            'has': {
                'ws': True,
                'watchOrderBook': True,
                'watchTicker': True,
                'watchTickers': True,
                'watchTrades': True,
                'watchTradesForSymbols': True,
                'watchMyTrades': True,
                'watchOHLCV': True,
                'watchBalance': True,
                'watchOrders': True,
                'watchPositions': True,
            },
            'urls': {
                'api': {
                    'ws': 'wss://ws.gate.io/v4',
                    'spot': 'wss://api.gateio.ws/ws/v4/',
                    'swap': {
                        'usdt': 'wss://fx-ws.gateio.ws/v4/ws/usdt',
                        'btc': 'wss://fx-ws.gateio.ws/v4/ws/btc',
                    },
                    'future': {
                        'usdt': 'wss://fx-ws.gateio.ws/v4/ws/delivery/usdt',
                        'btc': 'wss://fx-ws.gateio.ws/v4/ws/delivery/btc',
                    },
                    'option': {
                        'usdt': 'wss://op-ws.gateio.live/v4/ws/usdt',
                        'btc': 'wss://op-ws.gateio.live/v4/ws/btc',
                    },
                },
                'test': {
                    'swap': {
                        'usdt': 'wss://fx-ws-testnet.gateio.ws/v4/ws/usdt',
                        'btc': 'wss://fx-ws-testnet.gateio.ws/v4/ws/btc',
                    },
                    'future': {
                        'usdt': 'wss://fx-ws-testnet.gateio.ws/v4/ws/usdt',
                        'btc': 'wss://fx-ws-testnet.gateio.ws/v4/ws/btc',
                    },
                    'option': {
                        'usdt': 'wss://op-ws-testnet.gateio.live/v4/ws/usdt',
                        'btc': 'wss://op-ws-testnet.gateio.live/v4/ws/btc',
                    },
                },
            },
            'options': {
                'tradesLimit': 1000,
                'OHLCVLimit': 1000,
                'watchTradesSubscriptions': {},
                'watchTickerSubscriptions': {},
                'watchOrderBookSubscriptions': {},
                'watchTicker': {
                    'name': 'tickers',  # or book_ticker
                },
                'watchOrderBook': {
                    'interval': '100ms',
                    'snapshotDelay': 10,  # how many deltas to cache before fetching a snapshot
                    'snapshotMaxRetries': 3,
                },
                'watchBalance': {
                    'settle': 'usdt',  # or btc
                    'spot': 'spot.balances',  # spot.margin_balances, spot.funding_balances or spot.cross_balances
                },
                'watchPositions': {
                    'fetchPositionsSnapshot': True,  # or False
                    'awaitPositionsSnapshot': True,  # whether to wait for the positions snapshot before providing updates
                },
            },
            'exceptions': {
                'ws': {
                    'exact': {
                        '2': BadRequest,
                        '4': AuthenticationError,
                        '6': AuthenticationError,
                        '11': AuthenticationError,
                    },
                },
            },
        })

    async def watch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        """
        watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
        :param str symbol: unified symbol of the market to fetch the order book for
        :param int [limit]: the maximum amount of order book entries to return
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        marketId = market['id']
        interval, query = self.handle_option_and_params(params, 'watchOrderBook', 'interval', '100ms')
        messageType = self.get_type_by_market(market)
        channel = messageType + '.order_book_update'
        messageHash = 'orderbook' + ':' + symbol
        url = self.get_url_by_market(market)
        payload = [marketId, interval]
        if limit is None:
            limit = 100
        if market['contract']:
            stringLimit = str(limit)
            payload.append(stringLimit)
        subscription = {
            'symbol': symbol,
            'limit': limit,
        }
        orderbook = await self.subscribe_public(url, messageHash, payload, channel, query, subscription)
        return orderbook.limit()

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

    def handle_order_book(self, client: Client, message):
        #
        # spot
        #
        #     {
        #         "time": 1650189272,
        #         "channel": "spot.order_book_update",
        #         "event": "update",
        #         "result": {
        #             "t": 1650189272515,
        #             "e": "depthUpdate",
        #             "E": 1650189272,
        #             "s": "GMT_USDT",
        #             "U": 140595902,
        #             "u": 140595902,
        #             "b": [
        #                 ['2.51518', "228.119"],
        #                 ['2.50587', "1510.11"],
        #                 ['2.49944', "67.6"],
        #             ],
        #             "a": [
        #                 ['2.5182', "4.199"],
        #                 ["2.51926", "1874"],
        #                 ['2.53528', "96.529"],
        #             ]
        #         }
        #     }
        #
        # swap
        #
        #     {
        #         "id": null,
        #         "time": 1650188898,
        #         "channel": "futures.order_book_update",
        #         "event": "update",
        #         "error": null,
        #         "result": {
        #             "t": 1650188898938,
        #             "s": "GMT_USDT",
        #             "U": 1577718307,
        #             "u": 1577719254,
        #             "b": [
        #                 {p: "2.5178", s: 0},
        #                 {p: "2.5179", s: 0},
        #                 {p: "2.518", s: 0},
        #             ],
        #             "a": [
        #                 {p: "2.52", s: 0},
        #                 {p: "2.5201", s: 0},
        #                 {p: "2.5203", s: 0},
        #             ]
        #         }
        #     }
        #
        channel = self.safe_string(message, 'channel')
        channelParts = channel.split('.')
        rawMarketType = self.safe_string(channelParts, 0)
        isSpot = rawMarketType == 'spot'
        marketType = 'spot' if isSpot else 'contract'
        delta = self.safe_value(message, 'result')
        deltaStart = self.safe_integer(delta, 'U')
        deltaEnd = self.safe_integer(delta, 'u')
        marketId = self.safe_string(delta, 's')
        symbol = self.safe_symbol(marketId, None, '_', marketType)
        messageHash = 'orderbook:' + symbol
        storedOrderBook = self.safe_value(self.orderbooks, symbol, self.order_book({}))
        nonce = self.safe_integer(storedOrderBook, 'nonce')
        if nonce is None:
            cacheLength = 0
            if storedOrderBook is not None:
                cacheLength = len(storedOrderBook.cache)
            snapshotDelay = self.handle_option('watchOrderBook', 'snapshotDelay', 10)
            waitAmount = snapshotDelay if isSpot else 0
            if cacheLength == waitAmount:
                # max limit is 100
                subscription = client.subscriptions[messageHash]
                limit = self.safe_integer(subscription, 'limit')
                self.spawn(self.load_order_book, client, messageHash, symbol, limit, {})  # needed for c#, number of args needs to match
            storedOrderBook.cache.append(delta)
            return
        elif nonce >= deltaEnd:
            return
        elif nonce >= deltaStart - 1:
            self.handle_delta(storedOrderBook, delta)
        else:
            error = InvalidNonce(self.id + ' orderbook update has a nonce bigger than u')
            del client.subscriptions[messageHash]
            del self.orderbooks[symbol]
            client.reject(error, messageHash)
        client.resolve(storedOrderBook, messageHash)

    def get_cache_index(self, orderBook, cache):
        nonce = self.safe_integer(orderBook, 'nonce')
        firstDelta = cache[0]
        firstDeltaStart = self.safe_integer(firstDelta, 'U')
        if nonce < firstDeltaStart:
            return -1
        for i in range(0, len(cache)):
            delta = cache[i]
            deltaStart = self.safe_integer(delta, 'U')
            deltaEnd = self.safe_integer(delta, 'u')
            if (nonce >= deltaStart - 1) and (nonce < deltaEnd):
                return i
        return len(cache)

    def handle_bid_asks(self, bookSide, bidAsks):
        for i in range(0, len(bidAsks)):
            bidAsk = bidAsks[i]
            if isinstance(bidAsk, list):
                bookSide.storeArray(self.parse_bid_ask(bidAsk))
            else:
                price = self.safe_float(bidAsk, 'p')
                amount = self.safe_float(bidAsk, 's')
                bookSide.store(price, amount)

    def handle_delta(self, orderbook, delta):
        timestamp = self.safe_integer(delta, 't')
        orderbook['timestamp'] = timestamp
        orderbook['datetime'] = self.iso8601(timestamp)
        orderbook['nonce'] = self.safe_integer(delta, 'u')
        bids = self.safe_value(delta, 'b', [])
        asks = self.safe_value(delta, 'a', [])
        storedBids = orderbook['bids']
        storedAsks = orderbook['asks']
        self.handle_bid_asks(storedBids, bids)
        self.handle_bid_asks(storedAsks, asks)

    async def watch_ticker(self, symbol: str, params={}) -> Ticker:
        """
        :see: https://www.gate.io/docs/developers/apiv4/ws/en/#tickers-channel
        watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
        :param str symbol: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        params['callerMethodName'] = 'watchTicker'
        result = await self.watch_tickers([symbol], params)
        return self.safe_value(result, symbol)

    async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
        """
        :see: https://www.gate.io/docs/developers/apiv4/ws/en/#tickers-channel
        watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
        :param str[] symbols: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
        """
        return await self.subscribe_watch_tickers_and_bids_asks(symbols, 'watchTickers', self.extend({'method': 'tickers'}, params))

    def handle_ticker(self, client: Client, message):
        #
        #    {
        #        "time": 1649326221,
        #        "channel": "spot.tickers",
        #        "event": "update",
        #        "result": {
        #          "currency_pair": "BTC_USDT",
        #          "last": "43444.82",
        #          "lowest_ask": "43444.82",
        #          "highest_bid": "43444.81",
        #          "change_percentage": "-4.0036",
        #          "base_volume": "5182.5412425462",
        #          "quote_volume": "227267634.93123952",
        #          "high_24h": "47698",
        #          "low_24h": "42721.03"
        #        }
        #    }
        #
        self.handle_ticker_and_bid_ask('ticker', client, message)

    async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers:
        """
        :see: https://www.gate.io/docs/developers/apiv4/ws/en/#best-bid-or-ask-price
        :see: https://www.gate.io/docs/developers/apiv4/ws/en/#order-book-channel
        watches best bid & ask for symbols
        :param str[] symbols: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
        """
        return await self.subscribe_watch_tickers_and_bids_asks(symbols, 'watchBidsAsks', self.extend({'method': 'book_ticker'}, params))

    def handle_bid_ask(self, client: Client, message):
        #
        #    {
        #        "time": 1671363004,
        #        "time_ms": 1671363004235,
        #        "channel": "spot.book_ticker",
        #        "event": "update",
        #        "result": {
        #          "t": 1671363004228,
        #          "u": 9793320464,
        #          "s": "BTC_USDT",
        #          "b": "16716.8",
        #          "B": "0.0134",
        #          "a": "16716.9",
        #          "A": "0.0353"
        #        }
        #    }
        #
        self.handle_ticker_and_bid_ask('bidask', client, message)

    async def subscribe_watch_tickers_and_bids_asks(self, symbols: Strings = None, callerMethodName: Str = None, params={}) -> Tickers:
        await self.load_markets()
        callerMethodName, params = self.handle_param_string(params, 'callerMethodName', callerMethodName)
        symbols = self.market_symbols(symbols, None, False)
        market = self.market(symbols[0])
        messageType = self.get_type_by_market(market)
        marketIds = self.market_ids(symbols)
        channelName = None
        channelName, params = self.handle_option_and_params(params, callerMethodName, 'method')
        url = self.get_url_by_market(market)
        channel = messageType + '.' + channelName
        isWatchTickers = callerMethodName.find('watchTicker') >= 0
        prefix = 'ticker' if isWatchTickers else 'bidask'
        messageHashes = []
        for i in range(0, len(symbols)):
            symbol = symbols[i]
            messageHashes.append(prefix + ':' + symbol)
        tickerOrBidAsk = await self.subscribe_public_multiple(url, messageHashes, marketIds, channel, params)
        if self.newUpdates:
            items = {}
            items[tickerOrBidAsk['symbol']] = tickerOrBidAsk
            return items
        result = self.tickers if isWatchTickers else self.bidsasks
        return self.filter_by_array(result, 'symbol', symbols, True)

    def handle_ticker_and_bid_ask(self, objectName: str, client: Client, message):
        channel = self.safe_string(message, 'channel')
        parts = channel.split('.')
        rawMarketType = self.safe_string(parts, 0)
        marketType = 'contract' if (rawMarketType == 'futures') else 'spot'
        result = self.safe_value(message, 'result')
        results = []
        if isinstance(result, list):
            results = self.safe_list(message, 'result', [])
        else:
            rawTicker = self.safe_dict(message, 'result', {})
            results = [rawTicker]
        isTicker = (objectName == 'ticker')  # whether ticker or bid-ask
        for i in range(0, len(results)):
            rawTicker = results[i]
            marketId = self.safe_string(rawTicker, 's')
            market = self.safe_market(marketId, None, '_', marketType)
            parsedItem = self.parse_ticker(rawTicker, market)
            symbol = parsedItem['symbol']
            if isTicker:
                self.tickers[symbol] = parsedItem
            else:
                self.bidsasks[symbol] = parsedItem
            messageHash = objectName + ':' + symbol
            client.resolve(parsedItem, messageHash)

    async def watch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        get the list of most recent trades for a particular symbol
        :param str symbol: unified symbol of the market to fetch trades for
        :param int [since]: timestamp in ms of the earliest trade to fetch
        :param int [limit]: the maximum amount of trades to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
        """
        return await self.watch_trades_for_symbols([symbol], since, limit, params)

    async def watch_trades_for_symbols(self, symbols: List[str], since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        get the list of most recent trades for a particular symbol
        :param str symbol: unified symbol of the market to fetch trades for
        :param int [since]: timestamp in ms of the earliest trade to fetch
        :param int [limit]: the maximum amount of trades to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
        """
        await self.load_markets()
        symbols = self.market_symbols(symbols)
        marketIds = self.market_ids(symbols)
        market = self.market(symbols[0])
        messageType = self.get_type_by_market(market)
        channel = messageType + '.trades'
        messageHashes = []
        for i in range(0, len(symbols)):
            symbol = symbols[i]
            messageHashes.append('trades:' + symbol)
        url = self.get_url_by_market(market)
        trades = await self.subscribe_public_multiple(url, messageHashes, marketIds, channel, params)
        if self.newUpdates:
            first = self.safe_value(trades, 0)
            tradeSymbol = self.safe_string(first, 'symbol')
            limit = trades.getLimit(tradeSymbol, limit)
        return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)

    def handle_trades(self, client: Client, message):
        #
        # {
        #     "time": 1648725035,
        #     "channel": "spot.trades",
        #     "event": "update",
        #     "result": [{
        #       "id": 3130257995,
        #       "create_time": 1648725035,
        #       "create_time_ms": "1648725035923.0",
        #       "side": "sell",
        #       "currency_pair": "LTC_USDT",
        #       "amount": "0.0116",
        #       "price": "130.11"
        #     }]
        # }
        #
        result = self.safe_value(message, 'result')
        if not isinstance(result, list):
            result = [result]
        parsedTrades = self.parse_trades(result)
        for i in range(0, len(parsedTrades)):
            trade = parsedTrades[i]
            symbol = trade['symbol']
            cachedTrades = self.safe_value(self.trades, symbol)
            if cachedTrades is None:
                limit = self.safe_integer(self.options, 'tradesLimit', 1000)
                cachedTrades = ArrayCache(limit)
                self.trades[symbol] = cachedTrades
            cachedTrades.append(trade)
            hash = 'trades:' + symbol
            client.resolve(cachedTrades, hash)

    async def watch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
        """
        watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
        :param str symbol: unified symbol of the market to fetch OHLCV data for
        :param str timeframe: the length of time each candle represents
        :param int [since]: timestamp in ms of the earliest candle to fetch
        :param int [limit]: the maximum amount of candles to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns int[][]: A list of candles ordered, open, high, low, close, volume
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        marketId = market['id']
        interval = self.safe_string(self.timeframes, timeframe, timeframe)
        messageType = self.get_type_by_market(market)
        channel = messageType + '.candlesticks'
        messageHash = 'candles:' + interval + ':' + market['symbol']
        url = self.get_url_by_market(market)
        payload = [interval, marketId]
        ohlcv = await self.subscribe_public(url, messageHash, payload, channel, 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: Client, message):
        #
        # {
        #     "time": 1606292600,
        #     "channel": "spot.candlesticks",
        #     "event": "update",
        #     "result": {
        #       "t": "1606292580",  # total volume
        #       "v": "2362.32035",  # volume
        #       "c": "19128.1",  # close
        #       "h": "19128.1",  # high
        #       "l": "19128.1",  # low
        #       "o": "19128.1",  # open
        #       "n": "1m_BTC_USDT"  # sub
        #     }
        #   }
        #
        channel = self.safe_string(message, 'channel')
        channelParts = channel.split('.')
        rawMarketType = self.safe_string(channelParts, 0)
        marketType = 'spot' if (rawMarketType == 'spot') else 'contract'
        result = self.safe_value(message, 'result')
        if not isinstance(result, list):
            result = [result]
        marketIds = {}
        for i in range(0, len(result)):
            ohlcv = result[i]
            subscription = self.safe_string(ohlcv, 'n', '')
            parts = subscription.split('_')
            timeframe = self.safe_string(parts, 0)
            timeframeId = self.find_timeframe(timeframe)
            prefix = timeframe + '_'
            marketId = subscription.replace(prefix, '')
            symbol = self.safe_symbol(marketId, None, '_', marketType)
            parsed = self.parse_ohlcv(ohlcv)
            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][timeframeId] = stored
            stored.append(parsed)
            marketIds[symbol] = timeframe
        keys = list(marketIds.keys())
        for i in range(0, len(keys)):
            symbol = keys[i]
            timeframe = marketIds[symbol]
            interval = self.find_timeframe(timeframe)
            hash = 'candles' + ':' + interval + ':' + symbol
            stored = self.safe_value(self.ohlcvs[symbol], interval)
            client.resolve(stored, hash)

    async def watch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        watches information on multiple trades made by the user
        :param str symbol: unified market symbol of the market trades were made in
        :param int [since]: the earliest time in ms to fetch trades for
        :param int [limit]: the maximum number of trade structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of [trade structures]{@link https://docs.ccxt.com/#/?id=trade-structure
        """
        await self.load_markets()
        subType = None
        type = None
        marketId = '!' + 'all'
        market = None
        if symbol is not None:
            market = self.market(symbol)
            marketId = market['id']
        type, params = self.handle_market_type_and_params('watchMyTrades', market, params)
        subType, params = self.handle_sub_type_and_params('watchMyTrades', market, params)
        messageType = self.get_supported_mapping(type, {
            'spot': 'spot',
            'margin': 'spot',
            'future': 'futures',
            'swap': 'futures',
            'option': 'options',
        })
        channel = messageType + '.usertrades'
        messageHash = 'myTrades'
        if symbol is not None:
            messageHash += ':' + symbol
        isInverse = (subType == 'inverse')
        url = self.get_url_by_market_type(type, isInverse)
        payload = [marketId]
        # uid required for non spot markets
        requiresUid = (type != 'spot')
        trades = await self.subscribe_private(url, messageHash, payload, channel, params, requiresUid)
        if self.newUpdates:
            limit = trades.getLimit(symbol, limit)
        return self.filter_by_symbol_since_limit(trades, symbol, since, limit, True)

    def handle_my_trades(self, client: Client, message):
        #
        # {
        #     "time": 1543205083,
        #     "channel": "futures.usertrades",
        #     "event": "update",
        #     "error": null,
        #     "result": [
        #       {
        #         "id": "3335259",
        #         "create_time": 1628736848,
        #         "create_time_ms": 1628736848321,
        #         "contract": "BTC_USD",
        #         "order_id": "4872460",
        #         "size": 1,
        #         "price": "40000.4",
        #         "role": "maker"
        #       }
        #     ]
        # }
        #
        result = self.safe_value(message, 'result', [])
        tradesLength = len(result)
        if tradesLength == 0:
            return
        cachedTrades = self.myTrades
        if cachedTrades is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            cachedTrades = ArrayCacheBySymbolById(limit)
            self.myTrades = cachedTrades
        parsed = self.parse_trades(result)
        marketIds = {}
        for i in range(0, len(parsed)):
            trade = parsed[i]
            cachedTrades.append(trade)
            symbol = trade['symbol']
            marketIds[symbol] = True
        keys = list(marketIds.keys())
        for i in range(0, len(keys)):
            market = keys[i]
            hash = 'myTrades:' + market
            client.resolve(cachedTrades, hash)
        client.resolve(cachedTrades, 'myTrades')

    async def watch_balance(self, params={}) -> Balances:
        """
        watch balance and get the amount of funds available for trading or funds locked in orders
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
        """
        await self.load_markets()
        type = None
        subType = None
        type, params = self.handle_market_type_and_params('watchBalance', None, params)
        subType, params = self.handle_sub_type_and_params('watchBalance', None, params)
        isInverse = (subType == 'inverse')
        url = self.get_url_by_market_type(type, isInverse)
        requiresUid = (type != 'spot')
        channelType = self.get_supported_mapping(type, {
            'spot': 'spot',
            'margin': 'spot',
            'future': 'futures',
            'swap': 'futures',
            'option': 'options',
        })
        channel = channelType + '.balances'
        messageHash = type + '.balance'
        return await self.subscribe_private(url, messageHash, None, channel, params, requiresUid)

    def handle_balance(self, client: Client, message):
        #
        # spot order fill
        #   {
        #       "time": 1653664351,
        #       "channel": "spot.balances",
        #       "event": "update",
        #       "result": [
        #         {
        #           "timestamp": "1653664351",
        #           "timestamp_ms": "1653664351017",
        #           "user": "10406147",
        #           "currency": "LTC",
        #           "change": "-0.0002000000000000",
        #           "total": "0.09986000000000000000",
        #           "available": "0.09986000000000000000"
        #         }
        #       ]
        #   }
        #
        # account transfer
        #
        #    {
        #        "id": null,
        #        "time": 1653665088,
        #        "channel": "futures.balances",
        #        "event": "update",
        #        "error": null,
        #        "result": [
        #          {
        #            "balance": 25.035008537,
        #            "change": 25,
        #            "text": "-",
        #            "time": 1653665088,
        #            "time_ms": 1653665088286,
        #            "type": "dnw",
        #            "user": "10406147"
        #          }
        #        ]
        #   }
        #
        # swap order fill
        #   {
        #       "id": null,
        #       "time": 1653665311,
        #       "channel": "futures.balances",
        #       "event": "update",
        #       "error": null,
        #       "result": [
        #         {
        #           "balance": 20.031873037,
        #           "change": -0.0031355,
        #           "text": "LTC_USDT:165551103273",
        #           "time": 1653665311,
        #           "time_ms": 1653665311437,
        #           "type": "fee",
        #           "user": "10406147"
        #         }
        #       ]
        #   }
        #
        result = self.safe_value(message, 'result', [])
        timestamp = self.safe_integer(message, 'time_ms')
        self.balance['info'] = result
        self.balance['timestamp'] = timestamp
        self.balance['datetime'] = self.iso8601(timestamp)
        for i in range(0, len(result)):
            rawBalance = result[i]
            account = self.account()
            currencyId = self.safe_string(rawBalance, 'currency', 'USDT')  # when not present it is USDT
            code = self.safe_currency_code(currencyId)
            account['free'] = self.safe_string(rawBalance, 'available')
            account['total'] = self.safe_string_2(rawBalance, 'total', 'balance')
            self.balance[code] = account
        channel = self.safe_string(message, 'channel')
        parts = channel.split('.')
        rawType = self.safe_string(parts, 0)
        channelType = self.get_supported_mapping(rawType, {
            'spot': 'spot',
            'futures': 'swap',
            'options': 'option',
        })
        messageHash = channelType + '.balance'
        self.balance = self.safe_balance(self.balance)
        client.resolve(self.balance, messageHash)

    async def watch_positions(self, symbols: Strings = None, since: Int = None, limit: Int = None, params={}) -> List[Position]:
        """
        :see: https://www.gate.io/docs/developers/futures/ws/en/#positions-subscription
        :see: https://www.gate.io/docs/developers/delivery/ws/en/#positions-subscription
        :see: https://www.gate.io/docs/developers/options/ws/en/#positions-channel
        watch all open positions
        :param str[]|None symbols: list of unified market symbols
        :param dict params: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
        """
        await self.load_markets()
        market = None
        symbols = self.market_symbols(symbols)
        payload = ['!' + 'all']
        if not self.is_empty(symbols):
            market = self.get_market_from_symbols(symbols)
        type = None
        query = None
        type, query = self.handle_market_type_and_params('watchPositions', market, params)
        if type == 'spot':
            type = 'swap'
        typeId = self.get_supported_mapping(type, {
            'future': 'futures',
            'swap': 'futures',
            'option': 'options',
        })
        messageHash = type + ':positions'
        if not self.is_empty(symbols):
            messageHash += '::' + ','.join(symbols)
        channel = typeId + '.positions'
        subType = None
        subType, query = self.handle_sub_type_and_params('watchPositions', market, query)
        isInverse = (subType == 'inverse')
        url = self.get_url_by_market_type(type, isInverse)
        client = self.client(url)
        self.set_positions_cache(client, type, symbols)
        fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', True)
        awaitPositionsSnapshot = self.safe_bool('watchPositions', 'awaitPositionsSnapshot', True)
        cache = self.safe_value(self.positions, type)
        if fetchPositionsSnapshot and awaitPositionsSnapshot and cache is None:
            return await client.future(type + ':fetchPositionsSnapshot')
        positions = await self.subscribe_private(url, messageHash, payload, channel, query, True)
        if self.newUpdates:
            return positions
        return self.filter_by_symbols_since_limit(self.positions[type], symbols, since, limit, True)

    def set_positions_cache(self, client: Client, type, symbols: Strings = None):
        if self.positions is None:
            self.positions = {}
        if type in self.positions:
            return
        fetchPositionsSnapshot = self.handle_option('watchPositions', 'fetchPositionsSnapshot', False)
        if fetchPositionsSnapshot:
            messageHash = type + ':fetchPositionsSnapshot'
            if not (messageHash in client.futures):
                client.future(messageHash)
                self.spawn(self.load_positions_snapshot, client, messageHash, type)
        else:
            self.positions[type] = ArrayCacheBySymbolBySide()

    async def load_positions_snapshot(self, client, messageHash, type):
        positions = await self.fetch_positions(None, {'type': type})
        self.positions[type] = ArrayCacheBySymbolBySide()
        cache = self.positions[type]
        for i in range(0, len(positions)):
            position = positions[i]
            cache.append(position)
        # don't remove the future from the .futures cache
        future = client.futures[messageHash]
        future.resolve(cache)
        client.resolve(cache, type + ':position')

    def handle_positions(self, client, message):
        #
        #    {
        #        time: 1693158497,
        #        time_ms: 1693158497204,
        #        channel: 'futures.positions',
        #        event: 'update',
        #        result: [{
        #            contract: 'XRP_USDT',
        #            cross_leverage_limit: 0,
        #            entry_price: 0.5253,
        #            history_pnl: 0,
        #            history_point: 0,
        #            last_close_pnl: 0,
        #            leverage: 0,
        #            leverage_max: 50,
        #            liq_price: 0.0361,
        #            maintenance_rate: 0.01,
        #            margin: 4.89609962852,
        #            mode: 'single',
        #            realised_pnl: -0.0026265,
        #            realised_point: 0,
        #            risk_limit: 500000,
        #            size: 1,
        #            time: 1693158497,
        #            time_ms: 1693158497195,
        #            update_id: 1,
        #            user: '10444586'
        #        }]
        #    }
        #
        type = self.get_market_type_by_url(client.url)
        data = self.safe_value(message, 'result', [])
        cache = self.positions[type]
        newPositions = []
        for i in range(0, len(data)):
            rawPosition = data[i]
            position = self.parse_position(rawPosition)
            newPositions.append(position)
            cache.append(position)
        messageHashes = self.find_message_hashes(client, type + ':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, type + ':positions')

    async def watch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        watches information on multiple orders made by the user
        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.type]: spot, margin, swap, future, or option. Required if listening to all symbols.
        :param boolean [params.isInverse]: if future, listen to inverse or linear contracts
        :returns dict[]: a list of [order structures]{@link https://docs.ccxt.com/#/?id=order-structure
        """
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
            symbol = market['symbol']
        type = None
        query = None
        type, query = self.handle_market_type_and_params('watchOrders', market, params)
        typeId = self.get_supported_mapping(type, {
            'spot': 'spot',
            'margin': 'spot',
            'future': 'futures',
            'swap': 'futures',
            'option': 'options',
        })
        channel = typeId + '.orders'
        messageHash = 'orders'
        payload = ['!' + 'all']
        if symbol is not None:
            messageHash += ':' + market['id']
            payload = [market['id']]
        subType = None
        subType, query = self.handle_sub_type_and_params('watchOrders', market, query)
        isInverse = (subType == 'inverse')
        url = self.get_url_by_market_type(type, isInverse)
        # uid required for non spot markets
        requiresUid = (type != 'spot')
        orders = await self.subscribe_private(url, messageHash, payload, channel, query, requiresUid)
        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):
        #
        # {
        #     "time": 1605175506,
        #     "channel": "spot.orders",
        #     "event": "update",
        #     "result": [
        #       {
        #         "id": "30784435",
        #         "user": 123456,
        #         "text": "t-abc",
        #         "create_time": "1605175506",
        #         "create_time_ms": "1605175506123",
        #         "update_time": "1605175506",
        #         "update_time_ms": "1605175506123",
        #         "event": "put",
        #         "currency_pair": "BTC_USDT",
        #         "type": "limit",
        #         "account": "spot",
        #         "side": "sell",
        #         "amount": "1",
        #         "price": "10001",
        #         "time_in_force": "gtc",
        #         "left": "1",
        #         "filled_total": "0",
        #         "fee": "0",
        #         "fee_currency": "USDT",
        #         "point_fee": "0",
        #         "gt_fee": "0",
        #         "gt_discount": True,
        #         "rebated_fee": "0",
        #         "rebated_fee_currency": "USDT"
        #       }
        #     ]
        # }
        #
        orders = self.safe_value(message, 'result', [])
        limit = self.safe_integer(self.options, 'ordersLimit', 1000)
        if self.orders is None:
            self.orders = ArrayCacheBySymbolById(limit)
        stored = self.orders
        marketIds = {}
        parsedOrders = self.parse_orders(orders)
        for i in range(0, len(parsedOrders)):
            parsed = parsedOrders[i]
            # inject order status
            info = self.safe_value(parsed, 'info')
            event = self.safe_string(info, 'event')
            if event == 'put' or event == 'update':
                parsed['status'] = 'open'
            elif event == 'finish':
                status = self.safe_string(parsed, 'status')
                if status is None:
                    left = self.safe_number(info, 'left')
                    parsed['status'] = 'closed' if (left == 0) else 'canceled'
            stored.append(parsed)
            symbol = parsed['symbol']
            market = self.market(symbol)
            marketIds[market['id']] = True
        keys = list(marketIds.keys())
        for i in range(0, len(keys)):
            messageHash = 'orders:' + keys[i]
            client.resolve(self.orders, messageHash)
        client.resolve(self.orders, 'orders')

    def handle_error_message(self, client: Client, message):
        # {
        #     "time": 1647274664,
        #     "channel": "futures.orders",
        #     "event": "subscribe",
        #     "error": {code: 2, message: "unknown contract BTC_USDT_20220318"},
        # }
        # {
        #     "time": 1647276473,
        #     "channel": "futures.orders",
        #     "event": "subscribe",
        #     "error": {
        #       "code": 4,
        #       "message": "{"label":"INVALID_KEY","message":"Invalid key provided"}\n"
        #     },
        #     "result": null
        #   }
        error = self.safe_value(message, 'error')
        code = self.safe_integer(error, 'code')
        id = self.safe_string(message, 'id')
        if id is None:
            return False
        if code is not None:
            messageHash = self.safe_string(client.subscriptions, id)
            if messageHash is not None:
                try:
                    self.throw_exactly_matched_exception(self.exceptions['ws']['exact'], code, self.json(message))
                except Exception as e:
                    client.reject(e, messageHash)
                    if messageHash in client.subscriptions:
                        del client.subscriptions[messageHash]
            del client.subscriptions[id]
            return True
        return False

    def handle_balance_subscription(self, client: Client, message, subscription=None):
        self.balance = {}

    def handle_subscription_status(self, client: Client, message):
        channel = self.safe_string(message, 'channel')
        methods = {
            'balance': self.handle_balance_subscription,
            'spot.order_book_update': self.handle_order_book_subscription,
            'futures.order_book_update': self.handle_order_book_subscription,
        }
        id = self.safe_string(message, 'id')
        if channel in methods:
            subscriptionHash = self.safe_string(client.subscriptions, id)
            subscription = self.safe_value(client.subscriptions, subscriptionHash)
            method = methods[channel]
            method(client, message, subscription)
        if id in client.subscriptions:
            del client.subscriptions[id]

    def handle_message(self, client: Client, message):
        #
        # subscribe
        #    {
        #        "time": 1649062304,
        #        "id": 1649062303,
        #        "channel": "spot.candlesticks",
        #        "event": "subscribe",
        #        "result": {status: "success"}
        #    }
        #
        # candlestick
        #    {
        #        "time": 1649063328,
        #        "channel": "spot.candlesticks",
        #        "event": "update",
        #        "result": {
        #          "t": "1649063280",
        #          "v": "58932.23174896",
        #          "c": "45966.47",
        #          "h": "45997.24",
        #          "l": "45966.47",
        #          "o": "45975.18",
        #          "n": "1m_BTC_USDT",
        #          "a": "1.281699"
        #        }
        #     }
        #
        #  orders
        #   {
        #       "time": 1630654851,
        #       "channel": "options.orders", or futures.orders or spot.orders
        #       "event": "update",
        #       "result": [
        #          {
        #             "contract": "BTC_USDT-20211130-65000-C",
        #             "create_time": 1637897000,
        #               (...)
        #       ]
        #   }
        # orderbook
        #   {
        #       "time": 1649770525,
        #       "channel": "spot.order_book_update",
        #       "event": "update",
        #       "result": {
        #         "t": 1649770525653,
        #         "e": "depthUpdate",
        #         "E": 1649770525,
        #         "s": "LTC_USDT",
        #         "U": 2622525645,
        #         "u": 2622525665,
        #         "b": [
        #           [Array], [Array],
        #           [Array], [Array],
        #           [Array], [Array],
        #           [Array], [Array],
        #           [Array], [Array],
        #           [Array]
        #         ],
        #         "a": [
        #           [Array], [Array],
        #           [Array], [Array],
        #           [Array], [Array],
        #           [Array], [Array],
        #           [Array], [Array],
        #           [Array]
        #         ]
        #       }
        #     }
        #
        # balance update
        #
        #    {
        #        "time": 1653664351,
        #        "channel": "spot.balances",
        #        "event": "update",
        #        "result": [
        #          {
        #            "timestamp": "1653664351",
        #            "timestamp_ms": "1653664351017",
        #            "user": "10406147",
        #            "currency": "LTC",
        #            "change": "-0.0002000000000000",
        #            "total": "0.09986000000000000000",
        #            "available": "0.09986000000000000000"
        #          }
        #        ]
        #    }
        #
        if self.handle_error_message(client, message):
            return
        event = self.safe_string(message, 'event')
        if event == 'subscribe':
            self.handle_subscription_status(client, message)
            return
        channel = self.safe_string(message, 'channel', '')
        channelParts = channel.split('.')
        channelType = self.safe_value(channelParts, 1)
        v4Methods = {
            'usertrades': self.handle_my_trades,
            'candlesticks': self.handle_ohlcv,
            'orders': self.handle_order,
            'positions': self.handle_positions,
            'tickers': self.handle_ticker,
            'book_ticker': self.handle_bid_ask,
            'trades': self.handle_trades,
            'order_book_update': self.handle_order_book,
            'balances': self.handle_balance,
        }
        method = self.safe_value(v4Methods, channelType)
        if method is not None:
            method(client, message)

    def get_url_by_market(self, market):
        baseUrl = self.urls['api'][market['type']]
        if market['contract']:
            return baseUrl['usdt'] if market['linear'] else baseUrl['btc']
        else:
            return baseUrl

    def get_type_by_market(self, market):
        if market['spot']:
            return 'spot'
        elif market['option']:
            return 'options'
        else:
            return 'futures'

    def get_url_by_market_type(self, type, isInverse=False):
        api = self.urls['api']
        url = api[type]
        if (type == 'swap') or (type == 'future'):
            return url['btc'] if isInverse else url['usdt']
        else:
            return url

    def get_market_type_by_url(self, url: str):
        findBy = {
            'op-': 'option',
            'delivery': 'future',
            'fx': 'swap',
        }
        keys = list(findBy.keys())
        for i in range(0, len(keys)):
            key = keys[i]
            value = findBy[key]
            if url.find(key) >= 0:
                return value
        return 'spot'

    def request_id(self):
        # their support said that reqid must be an int32, not documented
        reqid = self.sum(self.safe_integer(self.options, 'reqid', 0), 1)
        self.options['reqid'] = reqid
        return reqid

    async def subscribe_public(self, url, messageHash, payload, channel, params={}, subscription=None):
        requestId = self.request_id()
        time = self.seconds()
        request = {
            'id': requestId,
            'time': time,
            'channel': channel,
            'event': 'subscribe',
            'payload': payload,
        }
        if subscription is not None:
            client = self.client(url)
            if not (messageHash in client.subscriptions):
                tempSubscriptionHash = str(requestId)
                client.subscriptions[tempSubscriptionHash] = messageHash
        message = self.extend(request, params)
        return await self.watch(url, messageHash, message, messageHash, subscription)

    async def subscribe_public_multiple(self, url, messageHashes, payload, channel, params={}):
        requestId = self.request_id()
        time = self.seconds()
        request = {
            'id': requestId,
            'time': time,
            'channel': channel,
            'event': 'subscribe',
            'payload': payload,
        }
        message = self.extend(request, params)
        return await self.watch_multiple(url, messageHashes, message, messageHashes)

    async def subscribe_private(self, url, messageHash, payload, channel, params, requiresUid=False):
        self.check_required_credentials()
        # uid is required for some subscriptions only so it's not a part of required credentials
        if requiresUid:
            if self.uid is None or len(self.uid) == 0:
                raise ArgumentsRequired(self.id + ' requires uid to subscribe')
            idArray = [self.uid]
            if payload is None:
                payload = idArray
            else:
                payload = self.array_concat(idArray, payload)
        time = self.seconds()
        event = 'subscribe'
        signaturePayload = 'channel=' + channel + '&' + 'event=' + event + '&' + 'time=' + str(time)
        signature = self.hmac(self.encode(signaturePayload), self.encode(self.secret), hashlib.sha512, 'hex')
        auth = {
            'method': 'api_key',
            'KEY': self.apiKey,
            'SIGN': signature,
        }
        requestId = self.request_id()
        request = {
            'id': requestId,
            'time': time,
            'channel': channel,
            'event': 'subscribe',
            'auth': auth,
        }
        if payload is not None:
            request['payload'] = payload
        client = self.client(url)
        if not (messageHash in client.subscriptions):
            tempSubscriptionHash = str(requestId)
            # in case of authenticationError we will throw
            client.subscriptions[tempSubscriptionHash] = messageHash
        message = self.extend(request, params)
        return await self.watch(url, messageHash, message, messageHash)
