# -*- 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, ArrayCacheByTimestamp
import hashlib
from ccxt.base.types import Any, Balances, Int, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade
from ccxt.async_support.base.ws.client import Client
from typing import List
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import BadRequest
from ccxt.base.errors import InvalidOrder
from ccxt.base.precise import Precise


class poloniex(ccxt.async_support.poloniex):

    def describe(self) -> Any:
        return self.deep_extend(super(poloniex, self).describe(), {
            'has': {
                'ws': True,
                'watchOHLCV': True,
                'watchOrderBook': True,
                'watchTicker': True,
                'watchTickers': True,
                'watchTrades': True,
                'watchTradesForSymbols': True,
                'watchBalance': True,
                'watchStatus': False,
                'watchOrders': True,
                'watchMyTrades': True,
                'createOrderWs': True,
                'editOrderWs': False,
                'fetchOpenOrdersWs': False,
                'fetchOrderWs': False,
                'cancelOrderWs': True,
                'cancelOrdersWs': True,
                'cancelAllOrdersWs': True,
                'fetchTradesWs': False,
                'fetchBalanceWs': False,
            },
            'urls': {
                'api': {
                    'ws': {
                        'public': 'wss://ws.poloniex.com/ws/public',
                        'private': 'wss://ws.poloniex.com/ws/private',
                    },
                },
            },
            'options': {
                'createMarketBuyOrderRequiresPrice': True,
                'tradesLimit': 1000,
                'ordersLimit': 1000,
                'OHLCVLimit': 1000,
                'watchOrderBook': {
                    'name': 'book_lv2',  # can also be 'book'
                },
                'connectionsLimit': 2000,  # 2000 public, 2000 private, 4000 total, only for subscribe events, unsubscribe not restricted
                'requestsLimit': 500,  # per second, only for subscribe events, unsubscribe not restricted
                'timeframes': {
                    '1m': 'candles_minute_1',
                    '5m': 'candles_minute_5',
                    '10m': 'candles_minute_10',
                    '15m': 'candles_minute_15',
                    '30m': 'candles_minute_30',
                    '1h': 'candles_hour_1',
                    '2h': 'candles_hour_2',
                    '4h': 'candles_hour_4',
                    '6h': 'candles_hour_6',
                    '12h': 'candles_hour_12',
                    '1d': 'candles_day_1',
                    '3d': 'candles_day_3',
                    '1w': 'candles_week_1',
                    '1M': 'candles_month_1',
                },
            },
            'streaming': {
                'keepAlive': 15000,
                'ping': self.ping,
            },
        })

    async def authenticate(self, params={}):
        """
 @ignore
        authenticates the user to access private web socket channels

        https://api-docs.poloniex.com/spot/websocket/authentication

        :returns dict: response from exchange
        """
        self.check_required_credentials()
        timestamp = self.number_to_string(self.milliseconds())
        url = self.urls['api']['ws']['private']
        messageHash = 'authenticated'
        client = self.client(url)
        future = self.safe_value(client.subscriptions, messageHash)
        if future is None:
            accessPath = '/ws'
            requestString = 'GET\n' + accessPath + '\nsignTimestamp=' + timestamp
            signature = self.hmac(self.encode(requestString), self.encode(self.secret), hashlib.sha256, 'base64')
            request: dict = {
                'event': 'subscribe',
                'channel': ['auth'],
                'params': {
                    'key': self.apiKey,
                    'signTimestamp': timestamp,
                    'signature': signature,
                    'signatureMethod': 'HmacSHA256',  # optional
                    'signatureVersion': '2',          # optional
                },
            }
            message = self.extend(request, params)
            future = await self.watch(url, messageHash, message, messageHash)
            #
            #    {
            #        "data": {
            #            "success": True,
            #            "ts": 1645597033915
            #        },
            #        "channel": "auth"
            #    }
            #
            #    # Failure to return results
            #
            #    {
            #        "data": {
            #            "success": False,
            #            "message": "Authentication failed!",
            #            "ts": 1646276295075
            #        },
            #        "channel": "auth"
            #    }
            #
            client.subscriptions[messageHash] = future
        return future

    async def subscribe(self, name: str, messageHash: str, isPrivate: bool, symbols: Strings = None, params={}):
        """
 @ignore
        Connects to a websocket channel
        :param str name: name of the channel
        :param str messageHash: unique identifier for the message
        :param boolean isPrivate: True for the authenticated url, False for the public url
        :param str[] [symbols]: CCXT market symbols
        :param dict [params]: extra parameters specific to the poloniex api
        :returns dict: data from the websocket stream
        """
        publicOrPrivate = 'private' if isPrivate else 'public'
        url = self.urls['api']['ws'][publicOrPrivate]
        subscribe: dict = {
            'event': 'subscribe',
            'channel': [
                name,
            ],
        }
        marketIds = []
        if self.is_empty(symbols):
            marketIds.append('all')
        else:
            messageHash = messageHash + '::' + ','.join(symbols)
            marketIds = self.market_ids(symbols)
        if name != 'balances':
            subscribe['symbols'] = marketIds
        request = self.extend(subscribe, params)
        return await self.watch(url, messageHash, request, messageHash)

    async def trade_request(self, name: str, params={}):
        """
 @ignore
        Connects to a websocket channel
        :param str name: name of the channel
        :param dict [params]: extra parameters specific to the poloniex api
        :returns dict: data from the websocket stream
        """
        url = self.urls['api']['ws']['private']
        messageHash = str(self.nonce())
        subscribe: dict = {
            'id': messageHash,
            'event': name,
            'params': params,
        }
        return await self.watch(url, messageHash, subscribe, messageHash)

    async def create_order_ws(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:
        """

        https://api-docs.poloniex.com/spot/websocket/trade-request#create-order

        create a trade order
        :param str symbol: unified symbol of the market to create an order in
        :param str type: 'market' or 'limit'
        :param str side: 'buy' or 'sell'
        :param float amount: how much of currency you want to trade in units of base currency
        :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
        :param dict [params]: extra parameters specific to the poloniex api endpoint
        :param str [params.timeInForce]: GTC(default), IOC, FOK
        :param str [params.clientOrderId]: Maximum 64-character length.*
        :param float [params.cost]: *spot market buy only* the quote quantity that can be used alternative for the amount

 EXCHANGE SPECIFIC PARAMETERS
        :param str [params.amount]: quote units for the order
        :param boolean [params.allowBorrow]: allow order to be placed by borrowing funds(Default: False)
        :param str [params.stpMode]: self-trade prevention, defaults to expire_taker, none: enable self-trade; expire_taker: taker order will be canceled when self-trade happens
        :param str [params.slippageTolerance]: used to control the maximum slippage ratio, the value range is greater than 0 and less than 1
        :returns dict: an `order structure <https://github.com/ccxt/ccxt/wiki/Manual#order-structure>`
        """
        await self.load_markets()
        await self.authenticate()
        market = self.market(symbol)
        uppercaseType = type.upper()
        uppercaseSide = side.upper()
        isPostOnly = self.is_post_only(uppercaseType == 'MARKET', uppercaseType == 'LIMIT_MAKER', params)
        if isPostOnly:
            uppercaseType = 'LIMIT_MAKER'
        request: dict = {
            'symbol': market['id'],
            'side': side.upper(),
            'type': type.upper(),
        }
        if (uppercaseType == 'MARKET') and (uppercaseSide == 'BUY'):
            quoteAmount = None
            createMarketBuyOrderRequiresPrice = True
            createMarketBuyOrderRequiresPrice, params = self.handle_option_and_params(params, 'createOrder', 'createMarketBuyOrderRequiresPrice', True)
            cost = self.safe_number(params, 'cost')
            params = self.omit(params, 'cost')
            if cost is not None:
                quoteAmount = self.cost_to_precision(symbol, cost)
            elif createMarketBuyOrderRequiresPrice:
                if price is None:
                    raise InvalidOrder(self.id + ' createOrder() requires the price argument for market buy orders to calculate the total cost to spend(amount * price), alternatively set the createMarketBuyOrderRequiresPrice option or param to False and pass the cost to spend(quote quantity) in the amount argument')
                else:
                    amountString = self.number_to_string(amount)
                    priceString = self.number_to_string(price)
                    costRequest = Precise.string_mul(amountString, priceString)
                    quoteAmount = self.cost_to_precision(symbol, costRequest)
            else:
                quoteAmount = self.cost_to_precision(symbol, amount)
            request['amount'] = quoteAmount
        else:
            request['quantity'] = self.amount_to_precision(market['symbol'], amount)
            if price is not None:
                request['price'] = self.price_to_precision(symbol, price)
        orders = await self.trade_request('createOrder', self.extend(request, params))
        order = self.safe_dict(orders, 0)
        return order

    async def cancel_order_ws(self, id: str, symbol: Str = None, params={}):
        """

        https://api-docs.poloniex.com/spot/websocket/trade-request#cancel-multiple-orders

        cancel multiple orders
        :param str id: order id
        :param str [symbol]: unified market symbol
        :param dict [params]: extra parameters specific to the poloniex api endpoint
        :param str [params.clientOrderId]: client order id
        :returns dict: an list of `order structures <https://github.com/ccxt/ccxt/wiki/Manual#order-structure>`
        """
        clientOrderId = self.safe_string(params, 'clientOrderId')
        if clientOrderId is not None:
            clientOrderIds = self.safe_value(params, 'clientOrderId', [])
            params['clientOrderIds'] = self.array_concat(clientOrderIds, [clientOrderId])
        orders = await self.cancel_orders_ws([id], symbol, params)
        order = self.safe_dict(orders, 0)
        return order

    async def cancel_orders_ws(self, ids: List[str], symbol: Str = None, params={}):
        """

        https://api-docs.poloniex.com/spot/websocket/trade-request#cancel-multiple-orders

        cancel multiple orders
        :param str[] ids: order ids
        :param str symbol: unified market symbol, default is None
        :param dict [params]: extra parameters specific to the poloniex api endpoint
        :param str[] [params.clientOrderIds]: client order ids
        :returns dict: an list of `order structures <https://github.com/ccxt/ccxt/wiki/Manual#order-structure>`
        """
        await self.load_markets()
        await self.authenticate()
        request: dict = {
            'orderIds': ids,
        }
        return await self.trade_request('cancelOrders', self.extend(request, params))

    async def cancel_all_orders_ws(self, symbol: Str = None, params={}):
        """

        https://api-docs.poloniex.com/spot/websocket/trade-request#cancel-all-orders

        cancel all open orders of a type. Only applicable to Option in Portfolio Margin mode, and MMP privilege is required.
        :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None
        :param dict [params]: extra parameters specific to the poloniex api endpoint
        :returns dict[]: a list of `order structures <https://github.com/ccxt/ccxt/wiki/Manual#order-structure>`
        """
        await self.load_markets()
        await self.authenticate()
        return await self.trade_request('cancelAllOrders', params)

    def handle_order_request(self, client: Client, message):
        #
        #    {
        #        "id": "1234567",
        #        "data": [{
        #           "orderId": 205343650954092544,
        #           "clientOrderId": "",
        #           "message": "",
        #           "code": 200
        #        }]
        #    }
        #
        messageHash = self.safe_string(message, 'id')
        data = self.safe_value(message, 'data', [])
        orders = []
        for i in range(0, len(data)):
            order = data[i]
            parsedOrder = self.parse_ws_order(order)
            orders.append(parsedOrder)
        client.resolve(orders, messageHash)

    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

        https://api-docs.poloniex.com/spot/websocket/market-data#candlesticks

        :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()
        timeframes = self.safe_value(self.options, 'timeframes', {})
        channel = self.safe_string(timeframes, timeframe, timeframe)
        if channel is None:
            raise BadRequest(self.id + ' watchOHLCV cannot take a timeframe of ' + timeframe)
        ohlcv = await self.subscribe(channel, channel, False, [symbol], params)
        if self.newUpdates:
            limit = ohlcv.getLimit(symbol, limit)
        return self.filter_by_since_limit(ohlcv, since, limit, 0, True)

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

        https://api-docs.poloniex.com/spot/websocket/market-data#ticker

        :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()
        symbol = self.symbol(symbol)
        tickers = await self.watch_tickers([symbol], params)
        return self.safe_value(tickers, symbol)

    async def watch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
        """
        watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market

        https://api-docs.poloniex.com/spot/websocket/market-data#ticker

        :param str[] symbols:
        :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()
        name = 'ticker'
        symbols = self.market_symbols(symbols)
        newTickers = await self.subscribe(name, name, False, symbols, params)
        if self.newUpdates:
            return newTickers
        return self.filter_by_array(self.tickers, 'symbol', symbols)

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

        https://api-docs.poloniex.com/spot/websocket/market-data#trades

        :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 list of symbols

        https://api-docs.poloniex.com/spot/websocket/market-data#trades

        :param str[] symbols: 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, None, False, True, True)
        name = 'trades'
        url = self.urls['api']['ws']['public']
        marketIds = self.market_ids(symbols)
        subscribe: dict = {
            'event': 'subscribe',
            'channel': [
                name,
            ],
            'symbols': marketIds,
        }
        request = self.extend(subscribe, params)
        messageHashes = []
        if symbols is not None:
            for i in range(0, len(symbols)):
                messageHashes.append(name + '::' + symbols[i])
        trades = await self.watch_multiple(url, messageHashes, request, messageHashes)
        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)

    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

        https://api-docs.poloniex.com/spot/websocket/market-data#book-level-2

        :param str symbol: unified symbol of the market to fetch the order book for
        :param int [limit]: not used by poloniex watchOrderBook
        :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()
        watchOrderBookOptions = self.safe_value(self.options, 'watchOrderBook')
        name = self.safe_string(watchOrderBookOptions, 'name', 'book_lv2')
        name, params = self.handle_option_and_params(params, 'method', 'name', name)
        orderbook = await self.subscribe(name, name, False, [symbol], params)
        return orderbook.limit()

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

        https://api-docs.poloniex.com/spot/websocket/order

        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: not used by poloniex watchOrders
        :param int [limit]: not used by poloniex watchOrders
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
        """
        await self.load_markets()
        name = 'orders'
        await self.authenticate()
        if symbol is not None:
            symbol = self.symbol(symbol)
        symbols = None if (symbol is None) else [symbol]
        orders = await self.subscribe(name, name, True, symbols, params)
        if self.newUpdates:
            limit = orders.getLimit(symbol, limit)
        return self.filter_by_since_limit(orders, since, limit, 'timestamp', True)

    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 using orders stream

        https://api-docs.poloniex.com/spot/websocket/order

        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: not used by poloniex watchMyTrades
        :param int [limit]: not used by poloniex watchMyTrades
        :param dict [params]: extra parameters specific to the poloniex strean
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
        """
        await self.load_markets()
        name = 'orders'
        messageHash = 'myTrades'
        await self.authenticate()
        if symbol is not None:
            symbol = self.symbol(symbol)
        symbols = None if (symbol is None) else [symbol]
        trades = await self.subscribe(name, messageHash, True, symbols, params)
        if self.newUpdates:
            limit = trades.getLimit(symbol, limit)
        return self.filter_by_since_limit(trades, since, limit, 'timestamp', True)

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

        https://api-docs.poloniex.com/spot/websocket/balance

        :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()
        name = 'balances'
        await self.authenticate()
        return await self.subscribe(name, name, True, None, params)

    def parse_ws_ohlcv(self, ohlcv, market=None) -> list:
        #
        #    {
        #        "symbol": "BTC_USDT",
        #        "amount": "840.7240416",
        #        "high": "24832.35",
        #        "quantity": "0.033856",
        #        "tradeCount": 1,
        #        "low": "24832.35",
        #        "closeTime": 1676942519999,
        #        "startTime": 1676942460000,
        #        "close": "24832.35",
        #        "open": "24832.35",
        #        "ts": 1676942492072
        #    }
        #
        return [
            self.safe_integer(ohlcv, 'startTime'),
            self.safe_number(ohlcv, 'open'),
            self.safe_number(ohlcv, 'high'),
            self.safe_number(ohlcv, 'low'),
            self.safe_number(ohlcv, 'close'),
            self.safe_number(ohlcv, 'quantity'),
        ]

    def handle_ohlcv(self, client: Client, message):
        #
        #    {
        #        "channel": "candles_minute_1",
        #        "data": [
        #            {
        #                "symbol": "BTC_USDT",
        #                "amount": "840.7240416",
        #                "high": "24832.35",
        #                "quantity": "0.033856",
        #                "tradeCount": 1,
        #                "low": "24832.35",
        #                "closeTime": 1676942519999,
        #                "startTime": 1676942460000,
        #                "close": "24832.35",
        #                "open": "24832.35",
        #                "ts": 1676942492072
        #            }
        #        ]
        #    }
        #
        data = self.safe_value(message, 'data')
        data = self.safe_value(data, 0)
        channel = self.safe_string(message, 'channel')
        marketId = self.safe_string(data, 'symbol')
        symbol = self.safe_symbol(marketId)
        market = self.safe_market(symbol)
        timeframes = self.safe_value(self.options, 'timeframes', {})
        timeframe = self.find_timeframe(channel, timeframes)
        messageHash = channel + '::' + symbol
        parsed = self.parse_ws_ohlcv(data, market)
        self.ohlcvs[symbol] = self.safe_value(self.ohlcvs, symbol, {})
        stored = self.safe_value(self.ohlcvs[symbol], timeframe)
        if symbol is not None:
            if stored is None:
                limit = self.safe_integer(self.options, 'OHLCVLimit', 1000)
                stored = ArrayCacheByTimestamp(limit)
                self.ohlcvs[symbol][timeframe] = stored
            stored.append(parsed)
            client.resolve(stored, messageHash)
        return message

    def handle_trade(self, client: Client, message):
        #
        #    {
        #        "channel": "trades",
        #        "data": [
        #            {
        #                "symbol": "BTC_USDT",
        #                "amount": "13.41634893",
        #                "quantity": "0.000537",
        #                "takerSide": "buy",
        #                "createTime": 1676950548834,
        #                "price": "24983.89",
        #                "id": "62486976",
        #                "ts": 1676950548839
        #            }
        #        ]
        #    }
        #
        data = self.safe_value(message, 'data', [])
        for i in range(0, len(data)):
            item = data[i]
            marketId = self.safe_string(item, 'symbol')
            if marketId is not None:
                trade = self.parse_ws_trade(item)
                symbol = trade['symbol']
                type = 'trades'
                messageHash = type + '::' + symbol
                tradesArray = self.safe_value(self.trades, symbol)
                if tradesArray is None:
                    tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
                    tradesArray = ArrayCache(tradesLimit)
                    self.trades[symbol] = tradesArray
                tradesArray.append(trade)
                client.resolve(tradesArray, messageHash)
        return message

    def parse_ws_trade(self, trade, market=None):
        #
        # handleTrade
        #
        #    {
        #        "symbol": "BTC_USDT",
        #        "amount": "13.41634893",
        #        "quantity": "0.000537",
        #        "takerSide": "buy",
        #        "createTime": 1676950548834,
        #        "price": "24983.89",
        #        "id": "62486976",
        #        "ts": 1676950548839
        #    }
        #
        # private trade
        #    {
        #        "orderId":"186250258089635840",
        #        "tradeId":"62036513",
        #        "clientOrderId":"",
        #        "accountType":"SPOT",
        #        "eventType":"trade",
        #        "symbol":"ADA_USDT",
        #        "side":"SELL",
        #        "type":"MARKET",
        #        "price":"0",
        #        "quantity":"3",
        #        "state":"FILLED",
        #        "createTime":1685371921891,
        #        "tradeTime":1685371921908,
        #        "tradePrice":"0.37694",
        #        "tradeQty":"3",
        #        "feeCurrency":"USDT",
        #        "tradeFee":"0.00226164",
        #        "tradeAmount":"1.13082",
        #        "filledQuantity":"3",
        #        "filledAmount":"1.13082",
        #        "ts":1685371921945,
        #        "source":"WEB",
        #        "orderAmount":"0",
        #        "matchRole":"TAKER"
        #     }
        #
        marketId = self.safe_string(trade, 'symbol')
        market = self.safe_market(marketId, market)
        timestamp = self.safe_integer(trade, 'createTime')
        takerMaker = self.safe_string_lower_2(trade, 'matchRole', 'taker')
        return self.safe_trade({
            'info': trade,
            'id': self.safe_string_2(trade, 'id', 'tradeId'),
            'symbol': self.safe_string(market, 'symbol'),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'order': self.safe_string(trade, 'orderId'),
            'type': self.safe_string_lower(trade, 'type'),
            'side': self.safe_string_lower_2(trade, 'takerSide', 'side'),
            'takerOrMaker': takerMaker,
            'price': self.omit_zero(self.safe_string_2(trade, 'tradePrice', 'price')),
            'amount': self.omit_zero(self.safe_string_2(trade, 'filledQuantity', 'quantity')),
            'cost': self.safe_string_2(trade, 'amount', 'filledAmount'),
            'fee': {
                'rate': None,
                'cost': self.safe_string(trade, 'tradeFee'),
                'currency': self.safe_string(trade, 'feeCurrency'),
            },
        }, market)

    def parse_status(self, status):
        statuses: dict = {
            'NEW': 'open',
            'PARTIALLY_FILLED': 'open',
            'FILLED': 'closed',
            'PENDING_CANCEL': 'open',
            'PARTIALLY_CANCELED': 'open',
            'CANCELED': 'canceled',
            # FAILED
        }
        return self.safe_string(statuses, status, status)

    def parse_ws_order_trade(self, trade, market=None):
        #
        #    {
        #        "symbol": "BTC_USDT",
        #        "type": "LIMIT",
        #        "quantity": "1",
        #        "orderId": "32471407854219264",
        #        "tradeFee": "0",
        #        "clientOrderId": "",
        #        "accountType": "SPOT",
        #        "feeCurrency": "",
        #        "eventType": "place",
        #        "source": "API",
        #        "side": "BUY",
        #        "filledQuantity": "0",
        #        "filledAmount": "0",
        #        "matchRole": "MAKER",
        #        "state": "NEW",
        #        "tradeTime": 0,
        #        "tradeAmount": "0",
        #        "orderAmount": "0",
        #        "createTime": 1648708186922,
        #        "price": "47112.1",
        #        "tradeQty": "0",
        #        "tradePrice": "0",
        #        "tradeId": "0",
        #        "ts": 1648708187469
        #    }
        #
        timestamp = self.safe_integer(trade, 'tradeTime')
        marketId = self.safe_string(trade, 'symbol')
        return self.safe_trade({
            'info': trade,
            'id': self.safe_string(trade, 'tradeId'),
            'symbol': self.safe_symbol(marketId, market),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'order': self.safe_string(trade, 'orderId'),
            'type': self.safe_string_lower(trade, 'type'),
            'side': self.safe_string(trade, 'side'),
            'takerOrMaker': self.safe_string_lower(trade, 'matchRole'),
            'price': self.safe_string(trade, 'price'),
            'amount': self.safe_string(trade, 'tradeAmount'),
            'cost': None,
            'fee': {
                'rate': None,
                'cost': self.safe_string(trade, 'tradeFee'),
                'currency': self.safe_string(trade, 'feeCurrency'),
            },
        }, market)

    def handle_order(self, client: Client, message):
        #
        # Order is created
        #
        #    {
        #        "channel": "orders",
        #        "data": [
        #            {
        #                "symbol": "BTC_USDT",
        #                "type": "LIMIT",
        #                "quantity": "1",
        #                "orderId": "32471407854219264",
        #                "tradeFee": "0",
        #                "clientOrderId": "",
        #                "accountType": "SPOT",
        #                "feeCurrency": "",
        #                "eventType": "place",
        #                "source": "API",
        #                "side": "BUY",
        #                "filledQuantity": "0",
        #                "filledAmount": "0",
        #                "matchRole": "MAKER",
        #                "state": "NEW",
        #                "tradeTime": 0,
        #                "tradeAmount": "0",
        #                "orderAmount": "0",
        #                "createTime": 1648708186922,
        #                "price": "47112.1",
        #                "tradeQty": "0",
        #                "tradePrice": "0",
        #                "tradeId": "0",
        #                "ts": 1648708187469
        #            }
        #        ]
        #    }
        #
        data = self.safe_value(message, 'data', [])
        orders = self.orders
        if orders is None:
            limit = self.safe_integer(self.options, 'ordersLimit')
            orders = ArrayCacheBySymbolById(limit)
            self.orders = orders
        marketIds = []
        for i in range(0, len(data)):
            order = self.safe_value(data, i)
            marketId = self.safe_string(order, 'symbol')
            eventType = self.safe_string(order, 'eventType')
            if marketId is not None:
                symbol = self.safe_symbol(marketId)
                orderId = self.safe_string(order, 'orderId')
                clientOrderId = self.safe_string(order, 'clientOrderId')
                if eventType == 'place' or eventType == 'canceled':
                    parsed = self.parse_ws_order(order)
                    orders.append(parsed)
                else:
                    previousOrders = self.safe_value(orders.hashmap, symbol, {})
                    previousOrder = self.safe_value_2(previousOrders, orderId, clientOrderId)
                    trade = self.parse_ws_trade(order)
                    self.handle_my_trades(client, trade)
                    if previousOrder['trades'] is None:
                        previousOrder['trades'] = []
                    previousOrder['trades'].append(trade)
                    previousOrder['lastTradeTimestamp'] = trade['timestamp']
                    totalCost = '0'
                    totalAmount = '0'
                    previousOrderTrades = previousOrder['trades']
                    for j in range(0, len(previousOrderTrades)):
                        previousOrderTrade = previousOrderTrades[j]
                        cost = self.number_to_string(previousOrderTrade['cost'])
                        amount = self.number_to_string(previousOrderTrade['amount'])
                        totalCost = Precise.string_add(totalCost, cost)
                        totalAmount = Precise.string_add(totalAmount, amount)
                    if Precise.string_gt(totalAmount, '0'):
                        previousOrder['average'] = self.parse_number(Precise.string_div(totalCost, totalAmount))
                    previousOrder['cost'] = self.parse_number(totalCost)
                    if previousOrder['filled'] is not None:
                        tradeAmount = self.number_to_string(trade['amount'])
                        previousOrderFilled = self.number_to_string(previousOrder['filled'])
                        previousOrderFilled = Precise.string_add(previousOrderFilled, tradeAmount)
                        previousOrder['filled'] = previousOrderFilled
                        if previousOrder['amount'] is not None:
                            previousOrderAmount = self.number_to_string(previousOrder['amount'])
                            previousOrder['remaining'] = self.parse_number(Precise.string_sub(previousOrderAmount, previousOrderFilled))
                    if previousOrder['fee'] is None:
                        previousOrder['fee'] = {
                            'rate': None,
                            'cost': 0,
                            'currency': trade['fee']['currency'],
                        }
                    if (previousOrder['fee']['cost'] is not None) and (trade['fee']['cost'] is not None):
                        stringOrderCost = self.number_to_string(previousOrder['fee']['cost'])
                        stringTradeCost = self.number_to_string(trade['fee']['cost'])
                        previousOrder['fee']['cost'] = Precise.string_add(stringOrderCost, stringTradeCost)
                    rawState = self.safe_string(order, 'state')
                    state = self.parse_status(rawState)
                    previousOrder['status'] = state
                    # update the newUpdates count
                    orders.append(previousOrder)
                marketIds.append(marketId)
        for i in range(0, len(marketIds)):
            marketId = marketIds[i]
            market = self.market(marketId)
            symbol = market['symbol']
            messageHash = 'orders::' + symbol
            client.resolve(orders, messageHash)
        client.resolve(orders, 'orders')
        return message

    def parse_ws_order(self, order, market=None):
        #
        #    {
        #        "symbol": "BTC_USDT",
        #        "type": "LIMIT",
        #        "quantity": "1",
        #        "orderId": "32471407854219264",
        #        "tradeFee": "0",
        #        "clientOrderId": "",
        #        "accountType": "SPOT",
        #        "feeCurrency": "",
        #        "eventType": "place",
        #        "source": "API",
        #        "side": "BUY",
        #        "filledQuantity": "0",
        #        "filledAmount": "0",
        #        "matchRole": "MAKER",
        #        "state": "NEW",
        #        "tradeTime": 0,
        #        "tradeAmount": "0",
        #        "orderAmount": "0",
        #        "createTime": 1648708186922,
        #        "price": "47112.1",
        #        "tradeQty": "0",
        #        "tradePrice": "0",
        #        "tradeId": "0",
        #        "ts": 1648708187469
        #    }
        #
        id = self.safe_string(order, 'orderId')
        clientOrderId = self.safe_string(order, 'clientOrderId')
        marketId = self.safe_string(order, 'symbol')
        timestamp = self.safe_string(order, 'ts')
        filledAmount = self.safe_string(order, 'filledAmount')
        status = self.safe_string(order, 'state')
        trades = None
        if not Precise.string_eq(filledAmount, '0'):
            trades = []
            trade = self.parse_ws_order_trade(order)
            trades.append(trade)
        return self.safe_order({
            'info': order,
            'symbol': self.safe_symbol(marketId, market),
            'id': id,
            'clientOrderId': clientOrderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': None,
            'type': self.safe_string(order, 'type'),
            'timeInForce': None,
            'postOnly': None,
            'side': self.safe_string(order, 'side'),
            'price': self.safe_string(order, 'price'),
            'stopPrice': None,
            'triggerPrice': None,
            'amount': self.safe_string(order, 'quantity'),
            'cost': None,
            'average': None,
            'filled': filledAmount,
            'remaining': self.safe_string(order, 'remaining_size'),
            'status': self.parse_status(status),
            'fee': {
                'rate': None,
                'cost': self.safe_string(order, 'tradeFee'),
                'currency': self.safe_string(order, 'feeCurrency'),
            },
            'trades': trades,
        })

    def handle_ticker(self, client: Client, message):
        #
        #    {
        #        "channel": "ticker",
        #        "data": [
        #            {
        #                "symbol": "BTC_USDT",
        #                "startTime": 1677280800000,
        #                "open": "23154.32",
        #                "high": "23212.21",
        #                "low": "22761.01",
        #                "close": "23148.86",
        #                "quantity": "105.179566",
        #                "amount": "2423161.17436702",
        #                "tradeCount": 17582,
        #                "dailyChange": "-0.0002",
        #                "markPrice": "23151.09",
        #                "closeTime": 1677367197924,
        #                "ts": 1677367251090
        #            }
        #        ]
        #    }
        #
        data = self.safe_value(message, 'data', [])
        newTickers: dict = {}
        for i in range(0, len(data)):
            item = data[i]
            marketId = self.safe_string(item, 'symbol')
            if marketId is not None:
                ticker = self.parse_ticker(item)
                symbol = ticker['symbol']
                self.tickers[symbol] = ticker
                newTickers[symbol] = ticker
        messageHashes = self.find_message_hashes(client, 'ticker::')
        for i in range(0, len(messageHashes)):
            messageHash = messageHashes[i]
            parts = messageHash.split('::')
            symbolsString = parts[1]
            symbols = symbolsString.split(',')
            tickers = self.filter_by_array(newTickers, 'symbol', symbols)
            if not self.is_empty(tickers):
                client.resolve(tickers, messageHash)
        client.resolve(newTickers, 'ticker')
        return message

    def handle_order_book(self, client: Client, message):
        #
        # snapshot
        #
        #    {
        #        "channel": "book_lv2",
        #        "data": [
        #            {
        #                "symbol": "BTC_USDT",
        #                "createTime": 1677368876253,
        #                "asks": [
        #                    ["5.65", "0.02"],
        #                    ...
        #                ],
        #                "bids": [
        #                    ["6.16", "0.6"],
        #                    ...
        #                ],
        #                "lastId": 164148724,
        #                "id": 164148725,
        #                "ts": 1677368876316
        #            }
        #        ],
        #        "action": "snapshot"
        #    }
        #
        # update
        #
        #    {
        #        "channel": "book_lv2",
        #        "data": [
        #            {
        #                "symbol": "BTC_USDT",
        #                "createTime": 1677368876882,
        #                "asks": [
        #                    ["6.35", "3"]
        #                ],
        #                "bids": [
        #                    ["5.65", "0.02"]
        #                ],
        #                "lastId": 164148725,
        #                "id": 164148726,
        #                "ts": 1677368876890
        #            }
        #        ],
        #        "action": "update"
        #    }
        #
        data = self.safe_value(message, 'data', [])
        type = self.safe_string(message, 'action')
        snapshot = type == 'snapshot'
        update = type == 'update'
        for i in range(0, len(data)):
            item = data[i]
            marketId = self.safe_string(item, 'symbol')
            market = self.safe_market(marketId)
            symbol = market['symbol']
            name = 'book_lv2'
            messageHash = name + '::' + symbol
            subscription = self.safe_value(client.subscriptions, messageHash, {})
            limit = self.safe_integer(subscription, 'limit')
            timestamp = self.safe_integer(item, 'ts')
            asks = self.safe_value(item, 'asks')
            bids = self.safe_value(item, 'bids')
            if snapshot or update:
                if snapshot:
                    self.orderbooks[symbol] = self.order_book({}, limit)
                orderbook = self.orderbooks[symbol]
                if bids is not None:
                    for j in range(0, len(bids)):
                        bid = self.safe_value(bids, j)
                        price = self.safe_number(bid, 0)
                        amount = self.safe_number(bid, 1)
                        bidsSide = orderbook['bids']
                        bidsSide.store(price, amount)
                if asks is not None:
                    for j in range(0, len(asks)):
                        ask = self.safe_value(asks, j)
                        price = self.safe_number(ask, 0)
                        amount = self.safe_number(ask, 1)
                        asksSide = orderbook['asks']
                        asksSide.store(price, amount)
                orderbook['symbol'] = symbol
                orderbook['timestamp'] = timestamp
                orderbook['datetime'] = self.iso8601(timestamp)
                client.resolve(orderbook, messageHash)

    def handle_balance(self, client: Client, message):
        #
        #    {
        #       "channel": "balances",
        #       "data": [
        #            {
        #                "changeTime": 1657312008411,
        #                "accountId": "1234",
        #                "accountType": "SPOT",
        #                "eventType": "place_order",
        #                "available": "9999999983.668",
        #                "currency": "BTC",
        #                "id": 60018450912695040,
        #                "userId": 12345,
        #                "hold": "16.332",
        #                "ts": 1657312008443
        #            }
        #        ]
        #    }
        #
        data = self.safe_value(message, 'data', [])
        messageHash = 'balances'
        self.balance = self.parse_ws_balance(data)
        client.resolve(self.balance, messageHash)

    def parse_ws_balance(self, response):
        #
        #    [
        #        {
        #            "changeTime": 1657312008411,
        #            "accountId": "1234",
        #            "accountType": "SPOT",
        #            "eventType": "place_order",
        #            "available": "9999999983.668",
        #            "currency": "BTC",
        #            "id": 60018450912695040,
        #            "userId": 12345,
        #            "hold": "16.332",
        #            "ts": 1657312008443
        #        }
        #    ]
        #
        firstBalance = self.safe_value(response, 0, {})
        timestamp = self.safe_integer(firstBalance, 'ts')
        result: dict = {
            'info': response,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
        }
        for i in range(0, len(response)):
            balance = self.safe_value(response, i)
            currencyId = self.safe_string(balance, 'currency')
            code = self.safe_currency_code(currencyId)
            newAccount = self.account()
            newAccount['free'] = self.safe_string(balance, 'available')
            newAccount['used'] = self.safe_string(balance, 'hold')
            result[code] = newAccount
        return self.safe_balance(result)

    def handle_my_trades(self, client: Client, parsedTrade):
        # emulated using the orders' stream
        messageHash = 'myTrades'
        symbol = parsedTrade['symbol']
        if self.myTrades is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            self.myTrades = ArrayCacheBySymbolById(limit)
        trades = self.myTrades
        trades.append(parsedTrade)
        client.resolve(trades, messageHash)
        symbolMessageHash = messageHash + ':' + symbol
        client.resolve(trades, symbolMessageHash)

    def handle_pong(self, client: Client):
        client.lastPong = self.milliseconds()

    def handle_message(self, client: Client, message):
        if self.handle_error_message(client, message):
            return
        type = self.safe_string(message, 'channel')
        event = self.safe_string(message, 'event')
        if event == 'pong':
            client.lastPong = self.milliseconds()
        methods: dict = {
            'candles_minute_1': self.handle_ohlcv,
            'candles_minute_5': self.handle_ohlcv,
            'candles_minute_10': self.handle_ohlcv,
            'candles_minute_15': self.handle_ohlcv,
            'candles_minute_30': self.handle_ohlcv,
            'candles_hour_1': self.handle_ohlcv,
            'candles_hour_2': self.handle_ohlcv,
            'candles_hour_4': self.handle_ohlcv,
            'candles_hour_6': self.handle_ohlcv,
            'candles_hour_12': self.handle_ohlcv,
            'candles_day_1': self.handle_ohlcv,
            'candles_day_3': self.handle_ohlcv,
            'candles_week_1': self.handle_ohlcv,
            'candles_month_1': self.handle_ohlcv,
            'book': self.handle_order_book,
            'book_lv2': self.handle_order_book,
            'ticker': self.handle_ticker,
            'trades': self.handle_trade,
            'orders': self.handle_order,
            'balances': self.handle_balance,
            'createOrder': self.handle_order_request,
            'cancelOrder': self.handle_order_request,
            'cancelAllOrders': self.handle_order_request,
            'auth': self.handle_authenticate,
        }
        method = self.safe_value(methods, type)
        if type == 'auth':
            self.handle_authenticate(client, message)
        elif type is None:
            self.handle_order_request(client, message)
        else:
            data = self.safe_value(message, 'data', [])
            dataLength = len(data)
            if dataLength > 0:
                method(client, message)

    def handle_error_message(self, client: Client, message):
        #
        #    {
        #        message: 'Invalid channel value ["ordersss"]',
        #        event: 'error'
        #    }
        #
        #    {
        #        "orderId": 0,
        #        "clientOrderId": null,
        #        "message": "Currency trade disabled",
        #        "code": 21352
        #    }
        #
        #    {
        #       "event": "error",
        #       "message": "Platform in maintenance mode"
        #    }
        #    {
        #       "id":"1722386782048",
        #       "data":[
        #          {
        #             "orderId":0,
        #             "clientOrderId":null,
        #             "message":"available insufficient",
        #             "code":21721
        #          }
        #       ]
        #    }
        #
        id = self.safe_string(message, 'id')
        event = self.safe_string(message, 'event')
        data = self.safe_list(message, 'data')
        first = self.safe_dict(data, 0)
        orderId = self.safe_string(first, 'orderId')
        if (event == 'error') or (orderId == '0'):
            try:
                error = self.safe_string(first, 'message')
                code = self.safe_string(first, 'code')
                feedback = self.id + ' ' + self.json(message)
                self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
                self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback)
                raise ExchangeError(feedback)
            except Exception as e:
                if isinstance(e, AuthenticationError):
                    messageHash = 'authenticated'
                    client.reject(e, messageHash)
                    if messageHash in client.subscriptions:
                        del client.subscriptions[messageHash]
                else:
                    client.reject(e, id)
                return True
        return False

    def handle_authenticate(self, client: Client, message):
        #
        #    {
        #        "success": True,
        #        "ret_msg": '',
        #        "op": "auth",
        #        "conn_id": "ce3dpomvha7dha97tvp0-2xh"
        #    }
        #
        data = self.safe_value(message, 'data')
        success = self.safe_value(data, '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 ping(self, client: Client):
        return {
            'event': 'ping',
        }
