<?php

namespace ccxt\pro;

// 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

use Exception; // a common import
use ccxt\ExchangeError;
use ccxt\AuthenticationError;
use ccxt\ArgumentsRequired;
use ccxt\Precise;
use \React\Async;
use \React\Promise\PromiseInterface;

class krakenfutures extends \ccxt\async\krakenfutures {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                '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' => array(
                'api' => array(
                    'ws' => 'wss://futures.kraken.com/ws/v1',
                ),
                'test' => array(
                    'ws' => 'wss://demo-futures.kraken.com/ws/v1',
                ),
            ),
            'options' => array(
                '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' => array(
                    'type' => null,
                ),
            ),
            'streaming' => array(
                'keepAlive' => 30000,
            ),
        ));
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            /**
             * @ignore
             * authenticates the user to access private web socket channels
             *
             * @see https://docs.futures.kraken.com/#websocket-api-public-feeds-challenge
             *
             * @return {array} response from exchange
             */
            $this->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 = $this->urls['api']['ws'];
            $messageHash = 'challenge';
            $client = $this->client($url);
            $future = $client->reusableFuture ($messageHash);
            $authenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                $request = array(
                    'event' => 'challenge',
                    'api_key' => $this->apiKey,
                );
                $message = $this->extend($request, $params);
                $this->watch($url, $messageHash, $message, $messageHash);
            }
            return Async\await($future);
        }) ();
    }

    public function watch_order_book_for_symbols(array $symbols, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://docs.futures.kraken.com/#websocket-api-public-feeds-challenge
             *
             * @param {string[]} $symbols unified array of $symbols
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market $symbols
             */
            $orderbook = Async\await($this->watch_multi_helper('orderbook', 'book', $symbols, array( 'limit' => $limit ), $params));
            return $orderbook->limit ();
        }) ();
    }

    public function subscribe_public(string $name, array $symbols, $params = array ()) {
        return Async\async(function () use ($name, $symbols, $params) {
            /**
             * @ignore
             * Connects to a websocket channel
             * @param {string} $name name of the channel
             * @param {string[]} $symbols CCXT $market $symbols
             * @param {array} [$params] extra parameters specific to the krakenfutures api
             * @return {array} data from the websocket stream
             */
            Async\await($this->load_markets());
            $url = $this->urls['api']['ws'];
            $subscribe = array(
                'event' => 'subscribe',
                'feed' => $name,
            );
            $marketIds = [ ];
            $messageHash = $name;
            if ($symbols === null) {
                $symbols = array();
            }
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $marketIds[] = $this->market_id($symbol);
            }
            $length = count($symbols);
            if ($length === 1) {
                $market = $this->market($marketIds[0]);
                $messageHash = $messageHash . ':' . $market['symbol'];
            }
            $subscribe['product_ids'] = $marketIds;
            $request = $this->extend($subscribe, $params);
            return Async\await($this->watch($url, $messageHash, $request, $messageHash));
        }) ();
    }

    public function subscribe_private(string $name, string $messageHash, $params = array ()) {
        return Async\async(function () use ($name, $messageHash, $params) {
            /**
             * @ignore
             * Connects to a websocket channel
             * @param {string} $name name of the channel
             * @param {string} $messageHash unique identifier for the message
             * @param {array} [$params] extra parameters specific to the krakenfutures api
             * @return {array} data from the websocket stream
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $url = $this->urls['api']['ws'];
            $subscribe = array(
                'event' => 'subscribe',
                'feed' => $name,
                'api_key' => $this->apiKey,
                'original_challenge' => $this->options['challenge'],
                'signed_challenge' => $this->options['signedChallenge'],
            );
            $request = $this->extend($subscribe, $params);
            return Async\await($this->watch($url, $messageHash, $request, $messageHash));
        }) ();
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
             *
             * @see https://docs.futures.kraken.com/#websocket-api-public-feeds-ticker
             *
             * @param {string} $symbol unified $symbol of the market to fetch the ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            Async\await($this->load_markets());
            $symbol = $this->symbol($symbol);
            $tickers = Async\await($this->watch_tickers(array( $symbol ), $params));
            return $tickers[$symbol];
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * watches a price $ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
             *
             * @see https://docs.futures.kraken.com/#websocket-api-public-feeds-$ticker
             *
             * @param {string[]} $symbols unified $symbols of the markets to fetch the $ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=$ticker-structure $ticker structure~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $ticker = Async\await($this->watch_multi_helper('ticker', 'ticker', $symbols, null, $params));
            if ($this->newUpdates) {
                $result = array();
                $result[$ticker['symbol']] = $ticker;
                return $result;
            }
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    public function watch_bids_asks(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://docs.futures.kraken.com/#websocket-api-public-feeds-$ticker-lite
             *
             * watches best bid & ask for $symbols
             * @param {string[]} $symbols unified symbol of the market to fetch the $ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=$ticker-structure $ticker structure~
             */
            $ticker = Async\await($this->watch_multi_helper('bidask', 'ticker_lite', $symbols, null, $params));
            if ($this->newUpdates) {
                $result = array();
                $result[$ticker['symbol']] = $ticker;
                return $result;
            }
            return $this->filter_by_array($this->bidsasks, 'symbol', $symbols);
        }) ();
    }

    public function watch_trades(?string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * get the list of most recent trades for a particular $symbol
             *
             * @see https://docs.futures.kraken.com/#websocket-api-public-feeds-trade
             *
             * @param {string} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            return Async\await($this->watch_trades_for_symbols(array( $symbol ), $since, $limit, $params));
        }) ();
    }

    public function watch_trades_for_symbols(array $symbols, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             *
             * @see https://docs.futures.kraken.com/#websocket-api-public-feeds-trade
             *
             * get the list of most recent $trades for a list of $symbols
             * @param {string[]} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-$trades trade structures~
             */
            $trades = Async\await($this->watch_multi_helper('trade', 'trade', $symbols, null, $params));
            if ($this->newUpdates) {
                $first = $this->safe_list($trades, 0);
                $tradeSymbol = $this->safe_string($first, 'symbol');
                $limit = $trades->getLimit ($tradeSymbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function watch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://docs.futures.kraken.com/#websocket-api-public-feeds-book
             *
             * @param {string} $symbol unified $symbol of the market to fetch the order book for
             * @param {int} [$limit] not used by krakenfutures watchOrderBook
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by market symbols
             */
            return Async\await($this->watch_order_book_for_symbols(array( $symbol ), $limit, $params));
        }) ();
    }

    public function watch_positions(?array $symbols = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             *
             * @see https://docs.futures.kraken.com/#websocket-api-private-feeds-open-positions
             *
             * watch all open positions
             * @param {string[]|null} $symbols list of unified market $symbols
             * @param $since
             * @param $limit
             * @param {array} $params extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of {@link https://docs.ccxt.com/en/latest/manual.html#position-structure position structure}
             */
            Async\await($this->load_markets());
            $messageHash = '';
            $symbols = $this->market_symbols($symbols);
            if (!$this->is_empty($symbols)) {
                $messageHash = '::' . implode(',', $symbols);
            }
            $messageHash = 'positions' . $messageHash;
            $newPositions = Async\await($this->subscribe_private('open_positions', $messageHash, $params));
            if ($this->newUpdates) {
                return $newPositions;
            }
            return $this->filter_by_symbols_since_limit($this->positions, $symbols, $since, $limit, true);
        }) ();
    }

    public function handle_positions($client, $message) {
        //
        //    {
        //        feed => 'open_positions',
        //        account => '3b111acc-4fcc-45be-a622-57e611fe9f7f',
        //        $positions => array(
        //            {
        //                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 ($this->positions === null) {
            $this->positions = new ArrayCacheBySymbolById ();
        }
        $cache = $this->positions;
        $rawPositions = $this->safe_value($message, 'positions', array());
        $newPositions = array();
        for ($i = 0; $i < count($rawPositions); $i++) {
            $rawPosition = $rawPositions[$i];
            $position = $this->parse_ws_position($rawPosition);
            $timestamp = $this->safe_integer($message, 'timestamp');
            $position['timestamp'] = $timestamp;
            $position['datetime'] = $this->iso8601($timestamp);
            $newPositions[] = $position;
            $cache->append ($position);
        }
        $messageHashes = $this->find_message_hashes($client, 'positions::');
        for ($i = 0; $i < count($messageHashes); $i++) {
            $messageHash = $messageHashes[$i];
            $parts = explode('::', $messageHash);
            $symbolsString = $parts[1];
            $symbols = explode(',', $symbolsString);
            $positions = $this->filter_by_array($newPositions, 'symbol', $symbols, false);
            if (!$this->is_empty($positions)) {
                $client->resolve ($positions, $messageHash);
            }
        }
        $client->resolve ($newPositions, 'positions');
    }

    public function parse_ws_position($position, $market = null) {
        //
        //        {
        //            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 = $this->safe_string($position, 'instrument');
        $hedged = 'both';
        $balance = $this->safe_number($position, 'balance');
        $side = ($balance > 0) ? 'long' : 'short';
        return $this->safe_position(array(
            'info' => $position,
            'id' => null,
            'symbol' => $this->safe_symbol($marketId),
            'notional' => null,
            'marginMode' => null,
            'liquidationPrice' => $this->safe_number($position, 'liquidation_threshold'),
            'entryPrice' => $this->safe_number($position, 'entry_price'),
            'unrealizedPnl' => $this->safe_number($position, 'pnl'),
            'percentage' => $this->safe_number($position, 'return_on_equity'),
            'contracts' => $this->parse_number(Precise::string_abs($this->number_to_string($balance))),
            'contractSize' => null,
            'markPrice' => $this->safe_number($position, 'mark_price'),
            'side' => $side,
            'hedged' => $hedged,
            'timestamp' => null,
            'datetime' => null,
            'maintenanceMargin' => $this->safe_number($position, 'maintenance_margin'),
            'maintenanceMarginPercentage' => null,
            'collateral' => null,
            'initialMargin' => $this->safe_number($position, 'initial_margin'),
            'initialMarginPercentage' => null,
            'leverage' => null,
            'marginRatio' => null,
        ));
    }

    public function watch_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $orders made by the user
             *
             * @see https://docs.futures.kraken.com/#websocket-api-private-feeds-open-$orders
             * @see https://docs.futures.kraken.com/#websocket-api-private-feeds-open-$orders-verbose
             *
             * @param {string} $symbol not used by krakenfutures watchOrders
             * @param {int} [$since] not used by krakenfutures watchOrders
             * @param {int} [$limit] not used by krakenfutures watchOrders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $name = 'open_orders';
            $messageHash = 'orders';
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $messageHash .= ':' . $market['symbol'];
            }
            $orders = Async\await($this->subscribe_private($name, $messageHash, $params));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($orders, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function watch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $trades made by the user
             *
             * @see https://docs.futures.kraken.com/#websocket-api-private-feeds-fills
             *
             * @param {string} $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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            Async\await($this->load_markets());
            $name = 'fills';
            $messageHash = 'myTrades';
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $messageHash .= ':' . $market['symbol'];
            }
            $trades = Async\await($this->subscribe_private($name, $messageHash, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * watches information on the user's $account balance
             *
             * @see https://docs.futures.kraken.com/#websocket-api-private-feeds-balances
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->account] can be either 'futures' or 'flex_futures'
             * @return {array} a object of wallet types each with a balance structure array(@link https://docs.ccxt.com/#/?id=balance-structure)
             */
            Async\await($this->load_markets());
            $name = 'balances';
            $messageHash = $name;
            $account = null;
            list($account, $params) = $this->handle_option_and_params($params, 'watchBalance', 'account');
            if ($account !== null) {
                if ($account !== 'futures' && $account !== 'flex_futures') {
                    throw new ArgumentsRequired($this->id . ' watchBalance $account must be either \'futures\' or \'flex_futures\'');
                }
                $messageHash .= ':' . $account;
            }
            return Async\await($this->subscribe_private($name, $messageHash, $params));
        }) ();
    }

    public function handle_trade(Client $client, $message) {
        //
        // snapshot
        //
        //    {
        //        "feed" => "trade_snapshot",
        //        "product_id" => "PI_XBTUSD",
        //        "trades" => array(
        //            array(
        //                "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 = $this->safe_string($message, 'feed');
        $marketId = $this->safe_string($message, 'product_id');
        if ($marketId !== null) {
            $market = $this->market($marketId);
            $symbol = $market['symbol'];
            $messageHash = $this->get_message_hash('trade', null, $symbol);
            if ($this->safe_list($this->trades, $symbol) === null) {
                $tradesLimit = $this->safe_integer($this->options, 'tradesLimit', 1000);
                $this->trades[$symbol] = new ArrayCache ($tradesLimit);
            }
            $tradesArray = $this->trades[$symbol];
            if ($channel === 'trade_snapshot') {
                $trades = $this->safe_list($message, 'trades', array());
                $length = count($trades);
                for ($i = 0; $i < $length; $i++) {
                    $index = $length - 1 - $i; // need reverse to correct chronology
                    $item = $trades[$index];
                    $trade = $this->parse_ws_trade($item);
                    $tradesArray->append ($trade);
                }
            } else {
                $trade = $this->parse_ws_trade($message);
                $tradesArray->append ($trade);
            }
            $client->resolve ($tradesArray, $messageHash);
        }
    }

    public function parse_ws_trade($trade, $market = null) {
        //
        //    {
        //        "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 = $this->safe_string($trade, 'product_id');
        $market = $this->safe_market($marketId, $market);
        $timestamp = $this->safe_integer($trade, 'time');
        return $this->safe_trade(array(
            'info' => $trade,
            'id' => $this->safe_string($trade, 'uid'),
            'symbol' => $this->safe_string($market, 'symbol'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'order' => null,
            'type' => $this->safe_string($trade, 'type'),
            'side' => $this->safe_string($trade, 'side'),
            'takerOrMaker' => 'taker',
            'price' => $this->safe_string($trade, 'price'),
            'amount' => $this->safe_string($trade, 'qty'),
            'cost' => null,
            'fee' => array(
                'rate' => null,
                'cost' => null,
                'currency' => null,
            ),
        ), $market);
    }

    public function parse_ws_order_trade($trade, $market = null) {
        //
        //    {
        //        "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 = $this->safe_integer($trade, 'tradeTime');
        $marketId = $this->safe_string($trade, 'symbol');
        return $this->safe_trade(array(
            'info' => $trade,
            'id' => $this->safe_string($trade, 'tradeId'),
            'symbol' => $this->safe_symbol($marketId, $market),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'order' => $this->safe_string($trade, 'orderId'),
            'type' => $this->safe_string_lower($trade, 'type'),
            'side' => $this->safe_string($trade, 'side'),
            'takerOrMaker' => $this->safe_string($trade, 'matchRole'),
            'price' => $this->safe_string($trade, 'price'),
            'amount' => $this->safe_string($trade, 'tradeAmount'), // ? tradeQty?
            'cost' => null,
            'fee' => array(
                'rate' => null,
                'cost' => $this->safe_string($trade, 'tradeFee'),
                'currency' => $this->safe_string($trade, 'feeCurrency'),
            ),
        ), $market);
    }

    public function handle_order(Client $client, $message) {
        //
        //  update (verbose)
        //
        //    {
        //        "feed" => "open_orders_verbose",
        //        "order" => array(
        //            "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" => array(
        //          "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" => array(
        //         "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 = $this->orders;
        if ($orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit');
            $orders = new ArrayCacheBySymbolById ($limit);
            $this->orders = $orders;
        }
        $order = $this->safe_value($message, 'order');
        if ($order !== null) {
            $marketId = $this->safe_string($order, 'instrument');
            $messageHash = 'orders';
            $symbol = $this->safe_symbol($marketId);
            $orderId = $this->safe_string($order, 'order_id');
            $previousOrders = $this->safe_value($orders->hashmap, $symbol, array());
            $previousOrder = $this->safe_value($previousOrders, $orderId);
            $reason = $this->safe_string($message, 'reason');
            if (($previousOrder === null) || ($reason === 'edited_by_user')) {
                $parsed = $this->parse_ws_order($order);
                $orders->append ($parsed);
                $client->resolve ($orders, $messageHash);
                $client->resolve ($orders, $messageHash . ':' . $symbol);
            } else {
                $trade = $this->parse_ws_trade($order);
                if ($previousOrder['trades'] === null) {
                    $previousOrder['trades'] = array();
                }
                $previousOrder['trades'][] = $trade;
                $previousOrder['lastTradeTimestamp'] = $trade['timestamp'];
                $totalCost = '0';
                $totalAmount = '0';
                $trades = $previousOrder['trades'];
                for ($i = 0; $i < count($trades); $i++) {
                    $currentTrade = $trades[$i];
                    $totalCost = Precise::string_add($totalCost, $this->number_to_string($currentTrade['cost']));
                    $totalAmount = Precise::string_add($totalAmount, $this->number_to_string($currentTrade['amount']));
                }
                if (Precise::string_gt($totalAmount, '0')) {
                    $previousOrder['average'] = Precise::string_div($totalCost, $totalAmount);
                }
                $previousOrder['cost'] = $totalCost;
                if ($previousOrder['filled'] !== null) {
                    $stringOrderFilled = $this->number_to_string($previousOrder['filled']);
                    $previousOrder['filled'] = Precise::string_add($stringOrderFilled, $this->number_to_string($trade['amount']));
                    if ($previousOrder['amount'] !== null) {
                        $previousOrder['remaining'] = Precise::string_sub($this->number_to_string($previousOrder['amount']), $stringOrderFilled);
                    }
                }
                if ($previousOrder['fee'] === null) {
                    $previousOrder['fee'] = array(
                        'rate' => null,
                        'cost' => '0',
                        'currency' => $this->number_to_string($trade['fee']['currency']),
                    );
                }
                if (($previousOrder['fee']['cost'] !== null) && ($trade['fee']['cost'] !== null)) {
                    $stringOrderCost = $this->number_to_string($previousOrder['fee']['cost']);
                    $stringTradeCost = $this->number_to_string($trade['fee']['cost']);
                    $previousOrder['fee']['cost'] = Precise::string_add($stringOrderCost, $stringTradeCost);
                }
                // update the newUpdates count
                $orders->append ($this->safe_order($previousOrder));
                $client->resolve ($orders, $messageHash . ':' . $symbol);
                $client->resolve ($orders, $messageHash);
            }
        } else {
            $isCancel = $this->safe_value($message, 'is_cancel');
            if ($isCancel) {
                // get $order without $symbol
                for ($i = 0; $i < count($orders); $i++) {
                    $currentOrder = $orders[$i];
                    if ($currentOrder['id'] === $message['order_id']) {
                        $orders[$i] = $this->extend($currentOrder, array(
                            'status' => 'canceled',
                        ));
                        $client->resolve ($orders, 'orders');
                        $client->resolve ($orders, 'orders:' . $currentOrder['symbol']);
                        break;
                    }
                }
            }
        }
        return $message;
    }

    public function handle_order_snapshot(Client $client, $message) {
        //
        // verbose
        //
        //    {
        //        "feed" => "open_orders_verbose_snapshot",
        //        "account" => "0f9c23b8-63e2-40e4-9592-6d5aa57c12ba",
        //        "orders" => array(
        //            array(
        //                "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" => array(
        //            array(
        //                "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 = $this->safe_value($message, 'orders', array());
        $limit = $this->safe_integer($this->options, 'ordersLimit');
        $this->orders = new ArrayCacheBySymbolById ($limit);
        $symbols = array();
        $cachedOrders = $this->orders;
        for ($i = 0; $i < count($orders); $i++) {
            $order = $orders[$i];
            $parsed = $this->parse_ws_order($order);
            $symbol = $parsed['symbol'];
            $symbols[$symbol] = true;
            $cachedOrders->append ($parsed);
        }
        $length = count($this->orders);
        if ($length > 0) {
            $client->resolve ($this->orders, 'orders');
            $keys = is_array($symbols) ? array_keys($symbols) : array();
            for ($i = 0; $i < count($keys); $i++) {
                $symbol = $keys[$i];
                $messageHash = 'orders:' . $symbol;
                $client->resolve ($this->orders, $messageHash);
            }
        }
    }

    public function parse_ws_order($order, $market = null) {
        //
        // update
        //
        //    {
        //        "feed" => "open_orders_verbose",
        //        "order" => array(
        //            "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 = $this->safe_value($order, 'is_cancel');
        $unparsedOrder = $order;
        $status = null;
        if ($isCancelled !== null) {
            $unparsedOrder = $this->safe_value($order, 'order');
            if ($isCancelled === true) {
                $status = 'cancelled';
            }
        }
        $marketId = $this->safe_string($unparsedOrder, 'instrument');
        $timestamp = $this->safe_string($unparsedOrder, 'time');
        $direction = $this->safe_integer($unparsedOrder, 'direction');
        return $this->safe_order(array(
            'info' => $order,
            'symbol' => $this->safe_symbol($marketId, $market),
            'id' => $this->safe_string($unparsedOrder, 'order_id'),
            'clientOrderId' => null,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => null,
            'type' => $this->safe_string($unparsedOrder, 'type'),
            'timeInForce' => null,
            'postOnly' => null,
            'side' => ($direction === 0) ? 'buy' : 'sell',
            'price' => $this->safe_string($unparsedOrder, 'limit_price'),
            'stopPrice' => $this->safe_string($unparsedOrder, 'stop_price'),
            'triggerPrice' => $this->safe_string($unparsedOrder, 'stop_price'),
            'amount' => $this->safe_string($unparsedOrder, 'qty'),
            'cost' => null,
            'average' => null,
            'filled' => $this->safe_string($unparsedOrder, 'filled'),
            'remaining' => null,
            'status' => $status,
            'fee' => array(
                'rate' => null,
                'cost' => null,
                'currency' => null,
            ),
            'trades' => null,
        ));
    }

    public function handle_ticker(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 = $this->safe_string($message, 'product_id');
        if ($marketId !== null) {
            $ticker = $this->parse_ws_ticker($message);
            $symbol = $ticker['symbol'];
            $this->tickers[$symbol] = $ticker;
            $messageHash = $this->get_message_hash('ticker', null, $symbol);
            $client->resolve ($ticker, $messageHash);
        }
    }

    public function handle_bid_ask(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 = $this->safe_string($message, 'product_id');
        if ($marketId !== null) {
            $ticker = $this->parse_ws_ticker($message);
            $symbol = $ticker['symbol'];
            $this->bidsasks[$symbol] = $ticker;
            $messageHash = $this->get_message_hash('bidask', null, $symbol);
            $client->resolve ($ticker, $messageHash);
        }
    }

    public function parse_ws_ticker($ticker, $market = null) {
        //
        //    {
        //        "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 = $this->safe_string($ticker, 'product_id');
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $timestamp = $this->parse8601($this->safe_string($ticker, 'lastTime'));
        $last = $this->safe_string($ticker, 'last');
        return $this->safe_ticker(array(
            'info' => $ticker,
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => null,
            'low' => null,
            'bid' => $this->safe_string($ticker, 'bid'),
            'bidVolume' => $this->safe_string($ticker, 'bid_size'),
            'ask' => $this->safe_string($ticker, 'ask'),
            'askVolume' => $this->safe_string($ticker, 'ask_size'),
            'vwap' => null,
            'open' => null,
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => $this->safe_string($ticker, 'change'),
            'percentage' => null,
            'average' => null,
            'baseVolume' => $this->safe_string($ticker, 'volume'),
            'quoteVolume' => $this->safe_string($ticker, 'volumeQuote'),
            'markPrice' => $this->safe_string($ticker, 'markPrice'),
            'indexPrice' => $this->safe_string($ticker, 'index'),
        ));
    }

    public function handle_order_book_snapshot(Client $client, $message) {
        //
        //    {
        //        "feed" => "book_snapshot",
        //        "product_id" => "PI_XBTUSD",
        //        "timestamp" => 1612269825817,
        //        "seq" => 326072249,
        //        "tickSize" => null,
        //        "bids" => array(
        //            array(
        //                "price" => 34892.5,
        //                "qty" => 6385
        //            ),
        //            array(
        //                "price" => 34892,
        //                "qty" => 10924
        //            ),
        //        ),
        //        "asks" => array(
        //            array(
        //                "price" => 34911.5,
        //                "qty" => 20598
        //            ),
        //            array(
        //                "price" => 34912,
        //                "qty" => 2300
        //            ),
        //        )
        //    }
        //
        $marketId = $this->safe_string($message, 'product_id');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $messageHash = $this->get_message_hash('orderbook', null, $symbol);
        $subscription = $this->safe_dict($client->subscriptions, $messageHash, array());
        $limit = $this->safe_integer($subscription, 'limit');
        $timestamp = $this->safe_integer($message, 'timestamp');
        $this->orderbooks[$symbol] = $this->order_book(array(), $limit);
        $orderbook = $this->orderbooks[$symbol];
        $bids = $this->safe_list($message, 'bids');
        $asks = $this->safe_list($message, 'asks');
        for ($i = 0; $i < count($bids); $i++) {
            $bid = $bids[$i];
            $price = $this->safe_number($bid, 'price');
            $qty = $this->safe_number($bid, 'qty');
            $bidsSide = $orderbook['bids'];
            $bidsSide->store ($price, $qty);
        }
        for ($i = 0; $i < count($asks); $i++) {
            $ask = $asks[$i];
            $price = $this->safe_number($ask, 'price');
            $qty = $this->safe_number($ask, 'qty');
            $asksSide = $orderbook['asks'];
            $asksSide->store ($price, $qty);
        }
        $orderbook['timestamp'] = $timestamp;
        $orderbook['datetime'] = $this->iso8601($timestamp);
        $orderbook['symbol'] = $symbol;
        $client->resolve ($orderbook, $messageHash);
    }

    public function handle_order_book(Client $client, $message) {
        //
        //    {
        //        "feed" => "book",
        //        "product_id" => "PI_XBTUSD",
        //        "side" => "sell",
        //        "seq" => 326094134,
        //        "price" => 34981,
        //        "qty" => 0,
        //        "timestamp" => 1612269953629
        //    }
        //
        $marketId = $this->safe_string($message, 'product_id');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $messageHash = $this->get_message_hash('orderbook', null, $symbol);
        $orderbook = $this->orderbooks[$symbol];
        $side = $this->safe_string($message, 'side');
        $price = $this->safe_number($message, 'price');
        $qty = $this->safe_number($message, 'qty');
        $timestamp = $this->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'] = $this->iso8601($timestamp);
        $client->resolve ($orderbook, $messageHash);
    }

    public function handle_balance(Client $client, $message) {
        //
        // snapshot
        //
        //    {
        //        "feed" => "balances_snapshot",
        //        "account" => "4a012c31-df95-484a-9473-d51e4a0c4ae7",
        //        "holding" => array(
        //            "USDT" => 4997.5012493753,
        //            "XBT" => 0.1285407184,
        //            ...
        //        ),
        //        "futures" => array(
        //            "F-ETH:EUR" => array(
        //                "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" => array(
        //            "currencies" => array(
        //                "USDT" => array(
        //                    "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":array(
        //            ),
        //            "cross":array(
        //                "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" => array(
        //            "USD" => 5000.0
        //        ),
        //        "futures" => array(),
        //        "timestamp" => 1640995200000,
        //        "seq" => 83
        //    }
        //
        //    Multi-Collateral
        //
        //    {
        //        "feed" => "balances"
        //        "account" => "7a641082-55c7-4411-a85f-930ec2e09617"
        //        "flex_futures" => array(
        //            "currencies" => array(
        //                "USDT" => array(
        //                    "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" => array(),
        //        "futures" => {
        //            "F-XBT:USD" => array(
        //                "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 = $this->safe_value($message, 'holding');
        $futures = $this->safe_value($message, 'futures');
        $flexFutures = $this->safe_value($message, 'flex_futures');
        $messageHash = 'balances';
        $timestamp = $this->safe_integer($message, 'timestamp');
        if ($holding !== null) {
            $holdingKeys = is_array($holding) ? array_keys($holding) : array();                  // cashAccount
            $holdingResult = array(
                'info' => $message,
                'timestamp' => $timestamp,
                'datetime' => $this->iso8601($timestamp),
            );
            for ($i = 0; $i < count($holdingKeys); $i++) {
                $key = $holdingKeys[$i];
                $code = $this->safe_currency_code($key);
                $newAccount = $this->account();
                $newAccount['total'] = $this->safe_string($holding, $key);
                $holdingResult[$code] = $newAccount;
            }
            $this->balance['cash'] = $holdingResult;
            $this->balance['cash'] = $this->safe_balance($this->balance['cash']);
            $client->resolve ($holdingResult, $messageHash);
        }
        if ($futures !== null) {
            $futuresKeys = is_array($futures) ? array_keys($futures) : array();                  // marginAccount
            $futuresResult = array(
                'info' => $message,
                'timestamp' => $timestamp,
                'datetime' => $this->iso8601($timestamp),
            );
            for ($i = 0; $i < count($futuresKeys); $i++) {
                $key = $futuresKeys[$i];
                $symbol = $this->safe_symbol($key);
                $newAccount = $this->account();
                $future = $this->safe_value($futures, $key);
                $currencyId = $this->safe_string($future, 'unit');
                $code = $this->safe_currency_code($currencyId);
                $newAccount['free'] = $this->safe_string($future, 'available');
                $newAccount['used'] = $this->safe_string($future, 'initial_margin');
                $newAccount['total'] = $this->safe_string($future, 'balance');
                $futuresResult[$symbol] = array();
                $futuresResult[$symbol][$code] = $newAccount;
            }
            $this->balance['margin'] = $futuresResult;
            $this->balance['margin'] = $this->safe_balance($this->balance['margin']);
            $client->resolve ($this->balance['margin'], $messageHash . 'futures');
        }
        if ($flexFutures !== null) {
            $flexFutureCurrencies = $this->safe_value($flexFutures, 'currencies', array());
            $flexFuturesKeys = is_array($flexFutureCurrencies) ? array_keys($flexFutureCurrencies) : array(); // multi-collateral margin account
            $flexFuturesResult = array(
                'info' => $message,
                'timestamp' => $timestamp,
                'datetime' => $this->iso8601($timestamp),
            );
            for ($i = 0; $i < count($flexFuturesKeys); $i++) {
                $key = $flexFuturesKeys[$i];
                $flexFuture = $this->safe_value($flexFutureCurrencies, $key);
                $code = $this->safe_currency_code($key);
                $newAccount = $this->account();
                $newAccount['free'] = $this->safe_string($flexFuture, 'available');
                $newAccount['used'] = $this->safe_string($flexFuture, 'collateral_value');
                $newAccount['total'] = $this->safe_string($flexFuture, 'quantity');
                $flexFuturesResult[$code] = $newAccount;
            }
            $this->balance['flex'] = $flexFuturesResult;
            $this->balance['flex'] = $this->safe_balance($this->balance['flex']);
            $client->resolve ($this->balance['flex'], $messageHash . 'flex_futures');
        }
        $client->resolve ($this->balance, $messageHash);
    }

    public function handle_my_trades(Client $client, $message) {
        //
        //    {
        //        "feed" => "fills_snapshot",
        //        "account" => "DemoUser",
        //        "fills" => array(
        //            array(
        //                "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 = $this->safe_value($message, 'fills', array());
        $stored = $this->myTrades;
        if ($stored === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $stored = new ArrayCacheBySymbolById ($limit);
            $this->myTrades = $stored;
        }
        $tradeSymbols = array();
        for ($i = 0; $i < count($trades); $i++) {
            $trade = $trades[$i];
            $parsedTrade = $this->parse_ws_my_trade($trade);
            $tradeSymbols[$parsedTrade['symbol']] = true;
            $stored->append ($parsedTrade);
        }
        $tradeSymbolKeys = is_array($tradeSymbols) ? array_keys($tradeSymbols) : array();
        for ($i = 0; $i < count($tradeSymbolKeys); $i++) {
            $symbol = $tradeSymbolKeys[$i];
            $messageHash = 'myTrades:' . $symbol;
            $client->resolve ($stored, $messageHash);
        }
        $client->resolve ($stored, 'myTrades');
    }

    public function parse_ws_my_trade($trade, $market = null) {
        //
        //    {
        //        "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 = $this->safe_integer($trade, 'time');
        $marketId = $this->safe_string($trade, 'instrument');
        $market = $this->safe_market($marketId, $market);
        $isBuy = $this->safe_value($trade, 'buy');
        $feeCurrencyId = $this->safe_string($trade, 'fee_currency');
        return $this->safe_trade(array(
            'info' => $trade,
            'id' => $this->safe_string($trade, 'fill_id'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $this->safe_string($market, 'symbol'),
            'order' => $this->safe_string($trade, 'order_id'),
            'type' => $this->safe_string($trade, 'type'),
            'side' => $isBuy ? 'buy' : 'sell',
            'takerOrMaker' => $this->safe_string($trade, 'fill_type'),
            'price' => $this->safe_string($trade, 'price'),
            'amount' => $this->safe_string($trade, 'qty'),
            'cost' => null,
            'fee' => array(
                'currency' => $this->safe_currency_code($feeCurrencyId),
                'cost' => $this->safe_string($trade, 'fee_paid'),
                'rate' => null,
            ),
        ));
    }

    public function watch_multi_helper(string $unifiedName, string $channelName, ?array $symbols = null, $subscriptionArgs = null, $params = array ()) {
        return Async\async(function () use ($unifiedName, $channelName, $symbols, $subscriptionArgs, $params) {
            Async\await($this->load_markets());
            // $symbols are required
            $symbols = $this->market_symbols($symbols, null, false, true, false);
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $messageHashes[] = $this->get_message_hash($unifiedName, null, $this->symbol($symbols[$i]));
            }
            $marketIds = $this->market_ids($symbols);
            $request = array(
                'event' => 'subscribe',
                'feed' => $channelName,
                'product_ids' => $marketIds,
            );
            $url = $this->urls['api']['ws'];
            return Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $messageHashes, $subscriptionArgs));
        }) ();
    }

    public function get_message_hash(string $unifiedElementName, ?string $subChannelName = null, ?string $symbol = null) {
        // $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 !== null;
        $messageHash = $unifiedElementName;
        if (!$withSymbol) {
            $messageHash .= 's';
        } else {
            $messageHash .= ':' . $symbol;
        }
        if ($subChannelName !== null) {
            $messageHash .= '#' . $subChannelName;
        }
        return $messageHash;
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        //    {
        //        event => 'alert',
        //        $message => 'Failed to subscribe to authenticated feed'
        //    }
        //
        $errMsg = $this->safe_string($message, 'message');
        try {
            throw new ExchangeError($this->id . ' ' . $errMsg);
        } catch (Exception $error) {
            $client->reject ($error);
            return false;
        }
    }

    public function handle_message($client, $message) {
        $event = $this->safe_string($message, 'event');
        if ($event === 'challenge') {
            $this->handle_authenticate($client, $message);
        } elseif ($event === 'alert') {
            $this->handle_error_message($client, $message);
        } elseif ($event === 'pong') {
            $client->lastPong = $this->milliseconds();
        } elseif ($event === null) {
            $feed = $this->safe_string($message, 'feed');
            $methods = array(
                'ticker' => array($this, 'handle_ticker'),
                'ticker_lite' => array($this, 'handle_bid_ask'),
                'trade' => array($this, 'handle_trade'),
                'trade_snapshot' => array($this, 'handle_trade'),
                // 'heartbeat' => $this->handleStatus,
                'book' => array($this, 'handle_order_book'),
                'book_snapshot' => array($this, 'handle_order_book_snapshot'),
                'open_orders_verbose' => array($this, 'handle_order'),
                'open_orders_verbose_snapshot' => array($this, 'handle_order_snapshot'),
                'fills' => array($this, 'handle_my_trades'),
                'fills_snapshot' => array($this, 'handle_my_trades'),
                'open_orders' => array($this, 'handle_order'),
                'open_orders_snapshot' => array($this, 'handle_order_snapshot'),
                'balances' => array($this, 'handle_balance'),
                'balances_snapshot' => array($this, 'handle_balance'),
                'open_positions' => array($this, 'handle_positions'),
            );
            $method = $this->safe_value($methods, $feed);
            if ($method !== null) {
                $method($client, $message);
            }
        }
    }

    public function handle_authenticate(Client $client, $message) {
        /**
         * @ignore
         * @see https://docs.futures.kraken.com/#websocket-api-websocket-api-introduction-sign-$challenge-challenge
         */
        //
        //    {
        //        "event" => "challenge",
        //        "message" => "226aee50-88fc-4618-a42a-34f7709570b2"
        //    }
        //
        $event = $this->safe_value($message, 'event');
        $messageHash = 'challenge';
        if ($event !== 'error') {
            $challenge = $this->safe_value($message, 'message');
            $hashedChallenge = $this->hash($this->encode($challenge), 'sha256', 'binary');
            $base64Secret = base64_decode($this->secret);
            $signature = $this->hmac($hashedChallenge, $base64Secret, 'sha512', 'base64');
            $this->options['challenge'] = $challenge;
            $this->options['signedChallenge'] = $signature;
            $future = $this->safe_value($client->futures, $messageHash);
            $future->resolve (true);
        } else {
            $error = new AuthenticationError ($this->id . ' ' . $this->json($message));
            $client->reject ($error, $messageHash);
            if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                unset($client->subscriptions[$messageHash]);
            }
        }
        return $message;
    }
}
