<?php

namespace ccxt;

// 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\abstract\cex as Exchange;

class cex extends Exchange {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'id' => 'cex',
            'name' => 'CEX.IO',
            'countries' => array( 'GB', 'EU', 'CY', 'RU' ),
            'rateLimit' => 300, // 200 req/min
            'pro' => true,
            'has' => array(
                'CORS' => null,
                'spot' => true,
                'margin' => false, // has, but not through api
                'swap' => false,
                'future' => false,
                'option' => false,
                'addMargin' => false,
                'borrowCrossMargin' => false,
                'borrowIsolatedMargin' => false,
                'borrowMargin' => false,
                'cancelAllOrders' => true,
                'cancelOrder' => true,
                'closeAllPositions' => false,
                'closePosition' => false,
                'createOrder' => true,
                'createOrderWithTakeProfitAndStopLoss' => false,
                'createOrderWithTakeProfitAndStopLossWs' => false,
                'createPostOnlyOrder' => false,
                'createReduceOnlyOrder' => false,
                'createStopOrder' => true,
                'createTriggerOrder' => true,
                'fetchAccounts' => true,
                'fetchBalance' => true,
                'fetchBorrowInterest' => false,
                'fetchBorrowRate' => false,
                'fetchBorrowRateHistories' => false,
                'fetchBorrowRateHistory' => false,
                'fetchBorrowRates' => false,
                'fetchBorrowRatesPerSymbol' => false,
                'fetchClosedOrder' => true,
                'fetchClosedOrders' => true,
                'fetchCrossBorrowRate' => false,
                'fetchCrossBorrowRates' => false,
                'fetchCurrencies' => true,
                'fetchDepositAddress' => true,
                'fetchDepositsWithdrawals' => true,
                'fetchFundingHistory' => false,
                'fetchFundingInterval' => false,
                'fetchFundingIntervals' => false,
                'fetchFundingRate' => false,
                'fetchFundingRateHistory' => false,
                'fetchFundingRates' => false,
                'fetchGreeks' => false,
                'fetchIndexOHLCV' => false,
                'fetchIsolatedBorrowRate' => false,
                'fetchIsolatedBorrowRates' => false,
                'fetchIsolatedPositions' => false,
                'fetchLedger' => true,
                'fetchLeverage' => false,
                'fetchLeverages' => false,
                'fetchLeverageTiers' => false,
                'fetchLiquidations' => false,
                'fetchLongShortRatio' => false,
                'fetchLongShortRatioHistory' => false,
                'fetchMarginAdjustmentHistory' => false,
                'fetchMarginMode' => false,
                'fetchMarginModes' => false,
                'fetchMarketLeverageTiers' => false,
                'fetchMarkets' => true,
                'fetchMarkOHLCV' => false,
                'fetchMarkPrices' => false,
                'fetchMyLiquidations' => false,
                'fetchMySettlementHistory' => false,
                'fetchOHLCV' => true,
                'fetchOpenInterest' => false,
                'fetchOpenInterestHistory' => false,
                'fetchOpenInterests' => false,
                'fetchOpenOrder' => true,
                'fetchOpenOrders' => true,
                'fetchOption' => false,
                'fetchOptionChain' => false,
                'fetchOrderBook' => true,
                'fetchPosition' => false,
                'fetchPositionHistory' => false,
                'fetchPositionMode' => false,
                'fetchPositions' => false,
                'fetchPositionsForSymbol' => false,
                'fetchPositionsHistory' => false,
                'fetchPositionsRisk' => false,
                'fetchPremiumIndexOHLCV' => false,
                'fetchSettlementHistory' => false,
                'fetchTicker' => true,
                'fetchTickers' => true,
                'fetchTime' => true,
                'fetchTrades' => true,
                'fetchTradingFees' => true,
                'fetchVolatilityHistory' => false,
                'reduceMargin' => false,
                'repayCrossMargin' => false,
                'repayIsolatedMargin' => false,
                'repayMargin' => false,
                'setLeverage' => false,
                'setMargin' => false,
                'setMarginMode' => false,
                'setPositionMode' => false,
                'transfer' => true,
            ),
            'urls' => array(
                'logo' => 'https://user-images.githubusercontent.com/1294454/27766442-8ddc33b0-5ed8-11e7-8b98-f786aef0f3c9.jpg',
                'api' => array(
                    'public' => 'https://trade.cex.io/api/spot/rest-public',
                    'private' => 'https://trade.cex.io/api/spot/rest',
                ),
                'www' => 'https://cex.io',
                'doc' => 'https://trade.cex.io/docs/',
                'fees' => array(
                    'https://cex.io/fee-schedule',
                    'https://cex.io/limits-commissions',
                ),
                'referral' => 'https://cex.io/r/0/up105393824/0/',
            ),
            'api' => array(
                'public' => array(
                    'get' => array(),
                    'post' => array(
                        'get_server_time' => 1,
                        'get_pairs_info' => 1,
                        'get_currencies_info' => 1,
                        'get_processing_info' => 10,
                        'get_ticker' => 1,
                        'get_trade_history' => 1,
                        'get_order_book' => 1,
                        'get_candles' => 1,
                    ),
                ),
                'private' => array(
                    'get' => array(),
                    'post' => array(
                        'get_my_current_fee' => 5,
                        'get_fee_strategy' => 1,
                        'get_my_volume' => 5,
                        'do_create_account' => 1,
                        'get_my_account_status_v3' => 5,
                        'get_my_wallet_balance' => 5,
                        'get_my_orders' => 5,
                        'do_my_new_order' => 1,
                        'do_cancel_my_order' => 1,
                        'do_cancel_all_orders' => 5,
                        'get_order_book' => 1,
                        'get_candles' => 1,
                        'get_trade_history' => 1,
                        'get_my_transaction_history' => 1,
                        'get_my_funding_history' => 5,
                        'do_my_internal_transfer' => 1,
                        'get_processing_info' => 10,
                        'get_deposit_address' => 5,
                        'do_deposit_funds_from_wallet' => 1,
                        'do_withdrawal_funds_to_wallet' => 1,
                    ),
                ),
            ),
            'features' => array(
                'spot' => array(
                    'sandbox' => false,
                    'createOrder' => array(
                        'marginMode' => false,
                        'triggerPrice' => true,
                        'triggerPriceType' => null,
                        'triggerDirection' => false,
                        'stopLossPrice' => false, // todo
                        'takeProfitPrice' => false, // todo
                        'attachedStopLossTakeProfit' => null,
                        'timeInForce' => array(
                            'IOC' => true,
                            'FOK' => true,
                            'PO' => false, // todo check
                            'GTD' => true,
                        ),
                        'hedged' => false,
                        'leverage' => false,
                        'marketBuyRequiresPrice' => false,
                        'marketBuyByCost' => true, // todo check
                        'selfTradePrevention' => false,
                        'trailing' => false,
                        'iceberg' => false,
                    ),
                    'createOrders' => null,
                    'fetchMyTrades' => null,
                    'fetchOrder' => null,
                    'fetchOpenOrders' => array(
                        'marginMode' => false,
                        'limit' => 1000,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOrders' => null,
                    'fetchClosedOrders' => array(
                        'marginMode' => false,
                        'limit' => 1000,
                        'daysBack' => 100000,
                        'daysBackCanceled' => 1,
                        'untilDays' => 100000,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOHLCV' => array(
                        'limit' => 1000,
                    ),
                ),
                'swap' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
                'future' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
            ),
            'precisionMode' => TICK_SIZE,
            'exceptions' => array(
                'exact' => array(),
                'broad' => array(
                    'You have negative balance on following accounts' => '\\ccxt\\InsufficientFunds',
                    'Mandatory parameter side should be one of BUY,SELL' => '\\ccxt\\BadRequest',
                    'API orders from Main account are not allowed' => '\\ccxt\\BadRequest',
                    'check failed' => '\\ccxt\\BadRequest',
                    'Insufficient funds' => '\\ccxt\\InsufficientFunds',
                    'Get deposit address for main account is not allowed' => '\\ccxt\\PermissionDenied',
                    'Market Trigger orders are not allowed' => '\\ccxt\\BadRequest', // for some reason, triggerPrice does not work for market orders
                    'key not passed or incorrect' => '\\ccxt\\AuthenticationError',
                ),
            ),
            'timeframes' => array(
                '1m' => '1m',
                '5m' => '5m',
                '15m' => '15m',
                '30m' => '30m',
                '1h' => '1h',
                '2h' => '2h',
                '4h' => '4h',
                '1d' => '1d',
            ),
            'options' => array(
                'networks' => array(
                    'BTC' => 'bitcoin',
                    'ERC20' => 'ERC20',
                    'BSC20' => 'binancesmartchain',
                    'DOGE' => 'dogecoin',
                    'ALGO' => 'algorand',
                    'XLM' => 'stellar',
                    'ATOM' => 'cosmos',
                    'LTC' => 'litecoin',
                    'XRP' => 'ripple',
                    'FTM' => 'fantom',
                    'MINA' => 'mina',
                    'THETA' => 'theta',
                    'XTZ' => 'tezos',
                    'TIA' => 'celestia',
                    'CRONOS' => 'cronos', // CRC20
                    'MATIC' => 'polygon',
                    'TON' => 'ton',
                    'TRC20' => 'tron',
                    'SOLANA' => 'solana',
                    'SGB' => 'songbird',
                    'DYDX' => 'dydx',
                    'DASH' => 'dash',
                    'ZIL' => 'zilliqa',
                    'EOS' => 'eos',
                    'AVALANCHEC' => 'avalanche',
                    'ETHPOW' => 'ethereumpow',
                    'NEAR' => 'near',
                    'ARB' => 'arbitrum',
                    'DOT' => 'polkadot',
                    'OPT' => 'optimism',
                    'INJ' => 'injective',
                    'ADA' => 'cardano',
                    'ONT' => 'ontology',
                    'ICP' => 'icp',
                    'KAVA' => 'kava',
                    'KSM' => 'kusama',
                    'SEI' => 'sei',
                    // 'OSM' => 'osmosis',
                    'NEO' => 'neo',
                    'NEO3' => 'neo3',
                    // 'TERRAOLD' => 'terra', // tbd
                    // 'TERRA' => 'terra2', // tbd
                    // 'EVER' => 'everscale', // tbd
                    'XDC' => 'xdc',
                ),
            ),
        ));
    }

    public function fetch_currencies($params = array ()): ?array {
        /**
         * fetches all available currencies on an exchange
         *
         * @see https://trade.cex.io/docs/#rest-public-api-calls-currencies-info
         *
         * @param {dict} [$params] extra parameters specific to the exchange API endpoint
         * @return {dict} an associative dictionary of currencies
         */
        $promises = array();
        $promises[] = $this->publicPostGetCurrenciesInfo ($params);
        //
        //    {
        //        "ok" => "ok",
        //        "data" => [
        //            array(
        //                "currency" => "ZAP",
        //                "fiat" => false,
        //                "precision" => "8",
        //                "walletPrecision" => "6",
        //                "walletDeposit" => true,
        //                "walletWithdrawal" => true
        //            ),
        //            ...
        //
        $promises[] = $this->publicPostGetProcessingInfo ($params);
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "ADA" => {
        //                "name" => "Cardano",
        //                "blockchains" => {
        //                    "cardano" => array(
        //                        "type" => "coin",
        //                        "deposit" => "enabled",
        //                        "minDeposit" => "1",
        //                        "withdrawal" => "enabled",
        //                        "minWithdrawal" => "5",
        //                        "withdrawalFee" => "1",
        //                        "withdrawalFeePercent" => "0",
        //                        "depositConfirmations" => "15"
        //                    }
        //                }
        //            ),
        //            ...
        //
        $responses = $promises;
        $dataCurrencies = $this->safe_list($responses[0], 'data', array());
        $dataNetworks = $this->safe_dict($responses[1], 'data', array());
        $currenciesIndexed = $this->index_by($dataCurrencies, 'currency');
        $data = $this->deep_extend($currenciesIndexed, $dataNetworks);
        return $this->parse_currencies($this->to_array($data));
    }

    public function parse_currency(array $rawCurrency): array {
        $id = $this->safe_string($rawCurrency, 'currency');
        $code = $this->safe_currency_code($id);
        $type = $this->safe_bool($rawCurrency, 'fiat') ? 'fiat' : 'crypto';
        $currencyPrecision = $this->parse_number($this->parse_precision($this->safe_string($rawCurrency, 'precision')));
        $networks = array();
        $rawNetworks = $this->safe_dict($rawCurrency, 'blockchains', array());
        $keys = is_array($rawNetworks) ? array_keys($rawNetworks) : array();
        for ($j = 0; $j < count($keys); $j++) {
            $networkId = $keys[$j];
            $rawNetwork = $rawNetworks[$networkId];
            $networkCode = $this->network_id_to_code($networkId);
            $deposit = $this->safe_string($rawNetwork, 'deposit') === 'enabled';
            $withdraw = $this->safe_string($rawNetwork, 'withdrawal') === 'enabled';
            $networks[$networkCode] = array(
                'id' => $networkId,
                'network' => $networkCode,
                'margin' => null,
                'deposit' => $deposit,
                'withdraw' => $withdraw,
                'active' => null,
                'fee' => $this->safe_number($rawNetwork, 'withdrawalFee'),
                'precision' => $currencyPrecision,
                'limits' => array(
                    'deposit' => array(
                        'min' => $this->safe_number($rawNetwork, 'minDeposit'),
                        'max' => null,
                    ),
                    'withdraw' => array(
                        'min' => $this->safe_number($rawNetwork, 'minWithdrawal'),
                        'max' => null,
                    ),
                ),
                'info' => $rawNetwork,
            );
        }
        return $this->safe_currency_structure(array(
            'id' => $id,
            'code' => $code,
            'name' => null,
            'type' => $type,
            'active' => null,
            'deposit' => $this->safe_bool($rawCurrency, 'walletDeposit'),
            'withdraw' => $this->safe_bool($rawCurrency, 'walletWithdrawal'),
            'fee' => null,
            'precision' => $currencyPrecision,
            'limits' => array(
                'amount' => array(
                    'min' => null,
                    'max' => null,
                ),
                'withdraw' => array(
                    'min' => null,
                    'max' => null,
                ),
            ),
            'networks' => $networks,
            'info' => $rawCurrency,
        ));
    }

    public function fetch_markets($params = array ()): array {
        /**
         * retrieves $data on all markets for ace
         *
         * @see https://trade.cex.io/docs/#rest-public-api-calls-pairs-info
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} an array of objects representing market $data
         */
        $response = $this->publicPostGetPairsInfo ($params);
        //
        //    {
        //        "ok" => "ok",
        //        "data" => [
        //            array(
        //                "base" => "AI",
        //                "quote" => "USD",
        //                "baseMin" => "30",
        //                "baseMax" => "2516000",
        //                "baseLotSize" => "0.000001",
        //                "quoteMin" => "10",
        //                "quoteMax" => "1000000",
        //                "quoteLotSize" => "0.01000000",
        //                "basePrecision" => "6",
        //                "quotePrecision" => "8",
        //                "pricePrecision" => "4",
        //                "minPrice" => "0.0377",
        //                "maxPrice" => "19.5000"
        //            ),
        //            ...
        //
        $data = $this->safe_list($response, 'data', array());
        return $this->parse_markets($data);
    }

    public function parse_market(array $market): array {
        $baseId = $this->safe_string($market, 'base');
        $base = $this->safe_currency_code($baseId);
        $quoteId = $this->safe_string($market, 'quote');
        $quote = $this->safe_currency_code($quoteId);
        $id = $base . '-' . $quote; // not actual $id, but for this exchange we can use this abbreviation, because e.g. tickers have hyphen in between
        $symbol = $base . '/' . $quote;
        return $this->safe_market_structure(array(
            'id' => $id,
            'symbol' => $symbol,
            'base' => $base,
            'baseId' => $baseId,
            'quote' => $quote,
            'quoteId' => $quoteId,
            'settle' => null,
            'settleId' => null,
            'type' => 'spot',
            'spot' => true,
            'margin' => false,
            'swap' => false,
            'future' => false,
            'option' => false,
            'contract' => false,
            'linear' => null,
            'inverse' => null,
            'contractSize' => null,
            'expiry' => null,
            'expiryDatetime' => null,
            'strike' => null,
            'optionType' => null,
            'limits' => array(
                'amount' => array(
                    'min' => $this->safe_number($market, 'baseMin'),
                    'max' => $this->safe_number($market, 'baseMax'),
                ),
                'price' => array(
                    'min' => $this->safe_number($market, 'minPrice'),
                    'max' => $this->safe_number($market, 'maxPrice'),
                ),
                'cost' => array(
                    'min' => $this->safe_number($market, 'quoteMin'),
                    'max' => $this->safe_number($market, 'quoteMax'),
                ),
                'leverage' => array(
                    'min' => null,
                    'max' => null,
                ),
            ),
            'precision' => array(
                'amount' => $this->safe_string($market, 'baseLotSize'),
                'price' => $this->parse_number($this->parse_precision($this->safe_string($market, 'pricePrecision'))),
                // 'cost' => $this->parse_number($this->parse_precision($this->safe_string($market, 'quoteLotSize'))), // buggy, doesn't reflect their documentation
                'base' => $this->parse_number($this->parse_precision($this->safe_string($market, 'basePrecision'))),
                'quote' => $this->parse_number($this->parse_precision($this->safe_string($market, 'quotePrecision'))),
            ),
            'active' => null,
            'created' => null,
            'info' => $market,
        ));
    }

    public function fetch_time($params = array ()): ?int {
        /**
         * fetches the current integer $timestamp in milliseconds from the exchange server
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {int} the current integer $timestamp in milliseconds from the exchange server
         */
        $response = $this->publicPostGetServerTime ($params);
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "timestamp" => "1728472063472",
        //            "ISODate" => "2024-10-09T11:07:43.472Z"
        //        }
        //    }
        //
        $data = $this->safe_dict($response, 'data');
        $timestamp = $this->safe_integer($data, 'timestamp');
        return $timestamp;
    }

    public function fetch_ticker(string $symbol, $params = array ()): array {
        /**
         * fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
         *
         * @see https://trade.cex.io/docs/#rest-public-api-calls-ticker
         *
         * @param {string} $symbol
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structures~
         */
        $this->load_markets();
        $response = $this->fetch_tickers(array( $symbol ), $params);
        return $this->safe_dict($response, $symbol, array());
    }

    public function fetch_tickers(?array $symbols = null, $params = array ()): array {
        /**
         * fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
         *
         * @see https://trade.cex.io/docs/#rest-public-api-calls-ticker
         *
         * @param {string[]|null} $symbols unified $symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structures~
         */
        $this->load_markets();
        $request = array();
        if ($symbols !== null) {
            $request['pairs'] = $this->market_ids($symbols);
        }
        $response = $this->publicPostGetTicker ($this->extend($request, $params));
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "AI-USD" => array(
        //                "bestBid" => "0.3917",
        //                "bestAsk" => "0.3949",
        //                "bestBidChange" => "0.0035",
        //                "bestBidChangePercentage" => "0.90",
        //                "bestAskChange" => "0.0038",
        //                "bestAskChangePercentage" => "0.97",
        //                "low" => "0.3787",
        //                "high" => "0.3925",
        //                "volume30d" => "2945.722277",
        //                "lastTradeDateISO" => "2024-10-11T06:18:42.077Z",
        //                "volume" => "120.736000",
        //                "quoteVolume" => "46.65654070",
        //                "lastTradeVolume" => "67.914000",
        //                "volumeUSD" => "46.65",
        //                "last" => "0.3949",
        //                "lastTradePrice" => "0.3925",
        //                "priceChange" => "0.0038",
        //                "priceChangePercentage" => "0.97"
        //            ),
        //            ...
        //
        $data = $this->safe_dict($response, 'data', array());
        return $this->parse_tickers($data, $symbols);
    }

    public function parse_ticker(array $ticker, ?array $market = null): array {
        $marketId = $this->safe_string($ticker, 'id');
        $symbol = $this->safe_symbol($marketId, $market);
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => null,
            'datetime' => null,
            'high' => $this->safe_number($ticker, 'high'),
            'low' => $this->safe_number($ticker, 'low'),
            'bid' => $this->safe_number($ticker, 'bestBid'),
            'bidVolume' => null,
            'ask' => $this->safe_number($ticker, 'bestAsk'),
            'askVolume' => null,
            'vwap' => null,
            'open' => null,
            'close' => $this->safe_string($ticker, 'last'), // last indicative price per api docs (difference also seen here => https://github.com/ccxt/ccxt/actions/runs/14593899575/job/40935513901?pr=25767#step:11:456 )
            'previousClose' => null,
            'change' => $this->safe_number($ticker, 'priceChange'),
            'percentage' => $this->safe_number($ticker, 'priceChangePercentage'),
            'average' => null,
            'baseVolume' => $this->safe_string($ticker, 'volume'),
            'quoteVolume' => $this->safe_string($ticker, 'quoteVolume'),
            'info' => $ticker,
        ), $market);
    }

    public function fetch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * get the list of most recent $trades for a particular $symbol
         *
         * @see https://trade.cex.io/docs/#rest-public-api-calls-trade-history
         *
         * @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
         * @param {int} [$params->until] timestamp in ms of the latest entry
         * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=public-$trades trade structures~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
        );
        if ($since !== null) {
            $request['fromDateISO'] = $this->iso8601($since);
        }
        $until = null;
        list($until, $params) = $this->handle_param_integer_2($params, 'until', 'till');
        if ($until !== null) {
            $request['toDateISO'] = $this->iso8601($until);
        }
        if ($limit !== null) {
            $request['pageSize'] = min ($limit, 10000); // has a bug, still returns more $trades
        }
        $response = $this->publicPostGetTradeHistory ($this->extend($request, $params));
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "pageSize" => "10",
        //            "trades" => [
        //                array(
        //                    "tradeId" => "1728630559823-0",
        //                    "dateISO" => "2024-10-11T07:09:19.823Z",
        //                    "side" => "SELL",
        //                    "price" => "60879.5",
        //                    "amount" => "0.00165962"
        //                ),
        //                ... followed by older $trades
        //
        $data = $this->safe_dict($response, 'data', array());
        $trades = $this->safe_list($data, 'trades', array());
        return $this->parse_trades($trades, $market, $since, $limit);
    }

    public function parse_trade(array $trade, ?array $market = null): array {
        //
        // public fetchTrades
        //
        //                array(
        //                    "tradeId" => "1728630559823-0",
        //                    "dateISO" => "2024-10-11T07:09:19.823Z",
        //                    "side" => "SELL",
        //                    "price" => "60879.5",
        //                    "amount" => "0.00165962"
        //                ),
        //
        $dateStr = $this->safe_string($trade, 'dateISO');
        $timestamp = $this->parse8601($dateStr);
        $market = $this->safe_market(null, $market);
        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' => null,
            'side' => $this->safe_string_lower($trade, 'side'),
            'price' => $this->safe_string($trade, 'price'),
            'amount' => $this->safe_string($trade, 'amount'),
            'cost' => null,
            'fee' => null,
        ), $market);
    }

    public function fetch_order_book(string $symbol, ?int $limit = null, $params = array ()): array {
        /**
         * fetches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
         *
         * @see https://trade.cex.io/docs/#rest-public-api-calls-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
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
        );
        $response = $this->publicPostGetOrderBook ($this->extend($request, $params));
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "timestamp" => "1728636922648",
        //            "currency1" => "BTC",
        //            "currency2" => "USDT",
        //            "bids" => array(
        //                [
        //                    "60694.1",
        //                    "13.12849761"
        //                ),
        //                array(
        //                    "60694.0",
        //                    "0.71829244"
        //                ),
        //                ...
        //
        $orderBook = $this->safe_dict($response, 'data', array());
        $timestamp = $this->safe_integer($orderBook, 'timestamp');
        return $this->parse_order_book($orderBook, $market['symbol'], $timestamp);
    }

    public function fetch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetches historical candlestick $data containing the open, high, low, and close price, and the volume of a $market
         *
         * @see https://trade.cex.io/docs/#rest-public-api-calls-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} [$params->until] timestamp in ms of the latest entry
         * @return {int[][]} A list of candles ordered, open, high, low, close, volume
         */
        $dataType = null;
        list($dataType, $params) = $this->handle_option_and_params($params, 'fetchOHLCV', 'dataType');
        if ($dataType === null) {
            throw new ArgumentsRequired($this->id . ' fetchOHLCV requires a parameter "dataType" to be either "bestBid" or "bestAsk"');
        }
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
            'resolution' => $this->timeframes[$timeframe],
            'dataType' => $dataType,
        );
        if ($since !== null) {
            $request['fromISO'] = $this->iso8601($since);
        }
        $until = null;
        list($until, $params) = $this->handle_param_integer_2($params, 'until', 'till');
        if ($until !== null) {
            $request['toISO'] = $this->iso8601($until);
        } elseif ($since === null) {
            // exchange still requires that we provide one of them
            $request['toISO'] = $this->iso8601($this->milliseconds());
        }
        if ($since !== null && $until !== null && $limit !== null) {
            throw new ArgumentsRequired($this->id . ' fetchOHLCV does not support fetching candles with both a $limit and since/until');
        } elseif (($since !== null || $until !== null) && $limit === null) {
            throw new ArgumentsRequired($this->id . ' fetchOHLCV requires a $limit parameter when fetching candles with $since or until');
        }
        if ($limit !== null) {
            $request['limit'] = $limit;
        }
        $response = $this->publicPostGetCandles ($this->extend($request, $params));
        //
        //    {
        //        "ok" => "ok",
        //        "data" => [
        //            array(
        //                "timestamp" => "1728643320000",
        //                "open" => "61061",
        //                "high" => "61095.1",
        //                "low" => "61048.5",
        //                "close" => "61087.8",
        //                "volume" => "0",
        //                "resolution" => "1m",
        //                "isClosed" => true,
        //                "timestampISO" => "2024-10-11T10:42:00.000Z"
        //            ),
        //            ...
        //
        $data = $this->safe_list($response, 'data', array());
        return $this->parse_ohlcvs($data, $market, $timeframe, $since, $limit);
    }

    public function parse_ohlcv($ohlcv, ?array $market = null): array {
        return array(
            $this->safe_integer($ohlcv, 'timestamp'),
            $this->safe_number($ohlcv, 'open'),
            $this->safe_number($ohlcv, 'high'),
            $this->safe_number($ohlcv, 'low'),
            $this->safe_number($ohlcv, 'close'),
            $this->safe_number($ohlcv, 'volume'),
        );
    }

    public function fetch_trading_fees($params = array ()): array {
        /**
         * fetch the trading $fees for multiple markets
         *
         * @see https://trade.cex.io/docs/#rest-public-api-calls-candles
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=fee-structure fee structures~ indexed by market symbols
         */
        $this->load_markets();
        $response = $this->privatePostGetMyCurrentFee ($params);
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "tradingFee" => {
        //                "AI-USD" => array(
        //                    "percent" => "0.25"
        //                ),
        //                ...
        //
        $data = $this->safe_dict($response, 'data', array());
        $fees = $this->safe_dict($data, 'tradingFee', array());
        return $this->parse_trading_fees($fees, true);
    }

    public function parse_trading_fees($response, $useKeyAsId = false): array {
        $result = array();
        $keys = is_array($response) ? array_keys($response) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $key = $keys[$i];
            $market = null;
            if ($useKeyAsId) {
                $market = $this->safe_market($key);
            }
            $parsed = $this->parse_trading_fee($response[$key], $market);
            $result[$parsed['symbol']] = $parsed;
        }
        for ($i = 0; $i < count($this->symbols); $i++) {
            $symbol = $this->symbols[$i];
            if (!(is_array($result) && array_key_exists($symbol, $result))) {
                $market = $this->market($symbol);
                $result[$symbol] = $this->parse_trading_fee($response, $market);
            }
        }
        return $result;
    }

    public function parse_trading_fee(array $fee, ?array $market = null): array {
        return array(
            'info' => $fee,
            'symbol' => $this->safe_string($market, 'symbol'),
            'maker' => $this->safe_number($fee, 'percent'),
            'taker' => $this->safe_number($fee, 'percent'),
            'percentage' => null,
            'tierBased' => null,
        );
    }

    public function fetch_accounts($params = array ()): array {
        $this->load_markets();
        $response = $this->privatePostGetMyAccountStatusV3 ($params);
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "convertedCurrency" => "USD",
        //            "balancesPerAccounts" => {
        //                "" => {
        //                    "AI" => array(
        //                        "balance" => "0.000000",
        //                        "balanceOnHold" => "0.000000"
        //                    ),
        //                    "USDT" => {
        //                        "balance" => "0.00000000",
        //                        "balanceOnHold" => "0.00000000"
        //                    }
        //                }
        //            }
        //        }
        //    }
        //
        $data = $this->safe_dict($response, 'data', array());
        $balances = $this->safe_dict($data, 'balancesPerAccounts', array());
        $arrays = $this->to_array($balances);
        return $this->parse_accounts($arrays, $params);
    }

    public function parse_account(array $account): array {
        return array(
            'id' => null,
            'type' => null,
            'code' => null,
            'info' => $account,
        );
    }

    public function fetch_balance($params = array ()): array {
        /**
         * query for balance and get the amount of funds available for trading or funds locked in orders
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-account-status-v3
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @param {array} [$params->method] 'privatePostGetMyWalletBalance' or 'privatePostGetMyAccountStatusV3'
         * @param {array} [$params->account]  in case 'privatePostGetMyAccountStatusV3' is chosen, this can specify the account name (default is empty string)
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
         */
        $accountName = null;
        list($accountName, $params) = $this->handle_param_string($params, 'account', ''); // default is empty string
        $method = null;
        list($method, $params) = $this->handle_param_string($params, 'method', 'privatePostGetMyWalletBalance');
        $accountBalance = null;
        if ($method === 'privatePostGetMyAccountStatusV3') {
            $response = $this->privatePostGetMyAccountStatusV3 ($params);
            //
            //    {
            //        "ok" => "ok",
            //        "data" => {
            //            "convertedCurrency" => "USD",
            //            "balancesPerAccounts" => {
            //                "" => {
            //                    "AI" => array(
            //                        "balance" => "0.000000",
            //                        "balanceOnHold" => "0.000000"
            //                    ),
            //                    ....
            //
            $data = $this->safe_dict($response, 'data', array());
            $balances = $this->safe_dict($data, 'balancesPerAccounts', array());
            $accountBalance = $this->safe_dict($balances, $accountName, array());
        } else {
            $response = $this->privatePostGetMyWalletBalance ($params);
            //
            //    {
            //        "ok" => "ok",
            //        "data" => {
            //            "AI" => array(
            //                "balance" => "25.606429"
            //            ),
            //            "USDT" => array(
            //                "balance" => "7.935449"
            //            ),
            //            ...
            //
            $accountBalance = $this->safe_dict($response, 'data', array());
        }
        return $this->parse_balance($accountBalance);
    }

    public function parse_balance($response): array {
        $result = array(
            'info' => $response,
        );
        $keys = is_array($response) ? array_keys($response) : array();
        for ($i = 0; $i < count($keys); $i++) {
            $key = $keys[$i];
            $balance = $this->safe_dict($response, $key, array());
            $code = $this->safe_currency_code($key);
            $account = array(
                'used' => $this->safe_string($balance, 'balanceOnHold'),
                'total' => $this->safe_string($balance, 'balance'),
            );
            $result[$code] = $account;
        }
        return $this->safe_balance($result);
    }

    public function fetch_orders_by_status(string $status, ?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetches information on multiple orders made by the user
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-orders
         *
         * @param {string} $status order $status to fetch for
         * @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} [$params->until] timestamp in ms of the latest entry
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        $this->load_markets();
        $request = array();
        $isClosedOrders = ($status === 'closed');
        if ($isClosedOrders) {
            $request['archived'] = true;
        }
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
            $request['pair'] = $market['id'];
        }
        if ($limit !== null) {
            $request['pageSize'] = $limit;
        }
        if ($since !== null) {
            $request['serverCreateTimestampFrom'] = $since;
        } elseif ($isClosedOrders) {
            // exchange requires a `$since` parameter for closed orders, so set default to allowed 365
            $request['serverCreateTimestampFrom'] = $this->milliseconds() - 364 * 24 * 60 * 60 * 1000;
        }
        $until = null;
        list($until, $params) = $this->handle_param_integer_2($params, 'until', 'till');
        if ($until !== null) {
            $request['serverCreateTimestampTo'] = $until;
        }
        $response = $this->privatePostGetMyOrders ($this->extend($request, $params));
        //
        // if called without `pair`
        //
        //    {
        //        "ok" => "ok",
        //        "data" => [
        //            array(
        //                "orderId" => "1313003",
        //                "clientOrderId" => "037F0AFEB93A",
        //                "clientId" => "up421412345",
        //                "accountId" => null,
        //                "status" => "FILLED",
        //                "statusIsFinal" => true,
        //                "currency1" => "AI",
        //                "currency2" => "USDT",
        //                "side" => "BUY",
        //                "orderType" => "Market",
        //                "timeInForce" => "IOC",
        //                "comment" => null,
        //                "rejectCode" => null,
        //                "rejectReason" => null,
        //                "initialOnHoldAmountCcy1" => null,
        //                "initialOnHoldAmountCcy2" => "10.23456700",
        //                "executedAmountCcy1" => "25.606429",
        //                "executedAmountCcy2" => "10.20904439",
        //                "requestedAmountCcy1" => null,
        //                "requestedAmountCcy2" => "10.20904439",
        //                "originalAmountCcy2" => "10.23456700",
        //                "feeAmount" => "0.02552261",
        //                "feeCurrency" => "USDT",
        //                "price" => null,
        //                "averagePrice" => "0.3986",
        //                "clientCreateTimestamp" => "1728474625320",
        //                "serverCreateTimestamp" => "1728474624956",
        //                "lastUpdateTimestamp" => "1728474628015",
        //                "expireTime" => null,
        //                "effectiveTime" => null
        //            ),
        //            ...
        //
        $data = $this->safe_list($response, 'data', array());
        return $this->parse_orders($data, $market, $since, $limit);
    }

    public function fetch_closed_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        /**
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-orders
         *
         * fetches information on multiple canceled orders made by the user
         * @param {string} $symbol unified market $symbol of the market orders were made in
         * @param {int} [$since] timestamp in ms of the earliest order, default is null
         * @param {int} [$limit] max number of orders to return, default is null
         * @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~
         */
        return $this->fetch_orders_by_status('closed', $symbol, $since, $limit, $params);
    }

    public function fetch_open_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        /**
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-orders
         *
         * fetches information on multiple canceled orders made by the user
         * @param {string} $symbol unified market $symbol of the market orders were made in
         * @param {int} [$since] timestamp in ms of the earliest order, default is null
         * @param {int} [$limit] max number of orders to return, default is null
         * @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~
         */
        return $this->fetch_orders_by_status('open', $symbol, $since, $limit, $params);
    }

    public function fetch_open_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * fetches information on an open order made by the user
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-orders
         *
         * @param {string} $id order $id
         * @param {string} [$symbol] unified $symbol of the market the order was made in
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        $this->load_markets();
        $request = array(
            'orderId' => intval($id),
        );
        $result = $this->fetch_open_orders($symbol, null, null, $this->extend($request, $params));
        return $result[0];
    }

    public function fetch_closed_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * fetches information on an closed order made by the user
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-orders
         *
         * @param {string} $id order $id
         * @param {string} [$symbol] unified $symbol of the market the order was made in
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        $this->load_markets();
        $request = array(
            'orderId' => intval($id),
        );
        $result = $this->fetch_closed_orders($symbol, null, null, $this->extend($request, $params));
        return $result[0];
    }

    public function parse_order_status(?string $status) {
        $statuses = array(
            'PENDING_NEW' => 'open',
            'NEW' => 'open',
            'PARTIALLY_FILLED' => 'open',
            'FILLED' => 'closed',
            'EXPIRED' => 'expired',
            'REJECTED' => 'rejected',
            'PENDING_CANCEL' => 'canceling',
            'CANCELLED' => 'canceled',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_order(array $order, ?array $market = null): array {
        //
        //                "orderId" => "1313003",
        //                "clientOrderId" => "037F0AFEB93A",
        //                "clientId" => "up421412345",
        //                "accountId" => null,
        //                "status" => "FILLED",
        //                "statusIsFinal" => true,
        //                "currency1" => "AI",
        //                "currency2" => "USDT",
        //                "side" => "BUY",
        //                "orderType" => "Market",
        //                "timeInForce" => "IOC",
        //                "comment" => null,
        //                "rejectCode" => null,
        //                "rejectReason" => null,
        //                "initialOnHoldAmountCcy1" => null,
        //                "initialOnHoldAmountCcy2" => "10.23456700",
        //                "executedAmountCcy1" => "25.606429",
        //                "executedAmountCcy2" => "10.20904439",
        //                "requestedAmountCcy1" => null,
        //                "requestedAmountCcy2" => "10.20904439",
        //                "originalAmountCcy2" => "10.23456700",
        //                "feeAmount" => "0.02552261",
        //                "feeCurrency" => "USDT",
        //                "price" => null,
        //                "averagePrice" => "0.3986",
        //                "clientCreateTimestamp" => "1728474625320",
        //                "serverCreateTimestamp" => "1728474624956",
        //                "lastUpdateTimestamp" => "1728474628015",
        //                "expireTime" => null,
        //                "effectiveTime" => null
        //
        $currency1 = $this->safe_string($order, 'currency1');
        $currency2 = $this->safe_string($order, 'currency2');
        $marketId = null;
        if ($currency1 !== null && $currency2 !== null) {
            $marketId = $currency1 . '-' . $currency2;
        }
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $status = $this->parse_order_status($this->safe_string($order, 'status'));
        $fee = array();
        $feeAmount = $this->safe_number($order, 'feeAmount');
        if ($feeAmount !== null) {
            $currencyId = $this->safe_string($order, 'feeCurrency');
            $feeCode = $this->safe_currency_code($currencyId);
            $fee['currency'] = $feeCode;
            $fee['cost'] = $feeAmount;
        }
        $timestamp = $this->safe_integer($order, 'serverCreateTimestamp');
        $requestedBase = $this->safe_number($order, 'requestedAmountCcy1');
        $executedBase = $this->safe_number($order, 'executedAmountCcy1');
        // $requestedQuote = $this->safe_number($order, 'requestedAmountCcy2');
        $executedQuote = $this->safe_number($order, 'executedAmountCcy2');
        return $this->safe_order(array(
            'id' => $this->safe_string($order, 'orderId'),
            'clientOrderId' => $this->safe_string($order, 'clientOrderId'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastUpdateTimestamp' => $this->safe_integer($order, 'lastUpdateTimestamp'),
            'lastTradeTimestamp' => null,
            'symbol' => $symbol,
            'type' => $this->safe_string_lower($order, 'orderType'),
            'timeInForce' => $this->safe_string($order, 'timeInForce'),
            'postOnly' => null,
            'side' => $this->safe_string_lower($order, 'side'),
            'price' => $this->safe_number($order, 'price'),
            'triggerPrice' => $this->safe_number($order, 'stopPrice'),
            'amount' => $requestedBase,
            'cost' => $executedQuote,
            'average' => $this->safe_number($order, 'averagePrice'),
            'filled' => $executedBase,
            'remaining' => null,
            'status' => $status,
            'fee' => $fee,
            'trades' => null,
            'info' => $order,
        ), $market);
    }

    public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        /**
         * create a trade order
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-new-order
         *
         * @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 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 {string} [$params->accountId] account-id to use (default is empty string)
         * @param {float} [$params->triggerPrice] the $price at which a trigger order is triggered at
         * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
         */
        $accountId = null;
        list($accountId, $params) = $this->handle_option_and_params($params, 'createOrder', 'accountId');
        if ($accountId === null) {
            throw new ArgumentsRequired($this->id . ' createOrder() : API trading is now allowed from main account, set $params["accountId"] or .options["createOrder"]["accountId"] to the name of your sub-account');
        }
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'clientOrderId' => $this->uuid(),
            'currency1' => $market['baseId'],
            'currency2' => $market['quoteId'],
            'accountId' => $accountId,
            'orderType' => $this->capitalize(strtolower($type)),
            'side' => strtoupper($side),
            'timestamp' => $this->milliseconds(),
            'amountCcy1' => $this->amount_to_precision($symbol, $amount),
        );
        $timeInForce = null;
        list($timeInForce, $params) = $this->handle_option_and_params($params, 'createOrder', 'timeInForce', 'GTC');
        if ($type === 'limit') {
            $request['price'] = $this->price_to_precision($symbol, $price);
            $request['timeInForce'] = $timeInForce;
        }
        $triggerPrice = null;
        list($triggerPrice, $params) = $this->handle_param_string($params, 'triggerPrice');
        if ($triggerPrice !== null) {
            $request['type'] = 'Stop Limit';
            $request['stopPrice'] = $triggerPrice;
        }
        $response = $this->privatePostDoMyNewOrder ($this->extend($request, $params));
        //
        // on success
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "messageType" => "executionReport",
        //            "clientId" => "up132245425",
        //            "orderId" => "1318485",
        //            "clientOrderId" => "b5b6cd40-154c-4c1c-bd51-4a442f3d50b9",
        //            "accountId" => "sub1",
        //            "status" => "FILLED",
        //            "currency1" => "LTC",
        //            "currency2" => "USDT",
        //            "side" => "BUY",
        //            "executedAmountCcy1" => "0.23000000",
        //            "executedAmountCcy2" => "15.09030000",
        //            "requestedAmountCcy1" => "0.23000000",
        //            "requestedAmountCcy2" => null,
        //            "orderType" => "Market",
        //            "timeInForce" => null,
        //            "comment" => null,
        //            "executionType" => "Trade",
        //            "executionId" => "1726747124624_101_41116",
        //            "transactTime" => "2024-10-15T15:08:12.794Z",
        //            "expireTime" => null,
        //            "effectiveTime" => null,
        //            "averagePrice" => "65.61",
        //            "lastQuantity" => "0.23000000",
        //            "lastAmountCcy1" => "0.23000000",
        //            "lastAmountCcy2" => "15.09030000",
        //            "lastPrice" => "65.61",
        //            "feeAmount" => "0.03772575",
        //            "feeCurrency" => "USDT",
        //            "clientCreateTimestamp" => "1729004892014",
        //            "serverCreateTimestamp" => "1729004891628",
        //            "lastUpdateTimestamp" => "1729004892786"
        //        }
        //    }
        //
        // on failure, there are extra fields
        //
        //             "status" => "REJECTED",
        //             "requestedAmountCcy1" => null,
        //             "orderRejectReason" => "array(\\" code \\ ":405,\\" reason \\ ":\\" Either AmountCcy1(OrderQty)or AmountCcy2(CashOrderQty)should be specified for $market order not both \\ ")",
        //             "rejectCode" => 405,
        //             "rejectReason" => "Either AmountCcy1 (OrderQty) or AmountCcy2 (CashOrderQty) should be specified for $market order not both",
        //
        $data = $this->safe_dict($response, 'data');
        return $this->parse_order($data, $market);
    }

    public function cancel_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * cancels an open order
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-cancel-order
         *
         * @param {string} $id order $id
         * @param {string} $symbol unified $symbol of the market the order was made in
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        $this->load_markets();
        $request = array(
            'orderId' => intval($id),
            'cancelRequestId' => 'c_' . (string) ($this->milliseconds()),
            'timestamp' => $this->milliseconds(),
        );
        $response = $this->privatePostDoCancelMyOrder ($this->extend($request, $params));
        //
        //      array("ok":"ok","data":array())
        //
        $data = $this->safe_dict($response, 'data', array());
        return $this->parse_order($data);
    }

    public function cancel_all_orders(?string $symbol = null, $params = array ()) {
        /**
         * cancel all open $orders in a market
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-cancel-all-$orders
         *
         * @param {string} $symbol alpaca cancelAllOrders cannot setting $symbol, it will cancel all open $orders
         * @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~
         */
        $this->load_markets();
        $response = $this->privatePostDoCancelAllOrders ($params);
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "clientOrderIds" => array(
        //                "3AF77B67109F"
        //            )
        //        }
        //    }
        //
        $data = $this->safe_dict($response, 'data', array());
        $ids = $this->safe_list($data, 'clientOrderIds', array());
        $orders = array();
        for ($i = 0; $i < count($ids); $i++) {
            $id = $ids[$i];
            $orders[] = array( 'clientOrderId' => $id );
        }
        return $this->parse_orders($orders);
    }

    public function fetch_ledger(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch the history of changes, actions done by the user or operations that altered the balance of the user
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-transaction-history
         *
         * @param {string} [$code] unified $currency $code
         * @param {int} [$since] timestamp in ms of the earliest ledger entry
         * @param {int} [$limit] max number of ledger entries to return
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @param {int} [$params->until] timestamp in ms of the latest ledger entry
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=ledger ledger structure~
         */
        $this->load_markets();
        $currency = null;
        $request = array();
        if ($code !== null) {
            $currency = $this->currency($code);
            $request['currency'] = $currency['id'];
        }
        if ($since !== null) {
            $request['dateFrom'] = $since;
        }
        if ($limit !== null) {
            $request['pageSize'] = $limit;
        }
        $until = null;
        list($until, $params) = $this->handle_param_integer_2($params, 'until', 'till');
        if ($until !== null) {
            $request['dateTo'] = $until;
        }
        $response = $this->privatePostGetMyTransactionHistory ($this->extend($request, $params));
        //
        //    {
        //        "ok" => "ok",
        //        "data" => [
        //            array(
        //                "transactionId" => "30367722",
        //                "timestamp" => "2024-10-14T14:08:49.987Z",
        //                "accountId" => "",
        //                "type" => "withdraw",
        //                "amount" => "-12.39060600",
        //                "details" => "Withdraw fundingId=1235039 clientId=up421412345 walletTxId=76337154166",
        //                "currency" => "USDT"
        //            ),
        //            ...
        //
        $data = $this->safe_list($response, 'data', array());
        return $this->parse_ledger($data, $currency, $since, $limit);
    }

    public function parse_ledger_entry(array $item, ?array $currency = null): array {
        $amount = $this->safe_string($item, 'amount');
        $direction = null;
        if (Precise::string_le($amount, '0')) {
            $direction = 'out';
            $amount = Precise::string_mul('-1', $amount);
        } else {
            $direction = 'in';
        }
        $currencyId = $this->safe_string($item, 'currency');
        $currency = $this->safe_currency($currencyId, $currency);
        $code = $this->safe_currency_code($currencyId, $currency);
        $timestampString = $this->safe_string($item, 'timestamp');
        $timestamp = $this->parse8601($timestampString);
        $type = $this->safe_string($item, 'type');
        return $this->safe_ledger_entry(array(
            'info' => $item,
            'id' => $this->safe_string($item, 'transactionId'),
            'direction' => $direction,
            'account' => $this->safe_string($item, 'accountId', ''),
            'referenceAccount' => null,
            'referenceId' => null,
            'type' => $this->parse_ledger_entry_type($type),
            'currency' => $code,
            'amount' => $this->parse_number($amount),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'before' => null,
            'after' => null,
            'status' => null,
            'fee' => null,
        ), $currency);
    }

    public function parse_ledger_entry_type($type) {
        $ledgerType = array(
            'deposit' => 'deposit',
            'withdraw' => 'withdrawal',
            'commission' => 'fee',
        );
        return $this->safe_string($ledgerType, $type, $type);
    }

    public function fetch_deposits_withdrawals(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch history of deposits and withdrawals
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-funding-history
         *
         * @param {string} [$code] unified $currency $code for the $currency of the deposit/withdrawals, default is null
         * @param {int} [$since] timestamp in ms of the earliest deposit/withdrawal, default is null
         * @param {int} [$limit] max number of deposit/withdrawals to return, default is null
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structure~
         */
        $this->load_markets();
        $request = array();
        $currency = null;
        if ($code !== null) {
            $currency = $this->currency($code);
        }
        if ($since !== null) {
            $request['dateFrom'] = $since;
        }
        if ($limit !== null) {
            $request['pageSize'] = $limit;
        }
        $until = null;
        list($until, $params) = $this->handle_param_integer_2($params, 'until', 'till');
        if ($until !== null) {
            $request['dateTo'] = $until;
        }
        $response = $this->privatePostGetMyFundingHistory ($this->extend($request, $params));
        //
        //    {
        //        "ok" => "ok",
        //        "data" => [
        //            array(
        //                "clientId" => "up421412345",
        //                "accountId" => "",
        //                "currency" => "USDT",
        //                "direction" => "withdraw",
        //                "amount" => "12.39060600",
        //                "commissionAmount" => "0.00000000",
        //                "status" => "approved",
        //                "updatedAt" => "2024-10-14T14:08:50.013Z",
        //                "txId" => "30367718",
        //                "details" => array()
        //            ),
        //            ...
        //
        $data = $this->safe_list($response, 'data', array());
        return $this->parse_transactions($data, $currency, $since, $limit);
    }

    public function parse_transaction(array $transaction, ?array $currency = null): array {
        $currencyId = $this->safe_string($transaction, 'currency');
        $direction = $this->safe_string($transaction, 'direction');
        $type = ($direction === 'withdraw') ? 'withdrawal' : 'deposit';
        $code = $this->safe_currency_code($currencyId, $currency);
        $updatedAt = $this->safe_string($transaction, 'updatedAt');
        $timestamp = $this->parse8601($updatedAt);
        return array(
            'info' => $transaction,
            'id' => $this->safe_string($transaction, 'txId'),
            'txid' => null,
            'type' => $type,
            'currency' => $code,
            'network' => null,
            'amount' => $this->safe_number($transaction, 'amount'),
            'status' => $this->parse_transaction_status($this->safe_string($transaction, 'status')),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'address' => null,
            'addressFrom' => null,
            'addressTo' => null,
            'tag' => null,
            'tagFrom' => null,
            'tagTo' => null,
            'updated' => null,
            'comment' => null,
            'fee' => array(
                'currency' => $code,
                'cost' => $this->safe_number($transaction, 'commissionAmount'),
            ),
            'internal' => null,
        );
    }

    public function parse_transaction_status(?string $status) {
        $statuses = array(
            'rejected' => 'rejected',
            'pending' => 'pending',
            'approved' => 'ok',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function transfer(string $code, float $amount, string $fromAccount, string $toAccount, $params = array ()): array {
        /**
         * $transfer currency internally between wallets on the same account
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-internal-$transfer
         *
         * @param {string} $code unified currency $code
         * @param {float} $amount amount to $transfer
         * @param {string} $fromAccount 'SPOT', 'FUND', or 'CONTRACT'
         * @param {string} $toAccount 'SPOT', 'FUND', or 'CONTRACT'
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=$transfer-structure $transfer structure~
         */
        $transfer = null;
        if ($toAccount !== '' && $fromAccount !== '') {
            $transfer = $this->transfer_between_sub_accounts($code, $amount, $fromAccount, $toAccount, $params);
        } else {
            $transfer = $this->transfer_between_main_and_sub_account($code, $amount, $fromAccount, $toAccount, $params);
        }
        $fillResponseFromRequest = $this->handle_option('transfer', 'fillResponseFromRequest', true);
        if ($fillResponseFromRequest) {
            $transfer['fromAccount'] = $fromAccount;
            $transfer['toAccount'] = $toAccount;
        }
        return $transfer;
    }

    public function transfer_between_main_and_sub_account(string $code, float $amount, string $fromAccount, string $toAccount, $params = array ()): array {
        $this->load_markets();
        $currency = $this->currency($code);
        $fromMain = ($fromAccount === '');
        $targetAccount = $fromMain ? $toAccount : $fromAccount;
        $guid = $this->safe_string($params, 'guid', $this->uuid());
        $request = array(
            'currency' => $currency['id'],
            'amount' => $this->currency_to_precision($code, $amount),
            'accountId' => $targetAccount,
            'clientTxId' => $guid,
        );
        $response = null;
        if ($fromMain) {
            $response = $this->privatePostDoDepositFundsFromWallet ($this->extend($request, $params));
        } else {
            $response = $this->privatePostDoWithdrawalFundsToWallet ($this->extend($request, $params));
        }
        // both endpoints return the same structure, the only difference is that
        // the "accountId" is filled with the "subAccount"
        //
        //     {
        //         "ok" => "ok",
        //         "data" => {
        //             "accountId" => "sub1",
        //             "clientTxId" => "27ba8284-67cf-4386-9ec7-80b3871abd45",
        //             "currency" => "USDT",
        //             "status" => "approved"
        //         }
        //     }
        //
        $data = $this->safe_dict($response, 'data', array());
        return $this->parse_transfer($data, $currency);
    }

    public function transfer_between_sub_accounts(string $code, float $amount, string $fromAccount, string $toAccount, $params = array ()): array {
        $this->load_markets();
        $currency = $this->currency($code);
        $request = array(
            'currency' => $currency['id'],
            'amount' => $this->currency_to_precision($code, $amount),
            'fromAccountId' => $fromAccount,
            'toAccountId' => $toAccount,
        );
        $response = $this->privatePostDoMyInternalTransfer ($this->extend($request, $params));
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "transactionId" => "30225415"
        //        }
        //    }
        //
        $data = $this->safe_dict($response, 'data', array());
        return $this->parse_transfer($data, $currency);
    }

    public function parse_transfer(array $transfer, ?array $currency = null): array {
        //
        // transferBetweenSubAccounts
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "transactionId" => "30225415"
        //        }
        //    }
        //
        // $transfer between main/sub
        //
        //     {
        //         "ok" => "ok",
        //         "data" => {
        //             "accountId" => "sub1",
        //             "clientTxId" => "27ba8284-67cf-4386-9ec7-80b3871abd45",
        //             "currency" => "USDT",
        //             "status" => "approved"
        //         }
        //     }
        //
        $currencyId = $this->safe_string($transfer, 'currency');
        $currencyCode = $this->safe_currency_code($currencyId, $currency);
        return array(
            'info' => $transfer,
            'id' => $this->safe_string_2($transfer, 'transactionId', 'clientTxId'),
            'timestamp' => null,
            'datetime' => null,
            'currency' => $currencyCode,
            'amount' => null,
            'fromAccount' => null,
            'toAccount' => null,
            'status' => $this->parse_transaction_status($this->safe_string($transfer, 'status')),
        );
    }

    public function fetch_deposit_address(string $code, $params = array ()): array {
        /**
         * fetch the deposit address for a $currency associated with this account
         *
         * @see https://trade.cex.io/docs/#rest-private-api-calls-deposit-address
         *
         * @param {string} $code unified $currency $code
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @param {string} [$params->accountId] account-id (default to empty string) to refer to (at this moment, only sub-accounts allowed by exchange)
         * @return {array} an ~@link https://docs.ccxt.com/#/?id=address-structure address structure~
         */
        $accountId = null;
        list($accountId, $params) = $this->handle_option_and_params($params, 'createOrder', 'accountId');
        if ($accountId === null) {
            throw new ArgumentsRequired($this->id . ' fetchDepositAddress() : main account is not allowed to fetch deposit address from api, set $params["accountId"] or .options["createOrder"]["accountId"] to the name of your sub-account');
        }
        $this->load_markets();
        $networkCode = null;
        list($networkCode, $params) = $this->handle_network_code_and_params($params);
        $currency = $this->currency($code);
        $request = array(
            'accountId' => $accountId,
            'currency' => $currency['id'], // documentation is wrong about this param
            'blockchain' => $this->network_code_to_id($networkCode),
        );
        $response = $this->privatePostGetDepositAddress ($this->extend($request, $params));
        //
        //    {
        //        "ok" => "ok",
        //        "data" => {
        //            "address" => "TCr..................1AE",
        //            "accountId" => "sub1",
        //            "currency" => "USDT",
        //            "blockchain" => "tron"
        //        }
        //    }
        //
        $data = $this->safe_dict($response, 'data', array());
        return $this->parse_deposit_address($data, $currency);
    }

    public function parse_deposit_address($depositAddress, ?array $currency = null): array {
        $address = $this->safe_string($depositAddress, 'address');
        $currencyId = $this->safe_string($depositAddress, 'currency');
        $currency = $this->safe_currency($currencyId, $currency);
        $this->check_address($address);
        return array(
            'info' => $depositAddress,
            'currency' => $currency['code'],
            'network' => $this->network_id_to_code($this->safe_string($depositAddress, 'blockchain')),
            'address' => $address,
            'tag' => null,
        );
    }

    public function sign($path, $api = 'public', $method = 'GET', $params = array (), $headers = null, $body = null) {
        $url = $this->urls['api'][$api] . '/' . $this->implode_params($path, $params);
        $query = $this->omit($params, $this->extract_params($path));
        if ($api === 'public') {
            if ($method === 'GET') {
                if ($query) {
                    $url .= '?' . $this->urlencode($query);
                }
            } else {
                $body = $this->json($query);
                $headers = array(
                    'Content-Type' => 'application/json',
                );
            }
        } else {
            $this->check_required_credentials();
            $seconds = (string) $this->seconds();
            $body = $this->json($query);
            $auth = $path . $seconds . $body;
            $signature = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256', 'base64');
            $headers = array(
                'Content-Type' => 'application/json',
                'X-AGGR-KEY' => $this->apiKey,
                'X-AGGR-TIMESTAMP' => $seconds,
                'X-AGGR-SIGNATURE' => $signature,
            );
        }
        return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers );
    }

    public function handle_errors(int $code, string $reason, string $url, string $method, array $headers, string $body, $response, $requestHeaders, $requestBody) {
        // in some cases, like from createOrder, exchange returns nested escaped JSON string:
        //      array("ok":"ok","data":array("messageType":"executionReport", "orderRejectReason":"array(\"code\":405)") )
        // and because of `.parseJson` bug, we need extra fix
        if ($response === null) {
            if ($body === null) {
                throw new NullResponse($this->id . ' returned empty response');
            } elseif ($body[0] === '{') {
                $fixed = $this->fix_stringified_json_members($body);
                $response = $this->parse_json($fixed);
            } else {
                throw new NullResponse($this->id . ' returned unparsed $response => ' . $body);
            }
        }
        $error = $this->safe_string($response, 'error');
        if ($error !== null) {
            $feedback = $this->id . ' ' . $body;
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $error, $feedback);
            $this->throw_broadly_matched_exception($this->exceptions['broad'], $error, $feedback);
            throw new ExchangeError($feedback);
        }
        // check errors in order-engine (the responses are not standard, so we parse here)
        if (mb_strpos($url, 'do_my_new_order') !== false) {
            $data = $this->safe_dict($response, 'data', array());
            $rejectReason = $this->safe_string($data, 'rejectReason');
            if ($rejectReason !== null) {
                $this->throw_broadly_matched_exception($this->exceptions['broad'], $rejectReason, $rejectReason);
                throw new ExchangeError($this->id . ' createOrder() ' . $rejectReason);
            }
        }
        return null;
    }
}
