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


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

    def describe(self):
        return self.deep_extend(super(bybit, self).describe(), {
            'has': {
                'ws': True,
                'watchBalance': True,
                'watchMyTrades': True,
                'watchOHLCV': True,
                'watchOrderBook': True,
                'watchOrders': True,
                'watchTicker': True,
                'watchTickers': False,  # for now
                'watchTrades': True,
                'watchPosition': None,
            },
            'urls': {
                'api': {
                    'ws': {
                        'public': {
                            'spot': 'wss://stream.{hostname}/spot/public/v3',
                            'inverse': 'wss://stream.{hostname}/contract/inverse/public/v3',
                            'usdt': 'wss://stream.{hostname}/contract/usdt/public/v3',
                            'usdc': {
                                'option': 'wss://stream.{hostname}/option/usdc/public/v3',
                                'swap': 'wss://stream.{hostname}/contract/usdc/public/v3',
                            },
                        },
                        'private': {
                            'spot': 'wss://stream.{hostname}/spot/private/v3',
                            'contract': {
                                'unified': 'wss://stream.{hostname}/unified/private/v3',
                                'nonUnified': 'wss://stream.{hostname}/contract/private/v3',
                            },
                        },
                    },
                },
                'test': {
                    'ws': {
                        'public': {
                            'spot': 'wss://stream-testnet.{hostname}/spot/public/v3',
                            'inverse': 'wss://stream-testnet.{hostname}/contract/inverse/public/v3',
                            'usdt': 'wss://stream-testnet.{hostname}/contract/usdt/public/v3',
                            'usdc': {
                                'option': 'wss://stream-testnet.{hostname}/option/usdc/public/v3',
                                'swap': 'wss://stream-testnet.{hostname}/contract/usdc/public/v3',
                            },
                        },
                        'private': {
                            'spot': 'wss://stream-testnet.{hostname}/spot/private/v3',
                            'contract': {
                                'unified': 'wss://stream-testnet.{hostname}/unified/private/v3',
                                'nonUnified': 'wss://stream-testnet.{hostname}/contract/private/v3',
                            },
                        },
                    },
                },
            },
            'options': {
                'watchTicker': {
                    'name': 'tickers',  # 'tickers' for 24hr statistical ticker or 'bookticker' for Best bid price and best ask price
                },
                'spot': {
                    'timeframes': {
                        '1m': '1m',
                        '3m': '3m',
                        '5m': '5m',
                        '15m': '15m',
                        '30m': '30m',
                        '1h': '1h',
                        '2h': '2h',
                        '4h': '4h',
                        '6h': '6h',
                        '12h': '12h',
                        '1d': '1d',
                        '1w': '1w',
                        '1M': '1M',
                    },
                },
                'contract': {
                    'timeframes': {
                        '1m': '1',
                        '3m': '3',
                        '5m': '5',
                        '15m': '15',
                        '30m': '30',
                        '1h': '60',
                        '2h': '120',
                        '4h': '240',
                        '6h': '360',
                        '12h': '720',
                        '1d': 'D',
                        '1w': 'W',
                        '1M': 'M',
                    },
                },
            },
            'streaming': {
                'ping': self.ping,
                'keepAlive': 20000,
            },
            'exceptions': {
                'ws': {
                    'exact': {
                    },
                },
            },
        })

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

    def get_url_by_market_type(self, symbol=None, isPrivate=False, isUnifiedMargin=False, method=None, params={}):
        accessibility = 'private' if isPrivate else 'public'
        isUsdcSettled = None
        isSpot = None
        type = None
        isUsdtSettled = None
        market = None
        url = self.urls['api']['ws']
        if symbol is not None:
            market = self.market(symbol)
            isUsdcSettled = market['settle'] == 'USDC'
            isUsdtSettled = market['settle'] == 'USDT'
            isSpot = market['spot']
            type = market['type']
        else:
            type, params = self.handle_market_type_and_params(method, None, params)
            defaultSettle = self.safe_string(self.options, 'defaultSettle')
            defaultSettle = self.safe_string_2(params, 'settle', 'defaultSettle', defaultSettle)
            isUsdcSettled = (defaultSettle == 'USDC')
            isUsdtSettled = (defaultSettle == 'USDT')
            isSpot = (type == 'spot')
        if isPrivate:
            if isSpot:
                url = url[accessibility]['spot']
            else:
                margin = 'unified' if isUnifiedMargin else 'nonUnified'
                url = url[accessibility]['contract'][margin]
        else:
            if isSpot:
                url = url[accessibility]['spot']
            elif isUsdcSettled:
                url = url[accessibility]['usdc'][type]
            elif isUsdtSettled:
                url = url[accessibility]['usdt']
            else:
                # inverse
                url = url[accessibility]['inverse']
        url = self.implode_hostname(url)
        return url

    def clean_params(self, params):
        params = self.omit(params, ['type', 'subType', 'settle', 'defaultSettle', 'unifiedMargin'])
        return params

    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 bybit 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)
        messageHash = 'ticker:' + market['symbol']
        url = self.get_url_by_market_type(symbol, False, False, params)
        params = self.clean_params(params)
        options = self.safe_value(self.options, 'watchTicker', {})
        topic = self.safe_string(options, 'name', 'tickers')
        if not market['spot'] and topic != 'tickers':
            raise BadRequest(self.id + ' watchTicker() only supports name tickers for contract markets')
        topic += '.' + market['id']
        topics = [topic]
        return await self.watch_topics(url, messageHash, topics, params)

    def handle_ticker(self, client, message):
        #
        #  spot - tickers
        #    {
        #        "data": {
        #            "t": 1661742216005,
        #            "s": "BTCUSDT",
        #            "o": "19820",
        #            "h": "20071.93",
        #            "l": "19365.85",
        #            "c": "19694.27",
        #            "v": "9997.246174",
        #            "qv": "197357775.97621786",
        #            "m": "-0.0063"
        #        },
        #        "type": "delta",
        #        "topic": "tickers.BTCUSDT",
        #        "ts": 1661742216011
        #    }
        #  spot - bookticker
        #    {
        #        "data": {
        #            "s": "BTCUSDT",
        #            "bp": "19693.04",
        #            "bq": "0.913957",
        #            "ap": "19694.27",
        #            "aq": "0.705447",
        #            "t": 1661742216108
        #        },
        #        "type": "delta",
        #        "topic": "bookticker.BTCUSDT",
        #        "ts": 1661742216109
        #    }
        #  swap
        #    {
        #        "topic":"tickers.BTCUSDT",
        #        "type":"snapshot",
        #        "data":{
        #            "symbol":"BTCUSDT",
        #            "tickDirection":"ZeroMinusTick",
        #            "price24hPcnt":"0.032786",
        #            "lastPrice":"22019.00",
        #            "prevPrice24h":"21320.00",
        #            "highPrice24h":"22522.00",
        #            "lowPrice24h":"20745.00",
        #            "prevPrice1h":"22186.50",
        #            "markPrice":"22010.11",
        #            "indexPrice":"22009.01",
        #            "openInterest":"44334.438",
        #            "turnover24h":"4609010554.786498",
        #            "volume24h":"213532.606",
        #            "fundingRate":"0.0001",
        #            "nextFundingTime":"2022-07-18T16:00:00Z",
        #            "bid1Price":"22019.00",
        #            "bid1Size":"41.530",
        #            "ask1Price":"22019.50",
        #            "ask1Size":"7.041",
        #            "basisRate":"0",
        #            "deliveryFeeRate":"0"
        #        },
        #        "cs":14236992078,
        #        "ts":1663203915102
        #    }
        #
        topic = self.safe_string(message, 'topic', '')
        updateType = self.safe_string(message, 'type', '')
        data = self.safe_value(message, 'data', {})
        isSpot = self.safe_string(data, 's') is not None
        symbol = None
        parsed = None
        if (updateType == 'snapshot') or isSpot:
            parsed = self.parse_ticker(data)
            symbol = parsed['symbol']
        elif updateType == 'delta':
            topicParts = topic.split('.')
            topicLength = len(topicParts)
            marketId = self.safe_string(topicParts, topicLength - 1)
            market = self.market(marketId)
            symbol = market['symbol']
            # update the info in place
            ticker = self.safe_value(self.tickers, symbol, {})
            rawTicker = self.safe_value(ticker, 'info', {})
            merged = self.extend(rawTicker, data)
            parsed = self.parse_ticker(merged)
        timestamp = self.safe_integer(message, 'ts')
        parsed['timestamp'] = timestamp
        parsed['datetime'] = self.iso8601(timestamp)
        self.tickers[symbol] = parsed
        messageHash = 'ticker:' + symbol
        client.resolve(self.tickers[symbol], messageHash)

    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 bybit 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']
        url = self.get_url_by_market_type(symbol, False, False, params)
        params = self.clean_params(params)
        ohlcv = None
        marketType = 'spot' if market['spot'] else 'contract'
        marketOptions = self.safe_value(self.options, marketType, {})
        timeframes = self.safe_value(marketOptions, 'timeframes', {})
        timeframeId = self.safe_string(timeframes, timeframe, timeframe)
        topics = ['kline.' + timeframeId + '.' + market['id']]
        messageHash = 'kline' + ':' + timeframeId + ':' + symbol
        ohlcv = await self.watch_topics(url, messageHash, topics, 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):
        #
        # swap
        #    {
        #        "topic":"kline.1.BTCUSDT",
        #        "data":[
        #          {
        #            "start":1658150220000,
        #            "end":1658150279999,
        #            "interval":"1",
        #            "open":"22212",
        #            "close":"22214",
        #            "high":"22214.5",
        #            "low":"22212",
        #            "volume":"5.456",
        #            "turnover":"121193.36",
        #            "confirm":false,
        #            "timestamp":1658150224542
        #          }
        #        ],
        #        "ts":1658150224542,
        #        "type":"snapshot"
        #    }
        #
        # spot
        #    {
        #        "data": {
        #            "t": 1661742000000,
        #            "s": "BTCUSDT",
        #            "c": "19685.55",
        #            "h": "19756.95",
        #            "l": "19674.61",
        #            "o": "19705.38",
        #            "v": "0.232154"
        #        },
        #        "type": "delta",
        #        "topic": "kline.1h.BTCUSDT",
        #        "ts": 1661745259605
        #    }
        #
        data = self.safe_value(message, 'data', {})
        topic = self.safe_string(message, 'topic')
        topicParts = topic.split('.')
        topicLength = len(topicParts)
        timeframeId = self.safe_string(topicParts, 1)
        marketId = self.safe_string(topicParts, topicLength - 1)
        isSpot = client.url.find('spot') > -1
        marketType = 'spot' if isSpot else 'contract'
        market = self.safe_market(marketId, None, None, marketType)
        symbol = market['symbol']
        ohlcvsByTimeframe = self.safe_value(self.ohlcvs, symbol)
        if ohlcvsByTimeframe is None:
            self.ohlcvs[symbol] = {}
        stored = self.safe_value(ohlcvsByTimeframe, timeframeId)
        if stored is None:
            limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
            stored = ArrayCacheByTimestamp(limit)
            self.ohlcvs[symbol][timeframeId] = stored
        if isinstance(data, list):
            for i in range(0, len(data)):
                parsed = self.parse_ws_contract_ohlcv(data[i])
                stored.append(parsed)
        else:
            parsed = self.parseSpotOHLCV(data)
            stored.append(parsed)
        messageHash = 'kline' + ':' + timeframeId + ':' + symbol
        client.resolve(stored, messageHash)

    def parse_ws_contract_ohlcv(self, ohlcv):
        #
        #     {
        #         "start": 1670363160000,
        #         "end": 1670363219999,
        #         "interval": "1",
        #         "open": "16987.5",
        #         "close": "16987.5",
        #         "high": "16988",
        #         "low": "16987.5",
        #         "volume": "23.511",
        #         "turnover": "399396.344",
        #         "confirm": False,
        #         "timestamp": 1670363219614
        #     }
        #
        return [
            self.safe_integer(ohlcv, 'timestamp'),
            self.safe_number(ohlcv, 'open'),
            self.safe_number(ohlcv, 'high'),
            self.safe_number(ohlcv, 'low'),
            self.safe_number(ohlcv, 'close'),
            self.safe_number_2(ohlcv, 'volume', 'turnover'),
        ]

    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 bybit 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
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        url = self.get_url_by_market_type(symbol, False, False, params)
        params = self.clean_params(params)
        messageHash = 'orderbook' + ':' + symbol
        if limit is None:
            if market['spot']:
                limit = 40
            else:
                limit = 200
        else:
            if not market['spot']:
                # bybit only support limit 1, 50 , 200 for contract
                if (limit != 1) and (limit != 50) and (limit != 200):
                    raise BadRequest(self.id + ' watchOrderBook() can only use limit 1, 50 and 200.')
        topics = ['orderbook.' + str(limit) + '.' + market['id']]
        orderbook = await self.watch_topics(url, messageHash, topics, params)
        return orderbook.limit()

    def handle_order_book(self, client, message):
        #
        # spot snapshot
        #     {
        #         "data": {
        #             "s": "BTCUSDT",
        #             "t": 1661743689733,
        #             "b": [
        #                 [
        #                     "19721.9",
        #                     "0.784806"
        #                 ],
        #                 ...
        #             ],
        #             "a": [
        #                 [
        #                     "19721.91",
        #                     "0.192687"
        #                 ],
        #                 ...
        #             ]
        #         },
        #         "type": "delta",  # docs say to ignore, always snapshot
        #         "topic": "orderbook.40.BTCUSDT",
        #         "ts": 1661743689735
        #     }
        #
        # contract
        #    {
        #        "topic": "orderbook.50.BTCUSDT",
        #        "type": "snapshot",
        #        "ts": 1668748553479,
        #        "data": {
        #            "s": "BTCUSDT",
        #            "b": [
        #                [
        #                    "17053.00",  #price
        #                    "0.021"  #size
        #                ],
        #                ....
        #            ],
        #            "a": [
        #                [
        #                    "17054.00",
        #                    "6.288"
        #                ],
        #                ....
        #            ],
        #            "u": 3083181,
        #            "seq": 7545268447
        #        }
        #    }
        #
        isSpot = client.url.find('spot') >= 0
        type = self.safe_string(message, 'type')
        isSnapshot = (type == 'snapshot')
        if isSpot:
            isSnapshot = True
        data = self.safe_value(message, 'data', {})
        marketId = self.safe_string(data, 's')
        marketType = 'spot' if isSpot else 'contract'
        market = self.safe_market(marketId, None, None, marketType)
        symbol = market['symbol']
        timestamp = self.safe_integer(message, 'ts')
        orderbook = self.safe_value(self.orderbooks, symbol)
        if orderbook is None:
            orderbook = self.order_book()
        if isSnapshot:
            snapshot = self.parse_order_book(data, symbol, timestamp, 'b', 'a')
            orderbook.reset(snapshot)
        else:
            asks = self.safe_value(data, 'a', [])
            bids = self.safe_value(data, 'b', [])
            self.handle_deltas(orderbook['asks'], asks)
            self.handle_deltas(orderbook['bids'], bids)
            orderbook['timestamp'] = timestamp
            orderbook['datetime'] = self.iso8601(timestamp)
        messageHash = 'orderbook' + ':' + symbol
        self.orderbooks[symbol] = orderbook
        client.resolve(orderbook, messageHash)

    def handle_delta(self, bookside, delta):
        bidAsk = self.parse_bid_ask(delta, 0, 1)
        bookside.storeArray(bidAsk)

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

    async def watch_trades(self, symbol, since=None, limit=None, params={}):
        """
        watches information on multiple trades made in a market
        :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 bybit api endpoint
        :returns [dict]: a list of [order structures]{@link https://docs.ccxt.com/en/latest/manual.html#order-structure
        """
        await self.load_markets()
        market = self.market(symbol)
        symbol = market['symbol']
        url = self.get_url_by_market_type(symbol, False, False, params)
        params = self.clean_params(params)
        messageHash = 'trade:' + symbol
        topic = None
        if market['spot']:
            topic = 'trade.' + market['id']
        else:
            topic = 'publicTrade.'
            if market['option']:
                topic += market['baseId']
            else:
                topic += market['id']
        trades = await self.watch_topics(url, messageHash, [topic], 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):
        #
        # swap
        #    {
        #        "topic": "publicTrade.BTCUSDT",
        #        "type": "snapshot",
        #        "ts": 1662694953823,
        #        "data": [
        #            {
        #                "T": 1662694953819,
        #                "s": "BTCUSDT",
        #                "S": "Buy",
        #                "v": "0.010",
        #                "p": "19792.50",
        #                "L": "PlusTick",
        #                "i": "5c9ab13e-6010-522c-aecd-02c4d9c8db3d",
        #                "BT": False
        #            }
        #        ]
        #    }
        #
        # spot
        #    {
        #        "data": {
        #            "v": "2100000000001992601",
        #            "t": 1661742109857,
        #            "p": "19706.87",
        #            "q": "0.000158",
        #            "m": True
        #        },
        #        "type": "delta",
        #        "topic": "trade.BTCUSDT",
        #        "ts": 1661742109863
        #    }
        #
        data = self.safe_value(message, 'data', {})
        topic = self.safe_string(message, 'topic')
        trades = None
        parts = topic.split('.')
        tradeType = self.safe_string(parts, 0)
        marketType = 'spot' if (tradeType == 'trade') else 'contract'
        marketId = self.safe_string(parts, 1)
        market = self.safe_market(marketId, None, None, marketType)
        if isinstance(data, list):
            # contract markets
            trades = data
        else:
            # spot markets
            trades = [data]
        symbol = market['symbol']
        stored = self.safe_value(self.trades, symbol)
        if stored is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            stored = ArrayCache(limit)
            self.trades[symbol] = stored
        for j in range(0, len(trades)):
            parsed = self.parse_ws_trade(trades[j], market)
            stored.append(parsed)
        messageHash = 'trade' + ':' + symbol
        client.resolve(stored, messageHash)

    def parse_ws_trade(self, trade, market=None):
        #
        # contract public
        #    {
        #         T: 1670198879981,
        #         s: 'BTCUSDT',
        #         S: 'Buy',
        #         v: '0.001',
        #         p: '17130.00',
        #         L: 'ZeroPlusTick',
        #         i: 'a807f4ee-2e8b-5f21-a02a-3e258ddbfdc9',
        #         BT: False
        #     }
        # contract private
        #
        # parsed by rest implementation
        #    {
        #        "symbol": "BITUSDT",
        #        "execFee": "0.02022",
        #        "execId": "beba036f-9fb4-59a7-84b7-2620e5d13e1c",
        #        "execPrice": "0.674",
        #        "execQty": "50",
        #        "execType": "Trade",
        #        "execValue": "33.7",
        #        "feeRate": "0.0006",
        #        "lastLiquidityInd": "RemovedLiquidity",
        #        "leavesQty": "0",
        #        "orderId": "ddbea432-2bd7-45dd-ab42-52d920b8136d",
        #        "orderLinkId": "b001",
        #        "orderPrice": "0.707",
        #        "orderQty": "50",
        #        "orderType": "Market",
        #        "stopOrderType": "UNKNOWN",
        #        "side": "Buy",
        #        "execTime": "1659057535081",
        #        "closedSize": "0"
        #    }
        #
        # spot public
        #
        #    {
        #      'symbol': 'BTCUSDT',  # artificially added
        #       v: '2290000000003002848',  # trade id
        #       t: 1652967602261,
        #       p: '29698.82',
        #       q: '0.189531',
        #       m: True
        #     }
        #
        # spot private
        #     {
        #         "e": "ticketInfo",
        #         "E": "1662348310386",
        #         "s": "BTCUSDT",
        #         "q": "0.001007",
        #         "t": "1662348310373",
        #         "p": "19842.02",
        #         "T": "2100000000002220938",
        #         "o": "1238261807653647872",
        #         "c": "spotx008",
        #         "O": "1238225004531834368",
        #         "a": "533287",
        #         "A": "642908",
        #         "m": False,
        #         "S": "BUY"
        #     }
        #
        id = self.safe_string_n(trade, ['i', 'T', 'v'])
        isContract = ('BT' in trade)
        marketType = 'contract' if isContract else 'spot'
        marketId = self.safe_string(trade, 's')
        market = self.safe_market(marketId, market, None, marketType)
        symbol = market['symbol']
        timestamp = self.safe_integer_2(trade, 't', 'T')
        side = self.safe_string_lower(trade, 'S')
        takerOrMaker = None
        m = self.safe_value(trade, 'm')
        if side is None:
            side = 'buy' if m else 'sell'
        else:
            # spot private
            takerOrMaker = m
        price = self.safe_string(trade, 'p')
        amount = self.safe_string_2(trade, 'q', 'v')
        orderId = self.safe_string(trade, 'o')
        return self.safe_trade({
            'id': id,
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'order': orderId,
            'type': None,
            'side': side,
            'takerOrMaker': takerOrMaker,
            'price': price,
            'amount': amount,
            'cost': None,
            'fee': None,
        }, market)

    def get_private_type(self, url):
        if url.find('spot') >= 0:
            return 'spot'
        elif url.find('unified') >= 0:
            return 'unified'
        elif url.find('contract') >= 0:
            return 'contract'

    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 bybit api endpoint
        :param boolean params['unifiedMargin']: use unified margin account
        :returns [dict]: a list of [order structures]{@link https://docs.ccxt.com/en/latest/manual.html#order-structure
        """
        method = 'watchMyTrades'
        messageHash = 'myTrades'
        await self.load_markets()
        if symbol is not None:
            symbol = self.symbol(symbol)
            messageHash += ':' + symbol
        unified = await self.isUnifiedEnabled()
        isUnifiedMargin = self.safe_value(unified, 0, False)
        url = self.get_url_by_market_type(symbol, True, isUnifiedMargin, method, params)
        await self.authenticate(url)
        topicByMarket = {
            'spot': 'ticketInfo',
            'contract': 'user.execution.contractAccount',
            'unified': 'user.execution.unifiedAccount',
        }
        topic = self.safe_value(topicByMarket, self.get_private_type(url))
        trades = await self.watch_topics(url, messageHash, [topic], params)
        if self.newUpdates:
            limit = trades.getLimit(symbol, limit)
        return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)

    def handle_my_trades(self, client, message):
        #
        #    {
        #        "type": "snapshot",
        #        "topic": "ticketInfo",
        #        "ts": "1662348310388",
        #        "data": [
        #            {
        #                "e": "ticketInfo",
        #                "E": "1662348310386",
        #                "s": "BTCUSDT",
        #                "q": "0.001007",
        #                "t": "1662348310373",
        #                "p": "19842.02",
        #                "T": "2100000000002220938",
        #                "o": "1238261807653647872",
        #                "c": "spotx008",
        #                "O": "1238225004531834368",
        #                "a": "533287",
        #                "A": "642908",
        #                "m": False,
        #                "S": "BUY"
        #            }
        #        ]
        #    }
        # contract
        #     {
        #         topic: 'user.execution.contractAccount',
        #         data: [
        #           {
        #             symbol: 'BTCUSD',
        #             execFee: '0.00000004',
        #             execId: '7d0f66da-8312-52a9-959b-9fba58a90af0',
        #             execPrice: '17228.00',
        #             execQty: '1',
        #             execType: 'Trade',
        #             execValue: '0.00005804',
        #             feeRate: '0.0006',
        #             lastLiquidityInd: 'RemovedLiquidity',
        #             leavesQty: '0',
        #             orderId: '6111f83d-2c8c-463a-b9a8-77885eae2f57',
        #             orderLinkId: '',
        #             orderPrice: '17744.50',
        #             orderQty: '1',
        #             orderType: 'Market',
        #             stopOrderType: 'UNKNOWN',
        #             side: 'Buy',
        #             execTime: '1670210101997',
        #             closedSize: '0'
        #           }
        #         ]
        #     }
        #
        topic = self.safe_string(message, 'topic')
        spot = topic == 'ticketInfo'
        data = self.safe_value(message, 'data', [])
        # unified margin
        data = self.safe_value(data, 'result', data)
        if self.myTrades is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            self.myTrades = ArrayCacheBySymbolById(limit)
        trades = self.myTrades
        symbols = {}
        method = 'parseWsTrade' if spot else 'parseTrade'
        for i in range(0, len(data)):
            rawTrade = data[i]
            parsed = getattr(self, method)(rawTrade)
            symbol = parsed['symbol']
            symbols[symbol] = True
            trades.append(parsed)
        keys = list(symbols.keys())
        for i in range(0, len(keys)):
            messageHash = 'myTrades:' + keys[i]
            client.resolve(trades, messageHash)
        # non-symbol specific
        messageHash = 'myTrades'
        client.resolve(trades, 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 bybit api endpoint
        :returns [dict]: a list of [order structures]{@link https://docs.ccxt.com/en/latest/manual.html#order-structure
        """
        await self.load_markets()
        method = 'watchOrders'
        messageHash = 'orders'
        if symbol is not None:
            symbol = self.symbol(symbol)
            messageHash += ':' + symbol
        unified = await self.isUnifiedEnabled()
        isUnifiedMargin = self.safe_value(unified, 0, False)
        url = self.get_url_by_market_type(None, True, isUnifiedMargin, method, params)
        await self.authenticate(url)
        topicsByMarket = {
            'spot': ['order', 'stopOrder'],
            'contract': ['user.order.contractAccount'],
            'unified': ['user.order.unifiedAccount'],
        }
        topics = self.safe_value(topicsByMarket, self.get_private_type(url))
        orders = await self.watch_topics(url, messageHash, topics, params)
        if self.newUpdates:
            limit = orders.getLimit(symbol, limit)
        return self.filter_by_symbol_since_limit(orders, symbol, since, limit, True)

    def handle_order(self, client, message, subscription=None):
        #
        #     spot
        #     {
        #         "type": "snapshot",
        #         "topic": "order",
        #         "ts": "1662348310441",
        #         "data": [
        #             {
        #                 "e": "order",
        #                 "E": "1662348310441",
        #                 "s": "BTCUSDT",
        #                 "c": "spotx008",
        #                 "S": "BUY",
        #                 "o": "MARKET_OF_QUOTE",
        #                 "f": "GTC",
        #                 "q": "20",
        #                 "p": "0",
        #                 "X": "CANCELED",
        #                 "i": "1238261807653647872",
        #                 "M": "1238225004531834368",
        #                 "l": "0.001007",
        #                 "z": "0.001007",
        #                 "L": "19842.02",
        #                 "n": "0",
        #                 "N": "BTC",
        #                 "u": True,
        #                 "w": True,
        #                 "m": False,
        #                 "O": "1662348310368",
        #                 "Z": "19.98091414",
        #                 "A": "0",
        #                 "C": False,
        #                 "v": "0",
        #                 "d": "NO_LIQ",
        #                 "t": "2100000000002220938"
        #             }
        #         ]
        #     }
        #  contract
        #     {
        #         "topic": "user.order.contractAccount",
        #         "data": [
        #             {
        #                 "symbol": "BTCUSD",
        #                 "orderId": "ee013d82-fafc-4504-97b1-d92aca21eedd",
        #                 "side": "Buy",
        #                 "orderType": "Market",
        #                 "stopOrderType": "UNKNOWN",
        #                 "price": "21920.00",
        #                 "qty": "200",
        #                 "timeInForce": "ImmediateOrCancel",
        #                 "orderStatus": "Filled",
        #                 "triggerPrice": "0.00",
        #                 "orderLinkId": "inv001",
        #                 "createdTime": "1661338622771",
        #                 "updatedTime": "1661338622775",
        #                 "takeProfit": "0.00",
        #                 "stopLoss": "0.00",
        #                 "tpTriggerBy": "UNKNOWN",
        #                 "slTriggerBy": "UNKNOWN",
        #                 "triggerBy": "UNKNOWN",
        #                 "reduceOnly": False,
        #                 "closeOnTrigger": False,
        #                 "triggerDirection": 0,
        #                 "leavesQty": "0",
        #                 "lastExecQty": "200",
        #                 "lastExecPrice": "21282.00",
        #                 "cumExecQty": "200",
        #                 "cumExecValue": "0.00939761"
        #             }
        #         ]
        #     }
        # unified
        #     {
        #         "id": "f91080af-5187-4261-a802-7604419771aa",
        #         "topic": "user.order.unifiedAccount",
        #         "ts": 1661932033707,
        #         "data": {
        #             "result": [
        #                 {
        #                     "orderId": "415f8961-4073-4d74-bc3e-df2830e52843",
        #                     "orderLinkId": "",
        #                     "symbol": "BTCUSDT",
        #                     "side": "Buy",
        #                     "orderType": "Limit",
        #                     "price": "17000.00000000",
        #                     "qty": "0.0100",
        #                     "timeInForce": "GoodTillCancel",
        #                     "orderStatus": "New",
        #                     "cumExecQty": "0.0000",
        #                     "cumExecValue": "0.00000000",
        #                     "cumExecFee": "0.00000000",
        #                     "stopOrderType": "UNKNOWN",
        #                     "triggerBy": "UNKNOWN",
        #                     "triggerPrice": "",
        #                     "reduceOnly": True,
        #                     "closeOnTrigger": True,
        #                     "createdTime": 1661932033636,
        #                     "updatedTime": 1661932033644,
        #                     "iv": "",
        #                     "orderIM": "",
        #                     "takeProfit": "",
        #                     "stopLoss": "",
        #                     "tpTriggerBy": "UNKNOWN",
        #                     "slTriggerBy": "UNKNOWN",
        #                     "basePrice": "",
        #                     "blockTradeId": "",
        #                     "leavesQty": "0.0100"
        #                 }
        #             ],
        #             "version": 284
        #         },
        #         "type": "snapshot"
        #     }
        #
        topic = self.safe_string(message, 'topic', '')
        if self.orders is None:
            limit = self.safe_integer(self.options, 'ordersLimit', 1000)
            self.orders = ArrayCacheBySymbolById(limit)
        orders = self.orders
        rawOrders = []
        parser = None
        if topic == 'order':
            rawOrders = self.safe_value(message, 'data', [])
            parser = 'parseWsSpotOrder'
        else:
            parser = 'parseContractOrder'
            rawOrders = self.safe_value(message, 'data', [])
            rawOrders = self.safe_value(rawOrders, 'result', rawOrders)
        symbols = {}
        for i in range(0, len(rawOrders)):
            parsed = getattr(self, parser)(rawOrders[i])
            symbol = parsed['symbol']
            symbols[symbol] = True
            orders.append(parsed)
        symbolsArray = list(symbols.keys())
        for i in range(0, len(symbolsArray)):
            messageHash = 'orders:' + symbolsArray[i]
            client.resolve(orders, messageHash)
        messageHash = 'orders'
        client.resolve(orders, messageHash)

    def parse_ws_spot_order(self, order, market=None):
        #
        #    {
        #        e: 'executionReport',
        #        E: '1653297251061',  # timestamp
        #        s: 'LTCUSDT',  # symbol
        #        c: '1653297250740',  # user id
        #        S: 'SELL',  # side
        #        o: 'MARKET_OF_BASE',  # order type
        #        f: 'GTC',  # time in force
        #        q: '0.16233',  # quantity
        #        p: '0',  # price
        #        X: 'NEW',  # status
        #        i: '1162336018974750208',  # order id
        #        M: '0',
        #        l: '0',  # last filled
        #        z: '0',  # total filled
        #        L: '0',  # last traded price
        #        n: '0',  # trading fee
        #        N: '',  # fee asset
        #        u: True,
        #        w: True,
        #        m: False,  # is limit_maker
        #        O: '1653297251042',  # order creation
        #        Z: '0',  # total filled
        #        A: '0',  # account id
        #        C: False,  # is close
        #        v: '0',  # leverage
        #        d: 'NO_LIQ'
        #    }
        #
        id = self.safe_string(order, 'i')
        marketId = self.safe_string(order, 's')
        symbol = self.safe_symbol(marketId, market, None, 'spot')
        timestamp = self.safe_integer(order, 'O')
        price = self.safe_string(order, 'p')
        if price == '0':
            price = None  # market orders
        filled = self.safe_string(order, 'z')
        status = self.parse_order_status(self.safe_string(order, 'X'))
        side = self.safe_string_lower(order, 'S')
        lastTradeTimestamp = self.safe_string(order, 'E')
        timeInForce = self.safe_string(order, 'f')
        amount = None
        cost = self.safe_string(order, 'Z')
        q = self.safe_string(order, 'q')
        type = self.safe_string_lower(order, 'o')
        if type.find('quote') >= 0:
            amount = filled
        else:
            amount = q
        if type.find('market') >= 0:
            type = 'market'
        fee = None
        feeCost = self.safe_string(order, 'n')
        if feeCost is not None and feeCost != '0':
            feeCurrencyId = self.safe_string(order, 'N')
            feeCurrencyCode = self.safe_currency_code(feeCurrencyId)
            fee = {
                'cost': feeCost,
                'currency': feeCurrencyCode,
            }
        return self.safe_order({
            'info': order,
            'id': id,
            'clientOrderId': self.safe_string(order, 'c'),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'symbol': symbol,
            'type': type,
            'timeInForce': timeInForce,
            'postOnly': None,
            'side': side,
            'price': price,
            'stopPrice': None,
            'triggerPrice': None,
            'amount': amount,
            'cost': cost,
            'average': None,
            'filled': filled,
            'remaining': None,
            'status': status,
            'fee': fee,
        }, 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 bybit api endpoint
        :returns dict: a `balance structure <https://docs.ccxt.com/en/latest/manual.html?#balance-structure>`
        """
        method = 'watchBalance'
        messageHash = 'balances'
        unified = await self.isUnifiedEnabled()
        isUnifiedMargin = self.safe_value(unified, 0, False)
        url = self.get_url_by_market_type(None, True, isUnifiedMargin, method, params)
        await self.authenticate(url)
        topicByMarket = {
            'spot': 'outboundAccountInfo',
            'contract': 'user.wallet.contractAccount',
            'unified': 'user.wallet.unifiedAccount',
        }
        topics = [self.safe_value(topicByMarket, self.get_private_type(url))]
        return await self.watch_topics(url, messageHash, topics, params)

    def handle_balance(self, client, message):
        #
        # spot
        #    {
        #        "type": "snapshot",
        #        "topic": "outboundAccountInfo",
        #        "ts": "1662107217641",
        #        "data": [
        #            {
        #                "e": "outboundAccountInfo",
        #                "E": "1662107217640",
        #                "T": True,
        #                "W": True,
        #                "D": True,
        #                "B": [
        #                    {
        #                        "a": "USDT",
        #                        "f": "176.81254174",
        #                        "l": "201.575"
        #                    }
        #                ]
        #            }
        #        ]
        #    }
        # contract
        #    {
        #        "topic": "user.wallet.contractAccount",
        #        "data": [
        #            {
        #                "coin": "USDT",
        #                "equity": "610.3984319",
        #                "walletBalance": "609.7384319",
        #                "positionMargin": "4.7582882",
        #                "availableBalance": "604.9801437",
        #                "orderMargin": "0",
        #                "unrealisedPnl": "0.66",
        #                "cumRealisedPnl": "-0.2615681"
        #            }
        #        ]
        #    }
        # unified
        #    {
        #        "id": "46bd0430-1d03-48f7-a503-c6c020d07536",
        #        "topic": "user.wallet.unifiedAccount",
        #        "ts": 1649150852199,
        #        "data": {
        #            "result": {
        #                "accountIMRate": "0.0002",
        #                "accountMMRate": "0.0000",
        #                "totalEquity": "510444.50000000",
        #                "totalWalletBalance": "510444.50000000",
        #                "totalMarginBalance": "510444.50000000",
        #                "totalAvailableBalance": "510333.52491801",
        #                "totalPerpUPL": "0.00000000",
        #                "totalInitialMargin": "110.97508199",
        #                "totalMaintenanceMargin": "9.13733489",
        #                "coin": [{
        #                    "currencyCoin": "USDC",
        #                    "equity": "0.00000000",
        #                    "usdValue": "0.00000000",
        #                    "walletBalance": "0.00000000",
        #                    "marginBalance": "510444.50000000",
        #                    "availableBalance": "510333.52491801",
        #                    "marginBalanceWithoutConvert": "0.00000000",
        #                    "availableBalanceWithoutConvert": "0.00000000",
        #                    "borrowSize": "0.00000000",
        #                    "availableToBorrow": "200000.00000000",
        #                    "accruedInterest": "0.00000000",
        #                    "totalOrderIM": "0.00000000",
        #                    "totalPositionIM": "0.00000000",
        #                    "totalPositionMM": "0.00000000"
        #                }]
        #            },
        #            "version": 19
        #        },
        #        "type": "snapshot"
        #    }
        #
        if self.balance is None:
            self.balance = {}
        messageHash = 'balance'
        topic = self.safe_value(message, 'topic')
        info = None
        rawBalances = []
        if topic == 'outboundAccountInfo':
            data = self.safe_value(message, 'data', [])
            for i in range(0, len(data)):
                B = self.safe_value(data[i], 'B', [])
                rawBalances = self.array_concat(rawBalances, B)
            info = rawBalances
        if topic == 'user.wallet.contractAccount':
            rawBalances = self.safe_value(message, 'data', [])
            info = rawBalances
        if topic == 'user.wallet.unifiedAccount':
            data = self.safe_value(message, 'data', {})
            result = self.safe_value(data, 'result', {})
            rawBalances = self.safe_value(result, 'coin', [])
            info = result
        for i in range(0, len(rawBalances)):
            self.parse_ws_balance(rawBalances[i])
        self.balance['info'] = info
        timestamp = self.safe_integer(message, 'ts')
        self.balance['timestamp'] = timestamp
        self.balance['datetime'] = self.iso8601(timestamp)
        self.balance = self.safe_balance(self.balance)
        messageHash = 'balances'
        client.resolve(self.balance, messageHash)

    def parse_ws_balance(self, balance):
        #
        # spot
        #    {
        #        "a": "USDT",
        #        "f": "176.81254174",
        #        "l": "201.575"
        #    }
        # contract
        #    {
        #        "coin": "USDT",
        #        "equity": "610.3984319",
        #        "walletBalance": "609.7384319",
        #        "positionMargin": "4.7582882",
        #        "availableBalance": "604.9801437",
        #        "orderMargin": "0",
        #        "unrealisedPnl": "0.66",
        #        "cumRealisedPnl": "-0.2615681"
        #    }
        # unified
        #    {
        #        "currencyCoin": "USDC",
        #        "equity": "0.00000000",
        #        "usdValue": "0.00000000",
        #        "walletBalance": "0.00000000",
        #        "marginBalance": "510444.50000000",
        #        "availableBalance": "510333.52491801",
        #        "marginBalanceWithoutConvert": "0.00000000",
        #        "availableBalanceWithoutConvert": "0.00000000",
        #        "borrowSize": "0.00000000",
        #        "availableToBorrow": "200000.00000000",
        #        "accruedInterest": "0.00000000",
        #        "totalOrderIM": "0.00000000",
        #        "totalPositionIM": "0.00000000",
        #        "totalPositionMM": "0.00000000"
        #    }
        #
        account = self.account()
        currencyId = self.safe_string_n(balance, ['a', 'currencyCoin', 'coin'])
        code = self.safe_currency_code(currencyId)
        account['free'] = self.safe_string_n(balance, ['availableBalanceWithoutConvert', 'availableBalance', 'f'])
        account['used'] = self.safe_string(balance, 'l')
        account['total'] = self.safe_string(balance, 'walletBalance')
        self.balance[code] = account

    async def watch_topics(self, url, messageHash, topics=[], params={}):
        request = {
            'op': 'subscribe',
            'req_id': self.request_id(),
            'args': topics,
        }
        message = self.extend(request, params)
        return await self.watch(url, messageHash, message, messageHash)

    def authenticate(self, url, params={}):
        self.check_required_credentials()
        messageHash = 'authenticated'
        client = self.client(url)
        future = self.safe_value(client.subscriptions, messageHash)
        if future is None:
            expires = self.milliseconds() + 10000
            expires = str(expires)
            path = 'GET/realtime'
            auth = path + expires
            signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256, 'hex')
            request = {
                'op': 'auth',
                'args': [
                    self.apiKey, expires, signature,
                ],
            }
            message = self.extend(request, params)
            future = self.watch(url, messageHash, message)
            client.subscriptions[messageHash] = future
        return future

    def handle_error_message(self, client, message):
        #
        #   {
        #       success: False,
        #       ret_msg: 'error:invalid op',
        #       conn_id: '5e079fdd-9c7f-404d-9dbf-969d650838b5',
        #       request: {op: '', args: null}
        #   }
        #
        # auth error
        #
        #   {
        #       success: False,
        #       ret_msg: 'error:USVC1111',
        #       conn_id: 'e73770fb-a0dc-45bd-8028-140e20958090',
        #       request: {
        #         op: 'auth',
        #         args: [
        #           '9rFT6uR4uz9Imkw4Wx',
        #           '1653405853543',
        #           '542e71bd85597b4db0290f0ce2d13ed1fd4bb5df3188716c1e9cc69a879f7889'
        #         ]
        #   }
        #
        #   {code: '-10009', desc: 'Invalid period!'}
        #
        code = self.safe_string_2(message, 'code', 'ret_code')
        try:
            if code is not None:
                feedback = self.id + ' ' + self.json(message)
                self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
            success = self.safe_value(message, 'success')
            if success is not None and not success:
                ret_msg = self.safe_string(message, 'ret_msg')
                request = self.safe_value(message, 'request', {})
                op = self.safe_string(request, 'op')
                if op == 'auth':
                    raise AuthenticationError('Authentication failed: ' + ret_msg)
                else:
                    raise ExchangeError(self.id + ' ' + ret_msg)
            return False
        except Exception as error:
            if isinstance(error, AuthenticationError):
                messageHash = 'authenticated'
                client.reject(error, messageHash)
                if messageHash in client.subscriptions:
                    del client.subscriptions[messageHash]
            else:
                client.reject(error)
            return True

    def handle_message(self, client, message):
        if self.handle_error_message(client, message):
            return
        # contract pong
        ret_msg = self.safe_string(message, 'ret_msg')
        if ret_msg == 'pong':
            self.handle_pong(client, message)
            return
        # spot pong
        pong = self.safe_integer(message, 'pong')
        if pong is not None:
            self.handle_pong(client, message)
            return
        # pong
        op = self.safe_string(message, 'op')
        if op == 'pong':
            self.handle_pong(client, message)
            return
        event = self.safe_string(message, 'event')
        if event == 'sub':
            self.handle_subscription_status(client, message)
            return
        topic = self.safe_string(message, 'topic', '')
        methods = {
            'orderbook': self.handle_order_book,
            'kline': self.handle_ohlcv,
            'order': self.handle_order,
            'stopOrder': self.handle_order,
            'ticker': self.handle_ticker,
            'trade': self.handle_trades,
            'publicTrade': self.handle_trades,
            'depth': self.handle_order_book,
            'wallet': self.handle_balance,
            'outboundAccountInfo': self.handle_balance,
            'execution': self.handle_my_trades,
            'ticketInfo': self.handle_my_trades,
        }
        keys = list(methods.keys())
        for i in range(0, len(keys)):
            key = keys[i]
            if topic.find(keys[i]) >= 0:
                method = methods[key]
                method(client, message)
                return
        # unified auth acknowledgement
        type = self.safe_string(message, 'type')
        if (op == 'auth') or (type == 'AUTH_RESP'):
            self.handle_authenticate(client, message)

    def ping(self, client):
        return {
            'req_id': self.request_id(),
            'op': 'ping',
        }

    def handle_pong(self, client, message):
        #
        #   {
        #       success: True,
        #       ret_msg: 'pong',
        #       conn_id: 'db3158a0-8960-44b9-a9de-ac350ee13158',
        #       request: {op: 'ping', args: null}
        #   }
        #
        #   {pong: 1653296711335}
        #
        client.lastPong = self.safe_integer(message, 'pong')
        return message

    def handle_authenticate(self, client, message):
        #
        #    {
        #        success: True,
        #        ret_msg: '',
        #        op: 'auth',
        #        conn_id: 'ce3dpomvha7dha97tvp0-2xh'
        #    }
        #
        success = self.safe_value(message, 'success')
        messageHash = 'authenticated'
        if success:
            client.resolve(message, messageHash)
        else:
            error = AuthenticationError(self.id + ' ' + self.json(message))
            client.reject(error, messageHash)
            if messageHash in client.subscriptions:
                del client.subscriptions[messageHash]
        return message

    def handle_subscription_status(self, client, message):
        #
        #    {
        #        topic: 'kline',
        #        event: 'sub',
        #        params: {
        #          symbol: 'LTCUSDT',
        #          binary: 'false',
        #          klineType: '1m',
        #          symbolName: 'LTCUSDT'
        #        },
        #        code: '0',
        #        msg: 'Success'
        #    }
        #
        return message
