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

class phemex extends \ccxt\async\phemex {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchTrades' => true,
                'watchMyTrades' => true,
                'watchOrders' => true,
                'watchOrderBook' => true,
                'watchOHLCV' => true,
                'watchPositions' => null, // TODO
                // mutli-endpoints are not supported => https://github.com/ccxt/ccxt/pull/21490
                'watchOrderBookForSymbols' => false,
                'watchTradesForSymbols' => false,
                'watchOHLCVForSymbols' => false,
                'watchBalance' => true,
            ),
            'urls' => array(
                'test' => array(
                    'ws' => 'wss://testnet-api.phemex.com/ws',
                ),
                'api' => array(
                    'ws' => 'wss://ws.phemex.com',
                ),
            ),
            'options' => array(
                'tradesLimit' => 1000,
                'OHLCVLimit' => 1000,
            ),
            'streaming' => array(
                'keepAlive' => 9000,
            ),
        ));
    }

    public function from_en($en, $scale) {
        if ($en === null) {
            return null;
        }
        $precise = new Precise ($en);
        $precise->decimals = $this->sum($precise->decimals, $scale);
        $precise->reduce ();
        return (string) $precise;
    }

    public function from_ep($ep, $market = null) {
        if (($ep === null) || ($market === null)) {
            return $ep;
        }
        return $this->from_en($ep, $this->safe_integer($market, 'priceScale'));
    }

    public function from_ev($ev, $market = null) {
        if (($ev === null) || ($market === null)) {
            return $ev;
        }
        return $this->from_en($ev, $this->safe_integer($market, 'valueScale'));
    }

    public function from_er($er, $market = null) {
        if (($er === null) || ($market === null)) {
            return $er;
        }
        return $this->from_en($er, $this->safe_integer($market, 'ratioScale'));
    }

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

    public function parse_swap_ticker($ticker, $market = null) {
        //
        //     {
        //         "close" => 442800,
        //         "fundingRate" => 10000,
        //         "high" => 445400,
        //         "indexPrice" => 442621,
        //         "low" => 428400,
        //         "markPrice" => 442659,
        //         "open" => 432200,
        //         "openInterest" => 744183,
        //         "predFundingRate" => 10000,
        //         "symbol" => "LTCUSD",
        //         "turnover" => 8133238294,
        //         "volume" => 934292
        //     }
        //
        $marketId = $this->safe_string($ticker, 'symbol');
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $timestamp = $this->safe_integer_product($ticker, 'timestamp', 0.000001);
        $lastString = $this->from_ep($this->safe_string($ticker, 'close'), $market);
        $last = $this->parse_number($lastString);
        $quoteVolume = $this->parse_number($this->from_ev($this->safe_string($ticker, 'turnover'), $market));
        $baseVolume = $this->parse_number($this->from_ev($this->safe_string($ticker, 'volume'), $market));
        $change = null;
        $percentage = null;
        $average = null;
        $openString = $this->omit_zero($this->from_ep($this->safe_string($ticker, 'open'), $market));
        $open = $this->parse_number($openString);
        if (($openString !== null) && ($lastString !== null)) {
            $change = $this->parse_number(Precise::string_sub($lastString, $openString));
            $average = $this->parse_number(Precise::string_div(Precise::string_add($lastString, $openString), '2'));
            $percentage = $this->parse_number(Precise::string_mul(Precise::string_sub(Precise::string_div($lastString, $openString), '1'), '100'));
        }
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $this->parse_number($this->from_ep($this->safe_string($ticker, 'high'), $market)),
            'low' => $this->parse_number($this->from_ep($this->safe_string($ticker, 'low'), $market)),
            'bid' => null,
            'bidVolume' => null,
            'ask' => null,
            'askVolume' => null,
            'vwap' => null,
            'open' => $open,
            'close' => $last,
            'last' => $last,
            'previousClose' => null, // previous day close
            'change' => $change,
            'percentage' => $percentage,
            'average' => $average,
            'baseVolume' => $baseVolume,
            'quoteVolume' => $quoteVolume,
            'markPrice' => $this->parse_number($this->from_ep($this->safe_string($ticker, 'markPrice'), $market)),
            'indexPrice' => $this->parse_number($this->from_ep($this->safe_string($ticker, 'indexPrice'), $market)),
            'info' => $ticker,
        ));
    }

    public function parse_perpetual_ticker($ticker, $market = null) {
        //
        //    array(
        //        "STXUSDT",
        //        "0.64649",
        //        "0.8628",
        //        "0.61215",
        //        "0.71737",
        //        "4519387",
        //        "3210827.98166",
        //        "697635",
        //        "0.71720205",
        //        "0.71720205",
        //        "0.0001",
        //        "0.0001",
        //    )
        //
        $marketId = $this->safe_string($ticker, 0);
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $lastString = $this->from_ep($this->safe_string($ticker, 4), $market);
        $last = $this->parse_number($lastString);
        $quoteVolume = $this->parse_number($this->from_ev($this->safe_string($ticker, 6), $market));
        $baseVolume = $this->parse_number($this->from_ev($this->safe_string($ticker, 5), $market));
        $change = null;
        $percentage = null;
        $average = null;
        $openString = $this->omit_zero($this->from_ep($this->safe_string($ticker, 1), $market));
        $open = $this->parse_number($openString);
        if (($openString !== null) && ($lastString !== null)) {
            $change = $this->parse_number(Precise::string_sub($lastString, $openString));
            $average = $this->parse_number(Precise::string_div(Precise::string_add($lastString, $openString), '2'));
            $percentage = $this->parse_number(Precise::string_mul(Precise::string_sub(Precise::string_div($lastString, $openString), '1'), '100'));
        }
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => null,
            'datetime' => null,
            'high' => $this->parse_number($this->from_ep($this->safe_string($ticker, 2), $market)),
            'low' => $this->parse_number($this->from_ep($this->safe_string($ticker, 3), $market)),
            'bid' => null,
            'bidVolume' => null,
            'ask' => null,
            'askVolume' => null,
            'vwap' => null,
            'open' => $open,
            'close' => $last,
            'last' => $last,
            'previousClose' => null, // previous day close
            'change' => $change,
            'percentage' => $percentage,
            'average' => $average,
            'baseVolume' => $baseVolume,
            'quoteVolume' => $quoteVolume,
            'info' => $ticker,
        ));
    }

    public function handle_ticker(Client $client, $message) {
        //
        //     {
        //         "spot_market24h" => array(
        //             "askEp" => 958148000000,
        //             "bidEp" => 957884000000,
        //             "highEp" => 962000000000,
        //             "lastEp" => 958220000000,
        //             "lowEp" => 928049000000,
        //             "openEp" => 935597000000,
        //             "symbol" => "sBTCUSDT",
        //             "turnoverEv" => 146074214388978,
        //             "volumeEv" => 15492228900
        //         ),
        //         "timestamp" => 1592847265888272100
        //     }
        //
        // swap
        //
        //     {
        //         "market24h" => array(
        //             "close" => 442800,
        //             "fundingRate" => 10000,
        //             "high" => 445400,
        //             "indexPrice" => 442621,
        //             "low" => 428400,
        //             "markPrice" => 442659,
        //             "open" => 432200,
        //             "openInterest" => 744183,
        //             "predFundingRate" => 10000,
        //             "symbol" => "LTCUSD",
        //             "turnover" => 8133238294,
        //             "volume" => 934292
        //         ),
        //         "timestamp" => 1592845585373374500
        //     }
        //
        // perpetual
        //
        //    {
        //        "data" => array(
        //            array(
        //                "STXUSDT",
        //                "0.64649",
        //                "0.8628",
        //                "0.61215",
        //                "0.71737",
        //                "4519387",
        //                "3210827.98166",
        //                "697635",
        //                "0.71720205",
        //                "0.71720205",
        //                "0.0001",
        //                "0.0001",
        //            ),
        //            ...
        //        ),
        //        "fields" => array(
        //            "symbol",
        //            "openRp",
        //            "highRp",
        //            "lowRp",
        //            "lastRp",
        //            "volumeRq",
        //            "turnoverRv",
        //            "openInterestRv",
        //            "indexRp",
        //            "markRp",
        //            "fundingRateRr",
        //            "predFundingRateRr",
        //        ),
        //        "method" => "perp_market24h_pack_p.update",
        //        "timestamp" => "1677094918686806209",
        //        "type" => "snapshot",
        //    }
        //
        $tickers = array();
        if (is_array($message) && array_key_exists('market24h', $message)) {
            $ticker = $this->safe_value($message, 'market24h');
            $tickers[] = $this->parse_swap_ticker($ticker);
        } elseif (is_array($message) && array_key_exists('spot_market24h', $message)) {
            $ticker = $this->safe_value($message, 'spot_market24h');
            $tickers[] = $this->parse_ticker($ticker);
        } elseif (is_array($message) && array_key_exists('data', $message)) {
            $data = $this->safe_value($message, 'data', array());
            for ($i = 0; $i < count($data); $i++) {
                $tickers[] = $this->parse_perpetual_ticker($data[$i]);
            }
        }
        for ($i = 0; $i < count($tickers); $i++) {
            $ticker = $tickers[$i];
            $symbol = $ticker['symbol'];
            $messageHash = 'ticker:' . $symbol;
            $timestamp = $this->safe_integer_product($message, 'timestamp', 0.000001);
            $ticker['timestamp'] = $timestamp;
            $ticker['datetime'] = $this->iso8601($timestamp);
            $this->tickers[$symbol] = $ticker;
            $client->resolve ($ticker, $messageHash);
        }
    }

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             *
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#subscribe-account-order-position-aop
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#subscribe-account-order-position-aop
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#subscribe-wallet-order-messages
             *
             * watch balance and get the amount of funds available for trading or funds locked in orders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->settle] set to USDT to use hedged perpetual api
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            $type = null;
            list($type, $params) = $this->handle_market_type_and_params('watchBalance', null, $params);
            $usePerpetualApi = $this->safe_string($params, 'settle') === 'USDT';
            $messageHash = ':balance';
            $messageHash = $usePerpetualApi ? 'perpetual' . $messageHash : $type . $messageHash;
            return Async\await($this->subscribe_private($type, $messageHash, $params));
        }) ();
    }

    public function handle_balance($type, $client, $message) {
        // spot
        //    array(
        //       array(
        //           "balanceEv" => 0,
        //           "currency" => "BTC",
        //           "lastUpdateTimeNs" => "1650442638722099092",
        //           "lockedTradingBalanceEv" => 0,
        //           "lockedWithdrawEv" => 0,
        //           "userID" => 2647224
        //         ),
        //         {
        //           "balanceEv" => 1154232337,
        //           "currency" => "USDT",
        //           "lastUpdateTimeNs" => "1650442617610017597",
        //           "lockedTradingBalanceEv" => 0,
        //           "lockedWithdrawEv" => 0,
        //           "userID" => 2647224
        //         }
        //    )
        // swap
        //    array(
        //        {
        //            "accountBalanceEv" => 0,
        //            "accountID" => 26472240001,
        //            "bonusBalanceEv" => 0,
        //            "currency" => "BTC",
        //            "totalUsedBalanceEv" => 0,
        //            "userID" => 2647224
        //        }
        //    )
        // perpetual
        //    array(
        //        {
        //            "accountBalanceRv" => "1508.452588802237",
        //            "accountID" => 9328670003,
        //            "bonusBalanceRv" => "0",
        //            "currency" => "USDT",
        //            "totalUsedBalanceRv" => "343.132599666883",
        //            "userID" => 932867
        //        }
        //    )
        //
        $this->balance['info'] = $message;
        for ($i = 0; $i < count($message); $i++) {
            $balance = $message[$i];
            $currencyId = $this->safe_string($balance, 'currency');
            $code = $this->safe_currency_code($currencyId);
            $currency = $this->safe_value($this->currencies, $code, array());
            $scale = $this->safe_integer($currency, 'valueScale', 8);
            $account = $this->account();
            $used = $this->safe_string($balance, 'totalUsedBalanceRv');
            if ($used === null) {
                $usedEv = $this->safe_string($balance, 'totalUsedBalanceEv');
                if ($usedEv === null) {
                    $lockedTradingBalanceEv = $this->safe_string($balance, 'lockedTradingBalanceEv');
                    $lockedWithdrawEv = $this->safe_string_2($balance, 'lockedWithdrawEv', 'lockedWithdrawRv');
                    $usedEv = Precise::string_add($lockedTradingBalanceEv, $lockedWithdrawEv);
                }
                $used = $this->from_en($usedEv, $scale);
            }
            $total = $this->safe_string($balance, 'accountBalanceRv');
            if ($total === null) {
                $totalEv = $this->safe_string_2($balance, 'accountBalanceEv', 'balanceEv');
                $total = $this->from_en($totalEv, $scale);
            }
            $account['used'] = $used;
            $account['total'] = $total;
            $this->balance[$code] = $account;
            $this->balance = $this->safe_balance($this->balance);
        }
        $messageHash = $type . ':balance';
        $client->resolve ($this->balance, $messageHash);
    }

    public function handle_trades(Client $client, $message) {
        //
        //     {
        //         "sequence" => 1795484727,
        //         "symbol" => "sBTCUSDT",
        //         "trades" => array(
        //             array( 1592891002064516600, "Buy", 964020000000, 1431000 ),
        //             array( 1592890978987934500, "Sell", 963704000000, 1401800 ),
        //             array( 1592890972918701800, "Buy", 963938000000, 2018600 ),
        //         ),
        //         "type" => "snapshot"
        //     }
        //  perpetual
        //     {
        //         "sequence" => 1230197759,
        //         "symbol" => "BTCUSDT",
        //         "trades_p" => array(
        //             array(
        //                 1677094244729433000,
        //                 "Buy",
        //                 "23800.4",
        //                 "2.455",
        //             ),
        //         ),
        //         "type" => "snapshot",
        //     }
        //
        $name = 'trade';
        $marketId = $this->safe_string($message, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $messageHash = $name . ':' . $symbol;
        $stored = $this->safe_value($this->trades, $symbol);
        if ($stored === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $stored = new ArrayCache ($limit);
            $this->trades[$symbol] = $stored;
        }
        $trades = $this->safe_value_2($message, 'trades', 'trades_p', array());
        $parsed = $this->parse_trades($trades, $market);
        for ($i = 0; $i < count($parsed); $i++) {
            $stored->append ($parsed[$i]);
        }
        $client->resolve ($stored, $messageHash);
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         "kline" => array(
        //             array( 1592905200, 60, 960688000000, 960709000000, 960709000000, 960400000000, 960400000000, 848100, 8146756046 ),
        //             array( 1592905140, 60, 960718000000, 960716000000, 960717000000, 960560000000, 960688000000, 4284900, 41163743512 ),
        //             array( 1592905080, 60, 960513000000, 960684000000, 960718000000, 960684000000, 960718000000, 4880500, 46887494349 ),
        //         ),
        //         "sequence" => 1804401474,
        //         "symbol" => "sBTCUSDT",
        //         "type" => "snapshot"
        //     }
        // perpetual
        //     {
        //         "kline_p" => array(
        //             array(
        //                 1677094560,
        //                 60,
        //                 "23746.2",
        //                 "23746.1",
        //                 "23757.6",
        //                 "23736.9",
        //                 "23754.8",
        //                 "34.273",
        //                 "813910.208",
        //             ),
        //         ),
        //         "sequence" => 1230786017,
        //         "symbol" => "BTCUSDT",
        //         "type" => "incremental",
        //     }
        //
        $marketId = $this->safe_string($message, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $candles = $this->safe_value_2($message, 'kline', 'kline_p', array());
        $first = $this->safe_value($candles, 0, array());
        $interval = $this->safe_string($first, 1);
        $timeframe = $this->find_timeframe($interval);
        if ($timeframe !== null) {
            $messageHash = 'kline:' . $timeframe . ':' . $symbol;
            $ohlcvs = $this->parse_ohlcvs($candles, $market);
            $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;
            }
            for ($i = 0; $i < count($ohlcvs); $i++) {
                $candle = $ohlcvs[$i];
                $stored->append ($candle);
            }
            $client->resolve ($stored, $messageHash);
        }
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             *
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#$subscribe-24-hours-ticker
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#$subscribe-24-hours-ticker
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#$subscribe-24-hours-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());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $isSwap = $market['swap'];
            $settleIsUSDT = $market['settle'] === 'USDT';
            $name = 'spot_market24h';
            if ($isSwap) {
                $name = $settleIsUSDT ? 'perp_market24h_pack_p' : 'market24h';
            }
            $url = $this->urls['api']['ws'];
            $requestId = $this->request_id();
            $subscriptionHash = $name . '.subscribe';
            $messageHash = 'ticker:' . $symbol;
            $subscribe = array(
                'method' => $subscriptionHash,
                'id' => $requestId,
                'params' => array(),
            );
            $request = $this->deep_extend($subscribe, $params);
            return Async\await($this->watch($url, $messageHash, $request, $subscriptionHash));
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#$subscribe-24-hours-$ticker
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#$subscribe-24-hours-$ticker
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#$subscribe-24-hours-$ticker
             *
             * 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
             * @param {string} [$params->channel] the channel to $subscribe to, tickers by default. Can be tickers, sprd-tickers, index-tickers, block-tickers
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=$ticker-structure $ticker structure~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $first = $symbols[0];
            $market = $this->market($first);
            $isSwap = $market['swap'];
            $settleIsUSDT = $market['settle'] === 'USDT';
            $name = 'spot_market24h';
            if ($isSwap) {
                $name = $settleIsUSDT ? 'perp_market24h_pack_p' : 'market24h';
            }
            $url = $this->urls['api']['ws'];
            $requestId = $this->request_id();
            $subscriptionHash = $name . '.subscribe';
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $messageHashes[] = 'ticker:' . $symbols[$i];
            }
            $subscribe = array(
                'method' => $subscriptionHash,
                'id' => $requestId,
                'params' => array(),
            );
            $request = $this->deep_extend($subscribe, $params);
            $ticker = Async\await($this->watch_multiple($url, $messageHashes, $request, $messageHashes));
            if ($this->newUpdates) {
                $result = array();
                $result[$ticker['symbol']] = $ticker;
                return $result;
            }
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    public function watch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             *
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#$subscribe-trade
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#$subscribe-trade
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#$subscribe-trade
             *
             * get the list of most recent $trades for a particular $symbol
             * @param {string} $symbol unified $symbol of the $market to fetch $trades for
             * @param {int} [$since] timestamp in ms of the earliest trade to fetch
             * @param {int} [$limit] the maximum amount of $trades to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-$trades trade structures~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $url = $this->urls['api']['ws'];
            $requestId = $this->request_id();
            $isSwap = $market['swap'];
            $settleIsUSDT = $market['settle'] === 'USDT';
            $name = ($isSwap && $settleIsUSDT) ? 'trade_p' : 'trade';
            $messageHash = 'trade:' . $symbol;
            $method = $name . '.subscribe';
            $subscribe = array(
                'method' => $method,
                'id' => $requestId,
                'params' => [
                    $market['id'],
                ],
            );
            $request = $this->deep_extend($subscribe, $params);
            $trades = Async\await($this->watch($url, $messageHash, $request, $messageHash));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function watch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             *
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#$subscribe-$orderbook
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#$subscribe-$orderbook-for-new-model
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#$subscribe-30-levels-$orderbook
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#$subscribe-full-$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());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $url = $this->urls['api']['ws'];
            $requestId = $this->request_id();
            $isSwap = $market['swap'];
            $settleIsUSDT = $market['settle'] === 'USDT';
            $name = ($isSwap && $settleIsUSDT) ? 'orderbook_p' : 'orderbook';
            $messageHash = 'orderbook:' . $symbol;
            $method = $name . '.subscribe';
            $subscribe = array(
                'method' => $method,
                'id' => $requestId,
                'params' => [
                    $market['id'],
                ],
            );
            $request = $this->deep_extend($subscribe, $params);
            $orderbook = Async\await($this->watch($url, $messageHash, $request, $messageHash));
            return $orderbook->limit ();
        }) ();
    }

    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) {
            /**
             *
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Hedged-Perpetual-API.md#$subscribe-kline
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Contract-API-en.md#$subscribe-kline
             * @see https://github.com/phemex/phemex-api-docs/blob/master/Public-Spot-API-en.md#$subscribe-kline
             *
             * watches historical candlestick data containing the open, high, low, and close price, and the volume of a $market
             * @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());
            $market = $this->market($symbol);
            $symbol = $market['symbol'];
            $url = $this->urls['api']['ws'];
            $requestId = $this->request_id();
            $isSwap = $market['swap'];
            $settleIsUSDT = $market['settle'] === 'USDT';
            $name = ($isSwap && $settleIsUSDT) ? 'kline_p' : 'kline';
            $messageHash = 'kline:' . $timeframe . ':' . $symbol;
            $method = $name . '.subscribe';
            $subscribe = array(
                'method' => $method,
                'id' => $requestId,
                'params' => [
                    $market['id'],
                    $this->safe_integer($this->timeframes, $timeframe),
                ],
            );
            $request = $this->deep_extend($subscribe, $params);
            $ohlcv = Async\await($this->watch($url, $messageHash, $request, $messageHash));
            if ($this->newUpdates) {
                $limit = $ohlcv->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcv, $since, $limit, 0, true);
        }) ();
    }

    public function custom_handle_delta($bookside, $delta, $market = null) {
        $bidAsk = $this->custom_parse_bid_ask($delta, 0, 1, $market);
        $bookside->storeArray ($bidAsk);
    }

    public function custom_handle_deltas($bookside, $deltas, $market = null) {
        for ($i = 0; $i < count($deltas); $i++) {
            $this->custom_handle_delta($bookside, $deltas[$i], $market);
        }
    }

    public function handle_order_book(Client $client, $message) {
        //
        //     {
        //         "book" => array(
        //             "asks" => array(
        //                 array( 960316000000, 6993800 ),
        //                 array( 960318000000, 13183000 ),
        //                 array( 960319000000, 9170200 ),
        //             ),
        //             "bids" => array(
        //                 array( 959941000000, 8385300 ),
        //                 array( 959939000000, 10296600 ),
        //                 array( 959930000000, 3672400 ),
        //             )
        //         ),
        //         "depth" => 30,
        //         "sequence" => 1805784701,
        //         "symbol" => "sBTCUSDT",
        //         "timestamp" => 1592908460404461600,
        //         "type" => "snapshot"
        //     }
        //  perpetual
        //    {
        //        "depth" => 30,
        //        "orderbook_p" => array(
        //            "asks" => array(
        //                array(
        //                    "23788.5",
        //                    "0.13",
        //                ),
        //            ),
        //            "bids" => array(
        //                array(
        //                    "23787.8",
        //                    "1.836",
        //                ),
        //            ),
        //        ),
        //        "sequence" => 1230347368,
        //        "symbol" => "BTCUSDT",
        //        "timestamp" => "1677093457306978852",
        //        "type" => "snapshot",
        //    }
        //
        $marketId = $this->safe_string($message, 'symbol');
        $market = $this->safe_market($marketId);
        $symbol = $market['symbol'];
        $type = $this->safe_string($message, 'type');
        $depth = $this->safe_integer($message, 'depth');
        $name = 'orderbook';
        $messageHash = $name . ':' . $symbol;
        $nonce = $this->safe_integer($message, 'sequence');
        $timestamp = $this->safe_integer_product($message, 'timestamp', 0.000001);
        if ($type === 'snapshot') {
            $book = $this->safe_value_2($message, 'book', 'orderbook_p', array());
            $snapshot = $this->custom_parse_order_book($book, $symbol, $timestamp, 'bids', 'asks', 0, 1, $market);
            $snapshot['nonce'] = $nonce;
            $orderbook = $this->order_book($snapshot, $depth);
            $this->orderbooks[$symbol] = $orderbook;
            $client->resolve ($orderbook, $messageHash);
        } else {
            if (is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks)) {
                $orderbook = $this->orderbooks[$symbol];
                $changes = $this->safe_dict_2($message, 'book', 'orderbook_p', array());
                $asks = $this->safe_list($changes, 'asks', array());
                $bids = $this->safe_list($changes, 'bids', array());
                $this->custom_handle_deltas($orderbook['asks'], $asks, $market);
                $this->custom_handle_deltas($orderbook['bids'], $bids, $market);
                $orderbook['nonce'] = $nonce;
                $orderbook['timestamp'] = $timestamp;
                $orderbook['datetime'] = $this->iso8601($timestamp);
                $this->orderbooks[$symbol] = $orderbook;
                $client->resolve ($orderbook, $messageHash);
            }
        }
    }

    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
             * @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 = null;
            $type = null;
            $messageHash = 'trades:';
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
                $messageHash = $messageHash . $market['symbol'];
                if ($market['settle'] === 'USDT') {
                    $params = $this->extend($params);
                    $params['settle'] = 'USDT';
                }
            }
            list($type, $params) = $this->handle_market_type_and_params('watchMyTrades', $market, $params);
            if ($symbol === null) {
                $settle = $this->safe_string($params, 'settle');
                $messageHash = ($settle === 'USDT') ? ($messageHash . 'perpetual') : ($messageHash . $type);
            }
            $trades = Async\await($this->subscribe_private($type, $messageHash, $params));
            if ($this->newUpdates) {
                $limit = $trades->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($trades, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_my_trades(Client $client, $message) {
        //
        // swap
        //    array(
        //        {
        //            "avgPriceEp":4138763000000,
        //            "baseCurrency":"BTC",
        //            "baseQtyEv":0,
        //            "clOrdID":"7956e0be-e8be-93a0-2887-ca504d85cda2",
        //            "execBaseQtyEv":30100,
        //            "execFeeEv":31,
        //            "execID":"d3b10cfa-84e3-5752-828e-78a79617e598",
        //            "execPriceEp":4138763000000,
        //            "execQuoteQtyEv":1245767663,
        //            "feeCurrency":"BTC",
        //            "lastLiquidityInd":"RemovedLiquidity",
        //            "ordType":"Market",
        //            "orderID":"34a4b1a8-ac3a-4580-b3e6-a6d039f27195",
        //            "priceEp":4549022000000,
        //            "qtyType":"ByQuote",
        //            "quoteCurrency":"USDT",
        //            "quoteQtyEv":1248000000,
        //            "side":"Buy",
        //            "symbol":"sBTCUSDT",
        //            "tradeType":"Trade",
        //            "transactTimeNs":"1650442617609928764",
        //            "userID":2647224
        //        }
        //    )
        // perpetual
        //    array(
        //        array(
        //            "accountID" => 9328670003,
        //            "action" => "New",
        //            "actionBy" => "ByUser",
        //            "actionTimeNs" => 1666858780876924611,
        //            "addedSeq" => 77751555,
        //            "apRp" => "0",
        //            "bonusChangedAmountRv" => "0",
        //            "bpRp" => "0",
        //            "clOrdID" => "c0327a7d-9064-62a9-28f6-2db9aaaa04e0",
        //            "closedPnlRv" => "0",
        //            "closedSize" => "0",
        //            "code" => 0,
        //            "cumFeeRv" => "0",
        //            "cumQty" => "0",
        //            "cumValueRv" => "0",
        //            "curAccBalanceRv" => "1508.489893982237",
        //            "curAssignedPosBalanceRv" => "24.62786650928",
        //            "curBonusBalanceRv" => "0",
        //            "curLeverageRr" => "-10",
        //            "curPosSide" => "Buy",
        //            "curPosSize" => "0.043",
        //            "curPosTerm" => 1,
        //            "curPosValueRv" => "894.0689",
        //            "curRiskLimitRv" => "1000000",
        //            "currency" => "USDT",
        //            "cxlRejReason" => 0,
        //            "displayQty" => "0.003",
        //            "execFeeRv" => "0",
        //            "execID" => "00000000-0000-0000-0000-000000000000",
        //            "execPriceRp" => "20723.7",
        //            "execQty" => "0",
        //            "execSeq" => 77751555,
        //            "execStatus" => "New",
        //            "execValueRv" => "0",
        //            "feeRateRr" => "0",
        //            "leavesQty" => "0.003",
        //            "leavesValueRv" => "63.4503",
        //            "message" => "No error",
        //            "ordStatus" => "New",
        //            "ordType" => "Market",
        //            "orderID" => "fa64c6f2-47a4-4929-aab4-b7fa9bbc4323",
        //            "orderQty" => "0.003",
        //            "pegOffsetValueRp" => "0",
        //            "posSide" => "Long",
        //            "priceRp" => "21150.1",
        //            "relatedPosTerm" => 1,
        //            "relatedReqNum" => 11,
        //            "side" => "Buy",
        //            "slTrigger" => "ByMarkPrice",
        //            "stopLossRp" => "0",
        //            "stopPxRp" => "0",
        //            "symbol" => "BTCUSDT",
        //            "takeProfitRp" => "0",
        //            "timeInForce" => "ImmediateOrCancel",
        //            "tpTrigger" => "ByLastPrice",
        //            "tradeType" => "Amend",
        //            "transactTimeNs" => 1666858780881545305,
        //            "userID" => 932867
        //        ),
        //        ...
        //    )
        //
        $channel = 'trades';
        $tradesLength = count($message);
        if ($tradesLength === 0) {
            return;
        }
        $cachedTrades = $this->myTrades;
        if ($cachedTrades === null) {
            $limit = $this->safe_integer($this->options, 'tradesLimit', 1000);
            $cachedTrades = new ArrayCacheBySymbolById ($limit);
        }
        $marketIds = array();
        $type = null;
        for ($i = 0; $i < count($message); $i++) {
            $rawTrade = $message[$i];
            $marketId = $this->safe_string($rawTrade, 'symbol');
            $market = $this->safe_market($marketId);
            $parsed = $this->parse_trade($rawTrade);
            $cachedTrades->append ($parsed);
            $symbol = $parsed['symbol'];
            if ($type === null) {
                $type = ($market['settle'] === 'USDT') ? 'perpetual' : $market['type'];
            }
            $marketIds[$symbol] = true;
        }
        $keys = is_array($marketIds) ? array_keys($marketIds) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $market = $keys[$i];
            $hash = $channel . ':' . $market;
            $client->resolve ($cachedTrades, $hash);
        }
        // generic subscription
        $messageHash = $channel . ':' . $type;
        $client->resolve ($cachedTrades, $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
             * @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:';
            $market = null;
            $type = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $symbol = $market['symbol'];
                $messageHash = $messageHash . $market['symbol'];
                if ($market['settle'] === 'USDT') {
                    $params = $this->extend($params);
                    $params['settle'] = 'USDT';
                }
            }
            list($type, $params) = $this->handle_market_type_and_params('watchOrders', $market, $params);
            $isUSDTSettled = $this->safe_string($params, 'settle') === 'USDT';
            if ($symbol === null) {
                $messageHash = ($isUSDTSettled) ? ($messageHash . 'perpetual') : ($messageHash . $type);
            }
            $orders = Async\await($this->subscribe_private($type, $messageHash, $params));
            if ($this->newUpdates) {
                $limit = $orders->getLimit ($symbol, $limit);
            }
            return $this->filter_by_symbol_since_limit($orders, $symbol, $since, $limit, true);
        }) ();
    }

    public function handle_orders(Client $client, $message) {
        // spot $update
        // {
        //        "closed":array(
        //           {
        //              "action":"New",
        //              "avgPriceEp":4138763000000,
        //              "baseCurrency":"BTC",
        //              "baseQtyEv":0,
        //              "bizError":0,
        //              "clOrdID":"7956e0be-e8be-93a0-2887-ca504d85cda2",
        //              "createTimeNs":"1650442617606017583",
        //              "cumBaseQtyEv":30100,
        //              "cumFeeEv":31,
        //              "cumQuoteQtyEv":1245767663,
        //              "cxlRejReason":0,
        //              "feeCurrency":"BTC",
        //              "leavesBaseQtyEv":0,
        //              "leavesQuoteQtyEv":0,
        //              "ordStatus":"Filled",
        //              "ordType":"Market",
        //              "orderID":"34a4b1a8-ac3a-4580-b3e6-a6d039f27195",
        //              "pegOffsetValueEp":0,
        //              "priceEp":4549022000000,
        //              "qtyType":"ByQuote",
        //              "quoteCurrency":"USDT",
        //              "quoteQtyEv":1248000000,
        //              "side":"Buy",
        //              "stopPxEp":0,
        //              "symbol":"sBTCUSDT",
        //              "timeInForce":"ImmediateOrCancel",
        //              "tradeType":"Trade",
        //              "transactTimeNs":"1650442617609928764",
        //              "triggerTimeNs":0,
        //              "userID":2647224
        //           }
        //        ),
        //        "fills":array(
        //           {
        //              "avgPriceEp":4138763000000,
        //              "baseCurrency":"BTC",
        //              "baseQtyEv":0,
        //              "clOrdID":"7956e0be-e8be-93a0-2887-ca504d85cda2",
        //              "execBaseQtyEv":30100,
        //              "execFeeEv":31,
        //              "execID":"d3b10cfa-84e3-5752-828e-78a79617e598",
        //              "execPriceEp":4138763000000,
        //              "execQuoteQtyEv":1245767663,
        //              "feeCurrency":"BTC",
        //              "lastLiquidityInd":"RemovedLiquidity",
        //              "ordType":"Market",
        //              "orderID":"34a4b1a8-ac3a-4580-b3e6-a6d039f27195",
        //              "priceEp":4549022000000,
        //              "qtyType":"ByQuote",
        //              "quoteCurrency":"USDT",
        //              "quoteQtyEv":1248000000,
        //              "side":"Buy",
        //              "symbol":"sBTCUSDT",
        //              "tradeType":"Trade",
        //              "transactTimeNs":"1650442617609928764",
        //              "userID":2647224
        //           }
        //        ),
        //        "open":array(
        //           array(
        //              "action":"New",
        //              "avgPriceEp":0,
        //              "baseCurrency":"LTC",
        //              "baseQtyEv":0,
        //              "bizError":0,
        //              "clOrdID":"2c0e5eb5-efb7-60d3-2e5f-df175df412ef",
        //              "createTimeNs":"1650446670073853755",
        //              "cumBaseQtyEv":0,
        //              "cumFeeEv":0,
        //              "cumQuoteQtyEv":0,
        //              "cxlRejReason":0,
        //              "feeCurrency":"LTC",
        //              "leavesBaseQtyEv":0,
        //              "leavesQuoteQtyEv":1000000000,
        //              "ordStatus":"New",
        //              "ordType":"Limit",
        //              "orderID":"d2aad92f-50f5-441a-957b-8184b146e3fb",
        //              "pegOffsetValueEp":0,
        //              "priceEp":5000000000,
        //              "qtyType":"ByQuote",
        //              "quoteCurrency":"USDT",
        //              "quoteQtyEv":1000000000,
        //              "side":"Buy",
        //            }
        //        )
        //  ),
        // perpetual
        //    array(
        //        array(
        //          "accountID" => 40183400003,
        //          "action" => "New",
        //          "actionBy" => "ByUser",
        //          "actionTimeNs" => "1674110665380190869",
        //          "addedSeq" => 678760103,
        //          "apRp" => "0",
        //          "bonusChangedAmountRv" => "0",
        //          "bpRp" => "0",
        //          "clOrdID" => '',
        //          "cl_req_code" => 0,
        //          "closedPnlRv" => "0",
        //          "closedSize" => "0",
        //          "code" => 0,
        //          "cumFeeRv" => "0",
        //          "cumQty" => "0.001",
        //          "cumValueRv" => "20.849",
        //          "curAccBalanceRv" => "19.9874906",
        //          "curAssignedPosBalanceRv" => "0",
        //          "curBonusBalanceRv" => "0",
        //          "curLeverageRr" => "-10",
        //          "curPosSide" => "Buy",
        //          "curPosSize" => "0.001",
        //          "curPosTerm" => 1,
        //          "curPosValueRv" => "20.849",
        //          "curRiskLimitRv" => "1000000",
        //          "currency" => "USDT",
        //          "cxlRejReason" => 0,
        //          "displayQty" => "0.001",
        //          "execFeeRv" => "0.0125094",
        //          "execID" => "b88d2950-04a2-52d8-8927-346059900242",
        //          "execPriceRp" => "20849",
        //          "execQty" => "0.001",
        //          "execSeq" => 678760103,
        //          "execStatus" => "TakerFill",
        //          "execValueRv" => "20.849",
        //          "feeRateRr" => "0.0006",
        //          "lastLiquidityInd" => "RemovedLiquidity",
        //          "leavesQty" => "0",
        //          "leavesValueRv" => "0",
        //          "message" => "No error",
        //          "ordStatus" => "Filled",
        //          "ordType" => "Market",
        //          "orderID" => "79620ed2-54c6-4645-a35c-7057e687c576",
        //          "orderQty" => "0.001",
        //          "pegOffsetProportionRr" => "0",
        //          "pegOffsetValueRp" => "0",
        //          "posSide" => "Long",
        //          "priceRp" => "21476.3",
        //          "relatedPosTerm" => 1,
        //          "relatedReqNum" => 4,
        //          "side" => "Buy",
        //          "slTrigger" => "ByMarkPrice",
        //          "stopLossRp" => "0",
        //          "stopPxRp" => "0",
        //          "symbol" => "BTCUSDT",
        //          "takeProfitRp" => "0",
        //          "timeInForce" => "ImmediateOrCancel",
        //          "tpTrigger" => "ByLastPrice",
        //          "tradeType" => "Trade",
        //          "transactTimeNs" => "1674110665387882268",
        //          "userID" => 4018340
        //        ),
        //        ...
        //    )
        //
        $trades = array();
        $parsedOrders = array();
        if ((is_array($message) && array_key_exists('closed', $message)) || (is_array($message) && array_key_exists('fills', $message)) || (is_array($message) && array_key_exists('open', $message))) {
            $closed = $this->safe_value($message, 'closed', array());
            $open = $this->safe_value($message, 'open', array());
            $orders = $this->array_concat($open, $closed);
            $ordersLength = count($orders);
            if ($ordersLength === 0) {
                return;
            }
            $trades = $this->safe_value($message, 'fills', array());
            for ($i = 0; $i < count($orders); $i++) {
                $rawOrder = $orders[$i];
                $parsedOrder = $this->parse_order($rawOrder);
                $parsedOrders[] = $parsedOrder;
            }
        } else {
            $messageLength = count($message);
            if ($messageLength === 0) {
                return;
            }
            for ($i = 0; $i < count($message); $i++) {
                $update = $message[$i];
                $action = $this->safe_string($update, 'action');
                if (($action !== null) && ($action !== 'Cancel')) {
                    // order . trade info together
                    $trades[] = $update;
                }
                $parsedOrder = $this->parse_ws_swap_order($update);
                $parsedOrders[] = $parsedOrder;
            }
        }
        $this->handle_my_trades($client, $trades);
        $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
        $marketIds = array();
        if ($this->orders === null) {
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $type = null;
        $stored = $this->orders;
        for ($i = 0; $i < count($parsedOrders); $i++) {
            $parsed = $parsedOrders[$i];
            $stored->append ($parsed);
            $symbol = $parsed['symbol'];
            $market = $this->market($symbol);
            if ($type === null) {
                $isUsdt = $market['settle'] === 'USDT';
                $type = $isUsdt ? 'perpetual' : $market['type'];
            }
            $marketIds[$symbol] = true;
        }
        $keys = is_array($marketIds) ? array_keys($marketIds) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $currentMessageHash = 'orders' . ':' . $keys[$i];
            $client->resolve ($this->orders, $currentMessageHash);
        }
        // resolve generic subscription (spot or swap)
        $messageHash = 'orders:' . $type;
        $client->resolve ($this->orders, $messageHash);
    }

    public function parse_ws_swap_order($order, $market = null) {
        //
        // swap
        //    {
        //        "accountID":26472240002,
        //        "action":"Cancel",
        //        "actionBy":"ByUser",
        //        "actionTimeNs":"1650450096104760797",
        //        "addedSeq":26975849309,
        //        "bonusChangedAmountEv":0,
        //        "clOrdID":"d9675963-5e4e-6fc8-898a-ec8b934c1c61",
        //        "closedPnlEv":0,
        //        "closedSize":0,
        //        "code":0,
        //        "cumQty":0,
        //        "cumValueEv":0,
        //        "curAccBalanceEv":400079,
        //        "curAssignedPosBalanceEv":0,
        //        "curBonusBalanceEv":0,
        //        "curLeverageEr":0,
        //        "curPosSide":"None",
        //        "curPosSize":0,
        //        "curPosTerm":1,
        //        "curPosValueEv":0,
        //        "curRiskLimitEv":5000000000,
        //        "currency":"USD",
        //        "cxlRejReason":0,
        //        "displayQty":0,
        //        "execFeeEv":0,
        //        "execID":"00000000-0000-0000-0000-000000000000",
        //        "execPriceEp":0,
        //        "execQty":1,
        //        "execSeq":26975862338,
        //        "execStatus":"Canceled",
        //        "execValueEv":0,
        //        "feeRateEr":0,
        //        "leavesQty":0,
        //        "leavesValueEv":0,
        //        "message":"No error",
        //        "ordStatus":"Canceled",
        //        "ordType":"Limit",
        //        "orderID":"8141deb9-8f94-48f6-9421-a4e3a791537b",
        //        "orderQty":1,
        //        "pegOffsetValueEp":0,
        //        "priceEp":9521,
        //        "relatedPosTerm":1,
        //        "relatedReqNum":4,
        //        "side":"Buy",
        //        "slTrigger":"ByMarkPrice",
        //        "stopLossEp":0,
        //        "stopPxEp":0,
        //        "symbol":"ADAUSD",
        //        "takeProfitEp":0,
        //        "timeInForce":"GoodTillCancel",
        //        "tpTrigger":"ByLastPrice",
        //        "transactTimeNs":"1650450096108143014",
        //        "userID":2647224
        //    }
        // perpetual
        //    {
        //        "accountID" => 40183400003,
        //        "action" => "New",
        //        "actionBy" => "ByUser",
        //        "actionTimeNs" => "1674110665380190869",
        //        "addedSeq" => 678760103,
        //        "apRp" => "0",
        //        "bonusChangedAmountRv" => "0",
        //        "bpRp" => "0",
        //        "clOrdID" => '',
        //        "cl_req_code" => 0,
        //        "closedPnlRv" => "0",
        //        "closedSize" => "0",
        //        "code" => 0,
        //        "cumFeeRv" => "0",
        //        "cumQty" => "0.001",
        //        "cumValueRv" => "20.849",
        //        "curAccBalanceRv" => "19.9874906",
        //        "curAssignedPosBalanceRv" => "0",
        //        "curBonusBalanceRv" => "0",
        //        "curLeverageRr" => "-10",
        //        "curPosSide" => "Buy",
        //        "curPosSize" => "0.001",
        //        "curPosTerm" => 1,
        //        "curPosValueRv" => "20.849",
        //        "curRiskLimitRv" => "1000000",
        //        "currency" => "USDT",
        //        "cxlRejReason" => 0,
        //        "displayQty" => "0.001",
        //        "execFeeRv" => "0.0125094",
        //        "execID" => "b88d2950-04a2-52d8-8927-346059900242",
        //        "execPriceRp" => "20849",
        //        "execQty" => "0.001",
        //        "execSeq" => 678760103,
        //        "execStatus" => "TakerFill",
        //        "execValueRv" => "20.849",
        //        "feeRateRr" => "0.0006",
        //        "lastLiquidityInd" => "RemovedLiquidity",
        //        "leavesQty" => "0",
        //        "leavesValueRv" => "0",
        //        "message" => "No error",
        //        "ordStatus" => "Filled",
        //        "ordType" => "Market",
        //        "orderID" => "79620ed2-54c6-4645-a35c-7057e687c576",
        //        "orderQty" => "0.001",
        //        "pegOffsetProportionRr" => "0",
        //        "pegOffsetValueRp" => "0",
        //        "posSide" => "Long",
        //        "priceRp" => "21476.3",
        //        "relatedPosTerm" => 1,
        //        "relatedReqNum" => 4,
        //        "side" => "Buy",
        //        "slTrigger" => "ByMarkPrice",
        //        "stopLossRp" => "0",
        //        "stopPxRp" => "0",
        //        "symbol" => "BTCUSDT",
        //        "takeProfitRp" => "0",
        //        "timeInForce" => "ImmediateOrCancel",
        //        "tpTrigger" => "ByLastPrice",
        //        "tradeType" => "Trade",
        //        "transactTimeNs" => "1674110665387882268",
        //        "userID" => 4018340
        //    }
        //
        $id = $this->safe_string($order, 'orderID');
        $clientOrderId = $this->safe_string($order, 'clOrdID');
        if (($clientOrderId !== null) && (strlen($clientOrderId) < 1)) {
            $clientOrderId = null;
        }
        $marketId = $this->safe_string($order, 'symbol');
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $status = $this->parse_order_status($this->safe_string($order, 'ordStatus'));
        $side = $this->safe_string_lower($order, 'side');
        $type = $this->parseOrderType ($this->safe_string($order, 'ordType'));
        $price = $this->safe_string($order, 'priceRp', $this->from_ep($this->safe_string($order, 'priceEp'), $market));
        $amount = $this->safe_string($order, 'orderQty');
        $filled = $this->safe_string($order, 'cumQty');
        $remaining = $this->safe_string($order, 'leavesQty');
        $timestamp = $this->safe_integer_product($order, 'actionTimeNs', 0.000001);
        $cost = $this->safe_string($order, 'cumValueRv', $this->from_ev($this->safe_string($order, 'cumValueEv'), $market));
        $lastTradeTimestamp = $this->safe_integer_product($order, 'transactTimeNs', 0.000001);
        if ($lastTradeTimestamp === 0) {
            $lastTradeTimestamp = null;
        }
        $timeInForce = $this->parse_time_in_force($this->safe_string($order, 'timeInForce'));
        $stopPrice = $this->safe_string($order, 'stopPx');
        $postOnly = ($timeInForce === 'PO');
        return $this->safe_order(array(
            'info' => $order,
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'datetime' => $this->iso8601($timestamp),
            'timestamp' => $timestamp,
            'lastTradeTimestamp' => $lastTradeTimestamp,
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => $timeInForce,
            'postOnly' => $postOnly,
            'side' => $side,
            'price' => $price,
            'stopPrice' => $stopPrice,
            'triggerPrice' => $stopPrice,
            'amount' => $amount,
            'filled' => $filled,
            'remaining' => $remaining,
            'cost' => $cost,
            'average' => null,
            'status' => $status,
            'fee' => null,
            'trades' => null,
        ), $market);
    }

    public function handle_message(Client $client, $message) {
        // private spot update
        // {
        //     "orders" => array( closed => [ ], fills => [ ], open => array() ),
        //     "sequence" => 40435835,
        //     "timestamp" => "1650443245600839241",
        //     "type" => "snapshot",
        //     "wallets" => array(
        //       array(
        //         "balanceEv" => 0,
        //         "currency" => "BTC",
        //         "lastUpdateTimeNs" => "1650442638722099092",
        //         "lockedTradingBalanceEv" => 0,
        //         "lockedWithdrawEv" => 0,
        //         "userID" => 2647224
        //       ),
        //       {
        //         "balanceEv" => 1154232337,
        //         "currency" => "USDT",
        //         "lastUpdateTimeNs" => "1650442617610017597",
        //         "lockedTradingBalanceEv" => 0,
        //         "lockedWithdrawEv" => 0,
        //         "userID" => 2647224
        //       }
        //     )
        // }
        // private swap update
        // {
        //     "sequence" => 83839628,
        //     "timestamp" => "1650382581827447829",
        //     "type" => "snapshot",
        //     "accounts" => array(
        //       {
        //         "accountBalanceEv" => 0,
        //         "accountID" => 26472240001,
        //         "bonusBalanceEv" => 0,
        //         "currency" => "BTC",
        //         "totalUsedBalanceEv" => 0,
        //         "userID" => 2647224
        //       }
        //     ),
        //     "orders" => array(),
        //     "positions" => array(
        //       {
        //         "accountID" => 26472240001,
        //         "assignedPosBalanceEv" => 0,
        //         "avgEntryPriceEp" => 0,
        //         "bankruptCommEv" => 0,
        //         "bankruptPriceEp" => 0,
        //         "buyLeavesQty" => 0,
        //         "buyLeavesValueEv" => 0,
        //         "buyValueToCostEr" => 1150750,
        //         "createdAtNs" => 0,
        //         "crossSharedBalanceEv" => 0,
        //         "cumClosedPnlEv" => 0,
        //         "cumFundingFeeEv" => 0,
        //         "cumTransactFeeEv" => 0,
        //         "curTermRealisedPnlEv" => 0,
        //         "currency" => "BTC",
        //         "dataVer" => 2,
        //         "deleveragePercentileEr" => 0,
        //         "displayLeverageEr" => 10000000000,
        //         "estimatedOrdLossEv" => 0,
        //         "execSeq" => 0,
        //         "freeCostEv" => 0,
        //         "freeQty" => 0,
        //         "initMarginReqEr" => 1000000,
        //         "lastFundingTime" => "1640601827712091793",
        //         "lastTermEndTime" => 0,
        //         "leverageEr" => 0,
        //         "liquidationPriceEp" => 0,
        //         "maintMarginReqEr" => 500000,
        //         "makerFeeRateEr" => 0,
        //         "markPriceEp" => 507806777,
        //         "orderCostEv" => 0,
        //         "posCostEv" => 0,
        //         "positionMarginEv" => 0,
        //         "positionStatus" => "Normal",
        //         "riskLimitEv" => 10000000000,
        //         "sellLeavesQty" => 0,
        //         "sellLeavesValueEv" => 0,
        //         "sellValueToCostEr" => 1149250,
        //         "side" => "None",
        //         "size" => 0,
        //         "symbol" => "BTCUSD",
        //         "takerFeeRateEr" => 0,
        //         "term" => 1,
        //         "transactTimeNs" => 0,
        //         "unrealisedPnlEv" => 0,
        //         "updatedAtNs" => 0,
        //         "usedBalanceEv" => 0,
        //         "userID" => 2647224,
        //         "valueEv" => 0
        //       }
        //     )
        // }
        $id = $this->safe_string($message, 'id');
        if (is_array($client->subscriptions) && array_key_exists($id, $client->subscriptions)) {
            $method = $client->subscriptions[$id];
            unset($client->subscriptions[$id]);
            if ($method !== true) {
                $method($client, $message);
                return;
            }
        }
        $methodName = $this->safe_string($message, 'method', '');
        if ((is_array($message) && array_key_exists('market24h', $message)) || (is_array($message) && array_key_exists('spot_market24h', $message)) || (mb_strpos($methodName, 'perp_market24h_pack_p') !== false)) {
            $this->handle_ticker($client, $message);
            return;
        } elseif ((is_array($message) && array_key_exists('trades', $message)) || (is_array($message) && array_key_exists('trades_p', $message))) {
            $this->handle_trades($client, $message);
            return;
        } elseif ((is_array($message) && array_key_exists('kline', $message)) || (is_array($message) && array_key_exists('kline_p', $message))) {
            $this->handle_ohlcv($client, $message);
            return;
        } elseif ((is_array($message) && array_key_exists('book', $message)) || (is_array($message) && array_key_exists('orderbook_p', $message))) {
            $this->handle_order_book($client, $message);
            return;
        }
        if ((is_array($message) && array_key_exists('orders', $message)) || (is_array($message) && array_key_exists('orders_p', $message))) {
            $orders = $this->safe_value_2($message, 'orders', 'orders_p', array());
            $this->handle_orders($client, $orders);
        }
        if ((is_array($message) && array_key_exists('accounts', $message)) || (is_array($message) && array_key_exists('accounts_p', $message)) || (is_array($message) && array_key_exists('wallets', $message))) {
            $type = (is_array($message) && array_key_exists('accounts', $message)) ? 'swap' : 'spot';
            if (is_array($message) && array_key_exists('accounts_p', $message)) {
                $type = 'perpetual';
            }
            $accounts = $this->safe_value_n($message, array( 'accounts', 'accounts_p', 'wallets' ), array());
            $this->handle_balance($type, $client, $accounts);
        }
    }

    public function handle_authenticate(Client $client, $message) {
        //
        // {
        //     "error" => null,
        //     "id" => 1234,
        //     "result" => {
        //       "status" => "success"
        //     }
        // }
        //
        $result = $this->safe_value($message, 'result');
        $status = $this->safe_string($result, 'status');
        $messageHash = 'authenticated';
        if ($status === 'success') {
            $client->resolve ($message, $messageHash);
        } else {
            $error = new AuthenticationError ($this->id . ' ' . $this->json($message));
            $client->reject ($error, $messageHash);
            if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                unset($client->subscriptions[$messageHash]);
            }
        }
    }

    public function subscribe_private($type, $messageHash, $params = array ()) {
        return Async\async(function () use ($type, $messageHash, $params) {
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $url = $this->urls['api']['ws'];
            $requestId = $this->seconds();
            $settleIsUSDT = ($this->safe_value($params, 'settle', '') === 'USDT');
            $params = $this->omit($params, 'settle');
            $channel = 'aop.subscribe';
            if ($type === 'spot') {
                $channel = 'wo.subscribe';
            }
            if ($settleIsUSDT) {
                $channel = 'aop_p.subscribe';
            }
            $request = array(
                'id' => $requestId,
                'method' => $channel,
                'params' => array(),
            );
            $request = $this->extend($request, $params);
            return Async\await($this->watch($url, $messageHash, $request, $channel));
        }) ();
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $this->check_required_credentials();
            $url = $this->urls['api']['ws'];
            $client = $this->client($url);
            $requestId = $this->request_id();
            $messageHash = 'authenticated';
            $future = $this->safe_value($client->subscriptions, $messageHash);
            if ($future === null) {
                $expiryDelta = $this->safe_integer($this->options, 'expires', 120);
                $expiration = $this->seconds() . $expiryDelta;
                $payload = $this->apiKey . (string) $expiration;
                $signature = $this->hmac($this->encode($payload), $this->encode($this->secret), 'sha256');
                $method = 'user.auth';
                $request = array(
                    'method' => $method,
                    'params' => array( 'API', $this->apiKey, $signature, $expiration ),
                    'id' => $requestId,
                );
                $subscriptionHash = (string) $requestId;
                $message = $this->extend($request, $params);
                if (!(is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions))) {
                    $client->subscriptions[$subscriptionHash] = array($this, 'handle_authenticate');
                }
                $future = Async\await($this->watch($url, $messageHash, $message, $messageHash));
                $client->subscriptions[$messageHash] = $future;
            }
            return $future;
        }) ();
    }
}
