<?php

namespace ccxt\async;

// 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\async\abstract\okcoin as Exchange;
use ccxt\ExchangeError;
use ccxt\ArgumentsRequired;
use ccxt\InvalidAddress;
use ccxt\InvalidOrder;
use ccxt\NotSupported;
use ccxt\Precise;
use \React\Async;
use \React\Promise\PromiseInterface;

class okcoin extends Exchange {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'id' => 'okcoin',
            'name' => 'OKCoin',
            'countries' => array( 'CN', 'US' ),
            'version' => 'v5',
            // cheapest endpoint is 100 requests per 2 seconds
            // 50 requests per second => 1000 / 50 = 20ms
            'rateLimit' => 20,
            'pro' => true,
            'has' => array(
                'CORS' => null,
                'spot' => true,
                'margin' => false,
                'swap' => false,
                'future' => true,
                'option' => null,
                'cancelOrder' => true,
                'createMarketBuyOrderWithCost' => true,
                'createMarketOrderWithCost' => false,
                'createMarketSellOrderWithCost' => false,
                'createOrder' => true,
                'createPostOnlyOrder' => true,
                'createReduceOnlyOrder' => true,
                'createStopLimitOrder' => true,
                'createStopMarketOrder' => true,
                'createStopOrder' => true,
                'createTriggerOrder' => true,
                'fetchBalance' => true,
                'fetchBorrowInterest' => false,
                'fetchBorrowRate' => false,
                'fetchBorrowRateHistories' => false,
                'fetchBorrowRateHistory' => false,
                'fetchBorrowRates' => false,
                'fetchBorrowRatesPerSymbol' => false,
                'fetchClosedOrders' => true,
                'fetchCurrencies' => true, // see below
                'fetchDepositAddress' => true,
                'fetchDepositAddresses' => false,
                'fetchDepositAddressesByNetwork' => false,
                'fetchDeposits' => true,
                'fetchFundingHistory' => false,
                'fetchFundingRate' => false,
                'fetchFundingRateHistory' => false,
                'fetchFundingRates' => false,
                'fetchLedger' => true,
                'fetchMarkets' => true,
                'fetchMyTrades' => true,
                'fetchOHLCV' => true,
                'fetchOpenOrders' => true,
                'fetchOrder' => true,
                'fetchOrderBook' => true,
                'fetchOrders' => null,
                'fetchOrderTrades' => true,
                'fetchPosition' => false,
                'fetchPositions' => false,
                'fetchTicker' => true,
                'fetchTickers' => true,
                'fetchTime' => true,
                'fetchTrades' => true,
                'fetchTransactions' => null,
                'fetchWithdrawals' => true,
                'reduceMargin' => false,
                'repayCrossMargin' => false,
                'repayIsolatedMargin' => false,
                'setMargin' => false,
                'transfer' => true,
                'withdraw' => true,
            ),
            'timeframes' => array(
                '1m' => '1m',
                '3m' => '3m',
                '5m' => '5m',
                '15m' => '15m',
                '30m' => '30m',
                '1h' => '1H',
                '2h' => '2H',
                '4h' => '4H',
                '6h' => '6H',
                '12h' => '12H',
                '1d' => '1D',
                '1w' => '1W',
                '1M' => '1M',
                '3M' => '3M',
            ),
            'hostname' => 'okcoin.com',
            'urls' => array(
                'logo' => 'https://user-images.githubusercontent.com/51840849/87295551-102fbf00-c50e-11ea-90a9-462eebba5829.jpg',
                'api' => array(
                    'rest' => 'https://www.{hostname}',
                ),
                'www' => 'https://www.okcoin.com',
                'doc' => 'https://www.okcoin.com/docs/en/',
                'fees' => 'https://www.okcoin.com/coin-fees',
                'referral' => 'https://www.okcoin.com/account/register?flag=activity&channelId=600001513',
                'test' => array(
                    'rest' => 'https://testnet.okex.com',
                ),
            ),
            'api' => array(
                'public' => array(
                    'get' => array(
                        'market/tickers' => 1,
                        'market/ticker' => 1,
                        'market/books' => 1 / 2,
                        'market/candles' => 1 / 2,
                        'market/history-candles' => 1 / 2,
                        'market/trades' => 1 / 5,
                        'market/history-trades' => 2,
                        'market/platform-24-volume' => 10,
                        'market/open-oracle' => 50,
                        'market/exchange-rate' => 20,
                        'public/instruments' => 1,
                        'public/time' => 2,
                    ),
                ),
                'private' => array(
                    'get' => array(
                        // trade
                        'trade/order' => 1 / 3,
                        'trade/orders-pending' => 1 / 3,
                        'trade/orders-history' => 1 / 2,
                        'trade/orders-history-archive' => 1 / 2,
                        'trade/fills' => 1 / 3,
                        'trade/fills-history' => 2.2,
                        'trade/fills-archive' => 2,
                        'trade/order-algo' => 1,
                        'trade/orders-algo-pending' => 1,
                        'trade/orders-algo-history' => 1,
                        // rfq
                        'otc/rfq/trade' => 4,
                        'otc/rfq/history' => 4,
                        // account
                        'account/balance' => 2,
                        'account/bills' => 5 / 3,
                        'account/bills-archive' => 5 / 3,
                        'account/config' => 4,
                        'account/max-size' => 4,
                        'account/max-avail-size' => 4,
                        'account/trade-fee' => 4,
                        'account/max-withdrawal' => 4,
                        // funding or assets
                        'asset/currencies' => 5 / 3,
                        'asset/balances' => 5 / 3,
                        'asset/asset-valuation' => 10,
                        'asset/transfer-state' => 10,
                        'asset/bills' => 5 / 3,
                        'asset/deposit-lightning' => 5,
                        'asset/deposit-address' => 5 / 3,
                        'asset/deposit-history' => 5 / 3,
                        'asset/withdrawal-history' => 5 / 3,
                        'asset/deposit-withdraw-status' => 20,
                        // fiat
                        'fiat/deposit-history' => 5 / 3,
                        'fiat-withdraw-history' => 5 / 3,
                        'fiat-channel' => 5 / 3,
                        // sub-account
                        'users/subaccount/list' => 10,
                        'users/subaccount/apiKey' => 10,
                        'account/subaccount/balances' => 10,
                        'asset/subaccount/balances' => 10,
                        'asset/subaccount/bills' => 10,
                    ),
                    'post' => array(
                        // trade
                        'trade/order' => 1 / 3,
                        'trade/batch-orders' => 1 / 15,
                        'trade/cancel-order' => 1 / 3,
                        'trade/cancel-batch-orders' => 1 / 15,
                        'trade/amend-order' => 1 / 3,
                        'trade/amend-batch-orders' => 1 / 150,
                        'trade/order-algo' => 1,
                        'trade/cancel-algos' => 1,
                        'trade/cancel-advance-algos' => 1,
                        // rfq
                        'otc/rfq/quote' => 4,
                        'otc/rfq/trade' => 4,
                        // funding
                        'asset/transfer' => 4,
                        'asset/withdrawal' => 4,
                        'asset/withdrawal-lightning' => 4,
                        'asset/withdrawal-cancel' => 4,
                        // fiat
                        'fiat/deposit' => 5 / 3,
                        'fiat/cancel-deposit' => 5 / 3,
                        'fiat/withdrawal' => 5 / 3,
                        'fiat/cancel-withdrawal' => 5 / 3,
                        // sub-account
                        'asset/subaccount/transfer' => 10,
                    ),
                ),
            ),
            'features' => array(
                'spot' => array(
                    'sandbox' => false,
                    'fetchCurrencies' => array(
                        'private' => true,
                    ),
                    'createOrder' => array(
                        'marginMode' => true,
                        'triggerPrice' => true,
                        'triggerDirection' => true, // todo
                        'triggerPriceType' => array(
                            'last' => true,
                            'mark' => false,
                            'index' => false,
                        ),
                        'stopLossPrice' => true, // todo revise trigger
                        'takeProfitPrice' => true, // todo revise trigger
                        'attachedStopLossTakeProfit' => array(
                            'triggerPriceType' => array(
                                'last' => true,
                                'mark' => false,
                                'index' => false,
                            ),
                            'price' => true,
                        ),
                        'timeInForce' => array(
                            'IOC' => true,
                            'FOK' => true,
                            'PO' => true,
                            'GTD' => false,
                        ),
                        'hedged' => false,
                        'trailing' => true, // todo
                        'leverage' => false,
                        'marketBuyByCost' => true,
                        'marketBuyRequiresPrice' => true,
                        'selfTradePrevention' => false,
                        'iceberg' => true, // todo
                    ),
                    'createOrders' => null, // todo
                    'fetchMyTrades' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'daysBack' => 90,
                        'untilDays' => 90, // todo
                        'symbolRequired' => false,
                    ),
                    'fetchOrder' => array(
                        'marginMode' => false,
                        'trigger' => true,
                        'trailing' => true, // todo
                        'symbolRequired' => true,
                    ),
                    'fetchOpenOrders' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'trigger' => true,
                        'trailing' => true,
                        'symbolRequired' => false,
                    ),
                    'fetchOrders' => null,
                    'fetchClosedOrders' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'daysBack' => 90, // todo
                        'daysBackCanceled' => 1 / 12, // todo => possible more with history endpoint
                        'untilDays' => 90, // todo
                        'trigger' => true,
                        'trailing' => true,
                        'symbolRequired' => false,
                    ),
                    'fetchOHLCV' => array(
                        'limit' => 100, // 300 is only possible for 'recent' 1440 candles, which does not make much sense
                    ),
                ),
                'swap' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
                'future' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
            ),
            'fees' => array(
                'trading' => array(
                    'taker' => 0.002,
                    'maker' => 0.001,
                ),
                'spot' => array(
                    'taker' => 0.0015,
                    'maker' => 0.0010,
                ),
            ),
            'requiredCredentials' => array(
                'apiKey' => true,
                'secret' => true,
                'password' => true,
            ),
            'exceptions' => array(
                'exact' => array(
                    // Public error codes from 50000-53999
                    // General Class
                    '1' => '\\ccxt\\ExchangeError', // Operation failed
                    '2' => '\\ccxt\\ExchangeError', // Bulk operation partially succeeded
                    '50000' => '\\ccxt\\BadRequest', // Body can not be empty
                    '50001' => '\\ccxt\\OnMaintenance', // Matching engine upgrading. Please try again later
                    '50002' => '\\ccxt\\BadRequest', // Json data format error
                    '50004' => '\\ccxt\\RequestTimeout', // Endpoint request timeout (does not indicate success or failure of order, please check order status)
                    '50005' => '\\ccxt\\ExchangeNotAvailable', // API is offline or unavailable
                    '50006' => '\\ccxt\\BadRequest', // Invalid Content_Type, please use "application/json" format
                    '50007' => '\\ccxt\\AccountSuspended', // Account blocked
                    '50008' => '\\ccxt\\AuthenticationError', // User does not exist
                    '50009' => '\\ccxt\\AccountSuspended', // Account is suspended due to ongoing liquidation
                    '50010' => '\\ccxt\\ExchangeError', // User ID can not be empty
                    '50011' => '\\ccxt\\RateLimitExceeded', // Request too frequent
                    '50012' => '\\ccxt\\ExchangeError', // Account status invalid
                    '50013' => '\\ccxt\\ExchangeNotAvailable', // System is busy, please try again later
                    '50014' => '\\ccxt\\BadRequest', // Parameter {0} can not be empty
                    '50015' => '\\ccxt\\ExchangeError', // Either parameter {0} or {1} is required
                    '50016' => '\\ccxt\\ExchangeError', // Parameter {0} does not match parameter {1}
                    '50017' => '\\ccxt\\ExchangeError', // The position is frozen due to ADL. Operation restricted
                    '50018' => '\\ccxt\\ExchangeError', // Currency {0} is frozen due to ADL. Operation restricted
                    '50019' => '\\ccxt\\ExchangeError', // The account is frozen due to ADL. Operation restricted
                    '50020' => '\\ccxt\\ExchangeError', // The position is frozen due to liquidation. Operation restricted
                    '50021' => '\\ccxt\\ExchangeError', // Currency {0} is frozen due to liquidation. Operation restricted
                    '50022' => '\\ccxt\\ExchangeError', // The account is frozen due to liquidation. Operation restricted
                    '50023' => '\\ccxt\\ExchangeError', // Funding fee frozen. Operation restricted
                    '50024' => '\\ccxt\\BadRequest', // Parameter {0} and {1} can not exist at the same time
                    '50025' => '\\ccxt\\ExchangeError', // Parameter {0} count exceeds the limit {1}
                    '50026' => '\\ccxt\\ExchangeNotAvailable', // System error, please try again later.
                    '50027' => '\\ccxt\\PermissionDenied', // The account is restricted from trading
                    '50028' => '\\ccxt\\ExchangeError', // Unable to take the order, please reach out to support center for details
                    '50029' => '\\ccxt\\ExchangeError', // This instrument ({0}) is unavailable at present due to risk management. Please contact customer service for help.
                    '50030' => '\\ccxt\\PermissionDenied', // No permission to use this API
                    '50032' => '\\ccxt\\AccountSuspended', // This asset is blocked, allow its trading and try again
                    '50033' => '\\ccxt\\AccountSuspended', // This instrument is blocked, allow its trading and try again
                    '50035' => '\\ccxt\\BadRequest', // This endpoint requires that APIKey must be bound to IP
                    '50036' => '\\ccxt\\BadRequest', // Invalid expTime
                    '50037' => '\\ccxt\\BadRequest', // Order expired
                    '50038' => '\\ccxt\\ExchangeError', // This feature is temporarily unavailable in demo trading
                    '50039' => '\\ccxt\\ExchangeError', // The before parameter is not available for implementing timestamp pagination
                    '50041' => '\\ccxt\\ExchangeError', // You are not currently on the whitelist, please contact customer service
                    '50044' => '\\ccxt\\BadRequest', // Must select one broker type
                    // API Class
                    '50100' => '\\ccxt\\ExchangeError', // API frozen, please contact customer service
                    '50101' => '\\ccxt\\AuthenticationError', // Broker id of APIKey does not match current environment
                    '50102' => '\\ccxt\\InvalidNonce', // Timestamp request expired
                    '50103' => '\\ccxt\\AuthenticationError', // Request header "OK_ACCESS_KEY" can not be empty
                    '50104' => '\\ccxt\\AuthenticationError', // Request header "OK_ACCESS_PASSPHRASE" can not be empty
                    '50105' => '\\ccxt\\AuthenticationError', // Request header "OK_ACCESS_PASSPHRASE" incorrect
                    '50106' => '\\ccxt\\AuthenticationError', // Request header "OK_ACCESS_SIGN" can not be empty
                    '50107' => '\\ccxt\\AuthenticationError', // Request header "OK_ACCESS_TIMESTAMP" can not be empty
                    '50108' => '\\ccxt\\ExchangeError', // Exchange ID does not exist
                    '50109' => '\\ccxt\\ExchangeError', // Exchange domain does not exist
                    '50110' => '\\ccxt\\PermissionDenied', // Invalid IP
                    '50111' => '\\ccxt\\AuthenticationError', // Invalid OK_ACCESS_KEY
                    '50112' => '\\ccxt\\AuthenticationError', // Invalid OK_ACCESS_TIMESTAMP
                    '50113' => '\\ccxt\\AuthenticationError', // Invalid signature
                    '50114' => '\\ccxt\\AuthenticationError', // Invalid authorization
                    '50115' => '\\ccxt\\BadRequest', // Invalid request method
                    // Trade Class
                    '51000' => '\\ccxt\\BadRequest', // Parameter {0} error
                    '51001' => '\\ccxt\\BadSymbol', // Instrument ID does not exist
                    '51002' => '\\ccxt\\BadSymbol', // Instrument ID does not match underlying index
                    '51003' => '\\ccxt\\BadRequest', // Either client order ID or order ID is required
                    '51004' => '\\ccxt\\InvalidOrder', // Order amount exceeds current tier limit
                    '51005' => '\\ccxt\\InvalidOrder', // Order amount exceeds the limit
                    '51006' => '\\ccxt\\InvalidOrder', // Order price out of the limit
                    '51007' => '\\ccxt\\InvalidOrder', // Order placement failed. Order amount should be at least 1 contract (showing up when placing an order with less than 1 contract)
                    '51008' => '\\ccxt\\InsufficientFunds', // Order placement failed due to insufficient balance
                    '51009' => '\\ccxt\\AccountSuspended', // Order placement function is blocked by the platform
                    '51010' => '\\ccxt\\AccountNotEnabled', // Account level too low array("code":"1","data":[array("clOrdId":"uJrfGFth9F","ordId":"","sCode":"51010","sMsg":"The current account mode does not support this API interface. ","tag":"")],"msg":"Operation failed.")
                    '51011' => '\\ccxt\\InvalidOrder', // Duplicated order ID
                    '51012' => '\\ccxt\\BadSymbol', // Token does not exist
                    '51014' => '\\ccxt\\BadSymbol', // Index does not exist
                    '51015' => '\\ccxt\\BadSymbol', // Instrument ID does not match instrument type
                    '51016' => '\\ccxt\\InvalidOrder', // Duplicated client order ID
                    '51017' => '\\ccxt\\ExchangeError', // Borrow amount exceeds the limit
                    '51018' => '\\ccxt\\ExchangeError', // User with option account can not hold net short positions
                    '51019' => '\\ccxt\\ExchangeError', // No net long positions can be held under isolated margin mode in options
                    '51020' => '\\ccxt\\InvalidOrder', // Order amount should be greater than the min available amount
                    '51023' => '\\ccxt\\ExchangeError', // Position does not exist
                    '51024' => '\\ccxt\\AccountSuspended', // Unified accountblocked
                    '51025' => '\\ccxt\\ExchangeError', // Order count exceeds the limit
                    '51026' => '\\ccxt\\BadSymbol', // Instrument type does not match underlying index
                    '51030' => '\\ccxt\\InvalidOrder', // Funding fee is being settled.
                    '51031' => '\\ccxt\\InvalidOrder', // This order price is not within the closing price range
                    '51032' => '\\ccxt\\InvalidOrder', // Closing all positions at market price.
                    '51033' => '\\ccxt\\InvalidOrder', // The total amount per order for this pair has reached the upper limit.
                    '51037' => '\\ccxt\\InvalidOrder', // The current account risk status only supports you to place IOC orders that can reduce the risk of your account.
                    '51038' => '\\ccxt\\InvalidOrder', // There is already an IOC order under the current risk module that reduces the risk of the account.
                    '51044' => '\\ccxt\\InvalidOrder', // The order type {0}, {1} is not allowed to set stop loss and take profit
                    '51046' => '\\ccxt\\InvalidOrder', // The take profit trigger price must be higher than the order price
                    '51047' => '\\ccxt\\InvalidOrder', // The stop loss trigger price must be lower than the order price
                    '51048' => '\\ccxt\\InvalidOrder', // The take profit trigger price should be lower than the order price
                    '51049' => '\\ccxt\\InvalidOrder', // The stop loss trigger price should be higher than the order price
                    '51050' => '\\ccxt\\InvalidOrder', // The take profit trigger price should be higher than the best ask price
                    '51051' => '\\ccxt\\InvalidOrder', // The stop loss trigger price should be lower than the best ask price
                    '51052' => '\\ccxt\\InvalidOrder', // The take profit trigger price should be lower than the best bid price
                    '51053' => '\\ccxt\\InvalidOrder', // The stop loss trigger price should be higher than the best bid price
                    '51054' => '\\ccxt\\BadRequest', // Getting information timed out, please try again later
                    '51056' => '\\ccxt\\InvalidOrder', // Action not allowed
                    '51058' => '\\ccxt\\InvalidOrder', // No available position for this algo order
                    '51059' => '\\ccxt\\InvalidOrder', // Strategy for the current state does not support this operation
                    '51100' => '\\ccxt\\InvalidOrder', // Trading amount does not meet the min tradable amount
                    '51102' => '\\ccxt\\InvalidOrder', // Entered amount exceeds the max pending count
                    '51103' => '\\ccxt\\InvalidOrder', // Entered amount exceeds the max pending order count of the underlying asset
                    '51108' => '\\ccxt\\InvalidOrder', // Positions exceed the limit for closing out with the market price
                    '51109' => '\\ccxt\\InvalidOrder', // No available offer
                    '51110' => '\\ccxt\\InvalidOrder', // You can only place a limit order after Call Auction has started
                    '51111' => '\\ccxt\\BadRequest', // Maximum {0} orders can be placed in bulk
                    '51112' => '\\ccxt\\InvalidOrder', // Close order size exceeds your available size
                    '51113' => '\\ccxt\\RateLimitExceeded', // Market-price liquidation requests too frequent
                    '51115' => '\\ccxt\\InvalidOrder', // Cancel all pending close-orders before liquidation
                    '51116' => '\\ccxt\\InvalidOrder', // Order price or trigger price exceeds {0}
                    '51117' => '\\ccxt\\InvalidOrder', // Pending close-orders count exceeds limit
                    '51118' => '\\ccxt\\InvalidOrder', // Total amount should exceed the min amount per order
                    '51119' => '\\ccxt\\InsufficientFunds', // Order placement failed due to insufficient balance
                    '51120' => '\\ccxt\\InvalidOrder', // Order quantity is less than {0}, please try again
                    '51121' => '\\ccxt\\InvalidOrder', // Order count should be the integer multiples of the lot size
                    '51122' => '\\ccxt\\InvalidOrder', // Order price should be higher than the min price {0}
                    '51124' => '\\ccxt\\InvalidOrder', // You can only place limit orders during call auction
                    '51125' => '\\ccxt\\InvalidOrder', // Currently there are reduce . reverse position pending orders in margin trading. Please cancel all reduce . reverse position pending orders and continue
                    '51126' => '\\ccxt\\InvalidOrder', // Currently there are reduce only pending orders in margin trading.Please cancel all reduce only pending orders and continue
                    '51127' => '\\ccxt\\InsufficientFunds', // Available balance is 0
                    '51128' => '\\ccxt\\InvalidOrder', // Multi-currency margin account can not do cross-margin trading
                    '51129' => '\\ccxt\\InvalidOrder', // The value of the position and buy order has reached the position limit, and no further buying is allowed
                    '51130' => '\\ccxt\\BadSymbol', // Fixed margin currency error
                    '51131' => '\\ccxt\\InsufficientFunds', // Insufficient balance
                    '51132' => '\\ccxt\\InvalidOrder', // Your position amount is negative and less than the minimum trading amount
                    '51133' => '\\ccxt\\InvalidOrder', // Reduce-only feature is unavailable for the spot transactions by multi-currency margin account
                    '51134' => '\\ccxt\\InvalidOrder', // Closing failed. Please check your holdings and pending orders
                    '51135' => '\\ccxt\\InvalidOrder', // Your closing price has triggered the limit price, and the max buy price is {0}
                    '51136' => '\\ccxt\\InvalidOrder', // Your closing price has triggered the limit price, and the min sell price is {0}
                    '51137' => '\\ccxt\\InvalidOrder', // Your opening price has triggered the limit price, and the max buy price is {0}
                    '51138' => '\\ccxt\\InvalidOrder', // Your opening price has triggered the limit price, and the min sell price is {0}
                    '51139' => '\\ccxt\\InvalidOrder', // Reduce-only feature is unavailable for the spot transactions by simple account
                    '51156' => '\\ccxt\\BadRequest', // You're leading trades in long/short mode and can't use this API endpoint to close positions
                    '51159' => '\\ccxt\\BadRequest', // You're leading trades in buy/sell mode. If you want to place orders using this API endpoint, the orders must be in the same direction existing positions and open orders.
                    '51162' => '\\ccxt\\InvalidOrder', // You have {instrument} open orders. Cancel these orders and try again
                    '51163' => '\\ccxt\\InvalidOrder', // You hold {instrument} positions. Close these positions and try again
                    '51166' => '\\ccxt\\InvalidOrder', // Currently, we don't support leading trades with this instrument
                    '51174' => '\\ccxt\\InvalidOrder', // The number of {param0} pending orders reached the upper limit of {param1} (orders).
                    '51201' => '\\ccxt\\InvalidOrder', // Value of per market order cannot exceed 100,000 USDT
                    '51202' => '\\ccxt\\InvalidOrder', // Market - order amount exceeds the max amount
                    '51203' => '\\ccxt\\InvalidOrder', // Order amount exceeds the limit {0}
                    '51204' => '\\ccxt\\InvalidOrder', // The price for the limit order can not be empty
                    '51205' => '\\ccxt\\InvalidOrder', // Reduce-Only is not available
                    '51250' => '\\ccxt\\InvalidOrder', // Algo order price is out of the available range
                    '51251' => '\\ccxt\\InvalidOrder', // Algo order type error (when user place an iceberg order)
                    '51252' => '\\ccxt\\InvalidOrder', // Algo order price is out of the available range
                    '51253' => '\\ccxt\\InvalidOrder', // Average amount exceeds the limit of per iceberg order
                    '51254' => '\\ccxt\\InvalidOrder', // Iceberg average amount error (when user place an iceberg order)
                    '51255' => '\\ccxt\\InvalidOrder', // Limit of per iceberg order => Total amount/1000 < x <= Total amount
                    '51256' => '\\ccxt\\InvalidOrder', // Iceberg order price variance error
                    '51257' => '\\ccxt\\InvalidOrder', // Trail order callback rate error
                    '51258' => '\\ccxt\\InvalidOrder', // Trail - order placement failed. The trigger price of a sell order should be higher than the last transaction price
                    '51259' => '\\ccxt\\InvalidOrder', // Trail - order placement failed. The trigger price of a buy order should be lower than the last transaction price
                    '51260' => '\\ccxt\\InvalidOrder', // Maximum {0} pending trail - orders can be held at the same time
                    '51261' => '\\ccxt\\InvalidOrder', // Each user can hold up to {0} pending stop - orders at the same time
                    '51262' => '\\ccxt\\InvalidOrder', // Maximum {0} pending iceberg orders can be held at the same time
                    '51263' => '\\ccxt\\InvalidOrder', // Maximum {0} pending time-weighted orders can be held at the same time
                    '51264' => '\\ccxt\\InvalidOrder', // Average amount exceeds the limit of per time-weighted order
                    '51265' => '\\ccxt\\InvalidOrder', // Time-weighted order limit error
                    '51267' => '\\ccxt\\InvalidOrder', // Time-weighted order strategy initiative rate error
                    '51268' => '\\ccxt\\InvalidOrder', // Time-weighted order strategy initiative range error
                    '51269' => '\\ccxt\\InvalidOrder', // Time-weighted order interval error, the interval should be {0}<= x<={1}
                    '51270' => '\\ccxt\\InvalidOrder', // The limit of time-weighted order price variance is 0 < x <= 1%
                    '51271' => '\\ccxt\\InvalidOrder', // Sweep ratio should be 0 < x <= 100%
                    '51272' => '\\ccxt\\InvalidOrder', // Price variance should be 0 < x <= 1%
                    '51273' => '\\ccxt\\InvalidOrder', // Total amount should be more than {0}
                    '51274' => '\\ccxt\\InvalidOrder', // Total quantity of time-weighted order must be larger than single order limit
                    '51275' => '\\ccxt\\InvalidOrder', // The amount of single stop-market order can not exceed the upper limit
                    '51276' => '\\ccxt\\InvalidOrder', // Stop - Market orders cannot specify a price
                    '51277' => '\\ccxt\\InvalidOrder', // TP trigger price can not be higher than the last price
                    '51278' => '\\ccxt\\InvalidOrder', // SL trigger price can not be lower than the last price
                    '51279' => '\\ccxt\\InvalidOrder', // TP trigger price can not be lower than the last price
                    '51280' => '\\ccxt\\InvalidOrder', // SL trigger price can not be higher than the last price
                    '51321' => '\\ccxt\\InvalidOrder', // You're leading trades. Currently, we don't support leading trades with arbitrage, iceberg, or TWAP bots
                    '51322' => '\\ccxt\\InvalidOrder', // You're leading trades that have been filled at market price. We've canceled your open stop orders to close your positions
                    '51323' => '\\ccxt\\BadRequest', // You're already leading trades with take profit or stop loss settings. Cancel your existing stop orders to proceed
                    '51324' => '\\ccxt\\BadRequest', // As a lead trader, you hold positions in {instrument}. To close your positions, place orders in the amount that equals the available amount for closing
                    '51325' => '\\ccxt\\InvalidOrder', // As a lead trader, you must use market price when placing stop orders
                    '51327' => '\\ccxt\\InvalidOrder', // closeFraction is only available for futures and perpetual swaps
                    '51328' => '\\ccxt\\InvalidOrder', // closeFraction is only available for reduceOnly orders
                    '51329' => '\\ccxt\\InvalidOrder', // closeFraction is only available in NET mode
                    '51330' => '\\ccxt\\InvalidOrder', // closeFraction is only available for stop market orders
                    '51400' => '\\ccxt\\OrderNotFound', // Cancellation failed order does not exist
                    '51401' => '\\ccxt\\OrderNotFound', // Cancellation failed order is already canceled
                    '51402' => '\\ccxt\\OrderNotFound', // Cancellation failed order is already completed
                    '51403' => '\\ccxt\\InvalidOrder', // Cancellation failed order type does not support cancellation
                    '51404' => '\\ccxt\\InvalidOrder', // Order cancellation unavailable during the second phase of call auction
                    '51405' => '\\ccxt\\ExchangeError', // Cancellation failed do not have any pending orders
                    '51406' => '\\ccxt\\ExchangeError', // Canceled - order count exceeds the limit {0}
                    '51407' => '\\ccxt\\BadRequest', // Either order ID or client order ID is required
                    '51408' => '\\ccxt\\ExchangeError', // Pair ID or name does not match the order info
                    '51409' => '\\ccxt\\ExchangeError', // Either pair ID or pair name ID is required
                    '51410' => '\\ccxt\\CancelPending', // Cancellation failed order is already under cancelling status
                    '51500' => '\\ccxt\\ExchangeError', // Either order price or amount is required
                    '51501' => '\\ccxt\\ExchangeError', // Maximum {0} orders can be modified
                    '51502' => '\\ccxt\\InsufficientFunds', // Order modification failed for insufficient margin
                    '51503' => '\\ccxt\\ExchangeError', // Order modification failed order does not exist
                    '51506' => '\\ccxt\\ExchangeError', // Order modification unavailable for the order type
                    '51508' => '\\ccxt\\ExchangeError', // Orders are not allowed to be modified during the call auction
                    '51509' => '\\ccxt\\ExchangeError', // Modification failed order has been canceled
                    '51510' => '\\ccxt\\ExchangeError', // Modification failed order has been completed
                    '51511' => '\\ccxt\\ExchangeError', // Modification failed order price did not meet the requirement for Post Only
                    '51600' => '\\ccxt\\ExchangeError', // Status not found
                    '51601' => '\\ccxt\\ExchangeError', // Order status and order ID cannot exist at the same time
                    '51602' => '\\ccxt\\ExchangeError', // Either order status or order ID is required
                    '51603' => '\\ccxt\\OrderNotFound', // Order does not exist
                    '51732' => '\\ccxt\\AuthenticationError', // Required user KYC level not met
                    '51733' => '\\ccxt\\AuthenticationError', // User is under risk control
                    '51734' => '\\ccxt\\AuthenticationError', // User KYC Country is not supported
                    '51735' => '\\ccxt\\ExchangeError', // Sub-account is not supported
                    '51736' => '\\ccxt\\InsufficientFunds', // Insufficient {ccy} balance
                    // Data class
                    '52000' => '\\ccxt\\ExchangeError', // No updates
                    // SPOT/MARGIN error codes 54000-54999
                    '54000' => '\\ccxt\\ExchangeError', // Margin transactions unavailable
                    '54001' => '\\ccxt\\ExchangeError', // Only Multi-currency margin account can be set to borrow coins automatically
                    // FUNDING error codes 58000-58999
                    '58000' => '\\ccxt\\ExchangeError', // Account type {0} does not supported when getting the sub-account balance
                    '58001' => '\\ccxt\\AuthenticationError', // Incorrect trade password
                    '58002' => '\\ccxt\\PermissionDenied', // Please activate Savings Account first
                    '58003' => '\\ccxt\\ExchangeError', // Currency type is not supported by Savings Account
                    '58004' => '\\ccxt\\AccountSuspended', // Account blocked (transfer & withdrawal endpoint => either end of the account does not authorize the transfer)
                    '58005' => '\\ccxt\\ExchangeError', // The redeemed amount must be no greater than {0}
                    '58006' => '\\ccxt\\ExchangeError', // Service unavailable for token {0}
                    '58007' => '\\ccxt\\ExchangeError', // Abnormal Assets interface. Please try again later
                    '58100' => '\\ccxt\\ExchangeError', // The trading product triggers risk control, and the platform has suspended the fund transfer-out function with related users. Please wait patiently
                    '58101' => '\\ccxt\\AccountSuspended', // Transfer suspended (transfer endpoint => either end of the account does not authorize the transfer)
                    '58102' => '\\ccxt\\RateLimitExceeded', // Too frequent transfer (transfer too frequently)
                    '58103' => '\\ccxt\\ExchangeError', // Parent account user id does not match sub-account user id
                    '58104' => '\\ccxt\\ExchangeError', // Since your P2P transaction is abnormal, you are restricted from making fund transfers. Please contact customer support to remove the restriction
                    '58105' => '\\ccxt\\ExchangeError', // Since your P2P transaction is abnormal, you are restricted from making fund transfers. Please transfer funds on our website or app to complete identity verification
                    '58106' => '\\ccxt\\ExchangeError', // Please enable the account for spot contract
                    '58107' => '\\ccxt\\ExchangeError', // Please enable the account for futures contract
                    '58108' => '\\ccxt\\ExchangeError', // Please enable the account for option contract
                    '58109' => '\\ccxt\\ExchangeError', // Please enable the account for swap contract
                    '58110' => '\\ccxt\\ExchangeError', // The contract triggers risk control, and the platform has suspended the fund transfer function of it. Please wait patiently
                    '58111' => '\\ccxt\\ExchangeError', // Funds transfer unavailable perpetual contract is charging the funding fee. Please try again later
                    '58112' => '\\ccxt\\ExchangeError', // Your fund transfer failed. Please try again later
                    '58114' => '\\ccxt\\ExchangeError', // Transfer amount must be more than 0
                    '58115' => '\\ccxt\\ExchangeError', // Sub-account does not exist
                    '58116' => '\\ccxt\\ExchangeError', // Transfer amount exceeds the limit
                    '58117' => '\\ccxt\\ExchangeError', // Account assets are abnormal, please deal with negative assets before transferring
                    '58125' => '\\ccxt\\BadRequest', // Non-tradable assets can only be transferred from sub-accounts to main accounts
                    '58126' => '\\ccxt\\BadRequest', // Non-tradable assets can only be transferred between funding accounts
                    '58127' => '\\ccxt\\BadRequest', // Main account API Key does not support current transfer 'type' parameter. Please refer to the API documentation.
                    '58128' => '\\ccxt\\BadRequest', // Sub-account API Key does not support current transfer 'type' parameter. Please refer to the API documentation.
                    '58200' => '\\ccxt\\ExchangeError', // Withdrawal from {0} to {1} is unavailable for this currency
                    '58201' => '\\ccxt\\ExchangeError', // Withdrawal amount exceeds the daily limit
                    '58202' => '\\ccxt\\ExchangeError', // The minimum withdrawal amount for NEO is 1, and the amount must be an integer
                    '58203' => '\\ccxt\\InvalidAddress', // Please add a withdrawal address
                    '58204' => '\\ccxt\\AccountSuspended', // Withdrawal suspended
                    '58205' => '\\ccxt\\ExchangeError', // Withdrawal amount exceeds the upper limit
                    '58206' => '\\ccxt\\ExchangeError', // Withdrawal amount is lower than the lower limit
                    '58207' => '\\ccxt\\InvalidAddress', // Withdrawal failed due to address error
                    '58208' => '\\ccxt\\ExchangeError', // Withdrawal failed. Please link your email
                    '58209' => '\\ccxt\\ExchangeError', // Withdrawal failed. Withdraw feature is not available for sub-accounts
                    '58210' => '\\ccxt\\ExchangeError', // Withdrawal fee exceeds the upper limit
                    '58211' => '\\ccxt\\ExchangeError', // Withdrawal fee is lower than the lower limit (withdrawal endpoint => incorrect fee)
                    '58212' => '\\ccxt\\ExchangeError', // Withdrawal fee should be {0}% of the withdrawal amount
                    '58213' => '\\ccxt\\AuthenticationError', // Please set trading password before withdrawal
                    '58221' => '\\ccxt\\BadRequest', // Missing label of withdrawal address.
                    '58222' => '\\ccxt\\BadRequest', // Illegal withdrawal address.
                    '58224' => '\\ccxt\\BadRequest', // This type of crypto does not support on-chain withdrawing to OKX addresses. Please withdraw through internal transfers.
                    '58227' => '\\ccxt\\BadRequest', // Withdrawal of non-tradable assets can be withdrawn all at once only
                    '58228' => '\\ccxt\\BadRequest', // Withdrawal of non-tradable assets requires that the API Key must be bound to an IP
                    '58229' => '\\ccxt\\InsufficientFunds', // Insufficient funding account balance to pay fees {fee} USDT
                    '58300' => '\\ccxt\\ExchangeError', // Deposit-address count exceeds the limit
                    '58350' => '\\ccxt\\InsufficientFunds', // Insufficient balance
                    // Account error codes 59000-59999
                    '59000' => '\\ccxt\\ExchangeError', // Your settings failed have positions or open orders
                    '59001' => '\\ccxt\\ExchangeError', // Switching unavailable have borrowings
                    '59100' => '\\ccxt\\ExchangeError', // You have open positions. Please cancel all open positions before changing the leverage
                    '59101' => '\\ccxt\\ExchangeError', // You have pending orders with isolated positions. Please cancel all the pending orders and adjust the leverage
                    '59102' => '\\ccxt\\ExchangeError', // Leverage exceeds the maximum leverage. Please adjust the leverage
                    '59103' => '\\ccxt\\InsufficientFunds', // Leverage is too low and no sufficient margin in your account. Please adjust the leverage
                    '59104' => '\\ccxt\\ExchangeError', // The leverage is too high. The borrowed position has exceeded the maximum position of this leverage. Please adjust the leverage
                    '59105' => '\\ccxt\\ExchangeError', // Leverage can not be less than {0}. Please adjust the leverage
                    '59106' => '\\ccxt\\ExchangeError', // The max available margin corresponding to your order tier is {0}. Please adjust your margin and place a new order
                    '59107' => '\\ccxt\\ExchangeError', // You have pending orders under the service, please modify the leverage after canceling all pending orders
                    '59108' => '\\ccxt\\InsufficientFunds', // Low leverage and insufficient margin, please adjust the leverage
                    '59109' => '\\ccxt\\ExchangeError', // Account equity less than the required margin amount after adjustment. Please adjust the leverage
                    '59128' => '\\ccxt\\InvalidOrder', // As a lead trader, you can't lead trades in {instrument} with leverage higher than {num}
                    '59200' => '\\ccxt\\InsufficientFunds', // Insufficient account balance
                    '59201' => '\\ccxt\\InsufficientFunds', // Negative account balance
                    '59216' => '\\ccxt\\BadRequest', // The position doesn't exist. Please try again
                    '59300' => '\\ccxt\\ExchangeError', // Margin call failed. Position does not exist
                    '59301' => '\\ccxt\\ExchangeError', // Margin adjustment failed for exceeding the max limit
                    '59313' => '\\ccxt\\ExchangeError', // Unable to repay. You haven't borrowed any {ccy} {ccyPair} in Quick margin mode.
                    '59401' => '\\ccxt\\ExchangeError', // Holdings already reached the limit
                    '59500' => '\\ccxt\\ExchangeError', // Only the APIKey of the main account has permission
                    '59501' => '\\ccxt\\ExchangeError', // Only 50 APIKeys can be created per account
                    '59502' => '\\ccxt\\ExchangeError', // Note name cannot be duplicate with the currently created APIKey note name
                    '59503' => '\\ccxt\\ExchangeError', // Each APIKey can bind up to 20 IP addresses
                    '59504' => '\\ccxt\\ExchangeError', // The sub account does not support the withdrawal function
                    '59505' => '\\ccxt\\ExchangeError', // The passphrase format is incorrect
                    '59506' => '\\ccxt\\ExchangeError', // APIKey does not exist
                    '59507' => '\\ccxt\\ExchangeError', // The two accounts involved in a transfer must be two different sub accounts under the same parent account
                    '59508' => '\\ccxt\\AccountSuspended', // The sub account of {0} is suspended
                    // WebSocket error Codes from 60000-63999
                    '60001' => '\\ccxt\\AuthenticationError', // "OK_ACCESS_KEY" can not be empty
                    '60002' => '\\ccxt\\AuthenticationError', // "OK_ACCESS_SIGN" can not be empty
                    '60003' => '\\ccxt\\AuthenticationError', // "OK_ACCESS_PASSPHRASE" can not be empty
                    '60004' => '\\ccxt\\AuthenticationError', // Invalid OK_ACCESS_TIMESTAMP
                    '60005' => '\\ccxt\\AuthenticationError', // Invalid OK_ACCESS_KEY
                    '60006' => '\\ccxt\\InvalidNonce', // Timestamp request expired
                    '60007' => '\\ccxt\\AuthenticationError', // Invalid sign
                    '60008' => '\\ccxt\\AuthenticationError', // Login is not supported for public channels
                    '60009' => '\\ccxt\\AuthenticationError', // Login failed
                    '60010' => '\\ccxt\\AuthenticationError', // Already logged in
                    '60011' => '\\ccxt\\AuthenticationError', // Please log in
                    '60012' => '\\ccxt\\BadRequest', // Illegal request
                    '60013' => '\\ccxt\\BadRequest', // Invalid args
                    '60014' => '\\ccxt\\RateLimitExceeded', // Requests too frequent
                    '60015' => '\\ccxt\\NetworkError', // Connection closed was no data transmission in the last 30 seconds
                    '60016' => '\\ccxt\\ExchangeNotAvailable', // Buffer is full, cannot write data
                    '60017' => '\\ccxt\\BadRequest', // Invalid url path
                    '60018' => '\\ccxt\\BadRequest', // The {0} {1} {2} {3} {4} does not exist
                    '60019' => '\\ccxt\\BadRequest', // Invalid op {op}
                    '63999' => '\\ccxt\\ExchangeError', // Internal system error
                    '70010' => '\\ccxt\\BadRequest', // Timestamp parameters need to be in Unix timestamp format in milliseconds.
                    '70013' => '\\ccxt\\BadRequest', // endTs needs to be bigger than or equal to beginTs.
                    '70016' => '\\ccxt\\BadRequest', // Please specify your instrument settings for at least one instType.
                ),
                'broad' => array(
                    'Internal Server Error' => '\\ccxt\\ExchangeNotAvailable', // array("code":500,"data":array(),"detailMsg":"","error_code":"500","error_message":"Internal Server Error","msg":"Internal Server Error")
                    'server error' => '\\ccxt\\ExchangeNotAvailable', // array("code":500,"data":array(),"detailMsg":"","error_code":"500","error_message":"server error 1236805249","msg":"server error 1236805249")
                ),
            ),
            'precisionMode' => TICK_SIZE,
            'options' => array(
                'fetchOHLCV' => array(
                    'type' => 'Candles', // Candles or HistoryCandles
                ),
                'createMarketBuyOrderRequiresPrice' => true,
                'fetchMarkets' => array( 'spot' ),
                'defaultType' => 'spot', // 'account', 'spot', 'futures', 'swap', 'option'
                'accountsByType' => array(
                    'classic' => '1',
                    'spot' => '1',
                    'funding' => '6',
                    'main' => '6',
                    'unified' => '18',
                ),
                'accountsById' => array(
                    '1' => 'spot',
                    '6' => 'funding',
                    '18' => 'unified',
                ),
                'auth' => array(
                    'time' => 'public',
                    'currencies' => 'private',
                    'instruments' => 'public',
                    'rate' => 'public',
                    '{instrument_id}/constituents' => 'public',
                ),
                'warnOnFetchCurrenciesWithoutAuthorization' => false,
                'defaultNetwork' => 'ERC20',
                'networks' => array(
                    'ERC20' => 'Ethereum',
                    'BTC' => 'Bitcoin',
                    'OMNI' => 'Omni',
                    'TRC20' => 'TRON',
                ),
            ),
            'commonCurrencies' => array(
                // OKEX refers to ERC20 version of Aeternity (AEToken)
                'AE' => 'AET', // https://github.com/ccxt/ccxt/issues/4981
                'BOX' => 'DefiBox',
                'HOT' => 'Hydro Protocol',
                'HSR' => 'HC',
                'MAG' => 'Maggie',
                'SBTC' => 'Super Bitcoin',
                'TRADE' => 'Unitrade',
                'YOYO' => 'YOYOW',
                'WIN' => 'WinToken', // https://github.com/ccxt/ccxt/issues/5701
            ),
        ));
    }

    public function fetch_time($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * 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 = Async\await($this->publicGetPublicTime ($params));
            //
            // {
            //     "code" => "0",
            //     "data":
            //         array(
            //             {
            //                 "ts" => "1737379360033"
            //             }
            //         ),
            //     "msg" => ""
            // }
            //
            $data = $this->safe_list($response, 'data');
            $timestamp = $this->safe_dict($data, 0);
            return $this->safe_integer($timestamp, 'ts');
        }) ();
    }

    public function fetch_markets($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-public-data-get-instruments
             *
             * retrieves data on all $markets for okcoin
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} an array of objects representing market data
             */
            $request = array(
                'instType' => 'SPOT',
            );
            $response = Async\await($this->publicGetPublicInstruments ($this->extend($request, $params)));
            $markets = $this->safe_value($response, 'data', array());
            return $this->parse_markets($markets);
        }) ();
    }

    public function parse_market(array $market): array {
        //
        // $spot markets
        //
        //     {
        //         "base_currency" => "EOS",
        //         "instrument_id" => "EOS-OKB",
        //         "min_size" => "0.01",
        //         "quote_currency" => "OKB",
        //         "size_increment" => "0.000001",
        //         "tick_size" => "0.0001"
        //     }
        //
        $id = $this->safe_string($market, 'instId');
        $type = $this->safe_string_lower($market, 'instType');
        if ($type === 'futures') {
            $type = 'future';
        }
        $spot = ($type === 'spot');
        $future = ($type === 'future');
        $swap = ($type === 'swap');
        $option = ($type === 'option');
        $contract = $swap || $future || $option;
        $baseId = $this->safe_string($market, 'baseCcy');
        $quoteId = $this->safe_string($market, 'quoteCcy');
        $base = $this->safe_currency_code($baseId);
        $quote = $this->safe_currency_code($quoteId);
        $symbol = $base . '/' . $quote;
        $tickSize = $this->safe_string($market, 'tickSz');
        $fees = $this->safe_value_2($this->fees, $type, 'trading', array());
        $maxLeverage = $this->safe_string($market, 'lever', '1');
        $maxLeverage = Precise::string_max($maxLeverage, '1');
        $maxSpotCost = $this->safe_number($market, 'maxMktSz');
        return $this->extend($fees, array(
            'id' => $id,
            'symbol' => $symbol,
            'base' => $base,
            'quote' => $quote,
            'settle' => null,
            'baseId' => $baseId,
            'quoteId' => $quoteId,
            'settleId' => null,
            'type' => $type,
            'spot' => $spot,
            'margin' => $spot && (Precise::string_gt($maxLeverage, '1')),
            'swap' => false,
            'future' => false,
            'option' => false,
            'active' => true,
            'contract' => false,
            'linear' => null,
            'inverse' => null,
            'contractSize' => $contract ? $this->safe_number($market, 'ctVal') : null,
            'expiry' => null,
            'expiryDatetime' => null,
            'strike' => null,
            'optionType' => null,
            'created' => $this->safe_integer($market, 'listTime'),
            'precision' => array(
                'amount' => $this->safe_number($market, 'lotSz'),
                'price' => $this->parse_number($tickSize),
            ),
            'limits' => array(
                'leverage' => array(
                    'min' => $this->parse_number('1'),
                    'max' => $this->parse_number($maxLeverage),
                ),
                'amount' => array(
                    'min' => $this->safe_number($market, 'minSz'),
                    'max' => null,
                ),
                'price' => array(
                    'min' => null,
                    'max' => null,
                ),
                'cost' => array(
                    'min' => null,
                    'max' => $contract ? null : $maxSpotCost,
                ),
            ),
            'info' => $market,
        ));
    }

    public function fetch_currencies($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * fetches all available currencies on an exchange
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an associative dictionary of currencies
             */
            if (!$this->check_required_credentials(false)) {
                if ($this->options['warnOnFetchCurrenciesWithoutAuthorization']) {
                    throw new ExchangeError($this->id . ' fetchCurrencies() is a private API endpoint that requires authentication with API keys. Set the API keys on the exchange instance or exchange.options["warnOnFetchCurrenciesWithoutAuthorization"] = false to suppress this warning message.');
                }
                return array();
            } else {
                $response = Async\await($this->privateGetAssetCurrencies ($params));
                $data = $this->safe_list($response, 'data', array());
                $result = array();
                $dataByCurrencyId = $this->group_by($data, 'ccy');
                $currencyIds = is_array($dataByCurrencyId) ? array_keys($dataByCurrencyId) : array();
                for ($i = 0; $i < count($currencyIds); $i++) {
                    $currencyId = $currencyIds[$i];
                    $code = $this->safe_currency_code($currencyId);
                    $chains = $dataByCurrencyId[$currencyId];
                    $networks = array();
                    for ($j = 0; $j < count($chains); $j++) {
                        $chain = $chains[$j];
                        $networkId = $this->safe_string($chain, 'chain');
                        if (($networkId !== null) && (mb_strpos($networkId, '-') !== false)) {
                            $parts = explode('-', $networkId);
                            $chainPart = $this->safe_string($parts, 1, $networkId);
                            $networkCode = $this->network_id_to_code($chainPart);
                            $networks[$networkCode] = array(
                                'id' => $networkId,
                                'network' => $networkCode,
                                'active' => null,
                                'deposit' => $this->safe_bool($chain, 'canDep'),
                                'withdraw' => $this->safe_bool($chain, 'canWd'),
                                'fee' => $this->safe_number($chain, 'minFee'),
                                'precision' => $this->parse_number($this->parse_precision($this->safe_string($chain, 'wdTickSz'))),
                                'limits' => array(
                                    'withdraw' => array(
                                        'min' => $this->safe_number($chain, 'minWd'),
                                        'max' => $this->safe_number($chain, 'maxWd'),
                                    ),
                                ),
                                'info' => $chain,
                            );
                        }
                    }
                    $firstChain = $this->safe_value($chains, 0);
                    $result[$code] = $this->safe_currency_structure(array(
                        'info' => $chains,
                        'code' => $code,
                        'id' => $currencyId,
                        'name' => $this->safe_string($firstChain, 'name'),
                        'active' => null,
                        'deposit' => null,
                        'withdraw' => null,
                        'fee' => null,
                        'precision' => null,
                        'limits' => array(
                            'amount' => array(
                                'min' => null,
                                'max' => null,
                            ),
                        ),
                        'networks' => $networks,
                    ));
                }
                return $result;
            }
        }) ();
    }

    public function fetch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-$market-$data-get-order-book
             *
             * fetches information on open orders with bid (buy) and ask (sell) prices, volumes and other $data
             * @param {string} $symbol unified $symbol of the $market to fetch the order book for
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by $market symbols
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
            );
            $limit = ($limit === null) ? 20 : $limit;
            if ($limit !== null) {
                $request['sz'] = $limit; // max 400
            }
            $response = Async\await($this->publicGetMarketBooks ($this->extend($request, $params)));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "",
            //         "data" => [
            //             {
            //                 "asks" => [
            //                     ["0.07228","4.211619","0","2"], // price, amount, liquidated orders, total open orders
            //                     ["0.0723","299.880364","0","2"],
            //                     ["0.07231","3.72832","0","1"],
            //                 ],
            //                 "bids" => [
            //                     ["0.07221","18.5","0","1"],
            //                     ["0.0722","18.5","0","1"],
            //                     ["0.07219","0.505407","0","1"],
            //                 ],
            //                 "ts" => "1621438475342"
            //             }
            //         ]
            //     }
            //
            $data = $this->safe_value($response, 'data', array());
            $first = $this->safe_value($data, 0, array());
            $timestamp = $this->safe_integer($first, 'ts');
            return $this->parse_order_book($first, $symbol, $timestamp);
        }) ();
    }

    public function parse_ticker(array $ticker, ?array $market = null): array {
        //
        //     {
        //         "instType" => "SPOT",
        //         "instId" => "ETH-BTC",
        //         "last" => "0.07319",
        //         "lastSz" => "0.044378",
        //         "askPx" => "0.07322",
        //         "askSz" => "4.2",
        //         "bidPx" => "0.0732",
        //         "bidSz" => "6.050058",
        //         "open24h" => "0.07801",
        //         "high24h" => "0.07975",
        //         "low24h" => "0.06019",
        //         "volCcy24h" => "11788.887619",
        //         "vol24h" => "167493.829229",
        //         "ts" => "1621440583784",
        //         "sodUtc0" => "0.07872",
        //         "sodUtc8" => "0.07345"
        //     }
        //
        $timestamp = $this->safe_integer($ticker, 'ts');
        $marketId = $this->safe_string($ticker, 'instId');
        $market = $this->safe_market($marketId, $market, '-');
        $symbol = $market['symbol'];
        $last = $this->safe_string($ticker, 'last');
        $open = $this->safe_string($ticker, 'open24h');
        $spot = $this->safe_bool($market, 'spot', false);
        $quoteVolume = $spot ? $this->safe_string($ticker, 'volCcy24h') : null;
        $baseVolume = $this->safe_string($ticker, 'vol24h');
        $high = $this->safe_string($ticker, 'high24h');
        $low = $this->safe_string($ticker, 'low24h');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $high,
            'low' => $low,
            'bid' => $this->safe_string($ticker, 'bidPx'),
            'bidVolume' => $this->safe_string($ticker, 'bidSz'),
            'ask' => $this->safe_string($ticker, 'askPx'),
            'askVolume' => $this->safe_string($ticker, 'askSz'),
            'vwap' => null,
            'open' => $open,
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => null,
            'percentage' => null,
            'average' => null,
            'baseVolume' => $baseVolume,
            'quoteVolume' => $quoteVolume,
            'info' => $ticker,
        ), $market);
    }

    public function fetch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-$market-$data-get-ticker
             *
             * fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific $market
             * @param {string} $symbol unified $symbol of the $market to fetch the ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
            );
            $response = Async\await($this->publicGetMarketTicker ($this->extend($request, $params)));
            $data = $this->safe_value($response, 'data', array());
            $first = $this->safe_value($data, 0, array());
            //
            //     {
            //         "code" => "0",
            //         "msg" => "",
            //         "data" => array(
            //             {
            //                 "instType" => "SPOT",
            //                 "instId" => "ETH-BTC",
            //                 "last" => "0.07319",
            //                 "lastSz" => "0.044378",
            //                 "askPx" => "0.07322",
            //                 "askSz" => "4.2",
            //                 "bidPx" => "0.0732",
            //                 "bidSz" => "6.050058",
            //                 "open24h" => "0.07801",
            //                 "high24h" => "0.07975",
            //                 "low24h" => "0.06019",
            //                 "volCcy24h" => "11788.887619",
            //                 "vol24h" => "167493.829229",
            //                 "ts" => "1621440583784",
            //                 "sodUtc0" => "0.07872",
            //                 "sodUtc8" => "0.07345"
            //             }
            //         )
            //     }
            //
            return $this->parse_ticker($first, $market);
        }) ();
    }

    public function fetch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-market-$data-get-tickers
             *
             * fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market
             * @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~
             */
            $symbols = $this->market_symbols($symbols);
            $request = array(
                'instType' => 'SPOT',
            );
            $response = Async\await($this->publicGetMarketTickers ($this->extend($request, $params)));
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_tickers($data, $symbols, $params);
        }) ();
    }

    public function parse_trade(array $trade, ?array $market = null): array {
        //
        // public fetchTrades
        //
        //     {
        //         "instId" => "ETH-BTC",
        //         "side" => "sell",
        //         "sz" => "0.119501",
        //         "px" => "0.07065",
        //         "tradeId" => "15826757",
        //         "ts" => "1621446178316"
        //     }
        //
        // private fetchMyTrades
        //
        //     {
        //         "side" => "buy",
        //         "fillSz" => "0.007533",
        //         "fillPx" => "2654.98",
        //         "fee" => "-0.000007533",
        //         "ordId" => "317321390244397056",
        //         "instType" => "SPOT",
        //         "instId" => "ETH-USDT",
        //         "clOrdId" => "",
        //         "posSide" => "net",
        //         "billId" => "317321390265368576",
        //         "tag" => "0",
        //         "execType" => "T",
        //         "tradeId" => "107601752",
        //         "feeCcy" => "ETH",
        //         "ts" => "1621927314985"
        //     }
        //
        $id = $this->safe_string($trade, 'tradeId');
        $marketId = $this->safe_string($trade, 'instId');
        $market = $this->safe_market($marketId, $market, '-');
        $symbol = $market['symbol'];
        $timestamp = $this->safe_integer($trade, 'ts');
        $price = $this->safe_string_2($trade, 'fillPx', 'px');
        $amount = $this->safe_string_2($trade, 'fillSz', 'sz');
        $side = $this->safe_string($trade, 'side');
        $orderId = $this->safe_string($trade, 'ordId');
        $feeCostString = $this->safe_string($trade, 'fee');
        $fee = null;
        if ($feeCostString !== null) {
            $feeCostSigned = Precise::string_neg($feeCostString);
            $feeCurrencyId = $this->safe_string($trade, 'feeCcy');
            $feeCurrencyCode = $this->safe_currency_code($feeCurrencyId);
            $fee = array(
                'cost' => $feeCostSigned,
                'currency' => $feeCurrencyCode,
            );
        }
        $takerOrMaker = $this->safe_string($trade, 'execType');
        if ($takerOrMaker === 'T') {
            $takerOrMaker = 'taker';
        } elseif ($takerOrMaker === 'M') {
            $takerOrMaker = 'maker';
        }
        return $this->safe_trade(array(
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'id' => $id,
            'order' => $orderId,
            'type' => null,
            'takerOrMaker' => $takerOrMaker,
            'side' => $side,
            'price' => $price,
            'amount' => $amount,
            'cost' => null,
            'fee' => $fee,
        ), $market);
    }

    public function fetch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-$market-$data-get-trades
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-$market-$data-get-trades-history
             *
             * get the list of most recent trades for a particular $symbol
             * @param {string} $symbol unified $symbol of the $market to fetch trades for
             * @param {int} [$since] timestamp in ms of the earliest trade to fetch
             * @param {int} [$limit] the maximum amount of trades to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            if (($limit === null) || ($limit > 100)) {
                $limit = 100; // maximum = default = 100
            }
            $request = array(
                'instId' => $market['id'],
            );
            $method = null;
            list($method, $params) = $this->handle_option_and_params($params, 'fetchTrades', 'method', 'publicGetMarketTrades');
            $response = null;
            if ($method === 'publicGetMarketTrades') {
                $response = Async\await($this->publicGetMarketTrades ($this->extend($request, $params)));
            } else {
                $response = Async\await($this->publicGetMarketHistoryTrades ($this->extend($request, $params)));
            }
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_trades($data, $market, $since, $limit);
        }) ();
    }

    public function parse_ohlcv($ohlcv, ?array $market = null): array {
        //
        //     array(
        //         "1678928760000", // timestamp
        //         "24341.4", // open
        //         "24344", // high
        //         "24313.2", // low
        //         "24323", // close
        //         "628", // contract volume
        //         "2.5819", // base volume
        //         "62800", // quote volume
        //         "0" // candlestick state
        //     )
        //
        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, 5),
        );
    }

    public function fetch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-$market-$data-get-candlesticks
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-$market-$data-get-candlesticks-history
             *
             * fetches historical candlestick $data containing the open, high, low, and close price, and the volume of a $market
             * @param {string} $symbol unified $symbol of the $market to fetch OHLCV $data for
             * @param {string} $timeframe the length of time each candle represents
             * @param {int} [$since] timestamp in ms of the earliest candle to fetch
             * @param {int} [$limit] the maximum amount of candles to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $duration = $this->parse_timeframe($timeframe);
            $options = $this->safe_value($this->options, 'fetchOHLCV', array());
            $bar = $this->safe_string($this->timeframes, $timeframe, $timeframe);
            $timezone = $this->safe_string($options, 'timezone', 'UTC');
            if (($timezone === 'UTC') && ($duration >= 21600)) { // if utc and $timeframe >= 6h
                $bar .= strtolower($timezone);
            }
            $request = array(
                'instId' => $market['id'],
                'bar' => $bar,
            );
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100, max 100
            }
            $method = null;
            list($method, $params) = $this->handle_option_and_params($params, 'fetchOHLCV', 'method', 'publicGetMarketCandles');
            $response = null;
            if ($method === 'publicGetMarketCandles') {
                $response = Async\await($this->publicGetMarketCandles ($this->extend($request, $params)));
            } else {
                $response = Async\await($this->publicGetMarketHistoryCandles ($this->extend($request, $params)));
            }
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_ohlcvs($data, $market, $timeframe, $since, $limit);
        }) ();
    }

    public function parse_account_balance($response) {
        //
        // $account
        //
        //     array(
        //         array(
        //             "balance" =>  0,
        //             "available" =>  0,
        //             "currency" => "BTC",
        //             "hold" =>  0
        //         ),
        //         {
        //             "balance" =>  0,
        //             "available" =>  0,
        //             "currency" => "ETH",
        //             "hold" =>  0
        //         }
        //     )
        //
        // spot
        //
        //     array(
        //         array(
        //             "frozen" => "0",
        //             "hold" => "0",
        //             "id" => "2149632",
        //             "currency" => "BTC",
        //             "balance" => "0.0000000497717339",
        //             "available" => "0.0000000497717339",
        //             "holds" => "0"
        //         ),
        //         {
        //             "frozen" => "0",
        //             "hold" => "0",
        //             "id" => "2149632",
        //             "currency" => "ICN",
        //             "balance" => "0.00000000925",
        //             "available" => "0.00000000925",
        //             "holds" => "0"
        //         }
        //     )
        //
        $result = array(
            'info' => $response,
            'timestamp' => null,
            'datetime' => null,
        );
        for ($i = 0; $i < count($response); $i++) {
            $balance = $response[$i];
            $currencyId = $this->safe_string($balance, 'currency');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['total'] = $this->safe_string($balance, 'balance');
            $account['used'] = $this->safe_string($balance, 'hold');
            $account['free'] = $this->safe_string($balance, 'available');
            $result[$code] = $account;
        }
        return $this->safe_balance($result);
    }

    public function fetch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * $query for 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
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            list($marketType, $query) = $this->handle_market_type_and_params('fetchBalance', null, $params);
            $request = array(
                // 'ccy' => 'BTC,ETH', // comma-separated list of currency ids
            );
            $response = null;
            if ($marketType === 'funding') {
                $response = Async\await($this->privateGetAssetBalances ($this->extend($request, $query)));
            } else {
                $response = Async\await($this->privateGetAccountBalance ($this->extend($request, $query)));
            }
            //
            //  {
            //         "code" => "0",
            //         "data" => array(
            //             {
            //                 "category" => "1",
            //                 "delivery" => "",
            //                 "exercise" => "",
            //                 "instType" => "SPOT",
            //                 "level" => "Lv1",
            //                 "maker" => "-0.0008",
            //                 "taker" => "-0.001",
            //                 "ts" => "1639043138472"
            //             }
            //         ),
            //         "msg" => ""
            //     }
            //
            if ($marketType === 'funding') {
                return $this->parse_funding_balance($response);
            } else {
                return $this->parse_trading_balance($response);
            }
        }) ();
    }

    public function parse_trading_balance($response) {
        $result = array( 'info' => $response );
        $data = $this->safe_value($response, 'data', array());
        $first = $this->safe_value($data, 0, array());
        $timestamp = $this->safe_integer($first, 'uTime');
        $details = $this->safe_value($first, 'details', array());
        for ($i = 0; $i < count($details); $i++) {
            $balance = $details[$i];
            $currencyId = $this->safe_string($balance, 'ccy');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            // it may be incorrect to use total, free and used for swap accounts
            $eq = $this->safe_string($balance, 'eq');
            $availEq = $this->safe_string($balance, 'availEq');
            if (($eq === null) || ($availEq === null)) {
                $account['free'] = $this->safe_string($balance, 'availBal');
                $account['used'] = $this->safe_string($balance, 'frozenBal');
            } else {
                $account['total'] = $eq;
                $account['free'] = $availEq;
            }
            $result[$code] = $account;
        }
        $result['timestamp'] = $timestamp;
        $result['datetime'] = $this->iso8601($timestamp);
        return $this->safe_balance($result);
    }

    public function parse_funding_balance($response) {
        $result = array( 'info' => $response );
        $data = $this->safe_value($response, 'data', array());
        for ($i = 0; $i < count($data); $i++) {
            $balance = $data[$i];
            $currencyId = $this->safe_string($balance, 'ccy');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            // it may be incorrect to use total, free and used for swap accounts
            $account['total'] = $this->safe_string($balance, 'bal');
            $account['free'] = $this->safe_string($balance, 'availBal');
            $account['used'] = $this->safe_string($balance, 'frozenBal');
            $result[$code] = $account;
        }
        return $this->safe_balance($result);
    }

    public function create_market_buy_order_with_cost(string $symbol, float $cost, $params = array ()) {
        return Async\async(function () use ($symbol, $cost, $params) {
            /**
             * create a $market buy order by providing the $symbol and $cost
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-place-order
             *
             * @param {string} $symbol unified $symbol of the $market to create an order in
             * @param {float} $cost how much you want to trade in units of the quote currency
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            if (!$market['spot']) {
                throw new NotSupported($this->id . ' createMarketBuyOrderWithCost() supports spot orders only');
            }
            $params['createMarketBuyOrderRequiresPrice'] = false;
            $params['tgtCcy'] = 'quote_ccy';
            return Async\await($this->create_order($symbol, 'market', 'buy', $cost, null, $params));
        }) ();
    }

    public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        return Async\async(function () use ($symbol, $type, $side, $amount, $price, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-place-$order
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-place-algo-$order
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-place-multiple-orders
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-cancel-advance-algo-$order
             *
             * create a trade $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 {bool} [$params->reduceOnly] MARGIN orders only, or swap/future orders in net mode
             * @param {bool} [$params->postOnly] true to place a post only $order
             * @param {float} [$params->triggerPrice] conditional orders only, the $price at which the $order is to be triggered
             * @param {array} [$params->takeProfit] *takeProfit object in $params* containing the triggerPrice at which the attached take profit $order will be triggered (perpetual swap markets only)
             * @param {float} [$params->takeProfit.triggerPrice] take profit trigger $price
             * @param {float} [$params->takeProfit.price] used for take profit limit orders, not used for take profit $market $price orders
             * @param {string} [$params->takeProfit.type] 'market' or 'limit' used to specify the take profit $price $type
             * @param {array} [$params->stopLoss] *stopLoss object in $params* containing the triggerPrice at which the attached stop loss $order will be triggered (perpetual swap markets only)
             * @param {float} [$params->stopLoss.triggerPrice] stop loss trigger $price
             * @param {float} [$params->stopLoss.price] used for stop loss limit orders, not used for stop loss $market $price orders
             * @param {string} [$params->stopLoss.type] 'market' or 'limit' used to specify the stop loss $price $type
             * @param {float} [$params->cost] *spot $market buy only* the quote quantity that can be used alternative for the $amount
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=$order-structure $order structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = $this->create_order_request($symbol, $type, $side, $amount, $price, $params);
            $method = $this->safe_string($this->options, 'createOrder', 'privatePostTradeBatchOrders');
            $requestOrdType = $this->safe_string($request, 'ordType');
            if (($requestOrdType === 'trigger') || ($requestOrdType === 'conditional') || ($type === 'oco') || ($type === 'move_order_stop') || ($type === 'iceberg') || ($type === 'twap')) {
                $method = 'privatePostTradeOrderAlgo';
            }
            if ($method === 'privatePostTradeBatchOrders') {
                // keep the $request body the same
                // submit a single $order in an array to the batch $order endpoint
                // because it has a lower ratelimit
                $request = array( $request );
            }
            $response = null;
            if ($method === 'privatePostTradeOrder') {
                $response = Async\await($this->privatePostTradeOrder ($request));
            } elseif ($method === 'privatePostTradeOrderAlgo') {
                $response = Async\await($this->privatePostTradeOrderAlgo ($request));
            } elseif ($method === 'privatePostTradeBatchOrders') {
                $response = Async\await($this->privatePostTradeBatchOrders ($request));
            } else {
                throw new ExchangeError($this->id . ' createOrder() $this->options["createOrder"] must be either privatePostTradeBatchOrders or privatePostTradeOrder or privatePostTradeOrderAlgo');
            }
            $data = $this->safe_value($response, 'data', array());
            $first = $this->safe_value($data, 0);
            $order = $this->parse_order($first, $market);
            $order['type'] = $type;
            $order['side'] = $side;
            return $order;
        }) ();
    }

    public function create_order_request(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        $market = $this->market($symbol);
        $request = array(
            'instId' => $market['id'],
            // 'ccy' => $currency['id'], // only applicable to cross MARGIN orders in single-$currency $margin
            // 'clOrdId' => $clientOrderId, // up to 32 characters, must be unique
            // 'tag' => tag, // up to 8 characters
            'side' => $side,
            // 'posSide' => 'long', // long, short, // required in the long/short mode, and can only be long or short (only for future or swap)
            'ordType' => $type,
            // 'ordType' => $type, // privatePostTradeOrder => $market, limit, post_only, $fok, $ioc, optimal_limit_ioc
            // 'ordType' => $type, // privatePostTradeOrderAlgo => $conditional, oco, $trigger, move_order_stop, iceberg, twap
            // 'sz' => $this->amount_to_precision($symbol, $amount),
            // 'px' => $this->price_to_precision($symbol, $price), // limit orders only
            // 'reduceOnly' => false,
            //
            // 'triggerPx' => 10, // stopPrice ($trigger orders)
            // 'orderPx' => 10, // Order $price if -1, the order will be executed at the $market $price-> ($trigger orders)
            // 'triggerPxType' => 'last', // Conditional default is last, mark or index ($trigger orders)
            //
            // 'tpTriggerPx' => 10, // $takeProfitPrice ($conditional orders)
            // 'tpTriggerPxType' => 'last', // Conditional default is last, mark or index ($conditional orders)
            // 'tpOrdPx' => 10, // Order $price for Take-Profit orders, if -1 will be executed at $market $price ($conditional orders)
            //
            // 'slTriggerPx' => 10, // $stopLossPrice ($conditional orders)
            // 'slTriggerPxType' => 'last', // Conditional default is last, mark or index ($conditional orders)
            // 'slOrdPx' => 10, // Order $price for Stop-Loss orders, if -1 will be executed at $market $price ($conditional orders)
        );
        $triggerPrice = $this->safe_value_n($params, array( 'triggerPrice', 'stopPrice', 'triggerPx' ));
        $timeInForce = $this->safe_string($params, 'timeInForce', 'GTC');
        $takeProfitPrice = $this->safe_value_2($params, 'takeProfitPrice', 'tpTriggerPx');
        $tpOrdPx = $this->safe_value($params, 'tpOrdPx', $price);
        $tpTriggerPxType = $this->safe_string($params, 'tpTriggerPxType', 'last');
        $stopLossPrice = $this->safe_value_2($params, 'stopLossPrice', 'slTriggerPx');
        $slOrdPx = $this->safe_value($params, 'slOrdPx', $price);
        $slTriggerPxType = $this->safe_string($params, 'slTriggerPxType', 'last');
        $clientOrderId = $this->safe_string_2($params, 'clOrdId', 'clientOrderId');
        $stopLoss = $this->safe_value($params, 'stopLoss');
        $stopLossDefined = ($stopLoss !== null);
        $takeProfit = $this->safe_value($params, 'takeProfit');
        $takeProfitDefined = ($takeProfit !== null);
        $defaultMarginMode = $this->safe_string_2($this->options, 'defaultMarginMode', 'marginMode', 'cross');
        $marginMode = $this->safe_string_2($params, 'marginMode', 'tdMode'); // cross or isolated, tdMode not ommited so be extended into the $request
        $margin = false;
        if (($marginMode !== null) && ($marginMode !== 'cash')) {
            $margin = true;
        } else {
            $marginMode = $defaultMarginMode;
            $margin = $this->safe_bool($params, 'margin', false);
        }
        if ($margin) {
            $defaultCurrency = ($side === 'buy') ? $market['quote'] : $market['base'];
            $currency = $this->safe_string($params, 'ccy', $defaultCurrency);
            $request['ccy'] = $this->safe_currency_code($currency);
        }
        $tradeMode = $margin ? $marginMode : 'cash';
        $request['tdMode'] = $tradeMode;
        $isMarketOrder = $type === 'market';
        $postOnly = false;
        list($postOnly, $params) = $this->handle_post_only($isMarketOrder, $type === 'post_only', $params);
        $params = $this->omit($params, array( 'currency', 'ccy', 'marginMode', 'timeInForce', 'stopPrice', 'triggerPrice', 'clientOrderId', 'stopLossPrice', 'takeProfitPrice', 'slOrdPx', 'tpOrdPx', 'margin', 'stopLoss', 'takeProfit' ));
        $ioc = ($timeInForce === 'IOC') || ($type === 'ioc');
        $fok = ($timeInForce === 'FOK') || ($type === 'fok');
        $trigger = ($triggerPrice !== null) || ($type === 'trigger');
        $conditional = ($stopLossPrice !== null) || ($takeProfitPrice !== null) || ($type === 'conditional');
        $marketIOC = ($isMarketOrder && $ioc) || ($type === 'optimal_limit_ioc');
        $defaultTgtCcy = $this->safe_string($this->options, 'tgtCcy', 'base_ccy');
        $tgtCcy = $this->safe_string($params, 'tgtCcy', $defaultTgtCcy);
        if ((!$margin)) {
            $request['tgtCcy'] = $tgtCcy;
        }
        if ($isMarketOrder || $marketIOC) {
            $request['ordType'] = 'market';
            if ($side === 'buy') {
                // spot $market buy => "sz" can refer either to base $currency units or to quote $currency units
                // see documentation => https://www.okx.com/docs-v5/en/#rest-api-trade-place-order
                if ($tgtCcy === 'quote_ccy') {
                    // quote_ccy => sz refers to units of quote $currency
                    $quoteAmount = null;
                    $createMarketBuyOrderRequiresPrice = true;
                    list($createMarketBuyOrderRequiresPrice, $params) = $this->handle_option_and_params($params, 'createOrder', 'createMarketBuyOrderRequiresPrice', true);
                    $cost = $this->safe_number_2($params, 'cost', 'sz');
                    $params = $this->omit($params, array( 'cost', 'sz' ));
                    if ($cost !== null) {
                        $quoteAmount = $this->cost_to_precision($symbol, $cost);
                    } elseif ($createMarketBuyOrderRequiresPrice) {
                        if ($price === null) {
                            throw new InvalidOrder($this->id . ' createOrder() requires the $price argument for $market buy orders to calculate the total $cost to spend ($amount * $price), alternatively set the $createMarketBuyOrderRequiresPrice option or param to false and pass the $cost to spend (quote quantity) in the $amount argument');
                        } else {
                            $amountString = $this->number_to_string($amount);
                            $priceString = $this->number_to_string($price);
                            $costRequest = Precise::string_mul($amountString, $priceString);
                            $quoteAmount = $this->cost_to_precision($symbol, $costRequest);
                        }
                    } else {
                        $quoteAmount = $this->cost_to_precision($symbol, $amount);
                    }
                    $request['sz'] = $quoteAmount;
                } else {
                    $request['sz'] = $this->amount_to_precision($symbol, $amount);
                }
            } else {
                $request['sz'] = $this->amount_to_precision($symbol, $amount);
            }
        } else {
            $request['sz'] = $this->amount_to_precision($symbol, $amount);
            if ((!$trigger) && (!$conditional)) {
                $request['px'] = $this->price_to_precision($symbol, $price);
            }
        }
        if ($postOnly) {
            $request['ordType'] = 'post_only';
        } elseif ($ioc && !$marketIOC) {
            $request['ordType'] = 'ioc';
        } elseif ($fok) {
            $request['ordType'] = 'fok';
        } elseif ($stopLossDefined || $takeProfitDefined) {
            if ($stopLossDefined) {
                $stopLossTriggerPrice = $this->safe_value_n($stopLoss, array( 'triggerPrice', 'stopPrice', 'slTriggerPx' ));
                if ($stopLossTriggerPrice === null) {
                    throw new InvalidOrder($this->id . ' createOrder() requires a $trigger $price in $params["stopLoss"]["triggerPrice"] for a stop loss order');
                }
                $request['slTriggerPx'] = $this->price_to_precision($symbol, $stopLossTriggerPrice);
                $stopLossLimitPrice = $this->safe_value_n($stopLoss, array( 'price', 'stopLossPrice', 'slOrdPx' ));
                $stopLossOrderType = $this->safe_string($stopLoss, 'type');
                if ($stopLossOrderType !== null) {
                    $stopLossLimitOrderType = ($stopLossOrderType === 'limit');
                    $stopLossMarketOrderType = ($stopLossOrderType === 'market');
                    if ((!$stopLossLimitOrderType) && (!$stopLossMarketOrderType)) {
                        throw new InvalidOrder($this->id . ' createOrder() $params["stopLoss"]["type"] must be either "limit" or "market"');
                    } elseif ($stopLossLimitOrderType) {
                        if ($stopLossLimitPrice === null) {
                            throw new InvalidOrder($this->id . ' createOrder() requires a limit $price in $params["stopLoss"]["price"] for a stop loss limit order');
                        } else {
                            $request['slOrdPx'] = $this->price_to_precision($symbol, $stopLossLimitPrice);
                        }
                    } elseif ($stopLossOrderType === 'market') {
                        $request['slOrdPx'] = '-1';
                    }
                } elseif ($stopLossLimitPrice !== null) {
                    $request['slOrdPx'] = $this->price_to_precision($symbol, $stopLossLimitPrice); // limit sl order
                } else {
                    $request['slOrdPx'] = '-1'; // $market sl order
                }
                $stopLossTriggerPriceType = $this->safe_string_2($stopLoss, 'triggerPriceType', 'slTriggerPxType', 'last');
                if ($stopLossTriggerPriceType !== null) {
                    if (($stopLossTriggerPriceType !== 'last') && ($stopLossTriggerPriceType !== 'index') && ($stopLossTriggerPriceType !== 'mark')) {
                        throw new InvalidOrder($this->id . ' createOrder() stop loss $trigger $price $type must be one of "last", "index" or "mark"');
                    }
                    $request['slTriggerPxType'] = $stopLossTriggerPriceType;
                }
            }
            if ($takeProfitDefined) {
                $takeProfitTriggerPrice = $this->safe_value_n($takeProfit, array( 'triggerPrice', 'stopPrice', 'tpTriggerPx' ));
                if ($takeProfitTriggerPrice === null) {
                    throw new InvalidOrder($this->id . ' createOrder() requires a $trigger $price in $params["takeProfit"]["triggerPrice"], or $params["takeProfit"]["stopPrice"], or $params["takeProfit"]["tpTriggerPx"] for a take profit order');
                }
                $request['tpTriggerPx'] = $this->price_to_precision($symbol, $takeProfitTriggerPrice);
                $takeProfitLimitPrice = $this->safe_value_n($takeProfit, array( 'price', 'takeProfitPrice', 'tpOrdPx' ));
                $takeProfitOrderType = $this->safe_string($takeProfit, 'type');
                if ($takeProfitOrderType !== null) {
                    $takeProfitLimitOrderType = ($takeProfitOrderType === 'limit');
                    $takeProfitMarketOrderType = ($takeProfitOrderType === 'market');
                    if ((!$takeProfitLimitOrderType) && (!$takeProfitMarketOrderType)) {
                        throw new InvalidOrder($this->id . ' createOrder() $params["takeProfit"]["type"] must be either "limit" or "market"');
                    } elseif ($takeProfitLimitOrderType) {
                        if ($takeProfitLimitPrice === null) {
                            throw new InvalidOrder($this->id . ' createOrder() requires a limit $price in $params["takeProfit"]["price"] or $params["takeProfit"]["tpOrdPx"] for a take profit limit order');
                        } else {
                            $request['tpOrdPx'] = $this->price_to_precision($symbol, $takeProfitLimitPrice);
                        }
                    } elseif ($takeProfitOrderType === 'market') {
                        $request['tpOrdPx'] = '-1';
                    }
                } elseif ($takeProfitLimitPrice !== null) {
                    $request['tpOrdPx'] = $this->price_to_precision($symbol, $takeProfitLimitPrice); // limit tp order
                } else {
                    $request['tpOrdPx'] = '-1'; // $market tp order
                }
                $takeProfitTriggerPriceType = $this->safe_string_2($takeProfit, 'triggerPriceType', 'tpTriggerPxType', 'last');
                if ($takeProfitTriggerPriceType !== null) {
                    if (($takeProfitTriggerPriceType !== 'last') && ($takeProfitTriggerPriceType !== 'index') && ($takeProfitTriggerPriceType !== 'mark')) {
                        throw new InvalidOrder($this->id . ' createOrder() take profit $trigger $price $type must be one of "last", "index" or "mark"');
                    }
                    $request['tpTriggerPxType'] = $takeProfitTriggerPriceType;
                }
            }
        } elseif ($trigger) {
            $request['ordType'] = 'trigger';
            $request['triggerPx'] = $this->price_to_precision($symbol, $triggerPrice);
            $request['orderPx'] = $isMarketOrder ? '-1' : $this->price_to_precision($symbol, $price);
        } elseif ($conditional) {
            $request['ordType'] = 'conditional';
            $twoWayCondition = (($takeProfitPrice !== null) && ($stopLossPrice !== null));
            // if TP and SL are sent together
            // 'conditional' only stop-loss order will be applied
            if ($twoWayCondition) {
                $request['ordType'] = 'oco';
            }
            if ($takeProfitPrice !== null) {
                $request['tpTriggerPx'] = $this->price_to_precision($symbol, $takeProfitPrice);
                $request['tpOrdPx'] = ($tpOrdPx === null) ? '-1' : $this->price_to_precision($symbol, $tpOrdPx);
                $request['tpTriggerPxType'] = $tpTriggerPxType;
            }
            if ($stopLossPrice !== null) {
                $request['slTriggerPx'] = $this->price_to_precision($symbol, $stopLossPrice);
                $request['slOrdPx'] = ($slOrdPx === null) ? '-1' : $this->price_to_precision($symbol, $slOrdPx);
                $request['slTriggerPxType'] = $slTriggerPxType;
            }
        }
        if ($clientOrderId === null) {
            $brokerId = $this->safe_string($this->options, 'brokerId');
            if ($brokerId !== null) {
                $request['clOrdId'] = $brokerId . $this->uuid16();
                $request['tag'] = $brokerId;
            }
        } else {
            $request['clOrdId'] = $clientOrderId;
            $params = $this->omit($params, array( 'clOrdId', 'clientOrderId' ));
        }
        return $this->extend($request, $params);
    }

    public function cancel_order(string $id, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-cancel-$order
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-cancel-algo-$order
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-cancel-advance-algo-$order
             *
             * cancels an open $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
             * @param {bool} [$params->trigger] True if cancel $trigger or conditional orders
             * @param {bool} [$params->advanced] True if canceling $advanced orders only
             * @return {array} An ~@link https://docs.ccxt.com/#/?$id=$order-structure $order structure~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' cancelOrder() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            $trigger = $this->safe_value_2($params, 'stop', 'trigger');
            $advanced = $this->safe_value($params, 'advanced');
            if ($trigger || $advanced) {
                $orderInner = Async\await($this->cancel_orders(array( $id ), $symbol, $params));
                return $this->safe_dict($orderInner, 0);
            }
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
                // 'ordId' => $id, // either ordId or clOrdId is required
                // 'clOrdId' => $clientOrderId,
            );
            $clientOrderId = $this->safe_string_2($params, 'clOrdId', 'clientOrderId');
            if ($clientOrderId !== null) {
                $request['clOrdId'] = $clientOrderId;
            } else {
                $request['ordId'] = (string) $id;
            }
            $query = $this->omit($params, array( 'clOrdId', 'clientOrderId' ));
            $response = Async\await($this->privatePostTradeCancelOrder ($this->extend($request, $query)));
            // array("code":"0","data":[array("clOrdId":"","ordId":"317251910906576896","sCode":"0","sMsg":"")],"msg":"")
            $data = $this->safe_value($response, 'data', array());
            $order = $this->safe_dict($data, 0);
            return $this->parse_order($order, $market);
        }) ();
    }

    public function parse_ids($ids) {
        /**
         * @ignore
         * @param {string[]|string} $ids order $ids
         * @return {string[]} list of order $ids
         */
        if (gettype($ids) === 'string') {
            return explode(',', $ids);
        } else {
            return $ids;
        }
    }

    public function cancel_orders($ids, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($ids, $symbol, $params) {
            /**
             * cancel multiple orders
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-cancel-multiple-orders
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-cancel-algo-order
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-cancel-advance-algo-order
             *
             * @param {string[]} $ids order $ids
             * @param {string} $symbol unified $market $symbol
             * @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 . ' cancelOrders() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            $trigger = $this->safe_value_2($params, 'stop', 'trigger');
            $advanced = $this->safe_value($params, 'advanced');
            $params = $this->omit($params, array( 'stop', 'trigger', 'advanced' ));
            $market = $this->market($symbol);
            $request = array();
            $clientOrderIds = $this->parse_ids($this->safe_value_2($params, 'clOrdId', 'clientOrderId'));
            $algoIds = $this->parse_ids($this->safe_value($params, 'algoId'));
            if ($clientOrderIds === null) {
                $ids = $this->parse_ids($ids);
                if ($algoIds !== null) {
                    for ($i = 0; $i < count($algoIds); $i++) {
                        $request[] = array(
                            'algoId' => $algoIds[$i],
                            'instId' => $market['id'],
                        );
                    }
                }
                for ($i = 0; $i < count($ids); $i++) {
                    if ($trigger || $advanced) {
                        $request[] = array(
                            'algoId' => $ids[$i],
                            'instId' => $market['id'],
                        );
                    } else {
                        $request[] = array(
                            'ordId' => $ids[$i],
                            'instId' => $market['id'],
                        );
                    }
                }
            } else {
                for ($i = 0; $i < count($clientOrderIds); $i++) {
                    $request[] = array(
                        'instId' => $market['id'],
                        'clOrdId' => $clientOrderIds[$i],
                    );
                }
            }
            $response = null;
            if ($trigger) {
                $response = Async\await($this->privatePostTradeCancelAlgos ($request));
            } elseif ($advanced) {
                $response = Async\await($this->privatePostTradeCancelAdvanceAlgos ($request));
            } else {
                $response = Async\await($this->privatePostTradeCancelBatchOrders ($request)); // * dont extend with $params, otherwise ARRAY will be turned into OBJECT
            }
            //
            //     {
            //         "code" => "0",
            //         "data" => array(
            //             array(
            //                 "clOrdId" => "e123456789ec4dBC1123456ba123b45e",
            //                 "ordId" => "405071912345641543",
            //                 "sCode" => "0",
            //                 "sMsg" => ""
            //             ),
            //             ...
            //         ),
            //         "msg" => ""
            //     }
            //
            //
            $ordersData = $this->safe_list($response, 'data', array());
            return $this->parse_orders($ordersData, $market, null, null, $params);
        }) ();
    }

    public function parse_order_status(?string $status) {
        $statuses = array(
            'canceled' => 'canceled',
            'live' => 'open',
            'partially_filled' => 'open',
            'filled' => 'closed',
            'effective' => 'closed',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_order(array $order, ?array $market = null): array {
        //
        // createOrder
        //
        //     {
        //         "clOrdId" => "oktswap6",
        //         "ordId" => "312269865356374016",
        //         "tag" => "",
        //         "sCode" => "0",
        //         "sMsg" => ""
        //     }
        //
        // editOrder
        //
        //     {
        //         "clOrdId" => "e847386590ce4dBCc1a045253497a547",
        //         "ordId" => "559176536793178112",
        //         "reqId" => "",
        //         "sCode" => "0",
        //         "sMsg" => ""
        //     }
        //
        // Spot and Swap fetchOrder, fetchOpenOrders
        //
        //     {
        //         "accFillSz" => "0",
        //         "avgPx" => "",
        //         "cTime" => "1621910749815",
        //         "category" => "normal",
        //         "ccy" => "",
        //         "clOrdId" => "",
        //         "fee" => "0",
        //         "feeCcy" => "ETH",
        //         "fillPx" => "",
        //         "fillSz" => "0",
        //         "fillTime" => "",
        //         "instId" => "ETH-USDT",
        //         "instType" => "SPOT",
        //         "lever" => "",
        //         "ordId" => "317251910906576896",
        //         "ordType" => "limit",
        //         "pnl" => "0",
        //         "posSide" => "net",
        //         "px" => "2000",
        //         "rebate" => "0",
        //         "rebateCcy" => "USDT",
        //         "side" => "buy",
        //         "slOrdPx" => "",
        //         "slTriggerPx" => "",
        //         "state" => "live",
        //         "sz" => "0.001",
        //         "tag" => "",
        //         "tdMode" => "cash",
        //         "tpOrdPx" => "",
        //         "tpTriggerPx" => "",
        //         "tradeId" => "",
        //         "uTime" => "1621910749815"
        //     }
        //
        // Algo Order fetchOpenOrders, fetchCanceledOrders, fetchClosedOrders
        //
        //     {
        //         "activePx" => "",
        //         "activePxType" => "",
        //         "actualPx" => "",
        //         "actualSide" => "buy",
        //         "actualSz" => "0",
        //         "algoId" => "431375349042380800",
        //         "cTime" => "1649119897778",
        //         "callbackRatio" => "",
        //         "callbackSpread" => "",
        //         "ccy" => "",
        //         "ctVal" => "0.01",
        //         "instId" => "BTC-USDT-SWAP",
        //         "instType" => "SWAP",
        //         "last" => "46538.9",
        //         "lever" => "125",
        //         "moveTriggerPx" => "",
        //         "notionalUsd" => "467.059",
        //         "ordId" => "",
        //         "ordPx" => "50000",
        //         "ordType" => "trigger",
        //         "posSide" => "long",
        //         "pxLimit" => "",
        //         "pxSpread" => "",
        //         "pxVar" => "",
        //         "side" => "buy",
        //         "slOrdPx" => "",
        //         "slTriggerPx" => "",
        //         "slTriggerPxType" => "",
        //         "state" => "live",
        //         "sz" => "1",
        //         "szLimit" => "",
        //         "tag" => "",
        //         "tdMode" => "isolated",
        //         "tgtCcy" => "",
        //         "timeInterval" => "",
        //         "tpOrdPx" => "",
        //         "tpTriggerPx" => "",
        //         "tpTriggerPxType" => "",
        //         "triggerPx" => "50000",
        //         "triggerPxType" => "last",
        //         "triggerTime" => "",
        //         "uly" => "BTC-USDT"
        //     }
        //
        $id = $this->safe_string_2($order, 'algoId', 'ordId');
        $timestamp = $this->safe_integer($order, 'cTime');
        $lastUpdateTimestamp = $this->safe_integer($order, 'uTime');
        $lastTradeTimestamp = $this->safe_integer($order, 'fillTime');
        $side = $this->safe_string($order, 'side');
        $type = $this->safe_string($order, 'ordType');
        $postOnly = null;
        $timeInForce = null;
        if ($type === 'post_only') {
            $postOnly = true;
            $type = 'limit';
        } elseif ($type === 'fok') {
            $timeInForce = 'FOK';
            $type = 'limit';
        } elseif ($type === 'ioc') {
            $timeInForce = 'IOC';
            $type = 'limit';
        }
        $marketId = $this->safe_string($order, 'instId');
        $market = $this->safe_market($marketId, $market);
        $symbol = $this->safe_symbol($marketId, $market, '-');
        $filled = $this->safe_string($order, 'accFillSz');
        $price = $this->safe_string_2($order, 'px', 'ordPx');
        $average = $this->safe_string($order, 'avgPx');
        $status = $this->parse_order_status($this->safe_string($order, 'state'));
        $feeCostString = $this->safe_string($order, 'fee');
        $amount = null;
        $cost = null;
        // spot $market buy => "sz" can refer either to base currency units or to quote currency units
        // see documentation => https://www.okx.com/docs-v5/en/#rest-api-trade-place-$order
        $defaultTgtCcy = $this->safe_string($this->options, 'tgtCcy', 'base_ccy');
        $tgtCcy = $this->safe_string($order, 'tgtCcy', $defaultTgtCcy);
        if (($side === 'buy') && ($type === 'market') && ($tgtCcy === 'quote_ccy')) {
            // "sz" refers to the $cost
            $cost = $this->safe_string($order, 'sz');
        } else {
            // "sz" refers to the trade currency $amount
            $amount = $this->safe_string($order, 'sz');
        }
        $fee = null;
        if ($feeCostString !== null) {
            $feeCostSigned = Precise::string_neg($feeCostString);
            $feeCurrencyId = $this->safe_string($order, 'feeCcy');
            $feeCurrencyCode = $this->safe_currency_code($feeCurrencyId);
            $fee = array(
                'cost' => $this->parse_number($feeCostSigned),
                'currency' => $feeCurrencyCode,
            );
        }
        $clientOrderId = $this->safe_string($order, 'clOrdId');
        if (($clientOrderId !== null) && (strlen($clientOrderId) < 1)) {
            $clientOrderId = null; // fix empty $clientOrderId string
        }
        $stopLossPrice = $this->safe_number_2($order, 'slTriggerPx', 'slOrdPx');
        $takeProfitPrice = $this->safe_number_2($order, 'tpTriggerPx', 'tpOrdPx');
        $reduceOnlyRaw = $this->safe_string($order, 'reduceOnly');
        $reduceOnly = false;
        if ($reduceOnly !== null) {
            $reduceOnly = ($reduceOnlyRaw === 'true');
        }
        return $this->safe_order(array(
            'info' => $order,
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => $lastTradeTimestamp,
            'lastUpdateTimestamp' => $lastUpdateTimestamp,
            'symbol' => $symbol,
            'type' => $type,
            'timeInForce' => $timeInForce,
            'postOnly' => $postOnly,
            'side' => $side,
            'price' => $price,
            'stopLossPrice' => $stopLossPrice,
            'takeProfitPrice' => $takeProfitPrice,
            'triggerPrice' => $this->safe_number_n($order, array( 'triggerPx', 'moveTriggerPx' )),
            'average' => $average,
            'cost' => $cost,
            'amount' => $amount,
            'filled' => $filled,
            'remaining' => null,
            'status' => $status,
            'fee' => $fee,
            'trades' => null,
            'reduceOnly' => $reduceOnly,
        ), $market);
    }

    public function fetch_order(string $id, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-get-$order-details
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-get-algo-$order-list
             *
             * fetches information on an $order made by the user
             * @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~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' fetchOrder() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
                // 'clOrdId' => 'abcdef12345', // optional, [a-z0-9]array(1,32)
                // 'ordId' => $id,
            );
            $clientOrderId = $this->safe_string_2($params, 'clOrdId', 'clientOrderId');
            $trigger = $this->safe_value_2($params, 'stop', 'trigger');
            if ($trigger) {
                if ($clientOrderId !== null) {
                    $request['algoClOrdId'] = $clientOrderId;
                } else {
                    $request['algoId'] = $id;
                }
            } else {
                if ($clientOrderId !== null) {
                    $request['clOrdId'] = $clientOrderId;
                } else {
                    $request['ordId'] = $id;
                }
            }
            $query = $this->omit($params, array( 'clientOrderId', 'stop', 'trigger' ));
            $response = null;
            if ($trigger) {
                $response = Async\await($this->privateGetTradeOrderAlgo ($this->extend($request, $query)));
            } else {
                $response = Async\await($this->privateGetTradeOrder ($this->extend($request, $query)));
            }
            $data = $this->safe_value($response, 'data', array());
            $order = $this->safe_dict($data, 0);
            return $this->parse_order($order);
        }) ();
    }

    public function fetch_open_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-get-order-list
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-get-algo-order-list
             *
             * fetch all unfilled currently open orders
             * @param {string} $symbol unified $market $symbol
             * @param {int} [$since] the earliest time in ms to fetch open orders for
             * @param {int} [$limit] the maximum number of  open orders structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {bool} [$params->trigger] True if fetching $trigger or conditional orders
             * @param {string} [$params->ordType] "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap"
             * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $request = array(
                // 'instId' => $market['id'],
                // 'ordType' => 'limit', // $market, $limit, post_only, fok, ioc, comma-separated, stop orders => conditional, oco, $trigger, move_order_stop, iceberg, or twap
                // 'state' => 'live', // live, partially_filled
                // 'after' => orderId,
                // 'before' => orderId,
                // 'limit' => $limit, // default 100, max 100
            );
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $request['instId'] = $market['id'];
            }
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100, max 100
            }
            $ordType = $this->safe_string($params, 'ordType');
            $trigger = $this->safe_value($params, 'stop') || ($this->safe_string($params, 'ordType') !== null);
            if ($trigger && ($ordType === null)) {
                $request['ordType'] = 'trigger'; // default to $trigger
            }
            $params = $this->omit($params, array( 'stop' ));
            $response = null;
            if ($trigger) {
                $response = Async\await($this->privateGetTradeOrdersAlgoPending ($this->extend($request, $params)));
            } else {
                $response = Async\await($this->privateGetTradeOrdersPending ($this->extend($request, $params)));
            }
            $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 ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-get-algo-order-history
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-get-order-history-last-3-months
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-get-order-history-last-7-days
             *
             * fetches information on multiple closed orders made by the user
             * @param {string} $symbol unified $market $symbol of the $market orders were made in
             * @param {int} [$since] the earliest time in ms to fetch orders for
             * @param {int} [$limit] the maximum number of order structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {bool} [$params->trigger] True if fetching $trigger or conditional orders
             * @param {string} [$params->ordType] "conditional", "oco", "trigger", "move_order_stop", "iceberg", or "twap"
             * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $request = array(
                'instType' => 'SPOT',
            );
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $request['instId'] = $market['id'];
            }
            $ordType = $this->safe_string($params, 'ordType');
            $trigger = $this->safe_value($params, 'stop') || ($this->safe_string($params, 'ordType') !== null);
            if ($trigger && ($ordType === null)) {
                $request['ordType'] = 'trigger'; // default to $trigger
            }
            $params = $this->omit($params, array( 'stop' ));
            $response = null;
            if ($trigger) {
                $response = Async\await($this->privateGetTradeOrdersAlgoHistory ($this->extend($request, $params)));
            } else {
                $method = null;
                list($method, $params) = $this->handle_option_and_params($params, 'fetchClosedOrders', 'method', 'privateGetTradeOrdersHistory');
                if ($method === 'privateGetTradeOrdersHistory') {
                    $response = Async\await($this->privateGetTradeOrdersHistory ($this->extend($request, $params)));
                } else {
                    $response = Async\await($this->privateGetTradeOrdersHistoryArchive ($this->extend($request, $params)));
                }
            }
            //     {
            //         "code" => "0",
            //         "data" => array(
            //             {
            //                 "accFillSz" => "0",
            //                 "avgPx" => "",
            //                 "cTime" => "1621910749815",
            //                 "category" => "normal",
            //                 "ccy" => "",
            //                 "clOrdId" => "",
            //                 "fee" => "0",
            //                 "feeCcy" => "ETH",
            //                 "fillPx" => "",
            //                 "fillSz" => "0",
            //                 "fillTime" => "",
            //                 "instId" => "ETH-USDT",
            //                 "instType" => "SPOT",
            //                 "lever" => "",
            //                 "ordId" => "317251910906576896",
            //                 "ordType" => "limit",
            //                 "pnl" => "0",
            //                 "posSide" => "net",
            //                 "px":"20 00",
            //                 "rebate" => "0",
            //                 "rebateCcy" => "USDT",
            //                 "side" => "buy",
            //                 "slOrdPx" => "",
            //                 "slTriggerPx" => "",
            //                 "state" => "live",
            //                 "sz":"0. 001",
            //                 "tag" => "",
            //                 "tdMode" => "cash",
            //                 "tpOrdPx" => "",
            //                 "tpTriggerPx" => "",
            //                 "tradeId" => "",
            //                 "uTime" => "1621910749815"
            //             }
            //         ),
            //         "msg":""
            //     }
            //
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_orders($data, $market, $since, $limit);
        }) ();
    }

    public function parse_deposit_address($depositAddress, ?array $currency = null): array {
        //
        //     {
        //         "addr" => "okbtothemoon",
        //         "memo" => "971668", // may be missing
        //         "tag":"52055", // may be missing
        //         "pmtId" => "", // may be missing
        //         "ccy" => "BTC",
        //         "to" => "6", // 1 SPOT, 3 FUTURES, 6 FUNDING, 9 SWAP, 12 OPTION, 18 Unified account
        //         "selected" => true
        //     }
        //
        //     {
        //         "ccy":"usdt-erc20",
        //         "to":"6",
        //         "addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa",
        //         "selected":true
        //     }
        //
        //     {
        //        "chain" => "ETH-OKExChain",
        //        "addrEx" => array( "comment" => "6040348" ), // some currencies like TON may have this field,
        //        "ctAddr" => "72315c",
        //        "ccy" => "ETH",
        //        "to" => "6",
        //        "addr" => "0x1c9f2244d1ccaa060bd536827c18925db10db102",
        //        "selected" => true
        //     }
        //
        $address = $this->safe_string($depositAddress, 'addr');
        $tag = $this->safe_string_n($depositAddress, array( 'tag', 'pmtId', 'memo' ));
        if ($tag === null) {
            $addrEx = $this->safe_value($depositAddress, 'addrEx', array());
            $tag = $this->safe_string($addrEx, 'comment');
        }
        $currencyId = $this->safe_string($depositAddress, 'ccy');
        $currency = $this->safe_currency($currencyId, $currency);
        $code = $currency['code'];
        $chain = $this->safe_string($depositAddress, 'chain');
        $networkId = str_replace($currencyId . '-', '', $chain);
        $network = $this->network_id_to_code($networkId);
        // inconsistent naming responses from exchange
        // with respect to $network naming provided in $currency info vs $address $chain-names and ids
        //
        // response from $address endpoint:
        //      {
        //          "chain" => "USDT-Polygon",
        //          "ctAddr" => "",
        //          "ccy" => "USDT",
        //          "to":"6" ,
        //          "addr" => "0x1903441e386cc49d937f6302955b5feb4286dcfa",
        //          "selected" => true
        //      }
        // $network information from $currency['networks'] field:
        // Polygon => {
        //        "info" => array(
        //            "canDep" => false,
        //            "canInternal" => false,
        //            "canWd" => false,
        //            "ccy" => "USDT",
        //            "chain" => "USDT-Polygon-Bridge",
        //            "mainNet" => false,
        //            "maxFee" => "26.879528",
        //            "minFee" => "13.439764",
        //            "minWd" => "0.001",
        //            "name" => ''
        //        ),
        //        "id" => "USDT-Polygon-Bridge",
        //        "network" => "Polygon",
        //        "active" => false,
        //        "deposit" => false,
        //        "withdraw" => false,
        //        "fee" => 13.439764,
        //        "precision" => null,
        //        "limits" => {
        //            "withdraw" => array(
        //                "min" => 0.001,
        //                "max" => null
        //            }
        //        }
        //     ),
        //
        $this->check_address($address);
        return array(
            'info' => $depositAddress,
            'currency' => $code,
            'network' => $network,
            'address' => $address,
            'tag' => $tag,
        );
    }

    public function fetch_deposit_address(string $code, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $params) {
            /**
             * fetch the deposit address for a currency associated with this account
             *
             * @see https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-deposit-address
             *
             * @param {string} $code unified currency $code
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=address-structure address structure~
             */
            Async\await($this->load_markets());
            $defaultNetwork = $this->safe_string($this->options, 'defaultNetwork', 'ERC20');
            $networkId = $this->safe_string($params, 'network', $defaultNetwork);
            $networkCode = $this->network_id_to_code($networkId);
            $params = $this->omit($params, 'network');
            $response = Async\await($this->fetch_deposit_addresses_by_network($code, $params));
            $result = $this->safe_value($response, $networkCode);
            if ($result === null) {
                throw new InvalidAddress($this->id . ' fetchDepositAddress() cannot find ' . $networkCode . ' deposit address for ' . $code);
            }
            return $result;
        }) ();
    }

    public function fetch_deposit_addresses_by_network(string $code, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $params) {
            /**
             * fetch a dictionary of addresses for a $currency, indexed by network
             *
             * @see https://www.okx.com/docs-v5/en/#funding-account-rest-api-get-deposit-address
             *
             * @param {string} $code unified $currency $code of the $currency for the deposit address
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=address-structure address structures~ indexed by the network
             */
            Async\await($this->load_markets());
            $currency = $this->currency($code);
            $request = array(
                'ccy' => $currency['id'],
            );
            $response = Async\await($this->privateGetAssetDepositAddress ($this->extend($request, $params)));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "",
            //         "data" => array(
            //             array(
            //                 "addr" => "okbtothemoon",
            //                 "memo" => "971668", // may be missing
            //                 "tag":"52055", // may be missing
            //                 "pmtId" => "", // may be missing
            //                 "ccy" => "BTC",
            //                 "to" => "6", // 1 SPOT, 3 FUTURES, 6 FUNDING, 9 SWAP, 12 OPTION, 18 Unified account
            //                 "selected" => true
            //             ),
            //             // array("ccy":"usdt-erc20","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true),
            //             // array("ccy":"usdt-trc20","to":"6","addr":"TRrd5SiSZrfQVRKm4e9SRSbn2LNTYqCjqx","selected":true),
            //             // array("ccy":"usdt_okexchain","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true),
            //             // array("ccy":"usdt_kip20","to":"6","addr":"0x696abb81974a8793352cbd33aadcf78eda3cfdfa","selected":true),
            //         )
            //     }
            //
            $data = $this->safe_value($response, 'data', array());
            $filtered = $this->filter_by($data, 'selected', true);
            $parsed = $this->parse_deposit_addresses($filtered, [ $currency['code'] ], false);
            return $this->index_by($parsed, 'network');
        }) ();
    }

    public function transfer(string $code, float $amount, string $fromAccount, string $toAccount, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $amount, $fromAccount, $toAccount, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-funding-funds-transfer
             *
             * transfer $currency internally between wallets on the same account
             * @param {string} $code unified $currency $code
             * @param {float} $amount amount to transfer
             * @param {string} $fromAccount account to transfer from
             * @param {string} $toAccount account to transfer to
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=transfer-structure transfer structure~
             */
            Async\await($this->load_markets());
            $currency = $this->currency($code);
            $accountsByType = $this->safe_value($this->options, 'accountsByType', array());
            $fromId = $this->safe_string($accountsByType, $fromAccount, $fromAccount);
            $toId = $this->safe_string($accountsByType, $toAccount, $toAccount);
            $request = array(
                'ccy' => $currency['id'],
                'amt' => $this->currency_to_precision($code, $amount),
                'type' => '0', // 0 = transfer within account by default, 1 = master account to sub-account, 2 = sub-account to master account, 3 = sub-account to master account (Only applicable to APIKey from sub-account), 4 = sub-account to sub-account
                'from' => $fromId, // remitting account, 6 => Funding account, 18 => Trading account
                'to' => $toId, // beneficiary account, 6 => Funding account, 18 => Trading account
                // 'subAcct' => 'sub-account-name', // optional, only required when type is 1, 2 or 4
                // 'loanTrans' => false, // Whether or not borrowed coins can be transferred out under Multi-$currency margin and Portfolio margin. The default is false
                // 'clientId' => 'client-supplied id', // A combination of case-sensitive alphanumerics, all numbers, or all letters of up to 32 characters
                // 'omitPosRisk' => false, // Ignore position risk. Default is false. Applicable to Portfolio margin
            );
            if ($fromId === 'master') {
                $request['type'] = '1';
                $request['subAcct'] = $toId;
                $request['from'] = $this->safe_string($params, 'from', '6');
                $request['to'] = $this->safe_string($params, 'to', '6');
            } elseif ($toId === 'master') {
                $request['type'] = '2';
                $request['subAcct'] = $fromId;
                $request['from'] = $this->safe_string($params, 'from', '6');
                $request['to'] = $this->safe_string($params, 'to', '6');
            }
            $response = Async\await($this->privatePostAssetTransfer ($this->extend($request, $params)));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "",
            //         "data" => array(
            //             {
            //                 "transId" => "754147",
            //                 "ccy" => "USDT",
            //                 "from" => "6",
            //                 "amt" => "0.1",
            //                 "to" => "18"
            //             }
            //         )
            //     }
            //
            $data = $this->safe_value($response, 'data', array());
            $rawTransfer = $this->safe_dict($data, 0, array());
            return $this->parse_transfer($rawTransfer, $currency);
        }) ();
    }

    public function parse_transfer(array $transfer, ?array $currency = null): array {
        //
        // $transfer
        //
        //     {
        //         "transId" => "754147",
        //         "ccy" => "USDT",
        //         "from" => "6",
        //         "amt" => "0.1",
        //         "to" => "18"
        //     }
        //
        // fetchTransfer
        //
        //     {
        //         "amt" => "5",
        //         "ccy" => "USDT",
        //         "from" => "18",
        //         "instId" => "",
        //         "state" => "success",
        //         "subAcct" => "",
        //         "to" => "6",
        //         "toInstId" => "",
        //         "transId" => "464424732",
        //         "type" => "0"
        //     }
        //
        // fetchTransfers
        //
        //     {
        //         "bal" => "70.6874353780312913",
        //         "balChg" => "-4.0000000000000000", // negative means "to funding", positive meand "from funding"
        //         "billId" => "588900695232225299",
        //         "ccy" => "USDT",
        //         "execType" => "",
        //         "fee" => "",
        //         "from" => "18",
        //         "instId" => "",
        //         "instType" => "",
        //         "mgnMode" => "",
        //         "notes" => "To Funding Account",
        //         "ordId" => "",
        //         "pnl" => "",
        //         "posBal" => "",
        //         "posBalChg" => "",
        //         "price" => "0",
        //         "subType" => "12",
        //         "sz" => "-4",
        //         "to" => "6",
        //         "ts" => "1686676866989",
        //         "type" => "1"
        //     }
        //
        $id = $this->safe_string_2($transfer, 'transId', 'billId');
        $currencyId = $this->safe_string($transfer, 'ccy');
        $code = $this->safe_currency_code($currencyId, $currency);
        $amount = $this->safe_number($transfer, 'amt');
        $fromAccountId = $this->safe_string($transfer, 'from');
        $toAccountId = $this->safe_string($transfer, 'to');
        $accountsById = $this->safe_value($this->options, 'accountsById', array());
        $timestamp = $this->safe_integer($transfer, 'ts', $this->milliseconds());
        $balanceChange = $this->safe_string($transfer, 'sz');
        if ($balanceChange !== null) {
            $amount = $this->parse_number(Precise::string_abs($balanceChange));
        }
        return array(
            'info' => $transfer,
            'id' => $id,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'currency' => $code,
            'amount' => $amount,
            'fromAccount' => $this->safe_string($accountsById, $fromAccountId),
            'toAccount' => $this->safe_string($accountsById, $toAccountId),
            'status' => $this->parse_transfer_status($this->safe_string($transfer, 'state')),
        );
    }

    public function parse_transfer_status(?string $status): ?string {
        $statuses = array(
            'success' => 'ok',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function withdraw(string $code, float $amount, string $address, ?string $tag = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $amount, $address, $tag, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-funding-withdrawal
             *
             * make a withdrawal
             * @param {string} $code unified $currency $code
             * @param {float} $amount the $amount to withdraw
             * @param {string} $address the $address to withdraw to
             * @param {string} $tag
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=$transaction-structure $transaction structure~
             */
            list($tag, $params) = $this->handle_withdraw_tag_and_params($tag, $params);
            $this->check_address($address);
            Async\await($this->load_markets());
            $currency = $this->currency($code);
            if (($tag !== null) && (strlen($tag) > 0)) {
                $address = $address . ':' . $tag;
            }
            $request = array(
                'ccy' => $currency['id'],
                'toAddr' => $address,
                'dest' => '4',
                'amt' => $this->number_to_string($amount),
            );
            $network = $this->safe_string($params, 'network'); // this line allows the user to specify either ERC20 or ETH
            if ($network !== null) {
                $networks = $this->safe_value($this->options, 'networks', array());
                $network = $this->safe_string($networks, strtoupper($network), $network); // handle ETH>ERC20 alias
                $request['chain'] = $currency['id'] . '-' . $network;
                $params = $this->omit($params, 'network');
            }
            $fee = $this->safe_string($params, 'fee');
            if ($fee === null) {
                $targetNetwork = $this->safe_value($currency['networks'], $this->network_id_to_code($network), array());
                $fee = $this->safe_string($targetNetwork, 'fee');
                if ($fee === null) {
                    throw new ArgumentsRequired($this->id . ' withdraw() requires a "fee" string parameter, $network $transaction $fee must be ≥ 0. Withdrawals to OKCoin or OKX are $fee-free, please set "0". Withdrawing to external digital asset $address requires $network $transaction $fee->');
                }
            }
            $request['fee'] = $this->number_to_string($fee); // withdrawals to OKCoin or OKX are $fee-free, please set 0
            $response = Async\await($this->privatePostAssetWithdrawal ($this->extend($request, $params)));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "",
            //         "data" => array(
            //             {
            //                 "amt" => "0.1",
            //                 "wdId" => "67485",
            //                 "ccy" => "BTC"
            //             }
            //         )
            //     }
            //
            $data = $this->safe_value($response, 'data', array());
            $transaction = $this->safe_dict($data, 0);
            return $this->parse_transaction($transaction, $currency);
        }) ();
    }

    public function fetch_deposits(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $since, $limit, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-funding-get-deposit-history
             *
             * fetch all deposits made to an account
             * @param {string} $code unified $currency $code
             * @param {int} [$since] the earliest time in ms to fetch deposits for
             * @param {int} [$limit] the maximum number of deposits structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structures~
             */
            Async\await($this->load_markets());
            $request = array(
                // 'ccy' => $currency['id'],
                // 'state' => 2, // 0 waiting for confirmation, 1 deposit credited, 2 deposit successful
                // 'after' => $since,
                // 'before' $this->milliseconds(),
                // 'limit' => $limit, // default 100, max 100
            );
            $currency = null;
            if ($code !== null) {
                $currency = $this->currency($code);
                $request['ccy'] = $currency['id'];
            }
            if ($since !== null) {
                $request['before'] = max ($since - 1, 0);
            }
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100, max 100
            }
            list($request, $params) = $this->handle_until_option('after', $request, $params);
            $response = Async\await($this->privateGetAssetDepositHistory ($this->extend($request, $params)));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "",
            //         "data" => array(
            //             array(
            //                 "amt" => "0.01044408",
            //                 "txId" => "1915737_3_0_0_asset",
            //                 "ccy" => "BTC",
            //                 "from" => "13801825426",
            //                 "to" => "",
            //                 "ts" => "1597026383085",
            //                 "state" => "2",
            //                 "depId" => "4703879"
            //             ),
            //             array(
            //                 "amt" => "491.6784211",
            //                 "txId" => "1744594_3_184_0_asset",
            //                 "ccy" => "OKB",
            //                 "from" => "",
            //                 "to" => "",
            //                 "ts" => "1597026383085",
            //                 "state" => "2",
            //                 "depId" => "4703809"
            //             ),
            //             {
            //                 "amt" => "223.18782496",
            //                 "txId" => "6d892c669225b1092c780bf0da0c6f912fc7dc8f6b8cc53b003288624c",
            //                 "ccy" => "USDT",
            //                 "from" => "",
            //                 "to" => "39kK4XvgEuM7rX9frgyHoZkWqx4iKu1spD",
            //                 "ts" => "1597026383085",
            //                 "state" => "2",
            //                 "depId" => "4703779"
            //             }
            //         )
            //     }
            //
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_transactions($data, $currency, $since, $limit, $params);
        }) ();
    }

    public function fetch_withdrawals(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $since, $limit, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-funding-get-withdrawal-history
             *
             * fetch all withdrawals made from an account
             * @param {string} $code unified $currency $code
             * @param {int} [$since] the earliest time in ms to fetch withdrawals for
             * @param {int} [$limit] the maximum number of withdrawals structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structures~
             */
            Async\await($this->load_markets());
            $request = array(
                // 'ccy' => $currency['id'],
                // 'state' => 2, // -3 => pending cancel, -2 canceled, -1 failed, 0, pending, 1 sending, 2 sent, 3 awaiting email verification, 4 awaiting manual verification, 5 awaiting identity verification
                // 'after' => $since,
                // 'before' => $this->milliseconds(),
                // 'limit' => $limit, // default 100, max 100
            );
            $currency = null;
            if ($code !== null) {
                $currency = $this->currency($code);
                $request['ccy'] = $currency['id'];
            }
            if ($since !== null) {
                $request['before'] = max ($since - 1, 0);
            }
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100, max 100
            }
            list($request, $params) = $this->handle_until_option('after', $request, $params);
            $response = Async\await($this->privateGetAssetWithdrawalHistory ($this->extend($request, $params)));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "",
            //         "data" => array(
            //             array(
            //                 "amt" => "0.094",
            //                 "wdId" => "4703879",
            //                 "fee" => "0.01000000eth",
            //                 "txId" => "0x62477bac6509a04512819bb1455e923a60dea5966c7caeaa0b24eb8fb0432b85",
            //                 "ccy" => "ETH",
            //                 "from" => "13426335357",
            //                 "to" => "0xA41446125D0B5b6785f6898c9D67874D763A1519",
            //                 "ts" => "1597026383085",
            //                 "state" => "2"
            //             ),
            //             {
            //                 "amt" => "0.01",
            //                 "wdId" => "4703879",
            //                 "fee" => "0.00000000btc",
            //                 "txId" => "",
            //                 "ccy" => "BTC",
            //                 "from" => "13426335357",
            //                 "to" => "13426335357",
            //                 "ts" => "1597026383085",
            //                 "state" => "2"
            //             }
            //         )
            //     }
            //
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_transactions($data, $currency, $since, $limit, $params);
        }) ();
    }

    public function parse_transaction_status(?string $status) {
        //
        // deposit $statuses
        //
        //     {
        //         "0" => "waiting for confirmation",
        //         "1" => "confirmation account",
        //         "2" => "recharge success"
        //     }
        //
        // withdrawal statues
        //
        //     {
        //        '-3' => "pending cancel",
        //        "-2" => "cancelled",
        //        "-1" => "failed",
        //         "0" => "pending",
        //         "1" => "sending",
        //         "2" => "sent",
        //         "3" => "email confirmation",
        //         "4" => "manual confirmation",
        //         "5" => "awaiting identity confirmation"
        //     }
        //
        $statuses = array(
            '-3' => 'pending',
            '-2' => 'canceled',
            '-1' => 'failed',
            '0' => 'pending',
            '1' => 'pending',
            '2' => 'ok',
            '3' => 'pending',
            '4' => 'pending',
            '5' => 'pending',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_transaction(array $transaction, ?array $currency = null): array {
        //
        // withdraw
        //
        //     {
        //         "amt" => "0.1",
        //         "wdId" => "67485",
        //         "ccy" => "BTC"
        //     }
        //
        // fetchWithdrawals
        //
        //     {
        //         "amt" => "0.094",
        //         "wdId" => "4703879",
        //         "fee" => "0.01000000eth",
        //         "txId" => "0x62477bac6509a04512819bb1455e923a60dea5966c7caeaa0b24eb8fb0432b85",
        //         "ccy" => "ETH",
        //         "from" => "13426335357",
        //         "to" => "0xA41446125D0B5b6785f6898c9D67874D763A1519",
        //         "tag",
        //         "pmtId",
        //         "memo",
        //         "ts" => "1597026383085",
        //         "state" => "2"
        //     }
        //
        // fetchDeposits
        //
        //     {
        //         "amt" => "0.01044408",
        //         "txId" => "1915737_3_0_0_asset",
        //         "ccy" => "BTC",
        //         "from" => "13801825426",
        //         "to" => "",
        //         "ts" => "1597026383085",
        //         "state" => "2",
        //         "depId" => "4703879"
        //     }
        //
        $type = null;
        $id = null;
        $withdrawalId = $this->safe_string($transaction, 'wdId');
        $addressFrom = $this->safe_string($transaction, 'from');
        $addressTo = $this->safe_string($transaction, 'to');
        $address = $addressTo;
        $tagTo = $this->safe_string_2($transaction, 'tag', 'memo');
        $tagTo = $this->safe_string_2($transaction, 'pmtId', $tagTo);
        if ($withdrawalId !== null) {
            $type = 'withdrawal';
            $id = $withdrawalId;
        } else {
            // the payment_id will appear on new deposits but appears to be removed from the response after 2 months
            $id = $this->safe_string($transaction, 'depId');
            $type = 'deposit';
        }
        $currencyId = $this->safe_string($transaction, 'ccy');
        $code = $this->safe_currency_code($currencyId);
        $amount = $this->safe_number($transaction, 'amt');
        $status = $this->parse_transaction_status($this->safe_string($transaction, 'state'));
        $txid = $this->safe_string($transaction, 'txId');
        $timestamp = $this->safe_integer($transaction, 'ts');
        $feeCost = null;
        if ($type === 'deposit') {
            $feeCost = 0;
        } else {
            $feeCost = $this->safe_number($transaction, 'fee');
        }
        // todo parse tags
        return array(
            'info' => $transaction,
            'id' => $id,
            'currency' => $code,
            'amount' => $amount,
            'network' => null,
            'addressFrom' => $addressFrom,
            'addressTo' => $addressTo,
            'address' => $address,
            'tagFrom' => null,
            'tagTo' => $tagTo,
            'tag' => $tagTo,
            'status' => $status,
            'type' => $type,
            'updated' => null,
            'txid' => $txid,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'comment' => null,
            'internal' => null,
            'fee' => array(
                'currency' => $code,
                'cost' => $feeCost,
            ),
        );
    }

    public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-get-transaction-details-last-3-days
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-trade-get-transaction-details-last-3-months
             *
             * fetch all trades made by the user
             * @param {string} $symbol unified $market $symbol
             * @param {int} [$since] the earliest time in ms to fetch trades for
             * @param {int} [$limit] the maximum number of trades structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            Async\await($this->load_markets());
            $request = array(
                'instType' => 'SPOT',
            );
            if (($limit !== null) && ($limit > 100)) {
                $limit = 100;
            }
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $request['instId'] = $market['id'];
            }
            $method = null;
            list($method, $params) = $this->handle_option_and_params($params, 'fetchMyTrades', 'method', 'privateGetTradeFillsHistory');
            $response = null;
            if ($method === 'privateGetTradeFillsHistory') {
                $response = Async\await($this->privateGetTradeFillsHistory ($this->extend($request, $params)));
            } else {
                $response = Async\await($this->privateGetTradeFills ($this->extend($request, $params)));
            }
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_trades($data, $market, $since, $limit);
        }) ();
    }

    public function fetch_order_trades(string $id, ?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $since, $limit, $params) {
            /**
             * fetch all the trades made from a single order
             * @param {string} $id order $id
             * @param {string} $symbol unified market $symbol
             * @param {int} [$since] the earliest time in ms to fetch trades for
             * @param {int} [$limit] the maximum number of trades to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?$id=trade-structure trade structures~
             */
            $request = array(
                // 'instrument_id' => market['id'],
                'order_id' => $id,
                // 'after' => '1', // return the page after the specified page number
                // 'before' => '1', // return the page before the specified page number
                // 'limit' => $limit, // optional, number of results per $request, default = maximum = 100
            );
            return Async\await($this->fetch_my_trades($symbol, $since, $limit, $this->extend($request, $params)));
        }) ();
    }

    public function fetch_ledger(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $since, $limit, $params) {
            /**
             * fetch the history of changes, actions done by the user or operations that altered the balance of the user
             *
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-funding-asset-bills-details
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days
             * @see https://www.okcoin.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months
             *
             * @param {string} [$code] unified $currency $code, default is null
             * @param {int} [$since] timestamp in ms of the earliest ledger entry, default is null
             * @param {int} [$limit] max number of ledger entries to return, default is null
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ledger ledger structure~
             */
            Async\await($this->load_markets());
            $method = null;
            list($method, $params) = $this->handle_option_and_params($params, 'fetchLedger', 'method', 'privateGetAccountBills');
            $request = array(
                // 'instType' => null, // 'SPOT', 'MARGIN', 'SWAP', 'FUTURES", 'OPTION'
                // 'ccy' => null, // $currency['id'],
                // 'ctType' => null, // 'linear', 'inverse', only applicable to FUTURES/SWAP
                // 'type' => varies depending the 'method' endpoint :
                //     - https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-7-days
                //     - https://www.okx.com/docs-v5/en/#rest-api-funding-asset-bills-details
                //     - https://www.okx.com/docs-v5/en/#rest-api-account-get-bills-details-last-3-months
                // 'after' => 'id', // return records earlier than the requested bill id
                // 'before' => 'id', // return records newer than the requested bill id
                // 'limit' => 100, // default 100, max 100
            );
            if ($limit !== null) {
                $request['limit'] = $limit;
            }
            $currency = null;
            if ($code !== null) {
                $currency = $this->currency($code);
                $request['ccy'] = $currency['id'];
            }
            list($request, $params) = $this->handle_until_option('end', $request, $params);
            $response = null;
            if ($method === 'privateGetAccountBillsArchive') {
                $response = Async\await($this->privateGetAccountBillsArchive ($this->extend($request, $params)));
            } elseif ($method === 'privateGetAssetBills') {
                $response = Async\await($this->privateGetAssetBills ($this->extend($request, $params)));
            } else {
                $response = Async\await($this->privateGetAccountBills ($this->extend($request, $params)));
            }
            //
            // privateGetAccountBills, privateGetAccountBillsArchive
            //
            //     {
            //         "code" => "0",
            //         "msg" => "",
            //         "data" => array(
            //             {
            //                 "bal" => "0.0000819307998198",
            //                 "balChg" => "-664.2679586599999802",
            //                 "billId" => "310394313544966151",
            //                 "ccy" => "USDT",
            //                 "fee" => "0",
            //                 "from" => "",
            //                 "instId" => "LTC-USDT",
            //                 "instType" => "SPOT",
            //                 "mgnMode" => "cross",
            //                 "notes" => "",
            //                 "ordId" => "310394313519800320",
            //                 "pnl" => "0",
            //                 "posBal" => "0",
            //                 "posBalChg" => "0",
            //                 "subType" => "2",
            //                 "sz" => "664.26795866",
            //                 "to" => "",
            //                 "ts" => "1620275771196",
            //                 "type" => "2"
            //             }
            //         )
            //     }
            //
            // privateGetAssetBills
            //
            //     {
            //         "code" => "0",
            //         "msg" => "",
            //         "data" => array(
            //             {
            //                 "billId" => "12344",
            //                 "ccy" => "BTC",
            //                 "balChg" => "2",
            //                 "bal" => "12",
            //                 "type" => "1",
            //                 "ts" => "1597026383085"
            //             }
            //         )
            //     }
            //
            $data = $this->safe_value($response, 'data', array());
            return $this->parse_ledger($data, $currency, $since, $limit);
        }) ();
    }

    public function parse_ledger_entry_type($type) {
        $types = array(
            '1' => 'transfer', // transfer
            '2' => 'trade', // trade
            '3' => 'trade', // delivery
            '4' => 'rebate', // auto token conversion
            '5' => 'trade', // liquidation
            '6' => 'transfer', // margin transfer
            '7' => 'trade', // interest deduction
            '8' => 'fee', // funding rate
            '9' => 'trade', // adl
            '10' => 'trade', // clawback
            '11' => 'trade', // system token conversion
        );
        return $this->safe_string($types, $type, $type);
    }

    public function parse_ledger_entry(array $item, ?array $currency = null): array {
        //
        // privateGetAccountBills, privateGetAccountBillsArchive
        //
        //     {
        //         "bal" => "0.0000819307998198",
        //         "balChg" => "-664.2679586599999802",
        //         "billId" => "310394313544966151",
        //         "ccy" => "USDT",
        //         "fee" => "0",
        //         "from" => "",
        //         "instId" => "LTC-USDT",
        //         "instType" => "SPOT",
        //         "mgnMode" => "cross",
        //         "notes" => "",
        //         "ordId" => "310394313519800320",
        //         "pnl" => "0",
        //         "posBal" => "0",
        //         "posBalChg" => "0",
        //         "subType" => "2",
        //         "sz" => "664.26795866",
        //         "to" => "",
        //         "ts" => "1620275771196",
        //         "type" => "2"
        //     }
        //
        // privateGetAssetBills
        //
        //     {
        //         "billId" => "12344",
        //         "ccy" => "BTC",
        //         "balChg" => "2",
        //         "bal" => "12",
        //         "type" => "1",
        //         "ts" => "1597026383085"
        //     }
        //
        $currencyId = $this->safe_string($item, 'ccy');
        $code = $this->safe_currency_code($currencyId, $currency);
        $currency = $this->safe_currency($currencyId, $currency);
        $timestamp = $this->safe_integer($item, 'ts');
        $feeCostString = $this->safe_string($item, 'fee');
        $fee = null;
        if ($feeCostString !== null) {
            $fee = array(
                'cost' => $this->parse_to_numeric(Precise::string_neg($feeCostString)),
                'currency' => $code,
            );
        }
        $marketId = $this->safe_string($item, 'instId');
        $symbol = $this->safe_symbol($marketId, null, '-');
        return $this->safe_ledger_entry(array(
            'info' => $item,
            'id' => $this->safe_string($item, 'billId'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'account' => null,
            'referenceId' => $this->safe_string($item, 'ordId'),
            'referenceAccount' => null,
            'type' => $this->parse_ledger_entry_type($this->safe_string($item, 'type')),
            'currency' => $code,
            'symbol' => $symbol,
            'amount' => $this->safe_number($item, 'balChg'),
            'before' => null, // balance before
            'after' => $this->safe_number($item, 'bal'), // balance after
            'status' => 'ok',
            'fee' => $fee,
        ), $currency);
    }

    public function sign($path, $api = 'public', $method = 'GET', $params = array (), $headers = null, $body = null) {
        $isArray = gettype($params) === 'array' && array_keys($params) === array_keys(array_keys($params));
        $request = '/api/' . $this->version . '/' . $this->implode_params($path, $params);
        $query = $this->omit($params, $this->extract_params($path));
        $url = $this->implode_hostname($this->urls['api']['rest']) . $request;
        if ($api === 'public') {
            if ($query) {
                $url .= '?' . $this->urlencode($query);
            }
        } elseif ($api === 'private') {
            $this->check_required_credentials();
            $timestamp = $this->iso8601($this->milliseconds());
            $headers = array(
                'OK-ACCESS-KEY' => $this->apiKey,
                'OK-ACCESS-PASSPHRASE' => $this->password,
                'OK-ACCESS-TIMESTAMP' => $timestamp,
                // 'OK-FROM' => '',
                // 'OK-TO' => '',
                // 'OK-LIMIT' => '',
            );
            $auth = $timestamp . $method . $request;
            if ($method === 'GET') {
                if ($query) {
                    $urlencodedQuery = '?' . $this->urlencode($query);
                    $url .= $urlencodedQuery;
                    $auth .= $urlencodedQuery;
                }
            } else {
                if ($isArray || $query) {
                    $body = $this->json($query);
                    $auth .= $body;
                }
                $headers['Content-Type'] = 'application/json';
            }
            $signature = $this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256', 'base64');
            $headers['OK-ACCESS-SIGN'] = $signature;
        }
        return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers );
    }

    public function parse_balance_by_type($type, $response) {
        if ($type === 'funding') {
            return $this->parse_funding_balance($response);
        } else {
            return $this->parse_trading_balance($response);
        }
    }

    public function handle_errors(int $httpCode, string $reason, string $url, string $method, array $headers, string $body, $response, $requestHeaders, $requestBody) {
        if (!$response) {
            return null; // fallback to default $error handler
        }
        //
        //    {
        //        "code" => "1",
        //        "data" => array(
        //            array(
        //                "clOrdId" => "",
        //                "ordId" => "",
        //                "sCode" => "51119",
        //                "sMsg" => "Order placement failed due to insufficient balance. ",
        //                "tag" => ""
        //            }
        //        ),
        //        "msg" => ""
        //    ),
        //    {
        //        "code" => "58001",
        //        "data" => array(),
        //        "msg" => "Incorrect trade password"
        //    }
        //
        $code = $this->safe_string($response, 'code');
        if ($code !== '0') {
            $feedback = $this->id . ' ' . $body;
            $data = $this->safe_value($response, 'data', array());
            for ($i = 0; $i < count($data); $i++) {
                $error = $data[$i];
                $errorCode = $this->safe_string($error, 'sCode');
                $message = $this->safe_string($error, 'sMsg');
                $this->throw_exactly_matched_exception($this->exceptions['exact'], $errorCode, $feedback);
                $this->throw_broadly_matched_exception($this->exceptions['broad'], $message, $feedback);
            }
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $code, $feedback);
            throw new ExchangeError($feedback); // unknown $message
        }
        return null;
    }
}
