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

class woofipro extends \ccxt\async\woofipro {

    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,
                'watchTickers' => true,
                'watchBidsAsks' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => false,
                'watchPositions' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => array(
                        'public' => 'wss://ws-evm.orderly.org/ws/stream',
                        'private' => 'wss://ws-private-evm.orderly.org/v2/ws/private/stream',
                    ),
                ),
                'test' => array(
                    'ws' => array(
                        'public' => 'wss://testnet-ws-evm.orderly.org/ws/stream',
                        'private' => 'wss://testnet-ws-private-evm.orderly.org/v2/ws/private/stream',
                    ),
                ),
            ),
            'requiredCredentials' => array(
                'apiKey' => true,
                'secret' => true,
                'accountId' => true,
            ),
            'options' => array(
                'tradesLimit' => 1000,
                'ordersLimit' => 1000,
                'requestId' => array(),
                'watchPositions' => array(
                    'fetchPositionsSnapshot' => true, // or false
                    'awaitPositionsSnapshot' => true, // whether to wait for the positions snapshot before providing updates
                ),
            ),
            'streaming' => array(
                'ping' => array($this, 'ping'),
                'keepAlive' => 10000,
            ),
            'exceptions' => array(
                'ws' => array(
                    'exact' => array(
                        'Auth is needed.' => '\\ccxt\\AuthenticationError',
                    ),
                ),
            ),
        ));
    }

    public function request_id($url) {
        $options = $this->safe_dict($this->options, 'requestId', array());
        $previousValue = $this->safe_integer($options, $url, 0);
        $newValue = $this->sum($previousValue, 1);
        $this->options['requestId'][$url] = $newValue;
        return $newValue;
    }

    public function watch_public($messageHash, $message) {
        return Async\async(function () use ($messageHash, $message) {
            // the default $id
            $id = 'OqdphuyCtYWxwzhxyLLjOWNdFP7sQt8RPWzmb5xY';
            if ($this->accountId !== null && $this->accountId !== '') {
                $id = $this->accountId;
            }
            $url = $this->urls['api']['ws']['public'] . '/' . $id;
            $requestId = $this->request_id($url);
            $subscribe = array(
                'id' => $requestId,
            );
            $request = $this->extend($subscribe, $message);
            return Async\await($this->watch($url, $messageHash, $request, $messageHash, $subscribe));
        }) ();
    }

    public function watch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             *
             * @see https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/orderbook
             *
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             * @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());
            $name = 'orderbook';
            $market = $this->market($symbol);
            $topic = $market['id'] . '@' . $name;
            $request = array(
                'event' => 'subscribe',
                'topic' => $topic,
            );
            $message = $this->extend($request, $params);
            $orderbook = Async\await($this->watch_public($topic, $message));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_order_book(Client $client, $message) {
        //
        //     {
        //         "topic" => "PERP_BTC_USDC@$orderbook",
        //         "ts" => 1650121915308,
        //         "data" => {
        //             "symbol" => "PERP_BTC_USDC",
        //             "bids" => array(
        //                 array(
        //                     0.30891,
        //                     2469.98
        //                 )
        //             ),
        //             "asks" => array(
        //                 array(
        //                     0.31075,
        //                     2379.63
        //                 )
        //             )
        //         }
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $marketId = $this->safe_string($data, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $topic = $this->safe_string($message, 'topic');
        if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
            $this->orderbooks[$symbol] = $this->order_book();
        }
        $orderbook = $this->orderbooks[$symbol];
        $timestamp = $this->safe_integer($message, 'ts');
        $snapshot = $this->parse_order_book($data, $symbol, $timestamp, 'bids', 'asks');
        $orderbook->reset ($snapshot);
        $client->resolve ($orderbook, $topic);
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             *
             * @see https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/24-hour-ticker
             *
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
             * @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());
            $name = 'ticker';
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $topic = $market['id'] . '@' . $name;
            $request = array(
                'event' => 'subscribe',
                'topic' => $topic,
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch_public($topic, $message));
        }) ();
    }

    public function parse_ws_ticker($ticker, $market = null) {
        //
        //     {
        //         "symbol" => "PERP_BTC_USDC",
        //         "open" => 19441.5,
        //         "close" => 20147.07,
        //         "high" => 20761.87,
        //         "low" => 19320.54,
        //         "volume" => 2481.103,
        //         "amount" => 50037935.0286,
        //         "count" => 3689
        //     }
        //
        return $this->safe_ticker(array(
            'symbol' => $this->safe_symbol(null, $market),
            'timestamp' => null,
            'datetime' => null,
            'high' => $this->safe_string($ticker, 'high'),
            'low' => $this->safe_string($ticker, 'low'),
            'bid' => null,
            'bidVolume' => null,
            'ask' => null,
            'askVolume' => null,
            'vwap' => null,
            'open' => $this->safe_string($ticker, 'open'),
            'close' => $this->safe_string($ticker, 'close'),
            'last' => null,
            'previousClose' => null,
            'change' => null,
            'percentage' => null,
            'average' => null,
            'baseVolume' => $this->safe_string($ticker, 'volume'),
            'quoteVolume' => $this->safe_string($ticker, 'amount'),
            'info' => $ticker,
        ), $market);
    }

    public function handle_ticker(Client $client, $message) {
        //
        //     {
        //         "topic" => "PERP_BTC_USDC@$ticker",
        //         "ts" => 1657120017000,
        //         "data" => {
        //             "symbol" => "PERP_BTC_USDC",
        //             "open" => 19441.5,
        //             "close" => 20147.07,
        //             "high" => 20761.87,
        //             "low" => 19320.54,
        //             "volume" => 2481.103,
        //             "amount" => 50037935.0286,
        //             "count" => 3689
        //         }
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $topic = $this->safe_string($message, 'topic');
        $marketId = $this->safe_string($data, 'symbol');
        $market = $this->safe_market($marketId);
        $timestamp = $this->safe_integer($message, 'ts');
        $data['date'] = $timestamp;
        $ticker = $this->parse_ws_ticker($data, $market);
        $ticker['symbol'] = $market['symbol'];
        $this->tickers[$market['symbol']] = $ticker;
        $client->resolve ($ticker, $topic);
        return $message;
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/24-hour-$tickers
             *
             * watches a price ticker, a statistical calculation with the information calculated over the past 24 hours for all markets of a specific list
             * @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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $name = 'tickers';
            $topic = $name;
            $request = array(
                'event' => 'subscribe',
                'topic' => $topic,
            );
            $message = $this->extend($request, $params);
            $tickers = Async\await($this->watch_public($topic, $message));
            return $this->filter_by_array($tickers, 'symbol', $symbols);
        }) ();
    }

    public function handle_tickers(Client $client, $message) {
        //
        //     {
        //         "topic":"tickers",
        //         "ts":1618820615000,
        //         "data":array(
        //             array(
        //                 "symbol":"PERP_NEAR_USDC",
        //                 "open":16.297,
        //                 "close":17.183,
        //                 "high":24.707,
        //                 "low":11.997,
        //                 "volume":0,
        //                 "amount":0,
        //                 "count":0
        //             ),
        //         ...
        //         )
        //     }
        //
        $topic = $this->safe_string($message, 'topic');
        $data = $this->safe_list($message, 'data', array());
        $timestamp = $this->safe_integer($message, 'ts');
        $result = array();
        for ($i = 0; $i < count($data); $i++) {
            $marketId = $this->safe_string($data[$i], 'symbol');
            $market = $this->safe_market($marketId);
            $ticker = $this->parse_ws_ticker($this->extend($data[$i], array( 'date' => $timestamp )), $market);
            $this->tickers[$market['symbol']] = $ticker;
            $result[] = $ticker;
        }
        $client->resolve ($result, $topic);
    }

    public function watch_bids_asks(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/bbos
             *
             * 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~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $name = 'bbos';
            $topic = $name;
            $request = array(
                'event' => 'subscribe',
                'topic' => $topic,
            );
            $message = $this->extend($request, $params);
            $tickers = Async\await($this->watch_public($topic, $message));
            return $this->filter_by_array($tickers, 'symbol', $symbols);
        }) ();
    }

    public function handle_bid_ask(Client $client, $message) {
        //
        //     {
        //       "topic" => "bbos",
        //       "ts" => 1726212495000,
        //       "data" => array(
        //         {
        //           "symbol" => "PERP_WOO_USDC",
        //           "ask" => 0.16570,
        //           "askSize" => 4224,
        //           "bid" => 0.16553,
        //           "bidSize" => 6645
        //         }
        //       )
        //     }
        //
        $topic = $this->safe_string($message, 'topic');
        $data = $this->safe_list($message, 'data', array());
        $timestamp = $this->safe_integer($message, 'ts');
        $result = array();
        for ($i = 0; $i < count($data); $i++) {
            $ticker = $this->parse_ws_bid_ask($this->extend($data[$i], array( 'ts' => $timestamp )));
            $this->tickers[$ticker['symbol']] = $ticker;
            $result[] = $ticker;
        }
        $client->resolve ($result, $topic);
    }

    public function parse_ws_bid_ask($ticker, $market = null) {
        $marketId = $this->safe_string($ticker, 'symbol');
        $market = $this->safe_market($marketId, $market);
        $symbol = $this->safe_string($market, 'symbol');
        $timestamp = $this->safe_integer($ticker, 'ts');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'ask' => $this->safe_string($ticker, 'ask'),
            'askVolume' => $this->safe_string($ticker, 'askSize'),
            'bid' => $this->safe_string($ticker, 'bid'),
            'bidVolume' => $this->safe_string($ticker, 'bidSize'),
            'info' => $ticker,
        ), $market);
    }

    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://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/k-line
             *
             * @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
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            if (($timeframe !== '1m') && ($timeframe !== '5m') && ($timeframe !== '15m') && ($timeframe !== '30m') && ($timeframe !== '1h') && ($timeframe !== '1d') && ($timeframe !== '1w') && ($timeframe !== '1M')) {
                throw new NotSupported($this->id . ' watchOHLCV $timeframe argument must be 1m, 5m, 15m, 30m, 1h, 1d, 1w, 1M');
            }
            $market = $this->market($symbol);
            $interval = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $name = 'kline';
            $topic = $market['id'] . '@' . $name . '_' . $interval;
            $request = array(
                'event' => 'subscribe',
                'topic' => $topic,
            );
            $message = $this->extend($request, $params);
            $ohlcv = Async\await($this->watch_public($topic, $message));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($market['symbol'], $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         "topic":"PERP_BTC_USDC@kline_1m",
        //         "ts":1618822432146,
        //         "data":{
        //             "symbol":"PERP_BTC_USDC",
        //             "type":"1m",
        //             "open":56948.97,
        //             "close":56891.76,
        //             "high":56948.97,
        //             "low":56889.06,
        //             "volume":44.00947568,
        //             "amount":2504584.9,
        //             "startTime":1618822380000,
        //             "endTime":1618822440000
        //         }
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $topic = $this->safe_string($message, 'topic');
        $marketId = $this->safe_string($data, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $interval = $this->safe_string($data, 'type');
        $timeframe = $this->find_timeframe($interval);
        $parsed = array(
            $this->safe_integer($data, 'startTime'),
            $this->safe_number($data, 'open'),
            $this->safe_number($data, 'high'),
            $this->safe_number($data, 'low'),
            $this->safe_number($data, 'close'),
            $this->safe_number($data, 'volume'),
        );
        $this->ohlcvs[$symbol] = $this->safe_value($this->ohlcvs, $symbol, array());
        $stored = $this->safe_value($this->ohlcvs[$symbol], $timeframe);
        if ($stored === null) {
            $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
            $stored = new ArrayCacheByTimestamp ($limit);
            $this->ohlcvs[$symbol][$timeframe] = $stored;
        }
        $ohlcvCache = $this->ohlcvs[$symbol][$timeframe];
        $ohlcvCache->append ($parsed);
        $client->resolve ($ohlcvCache, $topic);
    }

    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://orderly.network/docs/build-on-evm/evm-api/websocket-api/public/trade
             *
             * @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());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $topic = $market['id'] . '@trade';
            $request = array(
                'event' => 'subscribe',
                'topic' => $topic,
            );
            $message = $this->extend($request, $params);
            $trades = Async\await($this->watch_public($topic, $message));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($market['symbol'], $limit);
            }
            return $this->filter_by_symbol_since_limit($trades, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_trade(Client $client, $message) {
        //
        // {
        //     "topic":"PERP_ADA_USDC@$trade",
        //     "ts":1618820361552,
        //     "data":{
        //         "symbol":"PERP_ADA_USDC",
        //         "price":1.27988,
        //         "size":300,
        //         "side":"BUY",
        //     }
        // }
        //
        $topic = $this->safe_string($message, 'topic');
        $timestamp = $this->safe_integer($message, 'ts');
        $data = $this->safe_dict($message, 'data', array());
        $marketId = $this->safe_string($data, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $trade = $this->parse_ws_trade($this->extend($data, array( 'timestamp' => $timestamp )), $market);
        if (!(is_array($this->trades) && array_key_exists($symbol, $this->trades))) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $stored = new ArrayCache ($limit);
            $this->trades[$symbol] = $stored;
        }
        $trades = $this->trades[$symbol];
        $trades->append ($trade);
        $this->trades[$symbol] = $trades;
        $client->resolve ($trades, $topic);
    }

    public function parse_ws_trade($trade, $market = null) {
        //
        //     {
        //         "symbol":"PERP_ADA_USDC",
        //         "timestamp":1618820361552,
        //         "price":1.27988,
        //         "size":300,
        //         "side":"BUY",
        //     }
        // private stream
        //     {
        //         $symbol => 'PERP_XRP_USDC',
        //         clientOrderId => '',
        //         orderId => 1167632251,
        //         type => 'MARKET',
        //         $side => 'BUY',
        //         quantity => 20,
        //         $price => 0,
        //         tradeId => '1715179456664012',
        //         executedPrice => 0.5276,
        //         executedQuantity => 20,
        //         $fee => 0.006332,
        //         feeAsset => 'USDC',
        //         totalExecutedQuantity => 20,
        //         avgPrice => 0.5276,
        //         averageExecutedPrice => 0.5276,
        //         status => 'FILLED',
        //         reason => '',
        //         totalFee => 0.006332,
        //         visible => 0,
        //         visibleQuantity => 0,
        //         $timestamp => 1715179456660,
        //         orderTag => 'CCXT',
        //         createdTime => 1715179456656,
        //         $maker => false
        //     }
        //
        $marketId = $this->safe_string($trade, 'symbol');
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $price = $this->safe_string_2($trade, 'executedPrice', 'price');
        $amount = $this->safe_string_2($trade, 'executedQuantity', 'size');
        $cost = Precise::string_mul($price, $amount);
        $side = $this->safe_string_lower($trade, 'side');
        $timestamp = $this->safe_integer($trade, 'timestamp');
        $takerOrMaker = null;
        $maker = $this->safe_bool($trade, 'maker');
        if ($maker !== null) {
            $takerOrMaker = $maker ? 'maker' : 'taker';
        }
        $fee = null;
        $feeValue = $this->safe_string($trade, 'fee');
        if ($feeValue !== null) {
            $fee = array(
                'cost' => $feeValue,
                'currency' => $this->safe_currency_code($this->safe_string($trade, 'feeAsset')),
            );
        }
        return $this->safe_trade(array(
            'id' => $this->safe_string($trade, 'tradeId'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'side' => $side,
            'price' => $price,
            'amount' => $amount,
            'cost' => $cost,
            'order' => $this->safe_string($trade, 'orderId'),
            'takerOrMaker' => $takerOrMaker,
            'type' => $this->safe_string_lower($trade, 'type'),
            'fee' => $fee,
            'info' => $trade,
        ), $market);
    }

    public function handle_auth(Client $client, $message) {
        //
        //     {
        //         "event" => "auth",
        //         "success" => true,
        //         "ts" => 1657463158812
        //     }
        //
        $messageHash = 'authenticated';
        $success = $this->safe_value($message, 'success');
        if ($success) {
            // $client->resolve ($message, $messageHash);
            $future = $this->safe_value($client->futures, 'authenticated');
            $future->resolve (true);
        } else {
            $error = new AuthenticationError ($this->json($message));
            $client->reject ($error, $messageHash);
            // allows further authentication attempts
            if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                unset($client->subscriptions['authenticated']);
            }
        }
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $this->check_required_credentials();
            $url = $this->urls['api']['ws']['private'] . '/' . $this->accountId;
            $client = $this->client($url);
            $messageHash = 'authenticated';
            $event = 'auth';
            $future = $client->reusableFuture ($messageHash);
            $authenticated = $this->safe_value($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                $ts = (string) $this->nonce();
                $auth = $ts;
                $secret = $this->secret;
                if (mb_strpos($secret, 'ed25519:') !== false) {
                    $parts = explode('ed25519:', $secret);
                    $secret = $parts[1];
                }
                $signature = $this->eddsa($this->encode($auth), $this->base58_to_binary($secret), 'ed25519');
                $request = array(
                    'event' => $event,
                    'params' => array(
                        'orderly_key' => $this->apiKey,
                        'sign' => $signature,
                        'timestamp' => $ts,
                    ),
                );
                $message = $this->extend($request, $params);
                $this->watch($url, $messageHash, $message, $messageHash);
            }
            return Async\await($future);
        }) ();
    }

    public function watch_private($messageHash, $message, $params = array ()) {
        return Async\async(function () use ($messageHash, $message, $params) {
            Async\await($this->authenticate($params));
            $url = $this->urls['api']['ws']['private'] . '/' . $this->accountId;
            $requestId = $this->request_id($url);
            $subscribe = array(
                'id' => $requestId,
            );
            $request = $this->extend($subscribe, $message);
            return Async\await($this->watch($url, $messageHash, $request, $messageHash, $subscribe));
        }) ();
    }

    public function watch_private_multiple($messageHashes, $message, $params = array ()) {
        return Async\async(function () use ($messageHashes, $message, $params) {
            Async\await($this->authenticate($params));
            $url = $this->urls['api']['ws']['private'] . '/' . $this->accountId;
            $requestId = $this->request_id($url);
            $subscribe = array(
                'id' => $requestId,
            );
            $request = $this->extend($subscribe, $message);
            return Async\await($this->watch_multiple($url, $messageHashes, $request, $messageHashes, $subscribe));
        }) ();
    }

    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://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/execution-report
             * @see https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/algo-execution-report
             *
             * @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
             * @param {bool} [$params->trigger] true if $trigger order
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $trigger = $this->safe_bool_2($params, 'stop', 'trigger', false);
            $topic = ($trigger) ? 'algoexecutionreport' : 'executionreport';
            $params = $this->omit($params, array( 'stop', 'trigger' ));
            $messageHash = $topic;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
                $messageHash .= ':' . $symbol;
            }
            $request = array(
                'event' => 'subscribe',
                'topic' => $topic,
            );
            $message = $this->extend($request, $params);
            $orders = Async\await($this->watch_private($messageHash, $message));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, 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://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/execution-report
             * @see https://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/algo-execution-report
             *
             * @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
             * @param {bool} [$params->trigger] true if $trigger order
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $trigger = $this->safe_bool_2($params, 'stop', 'trigger', false);
            $topic = ($trigger) ? 'algoexecutionreport' : 'executionreport';
            $params = $this->omit($params, 'stop');
            $messageHash = 'myTrades';
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
                $messageHash .= ':' . $symbol;
            }
            $request = array(
                'event' => 'subscribe',
                'topic' => $topic,
            );
            $message = $this->extend($request, $params);
            $orders = Async\await($this->watch_private($messageHash, $message));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function parse_ws_order($order, $market = null) {
        //
        //     {
        //         "symbol" => "PERP_BTC_USDT",
        //         "clientOrderId" => 0,
        //         "orderId" => 52952826,
        //         "type" => "LIMIT",
        //         "side" => "SELL",
        //         "quantity" => 0.01,
        //         "price" => 22000,
        //         "tradeId" => 0,
        //         "executedPrice" => 0,
        //         "executedQuantity" => 0,
        //         "fee" => 0,
        //         "feeAsset" => "USDT",
        //         "totalExecutedQuantity" => 0,
        //         "status" => "NEW",
        //         "reason" => '',
        //         "orderTag" => "default",
        //         "totalFee" => 0,
        //         "visible" => 0.01,
        //         "timestamp" => 1657515556799,
        //         "reduceOnly" => false,
        //         "maker" => false
        //     }
        // algo $order
        //     {
        //         "symbol":"PERP_MATIC_USDC",
        //         "rootAlgoOrderId":123,
        //         "parentAlgoOrderId":123,
        //         "algoOrderId":123,
        //         "orderTag":"some tags",
        //         "algoType" => "STOP",
        //         "clientOrderId":"client_id",
        //         "type":"LIMIT",
        //         "side":"BUY",
        //         "quantity":7029.0,
        //         "price":0.7699,
        //         "tradeId":0,
        //         "triggerTradePrice":0,
        //         "triggerTime":1234567,
        //         "triggered" => false,
        //         "activated" => false,
        //         "executedPrice":0.0,
        //         "executedQuantity":0.0,
        //         "fee":0.0,
        //         "feeAsset":"USDC",
        //         "totalExecutedQuantity":0.0,
        //         "averageExecutedQuantity":0.0,
        //         "avgPrice":0,
        //         "triggerPrice":0.0,
        //         "triggerPriceType":"STOP",
        //         "isActivated" => false,
        //         "status":"NEW",
        //         "rootAlgoStatus" => "FILLED",
        //         "algoStatus" => "FILLED",
        //         "reason":"",
        //         "totalFee":0.0,
        //         "visible" => 7029.0,
        //         "visibleQuantity":7029.0,
        //         "timestamp":1704679472448,
        //         "maker":false,
        //         "isMaker":false,
        //         "createdTime":1704679472448
        //     }
        //
        $orderId = $this->safe_string($order, 'orderId');
        $marketId = $this->safe_string($order, 'symbol');
        $market = $this->market($marketId);
        $symbol = $market['symbol'];
        $timestamp = $this->safe_integer($order, 'timestamp');
        $fee = array(
            'cost' => $this->safe_string($order, 'totalFee'),
            'currency' => $this->safe_string($order, 'feeAsset'),
        );
        $priceString = $this->safe_string($order, 'price');
        $price = $this->safe_number($order, 'price');
        $avgPrice = $this->safe_number($order, 'avgPrice');
        if (Precise::string_eq($priceString, '0') && ($avgPrice !== null)) {
            $price = $avgPrice;
        }
        $amount = $this->safe_string($order, 'quantity');
        $side = $this->safe_string_lower($order, 'side');
        $type = $this->safe_string_lower($order, 'type');
        $filled = $this->safe_number($order, 'totalExecutedQuantity');
        $totalExecQuantity = $this->safe_string($order, 'totalExecutedQuantity');
        $remaining = $amount;
        if (Precise::string_ge($amount, $totalExecQuantity)) {
            $remaining = Precise::string_sub($remaining, $totalExecQuantity);
        }
        $rawStatus = $this->safe_string($order, 'status');
        $status = $this->parse_order_status($rawStatus);
        $trades = null;
        $clientOrderId = $this->safe_string($order, 'clientOrderId');
        $triggerPrice = $this->safe_number($order, 'triggerPrice');
        return $this->safe_order(array(
            'info' => $order,
            'symbol' => $symbol,
            'id' => $orderId,
            'clientOrderId' => $clientOrderId,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => $timestamp,
            'type' => $type,
            'timeInForce' => null,
            'postOnly' => null,
            'side' => $side,
            'price' => $price,
            'stopPrice' => $triggerPrice,
            'triggerPrice' => $triggerPrice,
            'amount' => $amount,
            'cost' => null,
            'average' => null,
            'filled' => $filled,
            'remaining' => $remaining,
            'status' => $status,
            'fee' => $fee,
            'trades' => $trades,
        ));
    }

    public function handle_order_update(Client $client, $message) {
        //
        //     {
        //         "topic" => "executionreport",
        //         "ts" => 1657515556799,
        //         "data" => {
        //             "symbol" => "PERP_BTC_USDT",
        //             "clientOrderId" => 0,
        //             "orderId" => 52952826,
        //             "type" => "LIMIT",
        //             "side" => "SELL",
        //             "quantity" => 0.01,
        //             "price" => 22000,
        //             "tradeId" => 0,
        //             "executedPrice" => 0,
        //             "executedQuantity" => 0,
        //             "fee" => 0,
        //             "feeAsset" => "USDT",
        //             "totalExecutedQuantity" => 0,
        //             "status" => "NEW",
        //             "reason" => '',
        //             "orderTag" => "default",
        //             "totalFee" => 0,
        //             "visible" => 0.01,
        //             "timestamp" => 1657515556799,
        //             "maker" => false
        //         }
        //     }
        //
        $topic = $this->safe_string($message, 'topic');
        $data = $this->safe_value($message, 'data');
        if (gettype($data) === 'array' && array_keys($data) === array_keys(array_keys($data))) {
            // algoexecutionreport
            for ($i = 0; $i < count($data); $i++) {
                $order = $data[$i];
                $tradeId = $this->omit_zero($this->safe_string($data, 'tradeId'));
                if ($tradeId !== null) {
                    $this->handle_my_trade($client, $order);
                }
                $this->handle_order($client, $order, $topic);
            }
        } else {
            // executionreport
            $tradeId = $this->omit_zero($this->safe_string($data, 'tradeId'));
            if ($tradeId !== null) {
                $this->handle_my_trade($client, $data);
            }
            $this->handle_order($client, $data, $topic);
        }
    }

    public function handle_order(Client $client, $message, $topic) {
        $parsed = $this->parse_ws_order($message);
        $symbol = $this->safe_string($parsed, 'symbol');
        $orderId = $this->safe_string($parsed, 'id');
        if ($symbol !== null) {
            if ($this->orders === null) {
                $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
                $this->orders = new ArrayCacheBySymbolById ($limit);
            }
            $cachedOrders = $this->orders;
            $orders = $this->safe_dict($cachedOrders->hashmap, $symbol, array());
            $order = $this->safe_dict($orders, $orderId);
            if ($order !== null) {
                $fee = $this->safe_value($order, 'fee');
                if ($fee !== null) {
                    $parsed['fee'] = $fee;
                }
                $fees = $this->safe_list($order, 'fees');
                if ($fees !== null) {
                    $parsed['fees'] = $fees;
                }
                $parsed['trades'] = $this->safe_list($order, 'trades');
                $parsed['timestamp'] = $this->safe_integer($order, 'timestamp');
                $parsed['datetime'] = $this->safe_string($order, 'datetime');
            }
            $cachedOrders->append ($parsed);
            $client->resolve ($this->orders, $topic);
            $messageHashSymbol = $topic . ':' . $symbol;
            $client->resolve ($this->orders, $messageHashSymbol);
        }
    }

    public function handle_my_trade(Client $client, $message) {
        //
        // {
        //     $symbol => 'PERP_XRP_USDC',
        //     clientOrderId => '',
        //     orderId => 1167632251,
        //     type => 'MARKET',
        //     side => 'BUY',
        //     quantity => 20,
        //     price => 0,
        //     tradeId => '1715179456664012',
        //     executedPrice => 0.5276,
        //     executedQuantity => 20,
        //     fee => 0.006332,
        //     feeAsset => 'USDC',
        //     totalExecutedQuantity => 20,
        //     avgPrice => 0.5276,
        //     averageExecutedPrice => 0.5276,
        //     status => 'FILLED',
        //     reason => '',
        //     totalFee => 0.006332,
        //     visible => 0,
        //     visibleQuantity => 0,
        //     timestamp => 1715179456660,
        //     orderTag => 'CCXT',
        //     createdTime => 1715179456656,
        //     maker => false
        // }
        //
        $messageHash = 'myTrades';
        $marketId = $this->safe_string($message, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $trade = $this->parse_ws_trade($message, $market);
        $trades = $this->myTrades;
        if ($trades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $trades = new ArrayCacheBySymbolById ($limit);
            $this->myTrades = $trades;
        }
        $trades->append ($trade);
        $client->resolve ($trades, $messageHash);
        $symbolSpecificMessageHash = $messageHash . ':' . $symbol;
        $client->resolve ($trades, $symbolSpecificMessageHash);
    }

    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://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/position-push
             *
             * watch all open positions
             * @param {string[]} [$symbols] list of unified market $symbols
             * @param $since timestamp in ms of the earliest position to fetch
             * @param $limit the maximum number of positions to fetch
             * @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());
            $messageHashes = array();
            $symbols = $this->market_symbols($symbols);
            if (!$this->is_empty($symbols)) {
                for ($i = 0; $i < count($symbols); $i++) {
                    $symbol = $symbols[$i];
                    $messageHashes[] = 'positions::' . $symbol;
                }
            } else {
                $messageHashes[] = 'positions';
            }
            $url = $this->urls['api']['ws']['private'] . '/' . $this->accountId;
            $client = $this->client($url);
            $this->set_positions_cache($client, $symbols);
            $fetchPositionsSnapshot = $this->handle_option('watchPositions', 'fetchPositionsSnapshot', true);
            $awaitPositionsSnapshot = $this->handle_option('watchPositions', 'awaitPositionsSnapshot', true);
            if ($fetchPositionsSnapshot && $awaitPositionsSnapshot && $this->positions === null) {
                $snapshot = Async\await($client->future ('fetchPositionsSnapshot'));
                return $this->filter_by_symbols_since_limit($snapshot, $symbols, $since, $limit, true);
            }
            $request = array(
                'event' => 'subscribe',
                'topic' => 'position',
            );
            $newPositions = Async\await($this->watch_private_multiple($messageHashes, $request, $params));
            if ($this->newUpdates) {
                return $newPositions;
            }
            return $this->filter_by_symbols_since_limit($this->positions, $symbols, $since, $limit, true);
        }) ();
    }

    public function set_positions_cache(Client $client, $type, ?array $symbols = null) {
        $fetchPositionsSnapshot = $this->handle_option('watchPositions', 'fetchPositionsSnapshot', false);
        if ($fetchPositionsSnapshot) {
            $messageHash = 'fetchPositionsSnapshot';
            if (!(is_array($client->futures) && array_key_exists($messageHash, $client->futures))) {
                $client->future ($messageHash);
                $this->spawn(array($this, 'load_positions_snapshot'), $client, $messageHash);
            }
        } else {
            $this->positions = new ArrayCacheBySymbolBySide ();
        }
    }

    public function load_positions_snapshot($client, $messageHash) {
        return Async\async(function () use ($client, $messageHash) {
            $positions = Async\await($this->fetch_positions());
            $this->positions = new ArrayCacheBySymbolBySide ();
            $cache = $this->positions;
            for ($i = 0; $i < count($positions); $i++) {
                $position = $positions[$i];
                $contracts = $this->safe_string($position, 'contracts', '0');
                if (Precise::string_gt($contracts, '0')) {
                    $cache->append ($position);
                }
            }
            // don't remove the $future from the .futures $cache
            $future = $client->futures[$messageHash];
            $future->resolve ($cache);
            $client->resolve ($cache, 'positions');
        }) ();
    }

    public function handle_positions($client, $message) {
        //
        //    {
        //        "topic":"position",
        //        "ts":1705292345255,
        //        "data":{
        //           "positions":array(
        //              {
        //                     "symbol":"PERP_ETH_USDC",
        //                     "positionQty":3.1408,
        //                     "costPosition":5706.51952,
        //                     "lastSumUnitaryFunding":0.804,
        //                     "sumUnitaryFundingVersion":0,
        //                     "pendingLongQty":0.0,
        //                     "pendingShortQty":-1.0,
        //                     "settlePrice":1816.9,
        //                     "averageOpenPrice":1804.51490427,
        //                     "unsettledPnl":-2.79856,
        //                     "pnl24H":-338.90179488,
        //                     "fee24H":4.242423,
        //                     "markPrice":1816.2,
        //                     "estLiqPrice":0.0,
        //                     "version":179967,
        //                     "imrwithOrders":0.1,
        //                     "mmrwithOrders":0.05,
        //                     "mmr":0.05,
        //                     "imr":0.1,
        //                     "timestamp":1685154032762
        //              }
        //           )
        //        }
        //    }
        //
        $data = $this->safe_dict($message, 'data', array());
        $rawPositions = $this->safe_list($data, 'positions', array());
        if ($this->positions === null) {
            $this->positions = new ArrayCacheBySymbolBySide ();
        }
        $cache = $this->positions;
        $newPositions = array();
        for ($i = 0; $i < count($rawPositions); $i++) {
            $rawPosition = $rawPositions[$i];
            $marketId = $this->safe_string($rawPosition, 'symbol');
            $market = $this->safe_market($marketId);
            $position = $this->parse_ws_position($rawPosition, $market);
            $newPositions[] = $position;
            $cache->append ($position);
            $messageHash = 'positions::' . $market['symbol'];
            $client->resolve ($position, $messageHash);
        }
        $client->resolve ($newPositions, 'positions');
    }

    public function parse_ws_position($position, $market = null) {
        //
        //     {
        //         "symbol":"PERP_ETH_USDC",
        //         "positionQty":3.1408,
        //         "costPosition":5706.51952,
        //         "lastSumUnitaryFunding":0.804,
        //         "sumUnitaryFundingVersion":0,
        //         "pendingLongQty":0.0,
        //         "pendingShortQty":-1.0,
        //         "settlePrice":1816.9,
        //         "averageOpenPrice":1804.51490427,
        //         "unsettledPnl":-2.79856,
        //         "pnl24H":-338.90179488,
        //         "fee24H":4.242423,
        //         "markPrice":1816.2,
        //         "estLiqPrice":0.0,
        //         "version":179967,
        //         "imrwithOrders":0.1,
        //         "mmrwithOrders":0.05,
        //         "mmr":0.05,
        //         "imr":0.1,
        //         "timestamp":1685154032762
        //     }
        //
        $contract = $this->safe_string($position, 'symbol');
        $market = $this->safe_market($contract, $market);
        $size = $this->safe_string($position, 'positionQty');
        $side = null;
        if (Precise::string_gt($size, '0')) {
            $side = 'long';
        } else {
            $side = 'short';
        }
        $contractSize = $this->safe_string($market, 'contractSize');
        $markPrice = $this->safe_string($position, 'markPrice');
        $timestamp = $this->safe_integer($position, 'timestamp');
        $entryPrice = $this->safe_string($position, 'averageOpenPrice');
        $unrealisedPnl = $this->safe_string($position, 'unsettledPnl');
        $size = Precise::string_abs($size);
        $notional = Precise::string_mul($size, $markPrice);
        return $this->safe_position(array(
            'info' => $position,
            'id' => null,
            'symbol' => $this->safe_string($market, 'symbol'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastUpdateTimestamp' => null,
            'initialMargin' => null,
            'initialMarginPercentage' => null,
            'maintenanceMargin' => null,
            'maintenanceMarginPercentage' => null,
            'entryPrice' => $this->parse_number($entryPrice),
            'notional' => $this->parse_number($notional),
            'leverage' => null,
            'unrealizedPnl' => $this->parse_number($unrealisedPnl),
            'contracts' => $this->parse_number($size),
            'contractSize' => $this->parse_number($contractSize),
            'marginRatio' => null,
            'liquidationPrice' => $this->safe_number($position, 'estLiqPrice'),
            'markPrice' => $this->parse_number($markPrice),
            'lastPrice' => null,
            'collateral' => null,
            'marginMode' => 'cross',
            'marginType' => null,
            'side' => $side,
            'percentage' => null,
            'hedged' => null,
            'stopLossPrice' => null,
            'takeProfitPrice' => null,
        ));
    }

    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://orderly.network/docs/build-on-evm/evm-api/websocket-api/private/balance
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            $topic = 'balance';
            $messageHash = $topic;
            $request = array(
                'event' => 'subscribe',
                'topic' => $topic,
            );
            $message = $this->extend($request, $params);
            return Async\await($this->watch_private($messageHash, $message));
        }) ();
    }

    public function handle_balance($client, $message) {
        //
        //     {
        //         "topic":"balance",
        //         "ts":1651836695254,
        //         "data":{
        //             "balances":{
        //                 "USDC":{
        //                     "holding":5555815.47398272,
        //                     "frozen":0,
        //                     "interest":0,
        //                     "pendingShortQty":0,
        //                     "pendingExposure":0,
        //                     "pendingLongQty":0,
        //                     "pendingLongExposure":0,
        //                     "version":894,
        //                     "staked":51370692,
        //                     "unbonding":0,
        //                     "vault":0,
        //                     "averageOpenPrice":0.00000574,
        //                     "pnl24H":0,
        //                     "fee24H":0.01914,
        //                     "markPrice":0.31885
        //                 }
        //             }
        //         }
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $balances = $this->safe_dict($data, 'balances', array());
        $keys = is_array($balances) ? array_keys($balances) : array();
        $ts = $this->safe_integer($message, 'ts');
        $this->balance['info'] = $data;
        $this->balance['timestamp'] = $ts;
        $this->balance['datetime'] = $this->iso8601($ts);
        for ($i = 0; $i < count($keys); $i++) {
            $key = $keys[$i];
            $value = $balances[$key];
            $code = $this->safe_currency_code($key);
            $account = (is_array($this->balance) && array_key_exists($code, $this->balance)) ? $this->balance[$code] : $this->account();
            $total = $this->safe_string($value, 'holding');
            $used = $this->safe_string($value, 'frozen');
            $account['total'] = $total;
            $account['used'] = $used;
            $account['free'] = Precise::string_sub($total, $used);
            $this->balance[$code] = $account;
        }
        $this->balance = $this->safe_balance($this->balance);
        $client->resolve ($this->balance, 'balance');
    }

    public function handle_error_message(Client $client, $message): Bool {
        //
        // array("id":"1","event":"subscribe","success":false,"ts":1710780997216,"errorMsg":"Auth is needed.")
        //
        if (!(is_array($message) && array_key_exists('success', $message))) {
            return false;
        }
        $success = $this->safe_bool($message, 'success');
        if ($success) {
            return false;
        }
        $errorMessage = $this->safe_string($message, 'errorMsg');
        try {
            if ($errorMessage !== null) {
                $feedback = $this->id . ' ' . $this->json($message);
                $this->throw_exactly_matched_exception($this->exceptions['exact'], $errorMessage, $feedback);
            }
            return false;
        } catch (Exception $error) {
            if ($error instanceof AuthenticationError) {
                $messageHash = 'authenticated';
                $client->reject ($error, $messageHash);
                if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                    unset($client->subscriptions[$messageHash]);
                }
            } else {
                $client->reject ($error);
            }
            return true;
        }
    }

    public function handle_message(Client $client, $message) {
        if ($this->handle_error_message($client, $message)) {
            return;
        }
        $methods = array(
            'ping' => array($this, 'handle_ping'),
            'pong' => array($this, 'handle_pong'),
            'subscribe' => array($this, 'handle_subscribe'),
            'orderbook' => array($this, 'handle_order_book'),
            'ticker' => array($this, 'handle_ticker'),
            'tickers' => array($this, 'handle_tickers'),
            'kline' => array($this, 'handle_ohlcv'),
            'trade' => array($this, 'handle_trade'),
            'auth' => array($this, 'handle_auth'),
            'executionreport' => array($this, 'handle_order_update'),
            'algoexecutionreport' => array($this, 'handle_order_update'),
            'position' => array($this, 'handle_positions'),
            'balance' => array($this, 'handle_balance'),
            'bbos' => array($this, 'handle_bid_ask'),
        );
        $event = $this->safe_string($message, 'event');
        $method = $this->safe_value($methods, $event);
        if ($method !== null) {
            $method($client, $message);
            return;
        }
        $topic = $this->safe_string($message, 'topic');
        if ($topic !== null) {
            $method = $this->safe_value($methods, $topic);
            if ($method !== null) {
                $method($client, $message);
                return;
            }
            $splitTopic = explode('@', $topic);
            $splitLength = count($splitTopic);
            if ($splitLength === 2) {
                $name = $this->safe_string($splitTopic, 1);
                $method = $this->safe_value($methods, $name);
                if ($method !== null) {
                    $method($client, $message);
                    return;
                }
                $splitName = explode('_', $name);
                $splitNameLength = count($splitTopic);
                if ($splitNameLength === 2) {
                    $method = $this->safe_value($methods, $this->safe_string($splitName, 0));
                    if ($method !== null) {
                        $method($client, $message);
                    }
                }
            }
        }
    }

    public function ping(Client $client) {
        return array( 'event' => 'ping' );
    }

    public function handle_ping(Client $client, $message) {
        return array( 'event' => 'pong' );
    }

    public function handle_pong(Client $client, $message) {
        //
        // array( event => "pong", ts => 1614667590000 )
        //
        $client->lastPong = $this->milliseconds();
        return $message;
    }

    public function handle_subscribe(Client $client, $message) {
        //
        //     {
        //         "id" => "666888",
        //         "event" => "subscribe",
        //         "success" => true,
        //         "ts" => 1657117712212
        //     }
        //
        return $message;
    }
}
