<?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 \React\Async;
use \React\Promise\PromiseInterface;

class hashkey extends \ccxt\async\hashkey {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchBalance' => true,
                'watchMyTrades' => true,
                'watchOHLCV' => true,
                'watchOrderBook' => true,
                'watchOrders' => true,
                'watchTicker' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => false,
                'watchPositions' => false,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => array(
                        'public' => 'wss://stream-glb.hashkey.com/quote/ws/v1',
                        'private' => 'wss://stream-glb.hashkey.com/api/v1/ws',
                    ),
                    'test' => array(
                        'ws' => array(
                            'public' => 'wss://stream-glb.sim.hashkeydev.com/quote/ws/v1',
                            'private' => 'wss://stream-glb.sim.hashkeydev.com/api/v1/ws',
                        ),
                    ),
                ),
            ),
            'options' => array(
                'listenKeyRefreshRate' => 3600000,
                'listenKey' => null,
                'watchBalance' => array(
                    'fetchBalanceSnapshot' => true, // or false
                    'awaitBalanceSnapshot' => false, // whether to wait for the balance snapshot before providing updates
                ),
            ),
            'streaming' => array(
                'keepAlive' => 10000,
            ),
        ));
    }

    public function wath_public(array $market, string $topic, string $messageHash, $params = array ()) {
        return Async\async(function () use ($market, $topic, $messageHash, $params) {
            $request = array(
                'symbol' => $market['id'],
                'topic' => $topic,
                'event' => 'sub',
            );
            $url = $this->urls['api']['ws']['public'];
            return Async\await($this->watch($url, $messageHash, $this->deep_extend($request, $params), $messageHash));
        }) ();
    }

    public function watch_private($messageHash) {
        return Async\async(function () use ($messageHash) {
            $listenKey = Async\await($this->authenticate());
            $url = $this->get_private_url($listenKey);
            return Async\await($this->watch($url, $messageHash, null, $messageHash));
        }) ();
    }

    public function get_private_url($listenKey) {
        return $this->urls['api']['ws']['private'] . '/' . $listenKey;
    }

    public function watch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, and close price, and the volume of a $market
             *
             * @see https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#public-stream
             *
             * @param {string} $symbol unified $symbol of the $market to fetch OHLCV data for
             * @param {string} $timeframe the length of time each candle represents
             * @param {int} [$since] timestamp in ms of the earliest candle to fetch
             * @param {int} [$limit] the maximum amount of candles to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {bool} [$params->binary] true or false - default false
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $interval = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $topic = 'kline_' . $interval;
            $messageHash = 'ohlcv:' . $symbol . ':' . $timeframe;
            $ohlcv = Async\await($this->wath_public($market, $topic, $messageHash, $params));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         "symbol" => "DOGEUSDT",
        //         "symbolName" => "DOGEUSDT",
        //         "topic" => "kline",
        //         "params" => array(
        //             "realtimeInterval" => "24h",
        //             "klineType" => "1m"
        //         ),
        //         "data" => array(
        //             {
        //                 "t" => 1722861660000,
        //                 "s" => "DOGEUSDT",
        //                 "sn" => "DOGEUSDT",
        //                 "c" => "0.08389",
        //                 "h" => "0.08389",
        //                 "l" => "0.08389",
        //                 "o" => "0.08389",
        //                 "v" => "0"
        //             }
        //         ),
        //         "f" => true,
        //         "sendTime" => 1722861664258,
        //         "shared" => false
        //     }
        //
        $marketId = $this->safe_string($message, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $this->safe_symbol($marketId, $market);
        if (!(is_array($this->ohlcvs) && array_key_exists($symbol, $this->ohlcvs))) {
            $this->ohlcvs[$symbol] = array();
        }
        $params = $this->safe_dict($message, 'params');
        $klineType = $this->safe_string($params, 'klineType');
        $timeframe = $this->find_timeframe($klineType);
        if (!(is_array($this->ohlcvs[$symbol]) && array_key_exists($timeframe, $this->ohlcvs[$symbol]))) {
            $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
            $this->ohlcvs[$symbol][$timeframe] = new ArrayCacheByTimestamp ($limit);
        }
        $data = $this->safe_list($message, 'data', array());
        $stored = $this->ohlcvs[$symbol][$timeframe];
        for ($i = 0; $i < count($data); $i++) {
            $candle = $this->safe_dict($data, $i, array());
            $parsed = $this->parse_ws_ohlcv($candle, $market);
            $stored->append ($parsed);
        }
        $messageHash = 'ohlcv:' . $symbol . ':' . $timeframe;
        $client->resolve ($stored, $messageHash);
    }

    public function parse_ws_ohlcv($ohlcv, ?array $market = null): array {
        //
        //     {
        //         "t" => 1722861660000,
        //         "s" => "DOGEUSDT",
        //         "sn" => "DOGEUSDT",
        //         "c" => "0.08389",
        //         "h" => "0.08389",
        //         "l" => "0.08389",
        //         "o" => "0.08389",
        //         "v" => "0"
        //     }
        //
        return array(
            $this->safe_integer($ohlcv, 't'),
            $this->safe_number($ohlcv, 'o'),
            $this->safe_number($ohlcv, 'h'),
            $this->safe_number($ohlcv, 'l'),
            $this->safe_number($ohlcv, 'c'),
            $this->safe_number($ohlcv, 'v'),
        );
    }

    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://hashkeyglobal-apidoc.readme.io/reference/websocket-api#public-stream
             *
             * @param {string} $symbol unified $symbol of the $market to fetch the ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {bool} [$params->binary] true or false - default false
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $topic = 'realtimes';
            $messageHash = 'ticker:' . $symbol;
            return Async\await($this->wath_public($market, $topic, $messageHash, $params));
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //     {
        //         "symbol" => "ETHUSDT",
        //         "symbolName" => "ETHUSDT",
        //         "topic" => "realtimes",
        //         "params" => array(
        //             "realtimeInterval" => "24h"
        //         ),
        //         "data" => array(
        //             {
        //                 "t" => 1722864411064,
        //                 "s" => "ETHUSDT",
        //                 "sn" => "ETHUSDT",
        //                 "c" => "2195",
        //                 "h" => "2918.85",
        //                 "l" => "2135.5",
        //                 "o" => "2915.78",
        //                 "v" => "666.5019",
        //                 "qv" => "1586902.757079",
        //                 "m" => "-0.2472",
        //                 "e" => 301
        //             }
        //         ),
        //         "f" => false,
        //         "sendTime" => 1722864411086,
        //         "shared" => false
        //     }
        //
        $data = $this->safe_list($message, 'data', array());
        $ticker = $this->parse_ticker($this->safe_dict($data, 0));
        $symbol = $ticker['symbol'];
        $messageHash = 'ticker:' . $symbol;
        $this->tickers[$symbol] = $ticker;
        $client->resolve ($this->tickers[$symbol], $messageHash);
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $trades made in a $market
             *
             * @see https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#public-stream
             *
             * @param {string} $symbol unified $market $symbol of the $market $trades were made in
             * @param {int} [$since] the earliest time in ms to fetch orders for
             * @param {int} [$limit] the maximum number of trade structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {bool} [$params->binary] true or false - default false
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $topic = 'trade';
            $messageHash = 'trades:' . $symbol;
            $trades = Async\await($this->wath_public($market, $topic, $messageHash, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function handle_trades(Client $client, $message) {
        //
        //     {
        //         "symbol" => "ETHUSDT",
        //         "symbolName" => "ETHUSDT",
        //         "topic" => "trade",
        //         "params" => array(
        //             "realtimeInterval" => "24h"
        //         ),
        //         "data" => array(
        //             array(
        //                 "v" => "1745922896272048129",
        //                 "t" => 1722866228075,
        //                 "p" => "2340.41",
        //                 "q" => "0.0132",
        //                 "m" => true
        //             ),
        //             ...
        //         ),
        //         "f" => true,
        //         "sendTime" => 1722869464248,
        //         "channelId" => "668498fffeba4108-00000001-00113184-562e27d215e43f9c-c188b319",
        //         "shared" => false
        //     }
        //
        $marketId = $this->safe_string($message, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        if (!(is_array($this->trades) && array_key_exists($symbol, $this->trades))) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $this->trades[$symbol] = new ArrayCache ($limit);
        }
        $stored = $this->trades[$symbol];
        $data = $this->safe_list($message, 'data');
        if ($data !== null) {
            $data = $this->sort_by($data, 't');
            for ($i = 0; $i < count($data); $i++) {
                $trade = $this->safe_dict($data, $i);
                $parsed = $this->parse_ws_trade($trade, $market);
                $stored->append ($parsed);
            }
        }
        $messageHash = 'trades' . ':' . $symbol;
        $client->resolve ($stored, $messageHash);
    }

    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://hashkeyglobal-apidoc.readme.io/reference/websocket-api#public-stream
             *
             * @param {string} $symbol unified $symbol of the $market to fetch the order book for
             * @param {int} [$limit] the maximum amount of order book entries to return.
             * @param {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
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $topic = 'depth';
            $messageHash = 'orderbook:' . $symbol;
            $orderbook = Async\await($this->wath_public($market, $topic, $messageHash, $params));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_order_book(Client $client, $message) {
        //
        //     {
        //         "symbol" => "ETHUSDT",
        //         "symbolName" => "ETHUSDT",
        //         "topic" => "depth",
        //         "params" => array( "realtimeInterval" => "24h" ),
        //         "data" => array(
        //             {
        //                 "e" => 301,
        //                 "s" => "ETHUSDT",
        //                 "t" => 1722873144371,
        //                 "v" => "84661262_18",
        //                 "b" => array(
        //                     array( "1650", "0.0864" ),
        //                     ...
        //                 ),
        //                 "a" => array(
        //                     ["4085", "0.0074" ),
        //                     ...
        //                 ),
        //                 "o" => 0
        //             }
        //         ],
        //         "f" => false,
        //         "sendTime" => 1722873144589,
        //         "channelId" => "2265aafffe68b588-00000001-0011510c-9e9ca710b1500854-551830bd",
        //         "shared" => false
        //     }
        //
        $marketId = $this->safe_string($message, 'symbol');
        $symbol = $this->safe_symbol($marketId);
        $messageHash = 'orderbook:' . $symbol;
        if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
            $this->orderbooks[$symbol] = $this->order_book(array());
        }
        $orderbook = $this->orderbooks[$symbol];
        $data = $this->safe_list($message, 'data', array());
        $dataEntry = $this->safe_dict($data, 0);
        $timestamp = $this->safe_integer($dataEntry, 't');
        $snapshot = $this->parse_order_book($dataEntry, $symbol, $timestamp, 'b', 'a');
        $orderbook->reset ($snapshot);
        $orderbook['nonce'] = $this->safe_integer($message, 'id');
        $this->orderbooks[$symbol] = $orderbook;
        $client->resolve ($orderbook, $messageHash);
    }

    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://hashkeyglobal-apidoc.readme.io/reference/websocket-api#private-stream
             *
             * @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=order-structure order structures~
             */
            Async\await($this->load_markets());
            $messageHash = 'orders';
            if ($symbol !== null) {
                $symbol = $this->symbol($symbol);
                $messageHash = $messageHash . ':' . $symbol;
            }
            $orders = Async\await($this->watch_private($messageHash));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_order(Client $client, $message) {
        //
        // swap
        //     {
        //         "e" => "contractExecutionReport",
        //         "E" => "1723037391181",
        //         "s" => "ETHUSDT-PERPETUAL",
        //         "c" => "1723037389677",
        //         "S" => "BUY_OPEN",
        //         "o" => "LIMIT",
        //         "f" => "IOC",
        //         "q" => "1",
        //         "p" => "2561.75",
        //         "X" => "FILLED",
        //         "i" => "1747358716129257216",
        //         "l" => "1",
        //         "z" => "1",
        //         "L" => "2463.36",
        //         "n" => "0.001478016",
        //         "N" => "USDT",
        //         "u" => true,
        //         "w" => true,
        //         "m" => false,
        //         "O" => "1723037391140",
        //         "Z" => "2463.36",
        //         "C" => false,
        //         "v" => "5",
        //         "reqAmt" => "0",
        //         "d" => "1747358716255075840",
        //         "r" => "0",
        //         "V" => "2463.36",
        //         "P" => "0",
        //         "lo" => false,
        //         "lt" => ""
        //     }
        //
        if ($this->orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $parsed = $this->parse_ws_order($message);
        $orders = $this->orders;
        $orders->append ($parsed);
        $messageHash = 'orders';
        $client->resolve ($orders, $messageHash);
        $symbol = $parsed['symbol'];
        $symbolSpecificMessageHash = $messageHash . ':' . $symbol;
        $client->resolve ($orders, $symbolSpecificMessageHash);
    }

    public function parse_ws_order(array $order, ?array $market = null): array {
        $marketId = $this->safe_string($order, 's');
        $market = $this->safe_market($marketId, $market);
        $timestamp = $this->safe_integer($order, 'O');
        $side = $this->safe_string_lower($order, 'S');
        $reduceOnly = null;
        list($side, $reduceOnly) = $this->parseOrderSideAndReduceOnly ($side);
        $type = $this->parseOrderType ($this->safe_string($order, 'o'));
        $timeInForce = $this->safe_string($order, 'f');
        $postOnly = null;
        list($type, $timeInForce, $postOnly) = $this->parseOrderTypeTimeInForceAndPostOnly ($type, $timeInForce);
        if ($market['contract']) { // swap orders are always have $type 'LIMIT', thus we can not define the correct $type
            $type = null;
        }
        return $this->safe_order(array(
            'id' => $this->safe_string($order, 'i'),
            'clientOrderId' => $this->safe_string($order, 'c'),
            'datetime' => $this->iso8601($timestamp),
            'timestamp' => $timestamp,
            'lastTradeTimestamp' => null,
            'lastUpdateTimestamp' => null,
            'status' => $this->parse_order_status($this->safe_string($order, 'X')),
            'symbol' => $market['symbol'],
            'type' => $type,
            'timeInForce' => $timeInForce,
            'side' => $side,
            'price' => $this->safe_string($order, 'p'),
            'average' => $this->safe_string($order, 'V'),
            'amount' => $this->omit_zero($this->safe_string($order, 'q')),
            'filled' => $this->safe_string($order, 'z'),
            'remaining' => $this->safe_string($order, 'r'),
            'stopPrice' => null,
            'triggerPrice' => null,
            'takeProfitPrice' => null,
            'stopLossPrice' => null,
            'cost' => $this->omit_zero($this->safe_string($order, 'Z')),
            'trades' => null,
            'fee' => array(
                'currency' => $this->safe_currency_code($this->safe_string($order, 'N')),
                'amount' => $this->omit_zero($this->safe_string($order, 'n')),
            ),
            'reduceOnly' => $reduceOnly,
            'postOnly' => $postOnly,
            'info' => $order,
        ), $market);
    }

    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://hashkeyglobal-apidoc.readme.io/reference/websocket-api#private-stream
             *
             * @param {string} $symbol unified market $symbol of the market $trades were made in
             * @param {int} [$since] the earliest time in ms to fetch $trades for
             * @param {int} [$limit] the maximum number of trade structures to retrieve
             * @param {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());
            $messageHash = 'myTrades';
            if ($symbol !== null) {
                $symbol = $this->symbol($symbol);
                $messageHash .= ':' . $symbol;
            }
            $trades = Async\await($this->watch_private($messageHash));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function handle_my_trade(Client $client, $message, $subscription = array ()) {
        //
        //     {
        //         "e" => "ticketInfo",
        //         "E" => "1723037391156",
        //         "s" => "ETHUSDT-PERPETUAL",
        //         "q" => "1.00",
        //         "t" => "1723037391147",
        //         "p" => "2463.36",
        //         "T" => "1747358716187197441",
        //         "o" => "1747358716129257216",
        //         "c" => "1723037389677",
        //         "a" => "1735619524953226496",
        //         "m" => false,
        //         "S" => "BUY"
        //     }
        //
        if ($this->myTrades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $this->myTrades = new ArrayCacheBySymbolById ($limit);
        }
        $tradesArray = $this->myTrades;
        $parsed = $this->parse_ws_trade($message);
        $tradesArray->append ($parsed);
        $this->myTrades = $tradesArray;
        $messageHash = 'myTrades';
        $client->resolve ($tradesArray, $messageHash);
        $symbol = $parsed['symbol'];
        $symbolSpecificMessageHash = $messageHash . ':' . $symbol;
        $client->resolve ($tradesArray, $symbolSpecificMessageHash);
    }

    public function parse_ws_trade($trade, $market = null): array {
        //
        // watchTrades
        //     {
        //         "v" => "1745922896272048129",
        //         "t" => 1722866228075,
        //         "p" => "2340.41",
        //         "q" => "0.0132",
        //         "m" => true
        //     }
        //
        // watchMyTrades
        //     {
        //         "e" => "ticketInfo",
        //         "E" => "1723037391156",
        //         "s" => "ETHUSDT-PERPETUAL",
        //         "q" => "1.00",
        //         "t" => "1723037391147",
        //         "p" => "2463.36",
        //         "T" => "1747358716187197441",
        //         "o" => "1747358716129257216",
        //         "c" => "1723037389677",
        //         "a" => "1735619524953226496",
        //         "m" => false,
        //         "S" => "BUY"
        //     }
        //
        $marketId = $this->safe_string($trade, 's');
        $market = $this->safe_market($marketId, $market);
        $timestamp = $this->safe_integer($trade, 't');
        $isMaker = $this->safe_bool($trade, 'm');
        $takerOrMaker = null;
        if ($isMaker !== null) {
            if ($isMaker) {
                $takerOrMaker = 'maker';
            } else {
                $takerOrMaker = 'taker';
            }
        }
        return $this->safe_trade(array(
            'id' => $this->safe_string_2($trade, 'v', 'T'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $market['symbol'],
            'side' => $this->safe_string_lower($trade, 'S'),
            'price' => $this->safe_string($trade, 'p'),
            'amount' => $this->safe_string($trade, 'q'),
            'cost' => null,
            'takerOrMaker' => $takerOrMaker,
            'type' => null,
            'order' => $this->safe_string($trade, 'o'),
            'fee' => null,
            'info' => $trade,
        ), $market);
    }

    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://hashkeyglobal-apidoc.readme.io/reference/websocket-api#private-stream
             *
             * watch all open $positions
             * @param {string[]} [$symbols] list of unified market $symbols to watch $positions for
             * @param {int} [$since] the earliest time in ms to fetch $positions for
             * @param {int} [$limit] the maximum number of $positions to retrieve
             * @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());
            $listenKey = Async\await($this->authenticate());
            $symbols = $this->market_symbols($symbols);
            $messageHash = 'positions';
            $messageHashes = array();
            if ($symbols === null) {
                $messageHashes[] = $messageHash;
            } else {
                for ($i = 0; $i < count($symbols); $i++) {
                    $symbol = $symbols[$i];
                    $messageHashes[] = $messageHash . ':' . $symbol;
                }
            }
            $url = $this->get_private_url($listenKey);
            $positions = Async\await($this->watch_multiple($url, $messageHashes, null, $messageHashes));
            if ($this->newUpdates) {
                return $positions;
            }
            return $this->filter_by_symbols_since_limit($this->positions, $symbols, $since, $limit, true);
        }) ();
    }

    public function handle_position(Client $client, $message) {
        //
        //     {
        //         "e" => "outboundContractPositionInfo",
        //         "E" => "1723084699801",
        //         "A" => "1735619524953226496",
        //         "s" => "ETHUSDT-PERPETUAL",
        //         "S" => "LONG",
        //         "p" => "2429.6",
        //         "P" => "2",
        //         "a" => "2",
        //         "f" => "10760.14",
        //         "m" => "1.0085",
        //         "r" => "-0.0029",
        //         "up" => "0.0478",
        //         "pr" => "0.0492",
        //         "pv" => "4.8592",
        //         "v" => "5.00",
        //         "mt" => "CROSS",
        //         "mm" => "0.0367"
        //     }
        //
        if ($this->positions === null) {
            $this->positions = new ArrayCacheBySymbolBySide ();
        }
        $positions = $this->positions;
        $parsed = $this->parse_ws_position($message);
        $positions->append ($parsed);
        $messageHash = 'positions';
        $client->resolve ($parsed, $messageHash);
        $symbol = $parsed['symbol'];
        $client->resolve ($parsed, $messageHash . ':' . $symbol);
    }

    public function parse_ws_position($position, ?array $market = null): array {
        $marketId = $this->safe_string($position, 's');
        $market = $this->safe_market($marketId);
        $timestamp = $this->safe_integer($position, 'E');
        return $this->safe_position(array(
            'symbol' => $market['symbol'],
            'id' => null,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'contracts' => $this->safe_number($position, 'P'),
            'contractSize' => null,
            'side' => $this->safe_string_lower($position, 'S'),
            'notional' => $this->safe_number($position, 'pv'),
            'leverage' => $this->safe_integer($position, 'v'),
            'unrealizedPnl' => $this->safe_number($position, 'up'),
            'realizedPnl' => $this->safe_number($position, 'r'),
            'collateral' => null,
            'entryPrice' => $this->safe_number($position, 'p'),
            'markPrice' => null,
            'liquidationPrice' => $this->safe_number($position, 'f'),
            'marginMode' => $this->safe_string_lower($position, 'mt'),
            'hedged' => true,
            'maintenanceMargin' => $this->safe_number($position, 'mm'),
            'maintenanceMarginPercentage' => null,
            'initialMargin' => $this->safe_number($position, 'm'), // todo check
            'initialMarginPercentage' => null,
            'marginRatio' => null,
            'lastUpdateTimestamp' => null,
            'lastPrice' => null,
            'stopLossPrice' => null,
            'takeProfitPrice' => null,
            'percentage' => null,
            'info' => $position,
        ));
    }

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * watch balance and get the amount of funds available for trading or funds locked in orders
             *
             * @see https://hashkeyglobal-apidoc.readme.io/reference/websocket-api#private-stream
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->type] 'spot' or 'swap' - the $type of the market to watch balance for (default 'spot')
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            $listenKey = Async\await($this->authenticate());
            Async\await($this->load_markets());
            $type = 'spot';
            list($type, $params) = $this->handle_market_type_and_params('watchBalance', null, $params, $type);
            $messageHash = 'balance:' . $type;
            $url = $this->get_private_url($listenKey);
            $client = $this->client($url);
            $this->set_balance_cache($client, $type, $messageHash);
            $fetchBalanceSnapshot = null;
            $awaitBalanceSnapshot = null;
            list($fetchBalanceSnapshot, $params) = $this->handle_option_and_params($this->options, 'watchBalance', 'fetchBalanceSnapshot', true);
            list($awaitBalanceSnapshot, $params) = $this->handle_option_and_params($this->options, 'watchBalance', 'awaitBalanceSnapshot', false);
            if ($fetchBalanceSnapshot && $awaitBalanceSnapshot) {
                Async\await($client->future ($type . ':fetchBalanceSnapshot'));
            }
            return Async\await($this->watch($url, $messageHash, null, $messageHash));
        }) ();
    }

    public function set_balance_cache(Client $client, $type, $subscribeHash) {
        if (is_array($client->subscriptions) && array_key_exists($subscribeHash, $client->subscriptions)) {
            return;
        }
        $options = $this->safe_dict($this->options, 'watchBalance');
        $snapshot = $this->safe_bool($options, 'fetchBalanceSnapshot', true);
        if ($snapshot) {
            $messageHash = $type . ':' . 'fetchBalanceSnapshot';
            if (!(is_array($client->futures) && array_key_exists($messageHash, $client->futures))) {
                $client->future ($messageHash);
                $this->spawn(array($this, 'load_balance_snapshot'), $client, $messageHash, $type);
            }
        }
        $this->balance[$type] = array();
        // without this comment, transpilation breaks for some reason...
    }

    public function load_balance_snapshot($client, $messageHash, $type) {
        return Async\async(function () use ($client, $messageHash, $type) {
            $response = Async\await($this->fetch_balance(array( 'type' => $type )));
            $this->balance[$type] = $this->extend($response, $this->safe_value($this->balance, $type, array()));
            // don't remove the $future from the .futures cache
            $future = $client->futures[$messageHash];
            $future->resolve ();
            $client->resolve ($this->balance[$type], 'balance:' . $type);
        }) ();
    }

    public function handle_balance(Client $client, $message) {
        //
        //     {
        //         "e" => "outboundContractAccountInfo",        // $event $type
        //                                                    // outboundContractAccountInfo
        //         "E" => "1714717314118",                      // $event time
        //         "T" => true,                                 // can trade
        //         "W" => true,                                 // can withdraw
        //         "D" => true,                                 // can deposit
        //         "B" => array(                                     // balances changed
        //             {
        //                 "a" => "USDT",                       // asset
        //                 "f" => "474960.65",                  // free amount
        //                 "l" => "24835.178056020383226869",   // locked amount
        //                 "r" => ""                            // to be released
        //             }
        //         )
        //     }
        //
        $event = $this->safe_string($message, 'e');
        $data = $this->safe_list($message, 'B', array());
        $balanceUpdate = $this->safe_dict($data, 0);
        $isSpot = $event === 'outboundAccountInfo';
        $type = $isSpot ? 'spot' : 'swap';
        if (!(is_array($this->balance) && array_key_exists($type, $this->balance))) {
            $this->balance[$type] = array();
        }
        $this->balance[$type]['info'] = $message;
        $currencyId = $this->safe_string($balanceUpdate, 'a');
        $code = $this->safe_currency_code($currencyId);
        $account = $this->account();
        $account['free'] = $this->safe_string($balanceUpdate, 'f');
        $account['used'] = $this->safe_string($balanceUpdate, 'l');
        $this->balance[$type][$code] = $account;
        $this->balance[$type] = $this->safe_balance($this->balance[$type]);
        $messageHash = 'balance:' . $type;
        $client->resolve ($this->balance[$type], $messageHash);
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $listenKey = $this->safe_string($this->options, 'listenKey');
            if ($listenKey !== null) {
                return $listenKey;
            }
            $response = Async\await($this->privatePostApiV1UserDataStream ($params));
            //
            //    {
            //        "listenKey" => "atbNEcWnBqnmgkfmYQeTuxKTpTStlZzgoPLJsZhzAOZTbAlxbHqGNWiYaUQzMtDz"
            //    }
            //
            $listenKey = $this->safe_string($response, 'listenKey');
            $this->options['listenKey'] = $listenKey;
            $listenKeyRefreshRate = $this->safe_integer($this->options, 'listenKeyRefreshRate', 3600000);
            $this->delay($listenKeyRefreshRate, array($this, 'keep_alive_listen_key'), $listenKey, $params);
            return $listenKey;
        }) ();
    }

    public function keep_alive_listen_key($listenKey, $params = array ()) {
        return Async\async(function () use ($listenKey, $params) {
            if ($listenKey === null) {
                return;
            }
            $request = array(
                'listenKey' => $listenKey,
            );
            try {
                Async\await($this->privatePutApiV1UserDataStream ($this->extend($request, $params)));
                $listenKeyRefreshRate = $this->safe_integer($this->options, 'listenKeyRefreshRate', 1200000);
                $this->delay($listenKeyRefreshRate, array($this, 'keep_alive_listen_key'), $listenKey, $params);
            } catch (Exception $error) {
                $url = $this->get_private_url($listenKey);
                $client = $this->client($url);
                $this->options['listenKey'] = null;
                $client->reject ($error);
                unset($this->clients[$url]);
            }
        }) ();
    }

    public function handle_message(Client $client, $message) {
        if (gettype($message) === 'array' && array_keys($message) === array_keys(array_keys($message))) {
            $message = $this->safe_dict($message, 0, array());
        }
        $topic = $this->safe_string_2($message, 'topic', 'e');
        if ($topic === 'kline') {
            $this->handle_ohlcv($client, $message);
        } elseif ($topic === 'realtimes') {
            $this->handle_ticker($client, $message);
        } elseif ($topic === 'trade') {
            $this->handle_trades($client, $message);
        } elseif ($topic === 'depth') {
            $this->handle_order_book($client, $message);
        } elseif (($topic === 'contractExecutionReport') || ($topic === 'executionReport')) {
            $this->handle_order($client, $message);
        } elseif ($topic === 'ticketInfo') {
            $this->handle_my_trade($client, $message);
        } elseif ($topic === 'outboundContractPositionInfo') {
            $this->handle_position($client, $message);
        } elseif (($topic === 'outboundAccountInfo') || ($topic === 'outboundContractAccountInfo')) {
            $this->handle_balance($client, $message);
        }
    }
}
