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


class krakenfutures(ccxt.async_support.krakenfutures):

    def describe(self) -> Any:
        return self.deep_extend(super(krakenfutures, self).describe(), {
            'has': {
                'ws': True,
                'cancelAllOrdersWs': False,
                'cancelOrdersWs': False,
                'cancelOrderWs': False,
                'createOrderWs': False,
                'editOrderWs': False,
                'fetchBalanceWs': False,
                'fetchOpenOrdersWs': False,
                'fetchOrderWs': False,
                'fetchTradesWs': False,
                'watchOHLCV': False,
                'watchOrderBook': True,
                'watchOrderBookForSymbols': True,
                'watchTicker': True,
                'watchTickers': True,
                'watchBidsAsks': True,
                'watchTrades': True,
                'watchTradesForSymbols': True,
                'watchBalance': True,
                # 'watchStatus': True,  # https://docs.futures.kraken.com/#websocket-api-public-feeds-heartbeat
                'watchOrders': True,
                'watchMyTrades': True,
                'watchPositions': True,
            },
            'urls': {
                'api': {
                    'ws': 'wss://futures.kraken.com/ws/v1',
                },
                'test': {
                    'ws': 'wss://demo-futures.kraken.com/ws/v1',
                },
            },
            'options': {
                'tradesLimit': 1000,
                'ordersLimit': 1000,
                'OHLCVLimit': 1000,
                'connectionLimit': 100,  # https://docs.futures.kraken.com/#websocket-api-websocket-api-introduction-subscriptions-limits
                'requestLimit': 100,  # per second
                'fetchBalance': {
                    'type': None,
                },
            },
            'streaming': {
                'keepAlive': 30000,
            },
        })

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

        https://docs.futures.kraken.com/#websocket-api-public-feeds-challenge

        :returns dict: response from exchange
        """
        self.check_required_credentials()
        # Hash the challenge with the SHA-256 algorithm
        # Base64-decode your api_secret
        # Use the result of step 2 to hash the result of step 1 with the HMAC-SHA-512 algorithm
        # Base64-encode the result of step 3
        url = self.urls['api']['ws']
        messageHash = 'challenge'
        client = self.client(url)
        future = client.reusableFuture(messageHash)
        authenticated = self.safe_value(client.subscriptions, messageHash)
        if authenticated is None:
            request: dict = {
                'event': 'challenge',
                'api_key': self.apiKey,
            }
            message = self.extend(request, params)
            self.watch(url, messageHash, message, messageHash)
        return await future

    async def watch_order_book_for_symbols(self, symbols: List[str], limit: Int = None, params={}) -> OrderBook:
        """
        watches information on open orders with bid(buy) and ask(sell) prices, volumes and other data

        https://docs.futures.kraken.com/#websocket-api-public-feeds-challenge

        :param str[] symbols: unified array of symbols
        :param int [limit]: the maximum amount of order book entries to return
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
        """
        orderbook = await self.watch_multi_helper('orderbook', 'book', symbols, {'limit': limit}, params)
        return orderbook.limit()

    async def subscribe_public(self, name: str, symbols: List[str], params={}):
        """
 @ignore
        Connects to a websocket channel
        :param str name: name of the channel
        :param str[] symbols: CCXT market symbols
        :param dict [params]: extra parameters specific to the krakenfutures api
        :returns dict: data from the websocket stream
        """
        await self.load_markets()
        url = self.urls['api']['ws']
        subscribe: dict = {
            'event': 'subscribe',
            'feed': name,
        }
        marketIds = []
        messageHash = name
        if symbols is None:
            symbols = []
        for i in range(0, len(symbols)):
            symbol = symbols[i]
            marketIds.append(self.market_id(symbol))
        length = len(symbols)
        if length == 1:
            market = self.market(marketIds[0])
            messageHash = messageHash + ':' + market['symbol']
        subscribe['product_ids'] = marketIds
        request = self.extend(subscribe, params)
        return await self.watch(url, messageHash, request, messageHash)

    async def subscribe_private(self, name: str, messageHash: str, params={}):
        """
 @ignore
        Connects to a websocket channel
        :param str name: name of the channel
        :param str messageHash: unique identifier for the message
        :param dict [params]: extra parameters specific to the krakenfutures api
        :returns dict: data from the websocket stream
        """
        await self.load_markets()
        await self.authenticate()
        url = self.urls['api']['ws']
        subscribe: dict = {
            'event': 'subscribe',
            'feed': name,
            'api_key': self.apiKey,
            'original_challenge': self.options['challenge'],
            'signed_challenge': self.options['signedChallenge'],
        }
        request = self.extend(subscribe, params)
        return await self.watch(url, messageHash, request, messageHash)

    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://docs.futures.kraken.com/#websocket-api-public-feeds-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 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://docs.futures.kraken.com/#websocket-api-public-feeds-ticker

        :param str[] symbols: unified symbols of the markets 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()
        symbols = self.market_symbols(symbols, None, False)
        ticker = await self.watch_multi_helper('ticker', 'ticker', symbols, None, params)
        if self.newUpdates:
            result: dict = {}
            result[ticker['symbol']] = ticker
            return result
        return self.filter_by_array(self.tickers, 'symbol', symbols)

    async def watch_bids_asks(self, symbols: Strings = None, params={}) -> Tickers:
        """

        https://docs.futures.kraken.com/#websocket-api-public-feeds-ticker-lite

        watches best bid & ask for symbols
        :param str[] symbols: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
        """
        ticker = await self.watch_multi_helper('bidask', 'ticker_lite', symbols, None, params)
        if self.newUpdates:
            result: dict = {}
            result[ticker['symbol']] = ticker
            return result
        return self.filter_by_array(self.bidsasks, '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://docs.futures.kraken.com/#websocket-api-public-feeds-trade

        :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]:
        """

        https://docs.futures.kraken.com/#websocket-api-public-feeds-trade

        get the list of most recent trades for a list of symbols
        :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>`
        """
        trades = await self.watch_multi_helper('trade', 'trade', symbols, None, params)
        if self.newUpdates:
            first = self.safe_list(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://docs.futures.kraken.com/#websocket-api-public-feeds-book

        :param str symbol: unified symbol of the market to fetch the order book for
        :param int [limit]: not used by krakenfutures 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
        """
        return await self.watch_order_book_for_symbols([symbol], limit, params)

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

        https://docs.futures.kraken.com/#websocket-api-private-feeds-open-positions

        watch all open positions
        :param str[]|None symbols: list of unified market symbols
 @param since
 @param limit
        :param dict params: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `position structure <https://docs.ccxt.com/en/latest/manual.html#position-structure>`
        """
        await self.load_markets()
        messageHash = ''
        symbols = self.market_symbols(symbols)
        if not self.is_empty(symbols):
            messageHash = '::' + ','.join(symbols)
        messageHash = 'positions' + messageHash
        newPositions = await self.subscribe_private('open_positions', messageHash, params)
        if self.newUpdates:
            return newPositions
        return self.filter_by_symbols_since_limit(self.positions, symbols, since, limit, True)

    def handle_positions(self, client, message):
        #
        #    {
        #        feed: 'open_positions',
        #        account: '3b111acc-4fcc-45be-a622-57e611fe9f7f',
        #        positions: [
        #            {
        #                instrument: 'PF_LTCUSD',
        #                balance: 0.5,
        #                pnl: -0.8628305877699987,
        #                entry_price: 70.53,
        #                mark_price: 68.80433882446,
        #                index_price: 68.8091,
        #                liquidation_threshold: 0,
        #                effective_leverage: 0.007028866753648637,
        #                return_on_equity: -1.2233525985679834,
        #                unrealized_funding: 0.0000690610530935388,
        #                initial_margin: 0.7053,
        #                initial_margin_with_orders: 0.7053,
        #                maintenance_margin: 0.35265,
        #                pnl_currency: 'USD'
        #            }
        #        ],
        #        seq: 0,
        #        timestamp: 1698608414910
        #    }
        #
        if self.positions is None:
            self.positions = ArrayCacheBySymbolById()
        cache = self.positions
        rawPositions = self.safe_value(message, 'positions', [])
        newPositions = []
        for i in range(0, len(rawPositions)):
            rawPosition = rawPositions[i]
            position = self.parse_ws_position(rawPosition)
            timestamp = self.safe_integer(message, 'timestamp')
            position['timestamp'] = timestamp
            position['datetime'] = self.iso8601(timestamp)
            newPositions.append(position)
            cache.append(position)
        messageHashes = self.find_message_hashes(client, 'positions::')
        for i in range(0, len(messageHashes)):
            messageHash = messageHashes[i]
            parts = messageHash.split('::')
            symbolsString = parts[1]
            symbols = symbolsString.split(',')
            positions = self.filter_by_array(newPositions, 'symbol', symbols, False)
            if not self.is_empty(positions):
                client.resolve(positions, messageHash)
        client.resolve(newPositions, 'positions')

    def parse_ws_position(self, position, market=None):
        #
        #        {
        #            instrument: 'PF_LTCUSD',
        #            balance: 0.5,
        #            pnl: -0.8628305877699987,
        #            entry_price: 70.53,
        #            mark_price: 68.80433882446,
        #            index_price: 68.8091,
        #            liquidation_threshold: 0,
        #            effective_leverage: 0.007028866753648637,
        #            return_on_equity: -1.2233525985679834,
        #            unrealized_funding: 0.0000690610530935388,
        #            initial_margin: 0.7053,
        #            initial_margin_with_orders: 0.7053,
        #            maintenance_margin: 0.35265,
        #            pnl_currency: 'USD'
        #        }
        #
        marketId = self.safe_string(position, 'instrument')
        hedged = 'both'
        balance = self.safe_number(position, 'balance')
        side = 'long' if (balance > 0) else 'short'
        return self.safe_position({
            'info': position,
            'id': None,
            'symbol': self.safe_symbol(marketId),
            'notional': None,
            'marginMode': None,
            'liquidationPrice': self.safe_number(position, 'liquidation_threshold'),
            'entryPrice': self.safe_number(position, 'entry_price'),
            'unrealizedPnl': self.safe_number(position, 'pnl'),
            'percentage': self.safe_number(position, 'return_on_equity'),
            'contracts': self.parse_number(Precise.string_abs(self.number_to_string(balance))),
            'contractSize': None,
            'markPrice': self.safe_number(position, 'mark_price'),
            'side': side,
            'hedged': hedged,
            'timestamp': None,
            'datetime': None,
            'maintenanceMargin': self.safe_number(position, 'maintenance_margin'),
            'maintenanceMarginPercentage': None,
            'collateral': None,
            'initialMargin': self.safe_number(position, 'initial_margin'),
            'initialMarginPercentage': None,
            'leverage': None,
            'marginRatio': None,
        })

    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://docs.futures.kraken.com/#websocket-api-private-feeds-open-orders
        https://docs.futures.kraken.com/#websocket-api-private-feeds-open-orders-verbose

        :param str symbol: not used by krakenfutures watchOrders
        :param int [since]: not used by krakenfutures watchOrders
        :param int [limit]: not used by krakenfutures 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 = 'open_orders'
        messageHash = 'orders'
        if symbol is not None:
            market = self.market(symbol)
            messageHash += ':' + market['symbol']
        orders = await self.subscribe_private(name, messageHash, 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

        https://docs.futures.kraken.com/#websocket-api-private-feeds-fills

        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
        """
        await self.load_markets()
        name = 'fills'
        messageHash = 'myTrades'
        if symbol is not None:
            market = self.market(symbol)
            messageHash += ':' + market['symbol']
        trades = await self.subscribe_private(name, messageHash, 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:
        """
        watches information on the user's account balance

        https://docs.futures.kraken.com/#websocket-api-private-feeds-balances

        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.account]: can be either 'futures' or 'flex_futures'
        :returns dict} a object of wallet types each with a balance structure {@link https://docs.ccxt.com/#/?id=balance-structure:
        """
        await self.load_markets()
        name = 'balances'
        messageHash = name
        account = None
        account, params = self.handle_option_and_params(params, 'watchBalance', 'account')
        if account is not None:
            if account != 'futures' and account != 'flex_futures':
                raise ArgumentsRequired(self.id + ' watchBalance account must be either \'futures\' or \'flex_futures\'')
            messageHash += ':' + account
        return await self.subscribe_private(name, messageHash, params)

    def handle_trade(self, client: Client, message):
        #
        # snapshot
        #
        #    {
        #        "feed": "trade_snapshot",
        #        "product_id": "PI_XBTUSD",
        #        "trades": [
        #            {
        #                "feed": "trade",
        #                "product_id": "PI_XBTUSD",
        #                "uid": "caa9c653-420b-4c24-a9f2-462a054d86f1",
        #                "side": "sell",
        #                "type": "fill",
        #                "seq": 655508,
        #                "time": 1612269657781,
        #                "qty": 440,
        #                "price": 34893
        #            },
        #            ...
        #        ]
        #    }
        #
        # update
        #
        #    {
        #        "feed": "trade",
        #        "product_id": "PI_XBTUSD",
        #        "uid": "05af78ac-a774-478c-a50c-8b9c234e071e",
        #        "side": "sell",
        #        "type": "fill",
        #        "seq": 653355,
        #        "time": 1612266317519,
        #        "qty": 15000,
        #        "price": 34969.5
        #    }
        #
        channel = self.safe_string(message, 'feed')
        marketId = self.safe_string(message, 'product_id')
        if marketId is not None:
            market = self.market(marketId)
            symbol = market['symbol']
            messageHash = self.get_message_hash('trade', None, symbol)
            if self.safe_list(self.trades, symbol) is None:
                tradesLimit = self.safe_integer(self.options, 'tradesLimit', 1000)
                self.trades[symbol] = ArrayCache(tradesLimit)
            tradesArray = self.trades[symbol]
            if channel == 'trade_snapshot':
                trades = self.safe_list(message, 'trades', [])
                length = len(trades)
                for i in range(0, length):
                    index = length - 1 - i  # need reverse to correct chronology
                    item = trades[index]
                    trade = self.parse_ws_trade(item)
                    tradesArray.append(trade)
            else:
                trade = self.parse_ws_trade(message)
                tradesArray.append(trade)
            client.resolve(tradesArray, messageHash)

    def parse_ws_trade(self, trade, market=None):
        #
        #    {
        #        "feed": "trade",
        #        "product_id": "PI_XBTUSD",
        #        "uid": "caa9c653-420b-4c24-a9f1-462a054d86f1",
        #        "side": "sell",
        #        "type": "fill",
        #        "seq": 655508,
        #        "time": 1612269657781,
        #        "qty": 440,
        #        "price": 34893
        #    }
        #
        marketId = self.safe_string(trade, 'product_id')
        market = self.safe_market(marketId, market)
        timestamp = self.safe_integer(trade, 'time')
        return self.safe_trade({
            'info': trade,
            'id': self.safe_string(trade, 'uid'),
            'symbol': self.safe_string(market, 'symbol'),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'order': None,
            'type': self.safe_string(trade, 'type'),
            'side': self.safe_string(trade, 'side'),
            'takerOrMaker': 'taker',
            'price': self.safe_string(trade, 'price'),
            'amount': self.safe_string(trade, 'qty'),
            'cost': None,
            'fee': {
                'rate': None,
                'cost': None,
                'currency': None,
            },
        }, market)

    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(trade, 'matchRole'),
            'price': self.safe_string(trade, 'price'),
            'amount': self.safe_string(trade, 'tradeAmount'),  # ? tradeQty?
            '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):
        #
        #  update(verbose)
        #
        #    {
        #        "feed": "open_orders_verbose",
        #        "order": {
        #            "instrument": "PI_XBTUSD",
        #            "time": 1567597581495,
        #            "last_update_time": 1567597581495,
        #            "qty": 102.0,
        #            "filled": 0.0,
        #            "limit_price": 10601.0,
        #            "stop_price": 0.0,
        #            "type": "limit",
        #            "order_id": "fa9806c9-cba9-4661-9f31-8c5fd045a95d",
        #            "direction": 0,
        #            "reduce_only": False
        #        },
        #        "is_cancel": True,
        #        "reason": "post_order_failed_because_it_would_be_filled"
        #    }
        #
        # update
        #
        #    {
        #        "feed": "open_orders",
        #        "order": {
        #          "instrument": "PI_XBTUSD",
        #          "time": 1567702877410,
        #          "last_update_time": 1567702877410,
        #          "qty": 304.0,
        #          "filled": 0.0,
        #          "limit_price": 10640.0,
        #          "stop_price": 0.0,
        #          "type": "limit",
        #          "order_id": "59302619-41d2-4f0b-941f-7e7914760ad3",
        #          "direction": 1,
        #          "reduce_only": True
        #        },
        #        "is_cancel": False,
        #        "reason": "new_placed_order_by_user"
        #    }
        #    {
        #        "feed": "open_orders",
        #        "order_id": "ea8a7144-37db-449b-bb4a-b53c814a0f43",
        #        "is_cancel": True,
        #        "reason": "cancelled_by_user"
        #    }
        #
        #     {
        #         "feed": 'open_orders',
        #         "order": {
        #         "instrument": 'PF_XBTUSD',
        #         "time": 1698159920097,
        #         "last_update_time": 1699835622988,
        #         "qty": 1.1,
        #         "filled": 0,
        #         "limit_price": 20000,
        #         "stop_price": 0,
        #         "type": 'limit',
        #         "order_id": '0eaf02b0-855d-4451-a3b7-e2b3070c1fa4',
        #         "direction": 0,
        #         "reduce_only": False
        #         },
        #         "is_cancel": False,
        #         "reason": 'edited_by_user'
        #     }
        #
        orders = self.orders
        if orders is None:
            limit = self.safe_integer(self.options, 'ordersLimit')
            orders = ArrayCacheBySymbolById(limit)
            self.orders = orders
        order = self.safe_value(message, 'order')
        if order is not None:
            marketId = self.safe_string(order, 'instrument')
            messageHash = 'orders'
            symbol = self.safe_symbol(marketId)
            orderId = self.safe_string(order, 'order_id')
            previousOrders = self.safe_value(orders.hashmap, symbol, {})
            previousOrder = self.safe_value(previousOrders, orderId)
            reason = self.safe_string(message, 'reason')
            if (previousOrder is None) or (reason == 'edited_by_user'):
                parsed = self.parse_ws_order(order)
                orders.append(parsed)
                client.resolve(orders, messageHash)
                client.resolve(orders, messageHash + ':' + symbol)
            else:
                trade = self.parse_ws_trade(order)
                if previousOrder['trades'] is None:
                    previousOrder['trades'] = []
                previousOrder['trades'].append(trade)
                previousOrder['lastTradeTimestamp'] = trade['timestamp']
                totalCost = '0'
                totalAmount = '0'
                trades = previousOrder['trades']
                for i in range(0, len(trades)):
                    currentTrade = trades[i]
                    totalCost = Precise.string_add(totalCost, self.number_to_string(currentTrade['cost']))
                    totalAmount = Precise.string_add(totalAmount, self.number_to_string(currentTrade['amount']))
                if Precise.string_gt(totalAmount, '0'):
                    previousOrder['average'] = Precise.string_div(totalCost, totalAmount)
                previousOrder['cost'] = totalCost
                if previousOrder['filled'] is not None:
                    stringOrderFilled = self.number_to_string(previousOrder['filled'])
                    previousOrder['filled'] = Precise.string_add(stringOrderFilled, self.number_to_string(trade['amount']))
                    if previousOrder['amount'] is not None:
                        previousOrder['remaining'] = Precise.string_sub(self.number_to_string(previousOrder['amount']), stringOrderFilled)
                if previousOrder['fee'] is None:
                    previousOrder['fee'] = {
                        'rate': None,
                        'cost': '0',
                        'currency': self.number_to_string(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)
                # update the newUpdates count
                orders.append(self.safe_order(previousOrder))
                client.resolve(orders, messageHash + ':' + symbol)
                client.resolve(orders, messageHash)
        else:
            isCancel = self.safe_value(message, 'is_cancel')
            if isCancel:
                # get order without symbol
                for i in range(0, len(orders)):
                    currentOrder = orders[i]
                    if currentOrder['id'] == message['order_id']:
                        orders[i] = self.extend(currentOrder, {
                            'status': 'canceled',
                        })
                        client.resolve(orders, 'orders')
                        client.resolve(orders, 'orders:' + currentOrder['symbol'])
                        break
        return message

    def handle_order_snapshot(self, client: Client, message):
        #
        # verbose
        #
        #    {
        #        "feed": "open_orders_verbose_snapshot",
        #        "account": "0f9c23b8-63e2-40e4-9592-6d5aa57c12ba",
        #        "orders": [
        #            {
        #                "instrument": "PI_XBTUSD",
        #                "time": 1567428848005,
        #                "last_update_time": 1567428848005,
        #                "qty": 100.0,
        #                "filled": 0.0,
        #                "limit_price": 8500.0,
        #                "stop_price": 0.0,
        #                "type": "limit",
        #                "order_id": "566942c8-a3b5-4184-a451-622b09493129",
        #                "direction": 0,
        #                "reduce_only": False
        #            },
        #            ...
        #        ]
        #    }
        #
        # regular
        #
        #    {
        #        "feed": "open_orders_snapshot",
        #        "account": "e258dba9-4dd4-4da5-bfef-75beb91c098e",
        #        "orders": [
        #            {
        #                "instrument": "PI_XBTUSD",
        #                "time": 1612275024153,
        #                "last_update_time": 1612275024153,
        #                "qty": 1000,
        #                "filled": 0,
        #                "limit_price": 34900,
        #                "stop_price": 13789,
        #                "type": "stop",
        #                "order_id": "723ba95f-13b7-418b-8fcf-ab7ba6620555",
        #                "direction": 1,
        #                "reduce_only": False,
        #                "triggerSignal": "last"
        #            },
        #            ...
        #        ]
        #    }
        orders = self.safe_value(message, 'orders', [])
        limit = self.safe_integer(self.options, 'ordersLimit')
        self.orders = ArrayCacheBySymbolById(limit)
        symbols: dict = {}
        cachedOrders = self.orders
        for i in range(0, len(orders)):
            order = orders[i]
            parsed = self.parse_ws_order(order)
            symbol = parsed['symbol']
            symbols[symbol] = True
            cachedOrders.append(parsed)
        length = len(self.orders)
        if length > 0:
            client.resolve(self.orders, 'orders')
            keys = list(symbols.keys())
            for i in range(0, len(keys)):
                symbol = keys[i]
                messageHash = 'orders:' + symbol
                client.resolve(self.orders, messageHash)

    def parse_ws_order(self, order, market=None):
        #
        # update
        #
        #    {
        #        "feed": "open_orders_verbose",
        #        "order": {
        #            "instrument": "PI_XBTUSD",
        #            "time": 1567597581495,
        #            "last_update_time": 1567597581495,
        #            "qty": 102.0,
        #            "filled": 0.0,
        #            "limit_price": 10601.0,
        #            "stop_price": 0.0,
        #            "type": "limit",
        #            "order_id": "fa9806c9-cba9-4661-9f31-8c5fd045a95d",
        #            "direction": 0,
        #            "reduce_only": False
        #        },
        #        "is_cancel": True,
        #        "reason": "post_order_failed_because_it_would_be_filled"
        #    }
        #
        # snapshot
        #
        #    {
        #        "instrument": "PI_XBTUSD",
        #        "time": 1567597581495,
        #        "last_update_time": 1567597581495,
        #        "qty": 102.0,
        #        "filled": 0.0,
        #        "limit_price": 10601.0,
        #        "stop_price": 0.0,
        #        "type": "limit",
        #        "order_id": "fa9806c9-cba9-4661-9f31-8c5fd045a95d",
        #        "direction": 0,
        #        "reduce_only": False
        #    }
        #
        isCancelled = self.safe_value(order, 'is_cancel')
        unparsedOrder = order
        status = None
        if isCancelled is not None:
            unparsedOrder = self.safe_value(order, 'order')
            if isCancelled is True:
                status = 'cancelled'
        marketId = self.safe_string(unparsedOrder, 'instrument')
        timestamp = self.safe_string(unparsedOrder, 'time')
        direction = self.safe_integer(unparsedOrder, 'direction')
        return self.safe_order({
            'info': order,
            'symbol': self.safe_symbol(marketId, market),
            'id': self.safe_string(unparsedOrder, 'order_id'),
            'clientOrderId': None,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': None,
            'type': self.safe_string(unparsedOrder, 'type'),
            'timeInForce': None,
            'postOnly': None,
            'side': 'buy' if (direction == 0) else 'sell',
            'price': self.safe_string(unparsedOrder, 'limit_price'),
            'stopPrice': self.safe_string(unparsedOrder, 'stop_price'),
            'triggerPrice': self.safe_string(unparsedOrder, 'stop_price'),
            'amount': self.safe_string(unparsedOrder, 'qty'),
            'cost': None,
            'average': None,
            'filled': self.safe_string(unparsedOrder, 'filled'),
            'remaining': None,
            'status': status,
            'fee': {
                'rate': None,
                'cost': None,
                'currency': None,
            },
            'trades': None,
        })

    def handle_ticker(self, client: Client, message):
        #
        #    {
        #        "time": 1680811086487,
        #        "product_id": "PI_XBTUSD",
        #        "funding_rate": 7.792297e-12,
        #        "funding_rate_prediction": -4.2671095e-11,
        #        "relative_funding_rate": 2.18013888889e-7,
        #        "relative_funding_rate_prediction": -0.0000011974,
        #        "next_funding_rate_time": 1680811200000,
        #        "feed": "ticker",
        #        "bid": 28060,
        #        "ask": 28070,
        #        "bid_size": 2844,
        #        "ask_size": 1902,
        #        "volume": 19628180,
        #        "dtm": 0,
        #        "leverage": "50x",
        #        "index": 28062.14,
        #        "premium": 0,
        #        "last": 28053.5,
        #        "change": -0.7710945651981715,
        #        "suspended": False,
        #        "tag": "perpetual",
        #        "pair": "XBT:USD",
        #        "openInterest": 28875946,
        #        "markPrice": 28064.92082724592,
        #        "maturityTime": 0,
        #        "post_only": False,
        #        "volumeQuote": 19628180
        #    }
        #
        marketId = self.safe_string(message, 'product_id')
        if marketId is not None:
            ticker = self.parse_ws_ticker(message)
            symbol = ticker['symbol']
            self.tickers[symbol] = ticker
            messageHash = self.get_message_hash('ticker', None, symbol)
            client.resolve(ticker, messageHash)

    def handle_bid_ask(self, client: Client, message):
        #
        #    {
        #        "feed": "ticker_lite",
        #        "product_id": "FI_ETHUSD_210625",
        #        "bid": 1753.45,
        #        "ask": 1760.35,
        #        "change": 13.448175559936647,
        #        "premium": 9.1,
        #        "volume": 6899673.0,
        #        "tag": "semiannual",
        #        "pair": "ETH:USD",
        #        "dtm": 141,
        #        "maturityTime": 1624633200000,
        #        "volumeQuote": 6899673.0
        #    }
        #
        marketId = self.safe_string(message, 'product_id')
        if marketId is not None:
            ticker = self.parse_ws_ticker(message)
            symbol = ticker['symbol']
            self.bidsasks[symbol] = ticker
            messageHash = self.get_message_hash('bidask', None, symbol)
            client.resolve(ticker, messageHash)

    def parse_ws_ticker(self, ticker, market=None):
        #
        #    {
        #        "time": 1680811086487,
        #        "product_id": "PI_XBTUSD",
        #        "funding_rate": 7.792297e-12,
        #        "funding_rate_prediction": -4.2671095e-11,
        #        "relative_funding_rate": 2.18013888889e-7,
        #        "relative_funding_rate_prediction": -0.0000011974,
        #        "next_funding_rate_time": 1680811200000,
        #        "feed": "ticker",
        #        "bid": 28060,
        #        "ask": 28070,
        #        "bid_size": 2844,
        #        "ask_size": 1902,
        #        "volume": 19628180,
        #        "dtm": 0,
        #        "leverage": "50x",
        #        "index": 28062.14,
        #        "premium": 0,
        #        "last": 28053.5,
        #        "change": -0.7710945651981715,
        #        "suspended": False,
        #        "tag": "perpetual",
        #        "pair": "XBT:USD",
        #        "openInterest": 28875946,
        #        "markPrice": 28064.92082724592,
        #        "maturityTime": 0,
        #        "post_only": False,
        #        "volumeQuote": 19628180
        #    }
        #
        # ticker_lite
        #
        #    {
        #        "feed": "ticker_lite",
        #        "product_id": "FI_ETHUSD_210625",
        #        "bid": 1753.45,
        #        "ask": 1760.35,
        #        "change": 13.448175559936647,
        #        "premium": 9.1,
        #        "volume": 6899673.0,
        #        "tag": "semiannual",
        #        "pair": "ETH:USD",
        #        "dtm": 141,
        #        "maturityTime": 1624633200000,
        #        "volumeQuote": 6899673.0
        #    }
        #
        marketId = self.safe_string(ticker, 'product_id')
        market = self.safe_market(marketId, market)
        symbol = market['symbol']
        timestamp = self.parse8601(self.safe_string(ticker, 'lastTime'))
        last = self.safe_string(ticker, 'last')
        return self.safe_ticker({
            'info': ticker,
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': None,
            'low': None,
            'bid': self.safe_string(ticker, 'bid'),
            'bidVolume': self.safe_string(ticker, 'bid_size'),
            'ask': self.safe_string(ticker, 'ask'),
            'askVolume': self.safe_string(ticker, 'ask_size'),
            'vwap': None,
            'open': None,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': self.safe_string(ticker, 'change'),
            'percentage': None,
            'average': None,
            'baseVolume': self.safe_string(ticker, 'volume'),
            'quoteVolume': self.safe_string(ticker, 'volumeQuote'),
            'markPrice': self.safe_string(ticker, 'markPrice'),
            'indexPrice': self.safe_string(ticker, 'index'),
        })

    def handle_order_book_snapshot(self, client: Client, message):
        #
        #    {
        #        "feed": "book_snapshot",
        #        "product_id": "PI_XBTUSD",
        #        "timestamp": 1612269825817,
        #        "seq": 326072249,
        #        "tickSize": null,
        #        "bids": [
        #            {
        #                "price": 34892.5,
        #                "qty": 6385
        #            },
        #            {
        #                "price": 34892,
        #                "qty": 10924
        #            },
        #        ],
        #        "asks": [
        #            {
        #                "price": 34911.5,
        #                "qty": 20598
        #            },
        #            {
        #                "price": 34912,
        #                "qty": 2300
        #            },
        #        ]
        #    }
        #
        marketId = self.safe_string(message, 'product_id')
        market = self.safe_market(marketId)
        symbol = market['symbol']
        messageHash = self.get_message_hash('orderbook', None, symbol)
        subscription = self.safe_dict(client.subscriptions, messageHash, {})
        limit = self.safe_integer(subscription, 'limit')
        timestamp = self.safe_integer(message, 'timestamp')
        self.orderbooks[symbol] = self.order_book({}, limit)
        orderbook = self.orderbooks[symbol]
        bids = self.safe_list(message, 'bids')
        asks = self.safe_list(message, 'asks')
        for i in range(0, len(bids)):
            bid = bids[i]
            price = self.safe_number(bid, 'price')
            qty = self.safe_number(bid, 'qty')
            bidsSide = orderbook['bids']
            bidsSide.store(price, qty)
        for i in range(0, len(asks)):
            ask = asks[i]
            price = self.safe_number(ask, 'price')
            qty = self.safe_number(ask, 'qty')
            asksSide = orderbook['asks']
            asksSide.store(price, qty)
        orderbook['timestamp'] = timestamp
        orderbook['datetime'] = self.iso8601(timestamp)
        orderbook['symbol'] = symbol
        client.resolve(orderbook, messageHash)

    def handle_order_book(self, client: Client, message):
        #
        #    {
        #        "feed": "book",
        #        "product_id": "PI_XBTUSD",
        #        "side": "sell",
        #        "seq": 326094134,
        #        "price": 34981,
        #        "qty": 0,
        #        "timestamp": 1612269953629
        #    }
        #
        marketId = self.safe_string(message, 'product_id')
        market = self.safe_market(marketId)
        symbol = market['symbol']
        messageHash = self.get_message_hash('orderbook', None, symbol)
        orderbook = self.orderbooks[symbol]
        side = self.safe_string(message, 'side')
        price = self.safe_number(message, 'price')
        qty = self.safe_number(message, 'qty')
        timestamp = self.safe_integer(message, 'timestamp')
        if side == 'sell':
            asks = orderbook['asks']
            asks.store(price, qty)
        else:
            bids = orderbook['bids']
            bids.store(price, qty)
        orderbook['timestamp'] = timestamp
        orderbook['datetime'] = self.iso8601(timestamp)
        client.resolve(orderbook, messageHash)

    def handle_balance(self, client: Client, message):
        #
        # snapshot
        #
        #    {
        #        "feed": "balances_snapshot",
        #        "account": "4a012c31-df95-484a-9473-d51e4a0c4ae7",
        #        "holding": {
        #            "USDT": 4997.5012493753,
        #            "XBT": 0.1285407184,
        #            ...
        #        },
        #        "futures": {
        #            "F-ETH:EUR": {
        #                "name": "F-ETH:EUR",
        #                "pair": "ETH/EUR",
        #                "unit": "EUR",
        #                "portfolio_value": 0.0,
        #                "balance": 0.0,
        #                "maintenance_margin": 0.0,
        #                "initial_margin": 0.0,
        #                "available": 0.0,
        #                "unrealized_funding": 0.0,
        #                "pnl": 0.0
        #            },
        #            ...
        #        },
        #        "flex_futures": {
        #            "currencies": {
        #                "USDT": {
        #                    "quantity": 0.0,
        #                    "value": 0.0,
        #                    "collateral_value": 0.0,
        #                    "available": 0.0,
        #                    "haircut": 0.0,
        #                    "conversion_spread": 0.0
        #                },
        #                ...
        #            },
        #            "balance_value":0.0,
        #            "portfolio_value":0.0,
        #            "collateral_value":0.0,
        #            "initial_margin":0.0,
        #            "initial_margin_without_orders":0.0,
        #            "maintenance_margin":0.0,
        #            "pnl":0.0,
        #            "unrealized_funding":0.0,
        #            "total_unrealized":0.0,
        #            "total_unrealized_as_margin":0.0,
        #            "margin_equity":0.0,
        #            "available_margin":0.0
        #            "isolated":{
        #            },
        #            "cross":{
        #                "balance_value":9963.66,
        #                "portfolio_value":9963.66,
        #                "collateral_value":9963.66,
        #                "initial_margin":0.0,
        #                "initial_margin_without_orders":0.0,
        #                "maintenance_margin":0.0,
        #                "pnl":0.0,
        #                "unrealized_funding":0.0,
        #                "total_unrealized":0.0,
        #                "total_unrealized_as_margin":0.0,
        #                "margin_equity":9963.66,
        #                "available_margin":9963.66,
        #                "effective_leverage":0.0
        #            },
        #        },
        #        "timestamp":1640995200000,
        #        "seq":0
        #    }
        #
        # update
        #
        #    Holding Wallet
        #
        #    {
        #        "feed": "balances",
        #        "account": "7a641082-55c7-4411-a85f-930ec2e09617",
        #        "holding": {
        #            "USD": 5000.0
        #        },
        #        "futures": {},
        #        "timestamp": 1640995200000,
        #        "seq": 83
        #    }
        #
        #    Multi-Collateral
        #
        #    {
        #        "feed": "balances"
        #        "account": "7a641082-55c7-4411-a85f-930ec2e09617"
        #        "flex_futures": {
        #            "currencies": {
        #                "USDT": {
        #                    "quantity": 0.0,
        #                    "value": 0.0,
        #                    "collateral_value": 0.0,
        #                    "available": 0.0,
        #                    "haircut": 0.0,
        #                    "conversion_spread": 0.0
        #                },
        #                ...
        #            },
        #            "balance_value": 5000.0,
        #            "portfolio_value": 5000.0,
        #            "collateral_value": 5000.0,
        #            "initial_margin": 0.0,
        #            "initial_margin_without_orders": 0.0,
        #            "maintenance_margin": 0.0,
        #            "pnl": 0.0,
        #            "unrealized_funding": 0.0,
        #            "total_unrealized": 0.0,
        #            "total_unrealized_as_margin": 0.0,
        #            "margin_equity": 5000.0,
        #            "available_margin": 5000.0
        #        },
        #        "timestamp": 1640995200000,
        #        "seq": 1
        #    }
        #
        #    Sample Single-Collateral Balance Delta
        #
        #    {
        #        "feed": "balances",
        #        "account": "7a641082-55c7-4411-a85f-930ec2e09617",
        #        "holding": {},
        #        "futures": {
        #            "F-XBT:USD": {
        #                "name": "F-XBT:USD",
        #                "pair": "XBT/USD",
        #                "unit": "XBT",
        #                "portfolio_value": 0.1219368845,
        #                "balance": 0.1219368845,
        #                "maintenance_margin": 0.0,
        #                "initial_margin": 0.0,
        #                "available": 0.1219368845,
        #                "unrealized_funding": 0.0,
        #                "pnl": 0.0
        #            }
        #        },
        #        "timestamp": 1640995200000,
        #        "seq": 2
        #    }
        #
        holding = self.safe_value(message, 'holding')
        futures = self.safe_value(message, 'futures')
        flexFutures = self.safe_value(message, 'flex_futures')
        messageHash = 'balances'
        timestamp = self.safe_integer(message, 'timestamp')
        if holding is not None:
            holdingKeys = list(holding.keys())                  # cashAccount
            holdingResult: dict = {
                'info': message,
                'timestamp': timestamp,
                'datetime': self.iso8601(timestamp),
            }
            for i in range(0, len(holdingKeys)):
                key = holdingKeys[i]
                code = self.safe_currency_code(key)
                newAccount = self.account()
                newAccount['total'] = self.safe_string(holding, key)
                holdingResult[code] = newAccount
            self.balance['cash'] = holdingResult
            self.balance['cash'] = self.safe_balance(self.balance['cash'])
            client.resolve(holdingResult, messageHash)
        if futures is not None:
            futuresKeys = list(futures.keys())                  # marginAccount
            futuresResult: dict = {
                'info': message,
                'timestamp': timestamp,
                'datetime': self.iso8601(timestamp),
            }
            for i in range(0, len(futuresKeys)):
                key = futuresKeys[i]
                symbol = self.safe_symbol(key)
                newAccount = self.account()
                future = self.safe_value(futures, key)
                currencyId = self.safe_string(future, 'unit')
                code = self.safe_currency_code(currencyId)
                newAccount['free'] = self.safe_string(future, 'available')
                newAccount['used'] = self.safe_string(future, 'initial_margin')
                newAccount['total'] = self.safe_string(future, 'balance')
                futuresResult[symbol] = {}
                futuresResult[symbol][code] = newAccount
            self.balance['margin'] = futuresResult
            self.balance['margin'] = self.safe_balance(self.balance['margin'])
            client.resolve(self.balance['margin'], messageHash + 'futures')
        if flexFutures is not None:
            flexFutureCurrencies = self.safe_value(flexFutures, 'currencies', {})
            flexFuturesKeys = list(flexFutureCurrencies.keys())  # multi-collateral margin account
            flexFuturesResult: dict = {
                'info': message,
                'timestamp': timestamp,
                'datetime': self.iso8601(timestamp),
            }
            for i in range(0, len(flexFuturesKeys)):
                key = flexFuturesKeys[i]
                flexFuture = self.safe_value(flexFutureCurrencies, key)
                code = self.safe_currency_code(key)
                newAccount = self.account()
                newAccount['free'] = self.safe_string(flexFuture, 'available')
                newAccount['used'] = self.safe_string(flexFuture, 'collateral_value')
                newAccount['total'] = self.safe_string(flexFuture, 'quantity')
                flexFuturesResult[code] = newAccount
            self.balance['flex'] = flexFuturesResult
            self.balance['flex'] = self.safe_balance(self.balance['flex'])
            client.resolve(self.balance['flex'], messageHash + 'flex_futures')
        client.resolve(self.balance, messageHash)

    def handle_my_trades(self, client: Client, message):
        #
        #    {
        #        "feed": "fills_snapshot",
        #        "account": "DemoUser",
        #        "fills": [
        #            {
        #                "instrument": "FI_XBTUSD_200925",
        #                "time": 1600256910739,
        #                "price": 10937.5,
        #                "seq": 36,
        #                "buy": True,
        #                "qty": 5000.0,
        #                "order_id": "9e30258b-5a98-4002-968a-5b0e149bcfbf",
        #                "cli_ord_id": "8b58d9da-fcaf-4f60-91bc-9973a3eba48d",  # only on update, not on snapshot
        #                "fill_id": "cad76f07-814e-4dc6-8478-7867407b6bff",
        #                "fill_type": "maker",
        #                "fee_paid": -0.00009142857,
        #                "fee_currency": "BTC",
        #                "taker_order_type": "ioc",
        #                "order_type": "limit"
        #            },
        #            ...
        #        ]
        #    }
        #
        trades = self.safe_value(message, 'fills', [])
        stored = self.myTrades
        if stored is None:
            limit = self.safe_integer(self.options, 'tradesLimit', 1000)
            stored = ArrayCacheBySymbolById(limit)
            self.myTrades = stored
        tradeSymbols: dict = {}
        for i in range(0, len(trades)):
            trade = trades[i]
            parsedTrade = self.parse_ws_my_trade(trade)
            tradeSymbols[parsedTrade['symbol']] = True
            stored.append(parsedTrade)
        tradeSymbolKeys = list(tradeSymbols.keys())
        for i in range(0, len(tradeSymbolKeys)):
            symbol = tradeSymbolKeys[i]
            messageHash = 'myTrades:' + symbol
            client.resolve(stored, messageHash)
        client.resolve(stored, 'myTrades')

    def parse_ws_my_trade(self, trade, market=None):
        #
        #    {
        #        "instrument": "FI_XBTUSD_200925",
        #        "time": 1600256910739,
        #        "price": 10937.5,
        #        "seq": 36,
        #        "buy": True,
        #        "qty": 5000.0,
        #        "order_id": "9e30258b-5a98-4002-968a-5b0e149bcfbf",
        #        "cli_ord_id": "8b58d9da-fcaf-4f60-91bc-9973a3eba48d",  # only on update, not on snapshot
        #        "fill_id": "cad76f07-814e-4dc6-8478-7867407b6bff",
        #        "fill_type": "maker",
        #        "fee_paid": -0.00009142857,
        #        "fee_currency": "BTC",
        #        "taker_order_type": "ioc",
        #        "order_type": "limit"
        #    }
        #
        timestamp = self.safe_integer(trade, 'time')
        marketId = self.safe_string(trade, 'instrument')
        market = self.safe_market(marketId, market)
        isBuy = self.safe_value(trade, 'buy')
        feeCurrencyId = self.safe_string(trade, 'fee_currency')
        return self.safe_trade({
            'info': trade,
            'id': self.safe_string(trade, 'fill_id'),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': self.safe_string(market, 'symbol'),
            'order': self.safe_string(trade, 'order_id'),
            'type': self.safe_string(trade, 'type'),
            'side': 'buy' if isBuy else 'sell',
            'takerOrMaker': self.safe_string(trade, 'fill_type'),
            'price': self.safe_string(trade, 'price'),
            'amount': self.safe_string(trade, 'qty'),
            'cost': None,
            'fee': {
                'currency': self.safe_currency_code(feeCurrencyId),
                'cost': self.safe_string(trade, 'fee_paid'),
                'rate': None,
            },
        })

    async def watch_multi_helper(self, unifiedName: str, channelName: str, symbols: Strings = None, subscriptionArgs=None, params={}):
        await self.load_markets()
        # symbols are required
        symbols = self.market_symbols(symbols, None, False, True, False)
        messageHashes = []
        for i in range(0, len(symbols)):
            messageHashes.append(self.get_message_hash(unifiedName, None, self.symbol(symbols[i])))
        marketIds = self.market_ids(symbols)
        request: dict = {
            'event': 'subscribe',
            'feed': channelName,
            'product_ids': marketIds,
        }
        url = self.urls['api']['ws']
        return await self.watch_multiple(url, messageHashes, self.extend(request, params), messageHashes, subscriptionArgs)

    def get_message_hash(self, unifiedElementName: str, subChannelName: Str = None, symbol: Str = None):
        # unifiedElementName can be : orderbook, trade, ticker, bidask ...
        # subChannelName only applies to channel that needs specific variation(i.e. depth_50, depth_100..) to be selected
        withSymbol = symbol is not None
        messageHash = unifiedElementName
        if not withSymbol:
            messageHash += 's'
        else:
            messageHash += ':' + symbol
        if subChannelName is not None:
            messageHash += '#' + subChannelName
        return messageHash

    def handle_error_message(self, client: Client, message) -> Bool:
        #
        #    {
        #        event: 'alert',
        #        message: 'Failed to subscribe to authenticated feed'
        #    }
        #
        errMsg = self.safe_string(message, 'message')
        try:
            raise ExchangeError(self.id + ' ' + errMsg)
        except Exception as error:
            client.reject(error)
            return False

    def handle_message(self, client, message):
        event = self.safe_string(message, 'event')
        if event == 'challenge':
            self.handle_authenticate(client, message)
        elif event == 'alert':
            self.handle_error_message(client, message)
        elif event == 'pong':
            client.lastPong = self.milliseconds()
        elif event is None:
            feed = self.safe_string(message, 'feed')
            methods: dict = {
                'ticker': self.handle_ticker,
                'ticker_lite': self.handle_bid_ask,
                'trade': self.handle_trade,
                'trade_snapshot': self.handle_trade,
                # 'heartbeat': self.handleStatus,
                'book': self.handle_order_book,
                'book_snapshot': self.handle_order_book_snapshot,
                'open_orders_verbose': self.handle_order,
                'open_orders_verbose_snapshot': self.handle_order_snapshot,
                'fills': self.handle_my_trades,
                'fills_snapshot': self.handle_my_trades,
                'open_orders': self.handle_order,
                'open_orders_snapshot': self.handle_order_snapshot,
                'balances': self.handle_balance,
                'balances_snapshot': self.handle_balance,
                'open_positions': self.handle_positions,
            }
            method = self.safe_value(methods, feed)
            if method is not None:
                method(client, message)

    def handle_authenticate(self, client: Client, message):
        """
 @ignore
        https://docs.futures.kraken.com/#websocket-api-websocket-api-introduction-sign-challenge-challenge
        """
        #
        #    {
        #        "event": "challenge",
        #        "message": "226aee50-88fc-4618-a42a-34f7709570b2"
        #    }
        #
        event = self.safe_value(message, 'event')
        messageHash = 'challenge'
        if event != 'error':
            challenge = self.safe_value(message, 'message')
            hashedChallenge = self.hash(self.encode(challenge), 'sha256', 'binary')
            base64Secret = self.base64_to_binary(self.secret)
            signature = self.hmac(hashedChallenge, base64Secret, hashlib.sha512, 'base64')
            self.options['challenge'] = challenge
            self.options['signedChallenge'] = signature
            future = self.safe_value(client.futures, messageHash)
            future.resolve(True)
        else:
            error = AuthenticationError(self.id + ' ' + self.json(message))
            client.reject(error, messageHash)
            if messageHash in client.subscriptions:
                del client.subscriptions[messageHash]
        return message
