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

class oxfun extends \ccxt\async\oxfun {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'has' => array(
                'ws' => true,
                'watchTrades' => true,
                'watchTradesForSymbols' => true,
                'watchOrderBook' => true,
                'watchOrderBookForSymbols' => true,
                'watchOHLCV' => true,
                'watchOHLCVForSymbols' => true,
                'watchOrders' => true,
                'watchMyTrades' => false,
                'watchTicker' => true,
                'watchTickers' => true,
                'watchBidsAsks' => true,
                'watchBalance' => true,
                'createOrderWs' => true,
                'editOrderWs' => true,
                'cancelOrderWs' => true,
                'cancelOrdersWs' => true,
            ),
            'urls' => array(
                'api' => array(
                    'ws' => 'wss://api.ox.fun/v2/websocket',
                    'test' => 'wss://stgapi.ox.fun/v2/websocket',
                ),
            ),
            'options' => array(
                'timeframes' => array(
                    '1m' => '60s',
                    '3m' => '180s',
                    '5m' => '300s',
                    '15m' => '900s',
                    '30m' => '1800s',
                    '1h' => '3600s',
                    '2h' => '7200s',
                    '4h' => '14400s',
                    '6h' => '21600s',
                    '12h' => '43200s',
                    '1d' => '86400s',
                ),
                'watchOrderBook' => array(
                    'channel' => 'depth', // depth, depthL5, depthL10, depthL25
                ),
            ),
            'streaming' => array(
                'ping' => array($this, 'ping'),
                'keepAlive' => 50000,
            ),
        ));
    }

    public function subscribe_multiple($messageHashes, $argsArray, $params = array ()) {
        return Async\async(function () use ($messageHashes, $argsArray, $params) {
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => $argsArray,
            );
            return Async\await($this->watch_multiple($url, $messageHashes, $this->extend($request, $params), $messageHashes));
        }) ();
    }

    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://docs.ox.fun/?json#trade
             *
             * @param {string} $symbol unified market $symbol of the market trades were made in
             * @param {int} [$since] the earliest time in ms to fetch orders for
             * @param {int} [$limit] the maximum number of trade structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            return Async\await($this->watch_trades_for_symbols(array( $symbol ), $since, $limit, $params));
        }) ();
    }

    public function watch_trades_for_symbols(array $symbols, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             * get the list of most recent $trades for a particular $symbol
             *
             * @see https://docs.ox.fun/?json#trade
             *
             * @param {string[]} $symbols
             * @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
             * @param {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=public-$trades trade structures~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols, null, false);
            $args = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $messageHash = 'trades' . ':' . $symbol;
                $messageHashes[] = $messageHash;
                $marketId = $this->market_id($symbol);
                $arg = 'trade:' . $marketId;
                $args[] = $arg;
            }
            $trades = Async\await($this->subscribe_multiple($messageHashes, $args, $params));
            if ($this->newUpdates) {
                $first = $this->safe_dict($trades, 0, array());
                $tradeSymbol = $this->safe_string($first, 'symbol');
                $limit = $trades->getLimit ($tradeSymbol, $limit);
            }
            return $this->filter_by_since_limit($trades, $since, $limit, 'timestamp', true);
        }) ();
    }

    public function handle_trades(Client $client, $message) {
        //
        //     {
        //         table => 'trade',
        //         $data => array(
        //             {
        //                 side => 'SELL',
        //                 quantity => '0.074',
        //                 matchType => 'TAKER',
        //                 price => '3079.5',
        //                 marketCode => 'ETH-USD-SWAP-LIN',
        //                 tradeId => '400017157974517783',
        //                 timestamp => '1716124156643'
        //             }
        //         )
        //     }
        //
        $data = $this->safe_list($message, 'data', array());
        for ($i = 0; $i < count($data); $i++) {
            $trade = $this->safe_dict($data, $i, array());
            $parsedTrade = $this->parse_ws_trade($trade);
            $symbol = $this->safe_string($parsedTrade, 'symbol');
            $messageHash = 'trades:' . $symbol;
            if (!(is_array($this->trades) && array_key_exists($symbol, $this->trades))) {
                $tradesLimit = $this->safe_integer($this->options, 'tradesLimit', 1000);
                $this->trades[$symbol] = new ArrayCache ($tradesLimit);
            }
            $stored = $this->trades[$symbol];
            $stored->append ($parsedTrade);
            $client->resolve ($stored, $messageHash);
        }
    }

    public function parse_ws_trade($trade, $market = null): array {
        //
        //     {
        //         side => 'SELL',
        //         quantity => '0.074',
        //         matchType => 'TAKER',
        //         price => '3079.5',
        //         marketCode => 'ETH-USD-SWAP-LIN',
        //         tradeId => '400017157974517783',
        //         $timestamp => '1716124156643'
        //     }
        //
        $marketId = $this->safe_string($trade, 'marketCode');
        $market = $this->safe_market($marketId, $market);
        $timestamp = $this->safe_integer($trade, 'timestamp');
        return $this->safe_trade(array(
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $market['symbol'],
            'id' => $this->safe_string($trade, 'tradeId'),
            'order' => null,
            'type' => null,
            'takerOrMaker' => $this->safe_string_lower($trade, 'matchType'),
            'side' => $this->safe_string_lower($trade, 'side'),
            'price' => $this->safe_number($trade, 'price'),
            'amount' => $this->safe_number($trade, 'quantity'),
            'cost' => null,
            'fee' => null,
        ));
    }

    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://docs.ox.fun/?json#candles
             *
             * @param {string} $symbol unified $symbol of the $market to fetch OHLCV data for
             * @param {string} $timeframe the length of time each candle represents
             * @param {int} [$since] timestamp in ms of the earliest candle to fetch
             * @param {int} [$limit] the maximum amount of candles to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $timeframes = $this->safe_dict($this->options, 'timeframes', array());
            $interval = $this->safe_string($timeframes, $timeframe, $timeframe);
            $args = 'candles' . $interval . ':' . $market['id'];
            $messageHash = 'ohlcv:' . $symbol . ':' . $timeframe;
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => array( $args ),
            );
            $ohlcvs = Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
            if ($this->newUpdates) {
                $limit = $ohlcvs->getLimit ($symbol, $limit);
            }
            return $this->filter_by_since_limit($ohlcvs, $since, $limit, 0, true);
        }) ();
    }

    public function watch_ohlcv_for_symbols(array $symbolsAndTimeframes, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($symbolsAndTimeframes, $since, $limit, $params) {
            /**
             * watches historical candlestick data containing the open, high, low, and close price, and the volume of a market
             *
             * @see https://docs.ox.fun/?json#$candles
             *
             * @param {string[][]} $symbolsAndTimeframes array of arrays containing unified symbols and $timeframes to fetch OHLCV data for, example [['BTC/USDT', '1m'], ['LTC/USDT', '5m']]
             * @param {int} [$since] timestamp in ms of the earliest candle to fetch
             * @param {int} [$limit] the maximum amount of $candles to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @return {int[][]} A list of $candles ordered, open, high, low, close, volume
             */
            $symbolsLength = count($symbolsAndTimeframes);
            if ($symbolsLength === 0 || gettype($symbolsAndTimeframes[0]) !== 'array' || array_keys($symbolsAndTimeframes[0]) !== array_keys(array_keys($symbolsAndTimeframes[0]))) {
                throw new ArgumentsRequired($this->id . " watchOHLCVForSymbols() requires a an array of symbols and $timeframes, like  [['BTC/USDT:OX', '1m'], ['OX/USDT', '5m']]");
            }
            Async\await($this->load_markets());
            $args = array();
            $messageHashes = array();
            $timeframes = $this->safe_dict($this->options, 'timeframes', array());
            for ($i = 0; $i < count($symbolsAndTimeframes); $i++) {
                $symbolAndTimeframe = $symbolsAndTimeframes[$i];
                $sym = $symbolAndTimeframe[0];
                $tf = $symbolAndTimeframe[1];
                $marketId = $this->market_id($sym);
                $interval = $this->safe_string($timeframes, $tf, $tf);
                $arg = 'candles' . $interval . ':' . $marketId;
                $args[] = $arg;
                $messageHash = 'multi:ohlcv:' . $sym . ':' . $tf;
                $messageHashes[] = $messageHash;
            }
            list($symbol, $timeframe, $candles) = Async\await($this->subscribe_multiple($messageHashes, $args, $params));
            if ($this->newUpdates) {
                $limit = $candles->getLimit ($symbol, $limit);
            }
            $filtered = $this->filter_by_since_limit($candles, $since, $limit, 0, true);
            return $this->create_ohlcv_object($symbol, $timeframe, $filtered);
        }) ();
    }

    public function handle_ohlcv(Client $client, $message) {
        //
        //     {
        //         "table" => "candles60s",
        //         "data" => array(
        //             {
        //                 "marketCode" => "BTC-USD-SWAP-LIN",
        //                 "candle" => array(
        //                     "1594313762698", //timestamp
        //                     "9633.1",        //open
        //                     "9693.9",        //high
        //                     "9238.1",        //low
        //                     "9630.2",        //close
        //                     "45247",         //volume in OX
        //                     "5.3"            //volume in Contracts
        //                 )
        //             }
        //         )
        //     }
        //
        $table = $this->safe_string($message, 'table');
        $parts = explode('candles', $table);
        $timeframeId = $this->safe_string($parts, 1, '');
        $timeframe = $this->find_timeframe($timeframeId);
        $messageData = $this->safe_list($message, 'data', array());
        $data = $this->safe_dict($messageData, 0, array());
        $marketId = $this->safe_string($data, 'marketCode');
        $market = $this->safe_market($marketId);
        $symbol = $this->safe_symbol($marketId, $market);
        if (!(is_array($this->ohlcvs) && array_key_exists($symbol, $this->ohlcvs))) {
            $this->ohlcvs[$symbol] = array();
        }
        if (!(is_array($this->ohlcvs[$symbol]) && array_key_exists($timeframe, $this->ohlcvs[$symbol]))) {
            $limit = $this->safe_integer($this->options, 'OHLCVLimit', 1000);
            $this->ohlcvs[$symbol][$timeframe] = new ArrayCacheByTimestamp ($limit);
        }
        $candle = $this->safe_list($data, 'candle', array());
        $parsed = $this->parse_ws_ohlcv($candle, $market);
        $stored = $this->ohlcvs[$symbol][$timeframe];
        $stored->append ($parsed);
        $messageHash = 'ohlcv:' . $symbol . ':' . $timeframe;
        $client->resolve ($stored, $messageHash);
        // for multiOHLCV we need special object, to other "multi"
        // methods, because OHLCV response item does not contain $symbol
        // or $timeframe, thus otherwise it would be unrecognizable
        $messageHashForMulti = 'multi:' . $messageHash;
        $client->resolve (array( $symbol, $timeframe, $stored ), $messageHashForMulti);
    }

    public function parse_ws_ohlcv($ohlcv, ?array $market = null): array {
        //
        //     array(
        //         "1594313762698", //timestamp
        //         "9633.1",        //open
        //         "9693.9",        //high
        //         "9238.1",        //low
        //         "9630.2",        //close
        //         "45247",         //volume in OX
        //         "5.3"            //volume in Contracts
        //     )
        //
        return array(
            $this->safe_integer($ohlcv, 0),
            $this->safe_number($ohlcv, 1),
            $this->safe_number($ohlcv, 2),
            $this->safe_number($ohlcv, 3),
            $this->safe_number($ohlcv, 4),
            $this->safe_number($ohlcv, 6),
        );
    }

    public function watch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://docs.ox.fun/?json#fixed-size-order-book
             * @see https://docs.ox.fun/?json#full-order-book
             *
             * @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
             */
            return Async\await($this->watch_order_book_for_symbols(array( $symbol ), $limit, $params));
        }) ();
    }

    public function watch_order_book_for_symbols(array $symbols, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $limit, $params) {
            /**
             * watches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
             *
             * @see https://docs.ox.fun/?json#fixed-size-order-book
             * @see https://docs.ox.fun/?json#full-order-book
             *
             * @param {string[]} $symbols unified array of $symbols
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @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());
            $symbols = $this->market_symbols($symbols);
            $channel = 'depth';
            $options = $this->safe_dict($this->options, 'watchOrderBook', array());
            $defaultChannel = $this->safe_string($options, 'channel');
            if ($defaultChannel !== null) {
                $channel = $defaultChannel;
            } elseif ($limit !== null) {
                if ($limit <= 5) {
                    $channel = 'depthL5';
                } elseif ($limit <= 10) {
                    $channel = 'depthL10';
                } elseif ($limit <= 25) {
                    $channel = 'depthL25';
                }
            }
            $args = array();
            $messageHashes = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $symbol = $symbols[$i];
                $messageHash = 'orderbook:' . $symbol;
                $messageHashes[] = $messageHash;
                $marketId = $this->market_id($symbol);
                $arg = $channel . ':' . $marketId;
                $args[] = $arg;
            }
            $orderbook = Async\await($this->subscribe_multiple($messageHashes, $args, $params));
            return $orderbook->limit ();
        }) ();
    }

    public function handle_order_book(Client $client, $message) {
        //
        //     {
        //         "table" => "depth",
        //         "data" => array(
        //             "seqNum" => "100170478917895032",
        //             "asks" => array(
        //                 array( 0.01, 100500 ),
        //                 ...
        //             ),
        //             "bids" => array(
        //                 array( 69.69696, 69 ),
        //                 ...
        //             ),
        //             "checksum" => 261021645,
        //             "marketCode" => "OX-USDT",
        //             "timestamp" => 1716204786184
        //         ),
        //         "action" => "partial"
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $marketId = $this->safe_string($data, 'marketCode');
        $symbol = $this->safe_symbol($marketId);
        $timestamp = $this->safe_integer($data, 'timestamp');
        $messageHash = 'orderbook:' . $symbol;
        if (!(is_array($this->orderbooks) && array_key_exists($symbol, $this->orderbooks))) {
            $this->orderbooks[$symbol] = $this->order_book(array());
        }
        $orderbook = $this->orderbooks[$symbol];
        $snapshot = $this->parse_order_book($data, $symbol, $timestamp, 'asks', 'bids');
        $orderbook->reset ($snapshot);
        $orderbook['nonce'] = $this->safe_integer($data, 'seqNum');
        $this->orderbooks[$symbol] = $orderbook;
        $client->resolve ($orderbook, $messageHash);
    }

    public function watch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             *
             * @see https://docs.ox.fun/?json#$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
             * @param {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=$ticker-structure $ticker structure~
             */
            $ticker = Async\await($this->watch_tickers(array( $symbol ), $params));
            return $this->safe_value($ticker, $symbol);
        }) ();
    }

    public function watch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://docs.ox.fun/?json#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 {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            Async\await($this->load_markets());
            $allSymbols = ($symbols === null);
            $sym = $symbols;
            $args = array();
            if ($allSymbols) {
                $sym = $this->symbols;
                $args[] = 'ticker:all';
            }
            $messageHashes = array();
            for ($i = 0; $i < count($sym); $i++) {
                $symbol = $sym[$i];
                $messageHash = 'tickers' . ':' . $symbol;
                $messageHashes[] = $messageHash;
                $marketId = $this->market_id($symbol);
                if (!$allSymbols) {
                    $args[] = 'ticker:' . $marketId;
                }
            }
            $newTicker = Async\await($this->subscribe_multiple($messageHashes, $args, $params));
            if ($this->newUpdates) {
                $result = array();
                $result[$newTicker['symbol']] = $newTicker;
                return $result;
            }
            return $this->filter_by_array($this->tickers, 'symbol', $symbols);
        }) ();
    }

    public function handle_ticker(Client $client, $message) {
        //
        //     {
        //         "table" => "ticker",
        //         "data" => array(
        //             array(
        //                 "last" => "3088.6",
        //                 "open24h" => "3087.2",
        //                 "high24h" => "3142.0",
        //                 "low24h" => "3053.9",
        //                 "volume24h" => "450512672.1800",
        //                 "currencyVolume24h" => "1458.579",
        //                 "openInterest" => "3786.801",
        //                 "marketCode" => "ETH-USD-SWAP-LIN",
        //                 "timestamp" => "1716212747050",
        //                 "lastQty" => "0.813",
        //                 "markPrice" => "3088.6",
        //                 "lastMarkPrice" => "3088.6",
        //                 "indexPrice" => "3086.5"
        //             ),
        //             ...
        //         )
        //     }
        //
        $data = $this->safe_list($message, 'data', array());
        for ($i = 0; $i < count($data); $i++) {
            $rawTicker = $this->safe_dict($data, $i, array());
            $ticker = $this->parse_ticker($rawTicker);
            $symbol = $ticker['symbol'];
            $messageHash = 'tickers:' . $symbol;
            $this->tickers[$symbol] = $ticker;
            $client->resolve ($ticker, $messageHash);
        }
    }

    public function watch_bids_asks(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://docs.ox.fun/?json#best-bid-ask
             *
             * 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, null, false);
            $messageHashes = array();
            $args = array();
            for ($i = 0; $i < count($symbols); $i++) {
                $market = $this->market($symbols[$i]);
                $args[] = 'bestBidAsk:' . $market['id'];
                $messageHashes[] = 'bidask:' . $market['symbol'];
            }
            $newTickers = Async\await($this->subscribe_multiple($messageHashes, $args, $params));
            if ($this->newUpdates) {
                $tickers = array();
                $tickers[$newTickers['symbol']] = $newTickers;
                return $tickers;
            }
            return $this->filter_by_array($this->bidsasks, 'symbol', $symbols);
        }) ();
    }

    public function handle_bid_ask(Client $client, $message) {
        //
        //     {
        //       "table" => "bestBidAsk",
        //       "data" => {
        //         "ask" => array(
        //           19045.0,
        //           1.0
        //         ),
        //         "checksum" => 3790706311,
        //         "marketCode" => "BTC-USD-SWAP-LIN",
        //         "bid" => array(
        //           19015.0,
        //           1.0
        //         ),
        //         "timestamp" => "1665456882928"
        //       }
        //     }
        //
        $data = $this->safe_dict($message, 'data', array());
        $parsedTicker = $this->parse_ws_bid_ask($data);
        $symbol = $parsedTicker['symbol'];
        $this->bidsasks[$symbol] = $parsedTicker;
        $messageHash = 'bidask:' . $symbol;
        $client->resolve ($parsedTicker, $messageHash);
    }

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

    public function watch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             *
             * @see https://docs.ox.fun/?json#balance-channel
             *
             * 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 {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            $this->authenticate();
            $args = 'balance:all';
            $messageHash = 'balance';
            $url = $this->urls['api']['ws'];
            $request = array(
                'op' => 'subscribe',
                'args' => array( $args ),
            );
            return Async\await($this->watch($url, $messageHash, $this->extend($request, $params), $messageHash));
        }) ();
    }

    public function handle_balance($client, $message) {
        //
        //     {
        //         "table" => "balance",
        //         "accountId" => "106464",
        //         "timestamp" => "1716549132780",
        //         "tradeType" => "PORTFOLIO",
        //         "data" => array(
        //             array(
        //                 "instrumentId" => "xOX",
        //                 "total" => "23.375591220",
        //                 "available" => "23.375591220",
        //                 "reserved" => "0",
        //                 "quantityLastUpdated" => "1716509744262",
        //                 "locked" => "0"
        //             ),
        //             ...
        //         )
        //     }
        //
        $balances = $this->safe_list($message, 'data');
        $timestamp = $this->safe_integer($message, 'timestamp');
        $this->balance['info'] = $message;
        $this->balance['timestamp'] = $timestamp;
        $this->balance['datetime'] = $this->iso8601($timestamp);
        for ($i = 0; $i < count($balances); $i++) {
            $balance = $this->safe_dict($balances, $i, array());
            $currencyId = $this->safe_string($balance, 'instrumentId');
            $code = $this->safe_currency_code($currencyId);
            if (!(is_array($this->balance) && array_key_exists($code, $this->balance))) {
                $this->balance[$code] = $this->account();
            }
            $account = $this->balance[$code];
            $account['total'] = $this->safe_string($balance, 'total');
            $account['used'] = $this->safe_string($balance, 'reserved');
            $account['free'] = $this->safe_string($balance, 'available');
            $this->balance[$code] = $account;
        }
        $this->balance = $this->safe_balance($this->balance);
        $client->resolve ($this->balance, 'balance');
    }

    public function watch_positions(?array $symbols = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $since, $limit, $params) {
            /**
             *
             * @see https://docs.ox.fun/?json#position-channel
             *
             * watch all open positions
             * @param {string[]|null} $symbols list of unified market $symbols
             * @param $since
             * @param $limit
             * @param {array} $params extra parameters specific to the exchange API endpoint
             * @param {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @return {array[]} a list of {@link https://docs.ccxt.com/en/latest/manual.html#position-structure position structure}
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $allSymbols = ($symbols === null);
            $sym = $symbols;
            $args = array();
            if ($allSymbols) {
                $sym = $this->symbols;
                $args[] = 'position:all';
            }
            $messageHashes = array();
            for ($i = 0; $i < count($sym); $i++) {
                $symbol = $sym[$i];
                $messageHash = 'positions' . ':' . $symbol;
                $messageHashes[] = $messageHash;
                $marketId = $this->market_id($symbol);
                if (!$allSymbols) {
                    $args[] = 'position:' . $marketId;
                }
            }
            $newPositions = Async\await($this->subscribe_multiple($messageHashes, $args, $params));
            if ($this->newUpdates) {
                return $newPositions;
            }
            return $this->filter_by_symbols_since_limit($this->positions, $symbols, $since, $limit, true);
        }) ();
    }

    public function handle_positions(Client $client, $message) {
        //
        //     {
        //         "table" => "position",
        //         "accountId" => "106464",
        //         "timestamp" => "1716550771582",
        //         "data" => array(
        //             {
        //                 "instrumentId" => "ETH-USD-SWAP-LIN",
        //                 "quantity" => "0.01",
        //                 "lastUpdated" => "1716550757299",
        //                 "contractValCurrency" => "ETH",
        //                 "entryPrice" => "3709.6",
        //                 "positionPnl" => "-5.000",
        //                 "estLiquidationPrice" => "743.4",
        //                 "margin" => "0",
        //                 "leverage" => "0"
        //             }
        //         )
        //     }
        //
        if ($this->positions === null) {
            $this->positions = new ArrayCacheBySymbolBySide ();
        }
        $cache = $this->positions;
        $data = $this->safe_list($message, 'data', array());
        for ($i = 0; $i < count($data); $i++) {
            $rawPosition = $this->safe_dict($data, $i, array());
            $position = $this->parse_ws_position($rawPosition);
            $symbol = $position['symbol'];
            $messageHash = 'positions:' . $symbol;
            $cache->append ($position);
            $client->resolve ($position, $messageHash);
        }
    }

    public function parse_ws_position($position, ?array $market = null) {
        //
        //     {
        //         "instrumentId" => "ETH-USD-SWAP-LIN",
        //         "quantity" => "0.01",
        //         "lastUpdated" => "1716550757299",
        //         "contractValCurrency" => "ETH",
        //         "entryPrice" => "3709.6",
        //         "positionPnl" => "-5.000",
        //         "estLiquidationPrice" => "743.4",
        //         "margin" => "0", // Currently always reports 0
        //         "leverage" => "0" // Currently always reports 0
        //     }
        //
        $marketId = $this->safe_string($position, 'instrumentId');
        $market = $this->safe_market($marketId, $market);
        return $this->safe_position(array(
            'info' => $position,
            'id' => null,
            'symbol' => $market['symbol'],
            'notional' => null,
            'marginMode' => 'cross',
            'liquidationPrice' => $this->safe_number($position, 'estLiquidationPrice'),
            'entryPrice' => $this->safe_number($position, 'entryPrice'),
            'unrealizedPnl' => $this->safe_number($position, 'positionPnl'),
            'realizedPnl' => null,
            'percentage' => null,
            'contracts' => $this->safe_number($position, 'quantity'),
            'contractSize' => null,
            'markPrice' => null,
            'lastPrice' => null,
            'side' => null,
            'hedged' => null,
            'timestamp' => null,
            'datetime' => null,
            'lastUpdateTimestamp' => $this->safe_integer($position, 'lastUpdated'),
            'maintenanceMargin' => null,
            'maintenanceMarginPercentage' => null,
            'collateral' => null,
            'initialMargin' => null,
            'initialMarginPercentage' => null,
            'leverage' => null,
            'marginRatio' => null,
            'stopLossPrice' => null,
            'takeProfitPrice' => null,
        ));
    }

    public function watch_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * watches information on multiple $orders made by the user
             *
             * @see https://docs.ox.fun/?json#order-channel
             *
             * @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 {int|string} [$params->tag] If given it will be echoed in the reply and the max size of tag is 32
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $messageHash = 'orders';
            $args = 'order:';
            $market = $this->safe_market($symbol);
            if ($symbol === null) {
                $args .= 'all';
            } else {
                $messageHash .= ':' . $symbol;
                $args .= ':' . $market['id'];
            }
            $request = array(
                'op' => 'subscribe',
                'args' => array(
                    $args,
                ),
            );
            $url = $this->urls['api']['ws'];
            $orders = Async\await($this->watch($url, $messageHash, $request, $messageHash));
            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) {
        //
        //     {
        //         "table" => "order",
        //         "data" => array(
        //             {
        //                 "accountId" => "106464",
        //                 "clientOrderId" => "1716713676233",
        //                 "orderId" => "1000116921319",
        //                 "price" => "1000.0",
        //                 "quantity" => "0.01",
        //                 "amount" => "0.0",
        //                 "side" => "BUY",
        //                 "status" => "OPEN",
        //                 "marketCode" => "ETH-USD-SWAP-LIN",
        //                 "timeInForce" => "MAKER_ONLY",
        //                 "timestamp" => "1716713677834",
        //                 "remainQuantity" => "0.01",
        //                 "limitPrice" => "1000.0",
        //                 "notice" => "OrderOpened",
        //                 "orderType" => "LIMIT",
        //                 "isTriggered" => "false",
        //                 "displayQuantity" => "0.01"
        //             }
        //         )
        //     }
        //
        $data = $this->safe_list($message, 'data', array());
        $messageHash = 'orders';
        if ($this->orders === null) {
            $limit = $this->safe_integer($this->options, 'ordersLimit', 1000);
            $this->orders = new ArrayCacheBySymbolById ($limit);
        }
        $orders = $this->orders;
        for ($i = 0; $i < count($data); $i++) {
            $order = $this->safe_dict($data, $i, array());
            $parsedOrder = $this->parse_order($order);
            $orders->append ($parsedOrder);
            $messageHash .= ':' . $parsedOrder['symbol'];
            $client->resolve ($this->orders, $messageHash);
        }
    }

    public function create_order_ws(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $type, $side, $amount, $price, $params) {
            /**
             *
             * @see https://docs.ox.fun/?json#order-commands
             *
             * create a trade order
             * @param {string} $symbol unified $symbol of the market to create an order in
             * @param {string} $type 'market', 'limit', 'STOP_LIMIT' or 'STOP_MARKET'
             * @param {string} $side 'buy' or 'sell'
             * @param {float} $amount how much of currency you want to trade in units of base currency
             * @param {float} [$price] the $price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int} [$params->clientOrderId] a unique id for the order
             * @param {int} [$params->timestamp] in milliseconds. If an order reaches the matching engine and the current $timestamp exceeds $timestamp . recvWindow, then the order will be rejected.
             * @param {int} [$params->recvWindow] in milliseconds. If an order reaches the matching engine and the current $timestamp exceeds $timestamp . recvWindow, then the order will be rejected. If $timestamp is provided without recvWindow, then a default recvWindow of 1000ms is used.
             * @param {float} [$params->cost] the quote quantity that can be used alternative for the $amount for market buy orders
             * @param {float} [$params->triggerPrice] The $price at which a trigger order is triggered at
             * @param {float} [$params->limitPrice] Limit $price for the STOP_LIMIT order
             * @param {bool} [$params->postOnly] if true, the order will only be posted if it will be a maker order
             * @param {string} [$params->timeInForce] GTC (default), IOC, FOK, PO, MAKER_ONLY or MAKER_ONLY_REPRICE (reprices order to the best maker only $price if the specified $price were to lead to a taker trade)
             * @param {string} [$params->selfTradePreventionMode] NONE, EXPIRE_MAKER, EXPIRE_TAKER or EXPIRE_BOTH for more info check here array(@link https://docs.ox.fun/?json#self-trade-prevention-modes)
             * @param {string} [$params->displayQuantity] for an iceberg order, pass both quantity and displayQuantity fields in the order $request
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $messageHash = (string) $this->nonce();
            $request = array(
                'op' => 'placeorder',
                'tag' => $messageHash,
            );
            $params = $this->omit($params, 'tag');
            $orderRequest = $this->create_order_request($symbol, $type, $side, $amount, $price, $params);
            $timestamp = $this->safe_integer($orderRequest, 'timestamp');
            if ($timestamp === null) {
                $orderRequest['timestamp'] = $this->milliseconds();
            }
            $request['data'] = $orderRequest;
            $url = $this->urls['api']['ws'];
            return Async\await($this->watch($url, $messageHash, $request, $messageHash));
        }) ();
    }

    public function edit_order_ws(string $id, string $symbol, string $type, string $side, ?float $amount = null, ?float $price = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($id, $symbol, $type, $side, $amount, $price, $params) {
            /**
             * edit a trade order
             *
             * @see https://docs.ox.fun/?json#modify-order
             *
             * @param {string} $id order $id
             * @param {string} $symbol unified $symbol of the market to create an order in
             * @param {string} $type 'market' or 'limit'
             * @param {string} $side 'buy' or 'sell'
             * @param {float} $amount how much of the currency you want to trade in units of the base currency
             * @param {float|null} [$price] the $price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int} [$params->timestamp] in milliseconds. If an order reaches the matching engine and the current $timestamp exceeds $timestamp . recvWindow, then the order will be rejected.
             * @param {int} [$params->recvWindow] in milliseconds. If an order reaches the matching engine and the current $timestamp exceeds $timestamp . recvWindow, then the order will be rejected. If $timestamp is provided without recvWindow, then a default recvWindow of 1000ms is used.
             * @return {array} an ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
             */
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $messageHash = (string) $this->nonce();
            $request = array(
                'op' => 'modifyorder',
                'tag' => $messageHash,
            );
            $params = $this->omit($params, 'tag');
            $orderRequest = $this->create_order_request($symbol, $type, $side, $amount, $price, $params);
            $orderRequest = $this->extend($orderRequest, array( 'orderId' => $id ));
            $timestamp = $this->safe_integer($orderRequest, 'timestamp');
            if ($timestamp === null) {
                $orderRequest['timestamp'] = $this->milliseconds();
            }
            $request['data'] = $orderRequest;
            $url = $this->urls['api']['ws'];
            return Async\await($this->watch($url, $messageHash, $request, $messageHash));
        }) ();
    }

    public function handle_place_orders(Client $client, $message) {
        //
        //     {
        //         "event" => "placeorder",
        //         "submitted" => true,
        //         "tag" => "1716934577",
        //         "timestamp" => "1716932973899",
        //         "data" => {
        //             "marketCode" => "ETH-USD-SWAP-LIN",
        //             "side" => "BUY",
        //             "orderType" => "LIMIT",
        //             "quantity" => "0.010",
        //             "timeInForce" => "GTC",
        //             "price" => "400.0",
        //             "limitPrice" => "400.0",
        //             "orderId" => "1000117429736",
        //             "source" => 13
        //         }
        //     }
        //
        //
        // Failure response format
        //     {
        //         "event" => "placeorder",
        //         "submitted" => false,
        //         "message" => "JSON $data format is invalid",
        //         "code" => "20009",
        //         "timestamp" => "1716932877381"
        //     }
        //
        $messageHash = $this->safe_string($message, 'tag');
        $submitted = $this->safe_bool($message, 'submitted');
        // filter out partial errors
        if (!$submitted) {
            $method = $this->safe_string($message, 'event');
            $stringMsg = $this->json($message);
            $code = $this->safe_integer($message, 'code');
            $this->handle_errors($code, '', $client->url, $method, array(), $stringMsg, $message, array(), array());
        }
        $data = $this->safe_value($message, 'data', array());
        $order = $this->parse_order($data);
        $client->resolve ($order, $messageHash);
    }

    public function cancel_order_ws(string $id, ?string $symbol = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             *
             * @see https://docs.ox.fun/?json#cancel-order
             *
             * cancels an open order
             * @param {string} $id order $id
             * @param {string} $symbol unified market $symbol, default is null
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an list of ~@link https://docs.ccxt.com/#/?$id=order-structure order structures~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' cancelOrderWs() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $messageHash = (string) $this->nonce();
            $data = array(
                'marketCode' => $this->market_id($symbol),
                'orderId' => $id,
            );
            $request = array(
                'op' => 'cancelorder',
                'tag' => $messageHash,
                'data' => $data,
            );
            $url = $this->urls['api']['ws'];
            return Async\await($this->watch($url, $messageHash, $request, $messageHash));
        }) ();
    }

    public function cancel_orders_ws(array $ids, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($ids, $symbol, $params) {
            /**
             *
             * @see https://www.okx.com/docs-v5/en/#order-book-trading-trade-ws-mass-cancel-order
             *
             * cancel multiple orders
             * @param {string[]} $ids order $ids
             * @param {string} $symbol unified market $symbol, default is null
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            $idsLength = count($ids);
            if ($idsLength > 20) {
                throw new BadRequest($this->id . ' cancelOrdersWs() accepts up to 20 $ids at a time');
            }
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' cancelOrdersWs() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            Async\await($this->authenticate());
            $messageHash = (string) $this->nonce();
            $marketId = $this->market_id($symbol);
            $dataArray = array();
            for ($i = 0; $i < $idsLength; $i++) {
                $data = array(
                    'instId' => $marketId,
                    'ordId' => $ids[$i],
                );
                $dataArray[] = $data;
            }
            $request = array(
                'op' => 'cancelorders',
                'tag' => $messageHash,
                'dataArray' => $dataArray,
            );
            $url = $this->urls['api']['ws'];
            return Async\await($this->watch($url, $messageHash, $this->deep_extend($request, $params), $messageHash));
        }) ();
    }

    public function authenticate($params = array ()) {
        return Async\async(function () use ($params) {
            $url = $this->urls['api']['ws'];
            $client = $this->client($url);
            $messageHash = 'authenticated';
            $future = $client->reusableFuture ($messageHash);
            $authenticated = $this->safe_dict($client->subscriptions, $messageHash);
            if ($authenticated === null) {
                $this->check_required_credentials();
                $timestamp = $this->milliseconds();
                $payload = (string) $timestamp . 'GET/auth/self/verify';
                $signature = $this->hmac($this->encode($payload), $this->encode($this->secret), 'sha256', 'base64');
                $request = array(
                    'op' => 'login',
                    'data' => array(
                        'apiKey' => $this->apiKey,
                        'timestamp' => $timestamp,
                        'signature' => $signature,
                    ),
                );
                $message = $this->extend($request, $params);
                $this->watch($url, $messageHash, $message, $messageHash);
            }
            return Async\await($future);
        }) ();
    }

    public function handle_authentication_message(Client $client, $message) {
        $authenticated = $this->safe_bool($message, 'success', false);
        $messageHash = 'authenticated';
        if ($authenticated) {
            // we resolve the $future here permanently so authentication only happens once
            $future = $this->safe_dict($client->futures, $messageHash);
            $future->resolve (true);
        } else {
            $error = new AuthenticationError ($this->json($message));
            $client->reject ($error, $messageHash);
            if (is_array($client->subscriptions) && array_key_exists($messageHash, $client->subscriptions)) {
                unset($client->subscriptions[$messageHash]);
            }
        }
    }

    public function ping(Client $client) {
        return 'ping';
    }

    public function handle_pong(Client $client, $message) {
        $client->lastPong = $this->milliseconds();
        return $message;
    }

    public function handle_message(Client $client, $message) {
        if ($message === 'pong') {
            $this->handle_pong($client, $message);
            return;
        }
        $table = $this->safe_string($message, 'table');
        $data = $this->safe_list($message, 'data', array());
        $event = $this->safe_string($message, 'event');
        if (($table !== null) && ($data !== null)) {
            if ($table === 'trade') {
                $this->handle_trades($client, $message);
            }
            if ($table === 'ticker') {
                $this->handle_ticker($client, $message);
            }
            if (mb_strpos($table, 'candles') > -1) {
                $this->handle_ohlcv($client, $message);
            }
            if (mb_strpos($table, 'depth') > -1) {
                $this->handle_order_book($client, $message);
            }
            if (mb_strpos($table, 'balance') > -1) {
                $this->handle_balance($client, $message);
            }
            if (mb_strpos($table, 'position') > -1) {
                $this->handle_positions($client, $message);
            }
            if (mb_strpos($table, 'order') > -1) {
                $this->handle_orders($client, $message);
            }
            if ($table === 'bestBidAsk') {
                $this->handle_bid_ask($client, $message);
            }
        } else {
            if ($event === 'login') {
                $this->handle_authentication_message($client, $message);
            }
            if (($event === 'placeorder') || ($event === 'modifyorder') || ($event === 'cancelorder')) {
                $this->handle_place_orders($client, $message);
            }
        }
    }
}
