<?php

namespace ccxt;

// PLEASE DO NOT EDIT THIS FILE, IT IS GENERATED AND WILL BE OVERWRITTEN:
// https://github.com/ccxt/ccxt/blob/master/CONTRIBUTING.md#how-to-contribute-code

use Exception; // a common import
use ccxt\abstract\coinmetro as Exchange;

class coinmetro extends Exchange {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'id' => 'coinmetro',
            'name' => 'Coinmetro',
            'countries' => array( 'EE' ), // Republic of Estonia
            'version' => 'v1',
            'rateLimit' => 200, // 1 request per 200 ms, 20 per minute, 300 per hour, 1k per day
            'certified' => false,
            'pro' => false,
            'has' => array(
                'CORS' => null,
                'spot' => true,
                'margin' => true,
                'swap' => false,
                'future' => false,
                'option' => false,
                'addMargin' => false,
                'borrowCrossMargin' => true,
                'borrowIsolatedMargin' => false,
                'cancelAllOrders' => false,
                'cancelOrder' => true,
                'cancelOrders' => false,
                'closeAllPositions' => false,
                'closePosition' => true,
                'createDepositAddress' => false,
                'createOrder' => true,
                'createPostOnlyOrder' => false,
                'createReduceOnlyOrder' => false,
                'createStopLimitOrder' => true,
                'createStopMarketOrder' => true,
                'createStopOrder' => true,
                'deposit' => false,
                'editOrder' => false,
                'fetchAccounts' => false,
                'fetchBalance' => true,
                'fetchBidsAsks' => true,
                'fetchBorrowInterest' => false,
                'fetchBorrowRateHistories' => false,
                'fetchBorrowRateHistory' => false,
                'fetchCanceledAndClosedOrders' => true,
                'fetchCanceledOrders' => false,
                'fetchClosedOrder' => false,
                'fetchClosedOrders' => false,
                'fetchCrossBorrowRate' => false,
                'fetchCrossBorrowRates' => false,
                'fetchCurrencies' => true,
                'fetchDeposit' => false,
                'fetchDepositAddress' => false,
                'fetchDepositAddresses' => false,
                'fetchDepositAddressesByNetwork' => false,
                'fetchDeposits' => false,
                'fetchDepositsWithdrawals' => false,
                'fetchDepositWithdrawFee' => false,
                'fetchDepositWithdrawFees' => false,
                'fetchFundingHistory' => false,
                'fetchFundingRate' => false,
                'fetchFundingRateHistory' => false,
                'fetchFundingRates' => false,
                'fetchIndexOHLCV' => false,
                'fetchIsolatedBorrowRate' => false,
                'fetchIsolatedBorrowRates' => false,
                'fetchL3OrderBook' => false,
                'fetchLedger' => true,
                'fetchLeverage' => false,
                'fetchLeverageTiers' => false,
                'fetchMarketLeverageTiers' => false,
                'fetchMarkets' => true,
                'fetchMarkOHLCV' => false,
                'fetchMyTrades' => true,
                'fetchOHLCV' => true,
                'fetchOpenInterestHistory' => false,
                'fetchOpenOrder' => false,
                'fetchOpenOrders' => true,
                'fetchOrder' => true,
                'fetchOrderBook' => true,
                'fetchOrderBooks' => false,
                'fetchOrders' => false,
                'fetchOrderTrades' => false,
                'fetchPosition' => false,
                'fetchPositions' => false,
                'fetchPositionsRisk' => false,
                'fetchPremiumIndexOHLCV' => false,
                'fetchStatus' => false,
                'fetchTicker' => false,
                'fetchTickers' => true,
                'fetchTime' => false,
                'fetchTrades' => true,
                'fetchTradingFee' => false,
                'fetchTradingFees' => false,
                'fetchTradingLimits' => false,
                'fetchTransactionFee' => false,
                'fetchTransactionFees' => false,
                'fetchTransactions' => false,
                'fetchTransfers' => false,
                'fetchWithdrawal' => false,
                'fetchWithdrawals' => false,
                'fetchWithdrawalWhitelist' => false,
                'reduceMargin' => false,
                'repayCrossMargin' => false,
                'repayIsolatedMargin' => false,
                'sandbox' => true,
                'setLeverage' => false,
                'setMargin' => false,
                'setMarginMode' => false,
                'setPositionMode' => false,
                'signIn' => false,
                'transfer' => false,
                'withdraw' => false,
                'ws' => false,
            ),
            'timeframes' => array(
                '1m' => '60000',
                '5m' => '300000',
                '30m' => '1800000',
                '4h' => '14400000',
                '1d' => '86400000',
            ),
            'urls' => array(
                'logo' => 'https://github.com/ccxt/ccxt/assets/43336371/e86f87ec-6ba3-4410-962b-f7988c5db539',
                'api' => array(
                    'public' => 'https://api.coinmetro.com',
                    'private' => 'https://api.coinmetro.com',
                ),
                'test' => array(
                    'public' => 'https://api.coinmetro.com/open',
                    'private' => 'https://api.coinmetro.com/open',
                ),
                'www' => 'https://coinmetro.com/',
                'doc' => array(
                    'https://documenter.getpostman.com/view/3653795/SVfWN6KS',
                ),
                'fees' => 'https://help.coinmetro.com/hc/en-gb/articles/6844007317789-What-are-the-fees-on-Coinmetro-',
                'referral' => 'https://go.coinmetro.com/?ref=crypto24',
            ),
            'api' => array(
                'public' => array(
                    'get' => array(
                        'demo/temp' => 1,
                        'exchange/candles/{pair}/{timeframe}/{from}/{to}' => 3,
                        'exchange/prices' => 1,
                        'exchange/ticks/{pair}/{from}' => 3,
                        'assets' => 1,
                        'markets' => 1,
                        'exchange/book/{pair}' => 3,
                        'exchange/bookUpdates/{pair}/{from}' => 1, // not unified
                    ),
                ),
                'private' => array(
                    'get' => array(
                        'users/balances' => 1,
                        'users/wallets' => 1,
                        'users/wallets/history/{since}' => 1.67,
                        'exchange/orders/status/{orderID}' => 1,
                        'exchange/orders/active' => 1,
                        'exchange/orders/history/{since}' => 1.67,
                        'exchange/fills/{since}' => 1.67,
                        'exchange/margin' => 1, // not unified
                    ),
                    'post' => array(
                        'jwt' => 1, // not unified
                        'jwtDevice' => 1, // not unified
                        'devices' => 1, // not unified
                        'jwt-read-only' => 1, // not unified
                        'exchange/orders/create' => 1,
                        'exchange/orders/modify/{orderID}' => 1, // not unified
                        'exchange/swap' => 1, // not unified
                        'exchange/swap/confirm/{swapId}' => 1, // not unified
                        'exchange/orders/close/{orderID}' => 1,
                        'exchange/orders/hedge' => 1, // not unified
                    ),
                    'put' => array(
                        'jwt' => 1, // not unified
                        'exchange/orders/cancel/{orderID}' => 1,
                        'users/margin/collateral' => 1,
                        'users/margin/primary/{currency}' => 1, // not unified
                    ),
                ),
            ),
            'requiredCredentials' => array(
                'apiKey' => false,
                'secret' => false,
                'uid' => true,
                'token' => true,
            ),
            'fees' => array(
                'trading' => array(
                    'feeSide' => 'get',
                    'tierBased' => false,
                    'percentage' => true,
                    'taker' => $this->parse_number('0.001'),
                    'maker' => $this->parse_number('0'),
                ),
            ),
            'precisionMode' => TICK_SIZE,
            // exchange-specific options
            'options' => array(
                'currenciesByIdForParseMarket' => null,
                'currencyIdsListForParseMarket' => array( 'QRDO' ),
            ),
            'features' => array(
                'spot' => array(
                    'sandbox' => true,
                    'createOrder' => array(
                        'marginMode' => true, // todo implement
                        'triggerPrice' => true,
                        'triggerPriceType' => null,
                        'triggerDirection' => false,
                        'stopLossPrice' => false, // todo
                        'takeProfitPrice' => false, // todo
                        'attachedStopLossTakeProfit' => array(
                            'triggerPriceType' => null,
                            'price' => false,
                        ),
                        'timeInForce' => array(
                            'IOC' => true,
                            'FOK' => true,
                            'PO' => false,
                            'GTD' => true,
                        ),
                        'hedged' => false,
                        'trailing' => false,
                        'leverage' => false,
                        'marketBuyByCost' => true,
                        'marketBuyRequiresPrice' => false,
                        'selfTradePrevention' => false,
                        'iceberg' => true,
                    ),
                    'createOrders' => null,
                    'fetchMyTrades' => array(
                        'marginMode' => false,
                        'limit' => null,
                        'daysBack' => 100000,
                        'untilDays' => null,
                        'symbolRequired' => false,
                    ),
                    'fetchOrder' => array(
                        'marginMode' => false,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOpenOrders' => array(
                        'marginMode' => false,
                        'limit' => null,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOrders' => array(
                        'marginMode' => false,
                        'limit' => null,
                        'daysBack' => 100000,
                        'untilDays' => null,
                        'trigger' => false,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchClosedOrders' => null,
                    'fetchOHLCV' => array(
                        'limit' => 1000,
                    ),
                ),
                'swap' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
                'future' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
            ),
            'exceptions' => array(
                // https://trade-docs.coinmetro.co/?javascript--nodejs#message-codes
                'exact' => array(
                    'Both buyingCurrency and sellingCurrency are required' => '\\ccxt\\InvalidOrder', // 422 - "Both buyingCurrency and sellingCurrency are required"
                    'One and only one of buyingQty and sellingQty is required' => '\\ccxt\\InvalidOrder', // 422 - "One and only one of buyingQty and sellingQty is required"
                    'Invalid buyingCurrency' => '\\ccxt\\InvalidOrder', // 422 - "Invalid buyingCurrency"
                    'Invalid \'from\'' => '\\ccxt\\BadRequest', // 422 Unprocessable Entity array("message":"Invalid 'from'")
                    'Invalid sellingCurrency' => '\\ccxt\\InvalidOrder', // 422 - "Invalid sellingCurrency"
                    'Invalid buyingQty' => '\\ccxt\\InvalidOrder', // 422 - "Invalid buyingQty"
                    'Invalid sellingQty' => '\\ccxt\\InvalidOrder', // 422 - "Invalid sellingQty"
                    'Insufficient balance' => '\\ccxt\\InsufficientFunds', // 422 - "Insufficient balance"
                    'Expiration date is in the past or too near in the future' => '\\ccxt\\InvalidOrder', // 422 Unprocessable Entity array("message":"Expiration date is in the past or too near in the future")
                    'Forbidden' => '\\ccxt\\PermissionDenied', // 403 Forbidden array("message":"Forbidden")
                    'Order Not Found' => '\\ccxt\\OrderNotFound', // 404 Not Found array("message":"Order Not Found")
                    'since must be a millisecond timestamp' => '\\ccxt\\BadRequest', // 422 Unprocessable Entity array("message":"since must be a millisecond timestamp")
                    'This pair is disabled on margin' => '\\ccxt\\BadSymbol', // 422 Unprocessable Entity array("message":"This pair is disabled on margin")
                ),
                'broad' => array(
                    'accessing from a new IP' => '\\ccxt\\PermissionDenied', // 403 Forbidden array("message":"You're accessing from a new IP. Please check your email.")
                    'available to allocate' => '\\ccxt\\InsufficientFunds', // 403 Forbidden array("message":"Insufficient EUR available to allocate")
                    'At least' => '\\ccxt\\BadRequest', // 422 Unprocessable Entity array("message":"At least 5 EUR per operation")
                    'collateral is not allowed' => '\\ccxt\\BadRequest', // 422 Unprocessable Entity array("message":"DOGE collateral is not allowed")
                    'Insufficient liquidity' => '\\ccxt\\InvalidOrder', // 503 Service Unavailable array("message":"Insufficient liquidity to fill the FOK order completely.")
                    'Insufficient order size' => '\\ccxt\\InvalidOrder', // 422 Unprocessable Entity array("message":"Insufficient order size - min 0.002 ETH")
                    'Invalid quantity' => '\\ccxt\\InvalidOrder', // 422 Unprocessable Entity array("message":"Invalid quantity!")
                    'Invalid Stop Loss' => '\\ccxt\\InvalidOrder', // 422 Unprocessable Entity array("message":"Invalid Stop Loss!")
                    'Invalid stop price!' => '\\ccxt\\InvalidOrder', // 422 Unprocessable Entity array("message":"Invalid stop price!")
                    'Not enough balance' => '\\ccxt\\InsufficientFunds', // 422 Unprocessable Entity array("message":"Not enough balance!")
                    'Not enough margin' => '\\ccxt\\InsufficientFunds', // Unprocessable Entity array("message":"Not enough margin!")
                    'orderType missing' => '\\ccxt\\BadRequest', // 422 Unprocessable Entity array("message":"orderType missing!")
                    'Server Timeout' => '\\ccxt\\ExchangeError', // 503 Service Unavailable array("message":"Server Timeout!")
                    'Time in force has to be IOC or FOK for market orders' => '\\ccxt\\InvalidOrder', // 422 Unprocessable Entity array(is_array(force has to be IOC or FOK for market orders!") && array_key_exists("message":"Time, force has to be IOC or FOK for market orders!"))
                    'Too many attempts' => '\\ccxt\\RateLimitExceeded', // 429 Too Many Requests array("message":"Too many attempts. Try again in 3 seconds")
                ),
            ),
        ));
    }

    public function fetch_currencies($params = array ()): ?array {
        /**
         * fetches all available currencies on an exchange
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#d5876d43-a3fe-4479-8c58-24d0f044edfb
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} an associative dictionary of currencies
         */
        $response = $this->publicGetAssets ($params);
        //
        //     array(
        //         array(
        //             "symbol" => "BTC",
        //             "name" => "Bitcoin",
        //             "color" => "#FFA500",
        //             "type" => "coin",
        //             "canDeposit" => true,
        //             "canWithdraw" => true,
        //             "canTrade" => true,
        //             "notabeneDecimals" => 8,
        //             "canMarket" => true,
        //             "maxSwap" => 10000,
        //             "digits" => 6,
        //             "multiplier" => 1000000,
        //             "bookDigits" => 8,
        //             "bookMultiplier" => 100000000,
        //             "sentimentData" => array(
        //                 "sentiment" => 51.59555555555555,
        //                 "interest" => 1.127511216044664
        //             ),
        //             "minQty" => 0.0001
        //         ),
        //         {
        //             "symbol" => "EUR",
        //             "name" => "Euro",
        //             "color" => "#1246FF",
        //             "type" => "fiat",
        //             "canDeposit" => true,
        //             "canWithdraw" => true,
        //             "canTrade" => true,
        //             "canMarket" => true,
        //             "maxSwap" => 10000,
        //             "digits" => 2,
        //             "multiplier" => 100,
        //             "bookDigits" => 3,
        //             "bookMultiplier" => 1000,
        //             "minQty" => 5
        //         }
        //         ...
        //     )
        //
        $result = array();
        for ($i = 0; $i < count($response); $i++) {
            $currency = $response[$i];
            $id = $this->safe_string($currency, 'symbol');
            $code = $this->safe_currency_code($id);
            $typeRaw = $this->safe_string($currency, 'type');
            $type = null;
            if ($typeRaw === 'coin' || $typeRaw === 'token' || $typeRaw === 'erc20') {
                $type = 'crypto';
            } elseif ($typeRaw === 'fiat') {
                $type = 'fiat';
            }
            $precisionDigits = $this->safe_string_2($currency, 'digits', 'notabeneDecimals');
            if ($code === 'RENDER') {
                // RENDER is an exception (with broken info)
                $precisionDigits = '4';
            }
            $result[$code] = $this->safe_currency_structure(array(
                'id' => $id,
                'code' => $code,
                'name' => $code,
                'type' => $type,
                'info' => $currency,
                'active' => $this->safe_bool($currency, 'canTrade'),
                'deposit' => $this->safe_bool($currency, 'canDeposit'),
                'withdraw' => $this->safe_bool($currency, 'canWithdraw'),
                'fee' => null,
                'precision' => $this->parse_number($this->parse_precision($precisionDigits)),
                'limits' => array(
                    'amount' => array(
                        'min' => $this->safe_number($currency, 'minQty'),
                        'max' => null,
                    ),
                    'withdraw' => array(
                        'min' => null,
                        'max' => null,
                    ),
                ),
                'networks' => array(),
            ));
        }
        if ($this->safe_value($this->options, 'currenciesByIdForParseMarket') === null) {
            $currenciesById = $this->index_by($result, 'id');
            $this->options['currenciesByIdForParseMarket'] = $currenciesById;
            $currentCurrencyIdsList = $this->safe_list($this->options, 'currencyIdsListForParseMarket', array());
            $currencyIdsList = is_array($currenciesById) ? array_keys($currenciesById) : array();
            for ($i = 0; $i < count($currencyIdsList); $i++) {
                $currentCurrencyIdsList[] = $currencyIdsList[$i];
            }
            $this->options['currencyIdsListForParseMarket'] = $currentCurrencyIdsList;
        }
        return $result;
    }

    public function fetch_markets($params = array ()): array {
        /**
         * retrieves data on all markets for coinmetro
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#9fd18008-338e-4863-b07d-722878a46832
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array[]} an array of objects representing $market data
         */
        $promises = array();
        $promises[] = $this->publicGetMarkets ($params);
        if ($this->safe_value($this->options, 'currenciesByIdForParseMarket') === null) {
            $promises[] = $this->fetch_currencies();
        }
        $responses = $promises;
        $response = $responses[0];
        //
        //     array(
        //         array(
        //             "pair" => "YFIEUR",
        //             "precision" => 5,
        //             "margin" => false
        //         ),
        //         array(
        //             "pair" => "BTCEUR",
        //             "precision" => 2,
        //             "margin" => true
        //         ),
        //         ...
        //     )
        //
        $result = array();
        for ($i = 0; $i < count($response); $i++) {
            $market = $this->parse_market($response[$i]);
            // there are several broken (unavailable info) markets
            if ($market['base'] === null || $market['quote'] === null) {
                continue;
            }
            $result[] = $market;
        }
        return $result;
    }

    public function parse_market(array $market): array {
        $id = $this->safe_string($market, 'pair');
        $parsedMarketId = $this->parse_market_id($id);
        $baseId = $this->safe_string($parsedMarketId, 'baseId');
        $quoteId = $this->safe_string($parsedMarketId, 'quoteId');
        $base = $this->safe_currency_code($baseId);
        $quote = $this->safe_currency_code($quoteId);
        $basePrecisionAndLimits = $this->parse_market_precision_and_limits($baseId);
        $quotePrecisionAndLimits = $this->parse_market_precision_and_limits($quoteId);
        $margin = $this->safe_bool($market, 'margin', false);
        $tradingFees = $this->safe_value($this->fees, 'trading', array());
        return $this->safe_market_structure(array(
            'id' => $id,
            'symbol' => $base . '/' . $quote,
            'base' => $base,
            'quote' => $quote,
            'settle' => null,
            'baseId' => $baseId,
            'quoteId' => $quoteId,
            'settleId' => null,
            'type' => 'spot',
            'spot' => true,
            'margin' => $margin,
            'swap' => false,
            'future' => false,
            'option' => false,
            'active' => true,
            'contract' => false,
            'linear' => null,
            'inverse' => null,
            'taker' => $this->safe_number($tradingFees, 'taker'),
            'maker' => $this->safe_number($tradingFees, 'maker'),
            'contractSize' => null,
            'expiry' => null,
            'expiryDatetime' => null,
            'strike' => null,
            'optionType' => null,
            'precision' => array(
                'amount' => $basePrecisionAndLimits['precision'],
                'price' => $this->parse_number($this->parse_precision($this->safe_string($market, 'precision'))),
            ),
            'limits' => array(
                'leverage' => array(
                    'min' => null,
                    'max' => null,
                ),
                'amount' => array(
                    'min' => $basePrecisionAndLimits['minLimit'],
                    'max' => null,
                ),
                'price' => array(
                    'min' => null,
                    'max' => null,
                ),
                'cost' => array(
                    'min' => $quotePrecisionAndLimits['minLimit'],
                    'max' => null,
                ),
            ),
            'created' => null,
            'info' => $market,
        ));
    }

    public function parse_market_id($marketId) {
        $baseId = null;
        $quoteId = null;
        $currencyIds = $this->safe_value($this->options, 'currencyIdsListForParseMarket', array());
        // Bubble sort by length (longest first)
        $currencyIdsLength = count($currencyIds);
        for ($i = 0; $i < $currencyIdsLength; $i++) {
            for ($j = 0; $j < $currencyIdsLength - $i - 1; $j++) {
                $a = $currencyIds[$j];
                $b = $currencyIds[$j + 1];
                if (strlen($a) < strlen($b)) {
                    $currencyIds[$j] = $b;
                    $currencyIds[$j + 1] = $a;
                }
            }
        }
        for ($i = 0; $i < count($currencyIds); $i++) {
            $currencyId = $currencyIds[$i];
            $entryIndex = mb_strpos($marketId, $currencyId);
            if ($entryIndex === 0) {
                $restId = str_replace($currencyId, '', $marketId);
                if ($this->in_array($restId, $currencyIds)) {
                    if ($entryIndex === 0) {
                        $baseId = $currencyId;
                        $quoteId = $restId;
                    } else {
                        $baseId = $restId;
                        $quoteId = $currencyId;
                    }
                    break;
                }
            }
        }
        if ($baseId === null || $quoteId === null) {
            // https://github.com/ccxt/ccxt/issues/26820
            if (str_ends_with($marketId, 'USDT')) {
                $baseId = str_replace('USDT', '', $marketId);
                $quoteId = 'USDT';
            }
            if (str_ends_with($marketId, 'USD')) {
                $baseId = str_replace('USD', '', $marketId);
                $quoteId = 'USD';
            }
        }
        $result = array(
            'baseId' => $baseId,
            'quoteId' => $quoteId,
        );
        return $result;
    }

    public function parse_market_precision_and_limits($currencyId) {
        $currencies = $this->safe_value($this->options, 'currenciesByIdForParseMarket', array());
        $currency = $this->safe_value($currencies, $currencyId, array());
        $limits = $this->safe_value($currency, 'limits', array());
        $amountLimits = $this->safe_value($limits, 'amount', array());
        $minLimit = $this->safe_number($amountLimits, 'min');
        $result = array(
            'precision' => $this->safe_number($currency, 'precision'),
            'minLimit' => $minLimit,
        );
        return $result;
    }

    public function fetch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetches historical candlestick data containing the open, high, low, and close price, and the volume of a $market
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#13cfb5bc-7bfb-4847-85e1-e0f35dfb3573
         *
         * @param {string} $symbol unified $symbol of the $market to fetch OHLCV data for
         * @param {string} $timeframe the length of time each candle represents
         * @param {int} [$since] timestamp in ms of the earliest candle to fetch
         * @param {int} [$limit] the maximum amount of candles to fetch
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @param {int} [$params->until] the latest time in ms to fetch entries for
         * @return {int[][]} A list of candles ordered, open, high, low, close, volume
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
            'timeframe' => $this->safe_string($this->timeframes, $timeframe, $timeframe),
        );
        $until = null;
        if ($since !== null) {
            $request['from'] = $since;
            if ($limit !== null) {
                $duration = $this->parse_timeframe($timeframe) * 1000;
                $until = $this->sum($since, $duration * ($limit));
            }
        } else {
            $request['from'] = ':from'; // this endpoint doesn't accept empty from and to $params (setting them into the value described in the documentation)
        }
        $until = $this->safe_integer($params, 'until', $until);
        if ($until !== null) {
            $params = $this->omit($params, array( 'until' ));
            $request['to'] = $until;
        } else {
            $request['to'] = ':to';
        }
        $response = $this->publicGetExchangeCandlesPairTimeframeFromTo ($this->extend($request, $params));
        //
        //     {
        //         "candleHistory" => array(
        //             array(
        //                 "pair" => "ETHUSDT",
        //                 "timeframe" => 86400000,
        //                 "timestamp" => 1697673600000,
        //                 "c" => 1567.4409353098604,
        //                 "h" => 1566.7514068472303,
        //                 "l" => 1549.4563666936847,
        //                 "o" => 1563.4490341395904,
        //                 "v" => 0
        //             ),
        //             array(
        //                 "pair" => "ETHUSDT",
        //                 "timeframe" => 86400000,
        //                 "timestamp" => 1697760000000,
        //                 "c" => 1603.7831363339324,
        //                 "h" => 1625.0356823666407,
        //                 "l" => 1565.4629390011505,
        //                 "o" => 1566.8387619426028,
        //                 "v" => 0
        //             ),
        //             ...
        //         )
        //     }
        //
        $candleHistory = $this->safe_list($response, 'candleHistory', array());
        return $this->parse_ohlcvs($candleHistory, $market, $timeframe, $since, $limit);
    }

    public function parse_ohlcv($ohlcv, ?array $market = null): array {
        return array(
            $this->safe_integer($ohlcv, 'timestamp'),
            $this->safe_number($ohlcv, 'o'),
            $this->safe_number($ohlcv, 'h'),
            $this->safe_number($ohlcv, 'l'),
            $this->safe_number($ohlcv, 'c'),
            $this->safe_number($ohlcv, 'v'),
        );
    }

    public function fetch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * get the list of most recent trades for a particular $symbol
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ee5d698-06da-4570-8c84-914185e05065
         *
         * @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 (default 200, max 500)
         * @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~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
        );
        if ($since !== null) {
            $request['from'] = $since;
        } else {
            // this endpoint accepts empty from param
            $request['from'] = '';
        }
        $response = $this->publicGetExchangeTicksPairFrom ($this->extend($request, $params));
        //
        //     {
        //         "tickHistory" => array(
        //             array(
        //                 "pair" => "ETHUSDT",
        //                 "price" => 2077.5623,
        //                 "qty" => 0.002888,
        //                 "timestamp" => 1700684689420,
        //                 "seqNum" => 10644554718
        //             ),
        //             array(
        //                 "pair" => "ETHUSDT",
        //                 "price" => 2078.3848,
        //                 "qty" => 0.003368,
        //                 "timestamp" => 1700684738410,
        //                 "seqNum" => 10644559561
        //             ),
        //             array(
        //                 "pair" => "ETHUSDT",
        //                 "price" => 2077.1513,
        //                 "qty" => 0.00337,
        //                 "timestamp" => 1700684816853,
        //                 "seqNum" => 10644567113
        //             ),
        //             ...
        //         )
        //     }
        //
        $tickHistory = $this->safe_list($response, 'tickHistory', array());
        return $this->parse_trades($tickHistory, $market, $since, $limit);
    }

    public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        /**
         * fetch all trades made by the user
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b
         *
         * @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 (default 500, max 1000)
         * @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~
         */
        $this->load_markets();
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
        }
        $request = array();
        if ($since !== null) {
            $request['since'] = $since;
        } else {
            // the exchange requires a value for the $since param
            $request['since'] = 0;
        }
        $response = $this->privateGetExchangeFillsSince ($this->extend($request, $params));
        //
        //     array(
        //         array(
        //             "pair" => "ETHUSDC",
        //             "seqNumber" => 10873722343,
        //             "timestamp" => 1702570610747,
        //             "qty" => 0.002,
        //             "price" => 2282,
        //             "side" => "buy",
        //             "orderID" => "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c"
        //         ),
        //         ...
        //     )
        //
        return $this->parse_trades($response, $market, $since, $limit);
    }

    public function parse_trade(array $trade, ?array $market = null): array {
        //
        // fetchTrades
        //     array(
        //         "pair" => "ETHUSDT",
        //         "price" => 2077.1513,
        //         "qty" => 0.00337,
        //         "timestamp" => 1700684816853,
        //         "seqNum" => 10644567113
        //     ),
        //
        // fetchMyTrades
        //     {
        //         "pair" => "ETHUSDC",
        //         "seqNumber" => 10873722343,
        //         "timestamp" => 1702570610747,
        //         "qty" => 0.002,
        //         "price" => 2282,
        //         "side" => "buy",
        //         "orderID" => "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c"
        //     }
        //
        // fetchOrders
        //     {
        //         "_id" => "657b31d360a9542449381bdc",
        //         "seqNumber" => 10873722343,
        //         "timestamp" => 1702570610747,
        //         "qty" => 0.002,
        //         "price" => 2282,
        //         "side" => "buy"
        //     }
        //
        //    {
        //        "pair":"ETHUSDC",
        //        "seqNumber":"10873722343",
        //        "timestamp":"1702570610747",
        //        "qty":"0.002",
        //        "price":"2282",
        //        "side":"buy",
        //        "orderID":"65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c",
        //        "userID":"65671262d93d9525ac009e36"
        //     }
        //
        $marketId = $this->safe_string_2($trade, 'symbol', 'pair');
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $id = $this->safe_string_n($trade, array( '_id', 'seqNum', 'seqNumber' ));
        $timestamp = $this->safe_integer($trade, 'timestamp');
        $priceString = $this->safe_string($trade, 'price');
        $amountString = $this->safe_string($trade, 'qty');
        $order = $this->safe_string($trade, 'orderID');
        $side = $this->safe_string($trade, 'side');
        return $this->safe_trade(array(
            'id' => $id,
            'order' => $order,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'type' => null,
            'side' => $side,
            'takerOrMaker' => null,
            'price' => $priceString,
            'amount' => $amountString,
            'cost' => null,
            'fee' => null,
            'info' => $trade,
        ), $market);
    }

    public function fetch_order_book(string $symbol, ?int $limit = null, $params = array ()): array {
        /**
         * fetches information on open orders with bid (buy) and ask (sell) prices, volumes and other data
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#26ad80d7-8c46-41b5-9208-386f439a8b87
         *
         * @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 (default 100, max 200)
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-$book-structure order $book structures~ indexed by $market symbols
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
            'pair' => $market['id'],
        );
        $response = $this->publicGetExchangeBookPair ($this->extend($request, $params));
        //
        //     {
        //         "book" => {
        //             "pair" => "ETHUSDT",
        //             "seqNumber" => 10800409239,
        //             "ask" => array(
        //                 "2354.2861" => 3.75,
        //                 "2354.3138" => 19,
        //                 "2354.7538" => 80,
        //                 "2355.5430" => 260,
        //                 "2356.4611" => 950,
        //                 "2361.7150" => 1500,
        //                 "206194.0000" => 0.01
        //             ),
        //             "bid" => array(
        //                 "2352.6339" => 3.75,
        //                 "2352.6002" => 19,
        //                 "2352.2402" => 80,
        //                 "2351.4582" => 260,
        //                 "2349.3111" => 950,
        //                 "2343.8601" => 1500,
        //                 "1.0000" => 5
        //             ),
        //             "checksum" => 2108177337
        //         }
        //     }
        //
        $book = $this->safe_value($response, 'book', array());
        $rawBids = $this->safe_value($book, 'bid', array());
        $rawAsks = $this->safe_value($book, 'ask', array());
        $rawOrderbook = array(
            'bids' => $rawBids,
            'asks' => $rawAsks,
        );
        $orderbook = $this->parse_order_book($rawOrderbook, $symbol);
        $orderbook['nonce'] = $this->safe_integer($book, 'seqNumber');
        return $orderbook;
    }

    public function parse_bids_asks($bidasks, int|string $priceKey = 0, int|string $amountKey = 1, int|string $countOrIdKey = 2) {
        $prices = is_array($bidasks) ? array_keys($bidasks) : array();
        $result = array();
        for ($i = 0; $i < count($prices); $i++) {
            $priceString = $this->safe_string($prices, $i);
            $price = $this->safe_number($prices, $i);
            $volume = $this->safe_number($bidasks, $priceString);
            $result[] = array( $price, $volume );
        }
        return $result;
    }

    public function fetch_tickers(?array $symbols = null, $params = array ()): array {
        /**
         * fetches price $tickers for multiple markets, statistical information calculated over the past 24 hours for each market
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485
         *
         * @param {string[]} [$symbols] unified $symbols of the markets to fetch the ticker for, all market $tickers are returned if not assigned
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structures~
         */
        $this->load_markets();
        $response = $this->publicGetExchangePrices ($params);
        //
        //     {
        //         "latestPrices" => array(
        //             array(
        //                 "pair" => "PERPEUR",
        //                 "timestamp" => 1702549840393,
        //                 "price" => 0.7899997816001223,
        //                 "qty" => 1e-12,
        //                 "ask" => 0.8,
        //                 "bid" => 0.7799995632002446
        //             ),
        //             array(
        //                 "pair" => "PERPUSD",
        //                 "timestamp" => 1702549841973,
        //                 "price" => 0.8615317721366659,
        //                 "qty" => 1e-12,
        //                 "ask" => 0.8742333599999257,
        //                 "bid" => 0.8490376365388491
        //             ),
        //             ...
        //         ),
        //         "24hInfo" => array(
        //             {
        //                 "delta" => 0.25396444229149906,
        //                 "h" => 0.78999978160012,
        //                 "l" => 0.630001740844,
        //                 "v" => 54.910000002833996,
        //                 "pair" => "PERPEUR",
        //                 "sentimentData" => array(
        //                     "sentiment" => 36.71333333333333,
        //                     "interest" => 0.47430830039525695
        //                     }
        //                 ),
        //             {
        //                 "delta" => 0.26915154078134096,
        //                 "h" => 0.86220315458898,
        //                 "l" => 0.67866757035154,
        //                 "v" => 2.835000000000001e-9,
        //                 "pair" => "PERPUSD",
        //                 "sentimentData" => array(
        //                     "sentiment" => 36.71333333333333,
        //                     "interest" => 0.47430830039525695
        //                 }
        //             ),
        //             ...
        //         )
        //     }
        //
        $latestPrices = $this->safe_value($response, 'latestPrices', array());
        $twentyFourHInfos = $this->safe_value($response, '24hInfo', array());
        $tickersObject = array();
        // merging info from two lists into one
        for ($i = 0; $i < count($latestPrices); $i++) {
            $latestPrice = $latestPrices[$i];
            $marketId = $this->safe_string($latestPrice, 'pair');
            if ($marketId !== null) {
                $tickersObject[$marketId] = $latestPrice;
            }
        }
        for ($i = 0; $i < count($twentyFourHInfos); $i++) {
            $twentyFourHInfo = $twentyFourHInfos[$i];
            $marketId = $this->safe_string($twentyFourHInfo, 'pair');
            if ($marketId !== null) {
                $latestPrice = $this->safe_value($tickersObject, $marketId, array());
                $tickersObject[$marketId] = $this->extend($twentyFourHInfo, $latestPrice);
            }
        }
        $tickers = is_array($tickersObject) ? array_values($tickersObject) : array();
        return $this->parse_tickers($tickers, $symbols);
    }

    public function fetch_bids_asks(?array $symbols = null, $params = array ()) {
        /**
         * fetches the bid and ask price and volume for multiple markets
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485
         *
         * @param {string[]} [$symbols] unified $symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structures~
         */
        $this->load_markets();
        $response = $this->publicGetExchangePrices ($params);
        $latestPrices = $this->safe_list($response, 'latestPrices', array());
        return $this->parse_tickers($latestPrices, $symbols);
    }

    public function parse_ticker(array $ticker, ?array $market = null): array {
        //
        //     {
        //         "pair" => "PERPUSD",
        //         "timestamp" => 1702549841973,
        //         "price" => 0.8615317721366659,
        //         "qty" => 1e-12,
        //         "ask" => 0.8742333599999257,
        //         "bid" => 0.8490376365388491
        //         "delta" => 0.26915154078134096,
        //         "h" => 0.86220315458898,
        //         "l" => 0.67866757035154,
        //         "v" => 2.835000000000001e-9,
        //         "sentimentData" => {
        //             "sentiment" => 36.71333333333333,
        //             "interest" => 0.47430830039525695
        //         }
        //     }
        //
        $marketId = $this->safe_string($ticker, 'pair');
        $market = $this->safe_market($marketId, $market);
        $timestamp = $this->safe_integer($ticker, 'timestamp');
        $bid = $this->safe_string($ticker, 'bid');
        $ask = $this->safe_string($ticker, 'ask');
        $high = $this->safe_string($ticker, 'h');
        $low = $this->safe_string($ticker, 'l');
        $last = $this->safe_string($ticker, 'price');
        $baseVolume = $this->safe_string($ticker, 'v');
        $delta = $this->safe_string($ticker, 'delta');
        $percentage = Precise::string_mul($delta, '100');
        return $this->safe_ticker(array(
            'symbol' => $market['symbol'],
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'open' => null,
            'high' => $high,
            'low' => $low,
            'close' => null,
            'last' => $last,
            'bid' => $bid,
            'bidVolume' => null,
            'ask' => $ask,
            'askVolume' => null,
            'vwap' => null,
            'previousClose' => null,
            'change' => null,
            'percentage' => $percentage,
            'average' => null,
            'baseVolume' => $baseVolume,
            'quoteVolume' => null,
            'info' => $ticker,
        ), $market);
    }

    public function fetch_balance($params = array ()): array {
        /**
         * query for balance and get the amount of funds available for trading or funds locked in orders
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#741a1dcc-7307-40d0-acca-28d003d1506a
         *
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
         */
        $this->load_markets();
        $response = $this->privateGetUsersWallets ($params);
        $list = $this->safe_list($response, 'list', array());
        return $this->parse_balance($list);
    }

    public function parse_balance($balances): array {
        //
        //     array(
        //         array(
        //             "xcmLocks" => array(),
        //             "xcmLockAmounts" => array(),
        //             "refList" => array(),
        //             "balanceHistory" => array(),
        //             "_id" => "5fecd3c998e75c2e4d63f7c3",
        //             "currency" => "BTC",
        //             "label" => "BTC",
        //             "userId" => "5fecd3c97fbfed1521db23bd",
        //             "__v" => 0,
        //             "balance" => 0.5,
        //             "createdAt" => "2020-12-30T19:23:53.646Z",
        //             "disabled" => false,
        //             "updatedAt" => "2020-12-30T19:23:53.653Z",
        //             "reserved" => 0,
        //             "id" => "5fecd3c998e75c2e4d63f7c3"
        //         ),
        //         ...
        //     )
        //
        $result = array(
            'info' => $balances,
        );
        for ($i = 0; $i < count($balances); $i++) {
            $balanceEntry = $this->safe_dict($balances, $i, array());
            $currencyId = $this->safe_string($balanceEntry, 'currency');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            $account['total'] = $this->safe_string($balanceEntry, 'balance');
            $account['used'] = $this->safe_string($balanceEntry, 'reserved');
            $result[$code] = $account;
        }
        return $this->safe_balance($result);
    }

    public function fetch_ledger(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch the history of changes, actions done by the user or operations that altered the balance of the user
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#4e7831f7-a0e7-4c3e-9336-1d0e5dcb15cf
         *
         * @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 200, max 500)
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @param {int} [$params->until] the latest time in ms to fetch entries for
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=$ledger ledger structure~
         */
        $this->load_markets();
        $request = array();
        if ($since !== null) {
            $request['since'] = $since;
        } else {
            // this endpoint accepts empty $since param
            $request['since'] = '';
        }
        $currency = null;
        if ($code !== null) {
            $currency = $this->currency($code);
        }
        $response = $this->privateGetUsersWalletsHistorySince ($this->extend($request, $params));
        //
        //     {
        //         "list" => array(
        //             array(
        //                 "currency" => "USDC",
        //                 "label" => "USDC",
        //                 "userId" => "65671262d93d9525ac009e36",
        //                 "balance" => 0,
        //                 "disabled" => false,
        //                 "balanceHistory" => array(
        //                     array(
        //                         "description" => "Deposit - 657973a9b6eadf0f33d70100",
        //                         "JSONdata" => array(
        //                             "fees" => 0,
        //                             "notes" => "Via Crypto",
        //                             "txHash" => "0x2e4875185b0f312d8e24b2d26d46bf9877db798b608ad2ff97b2b8bc7d8134e5",
        //                             "last4Digits" => null,
        //                             "IBAN" => null,
        //                             "alternativeChain" => "polygon",
        //                             "referenceId" => "657973a9b6eadf0f33d70100",
        //                             "status" => "completed",
        //                             "tracked" => true
        //                         ),
        //                         "amount" => 99,
        //                         "timestamp" => "2023-12-13T09:04:51.270Z",
        //                         "amountEUR" => 91.79310117335974
        //                     ),
        //                     array(
        //                         "description" => "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342",
        //                         "JSONdata" => array(
        //                             "price" => "2282.00 ETH/USDC",
        //                             "fees" => 0,
        //                             "notes" => "Order 3a8c5b4d6c"
        //                         ),
        //                         "amount" => -4.564,
        //                         "timestamp" => "2023-12-14T16:16:50.760Z",
        //                         "amountEUR" => -4.150043849187587
        //                     ),
        //                     ...
        //                 )
        //             ),
        //             array(
        //                 "currency" => "ETH",
        //                 "label" => "ETH",
        //                 "userId" => "65671262d93d9525ac009e36",
        //                 "balance" => 0,
        //                 "disabled" => false,
        //                 "balanceHistory" => array(
        //                     array(
        //                         "description" => "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342",
        //                         "JSONdata" => array(
        //                             "price" => "2282.00 ETH/USDC",
        //                             "fees" => 0.000002,
        //                             "notes" => "Order 3a8c5b4d6c"
        //                         ),
        //                         "amount" => 0.001998,
        //                         "timestamp" => "2023-12-14T16:16:50.761Z",
        //                         "amountEUR" => 4.144849415806856
        //                     ),
        //                     ...
        //                 )
        //             ),
        //             {
        //                 "currency" => "DOGE",
        //                 "label" => "DOGE",
        //                 "userId" => "65671262d93d9525ac009e36",
        //                 "balance" => 0,
        //                 "disabled" => false,
        //                 "balanceHistory" => array(
        //                     array(
        //                         "description" => "Order 65671262d93d9525ac009e361702905785319b5d9016dc20736034d13ca6a - Swap",
        //                         "JSONdata" => array(
        //                             "swap" => true,
        //                             "subtype" => "swap",
        //                             "fees" => 0,
        //                             "price" => "0.0905469 DOGE/USDC",
        //                             "notes" => "Swap 034d13ca6a"
        //                         ),
        //                         "amount" => 70,
        //                         "timestamp" => "2023-12-18T13:23:05.836Z",
        //                         "amountEUR" => 5.643627624549227
        //                     }
        //                 )
        //             ),
        //             ...
        //         )
        //     }
        //
        $ledgerByCurrencies = $this->safe_value($response, 'list', array());
        $ledger = array();
        for ($i = 0; $i < count($ledgerByCurrencies); $i++) {
            $currencyLedger = $ledgerByCurrencies[$i];
            $currencyId = $this->safe_string($currencyLedger, 'currency');
            $balanceHistory = $this->safe_value($currencyLedger, 'balanceHistory', array());
            for ($j = 0; $j < count($balanceHistory); $j++) {
                $rawLedgerEntry = $balanceHistory[$j];
                $rawLedgerEntry['currencyId'] = $currencyId;
                $ledger[] = $rawLedgerEntry;
            }
        }
        return $this->parse_ledger($ledger, $currency, $since, $limit);
    }

    public function parse_ledger_entry(array $item, ?array $currency = null): array {
        $datetime = $this->safe_string($item, 'timestamp');
        $currencyId = $this->safe_string($item, 'currencyId');
        $item = $this->omit($item, 'currencyId');
        $currency = $this->safe_currency($currencyId, $currency);
        $description = $this->safe_string($item, 'description', '');
        list($type, $referenceId) = $this->parse_ledger_entry_description($description);
        $JSONdata = $this->safe_value($item, 'JSONdata', array());
        $feeCost = $this->safe_string($JSONdata, 'fees');
        $fee = array(
            'cost' => $feeCost,
            'currency' => null,
        );
        $amount = $this->safe_string($item, 'amount');
        $direction = null;
        if ($amount !== null) {
            if (Precise::string_lt($amount, '0')) {
                $direction = 'out';
                $amount = Precise::string_abs($amount);
            } elseif (Precise::string_gt($amount, '0')) {
                $direction = 'in';
            }
        }
        return $this->safe_ledger_entry(array(
            'info' => $item,
            'id' => null,
            'timestamp' => $this->parse8601($datetime),
            'datetime' => $datetime,
            'direction' => $direction,
            'account' => null,
            'referenceId' => $referenceId,
            'referenceAccount' => null,
            'type' => $type,
            'currency' => $currency,
            'amount' => $amount,
            'before' => null,
            'after' => null,
            'status' => null,
            'fee' => $fee,
        ), $currency);
    }

    public function parse_ledger_entry_description($description) {
        $descriptionArray = array();
        if ($description !== null) {
            $descriptionArray = explode(' ', $description);
        }
        $type = null;
        $referenceId = null;
        $length = count($descriptionArray);
        if ($length > 1) {
            $type = $this->parse_ledger_entry_type($descriptionArray[0]);
            if ($descriptionArray[1] !== '-') {
                $referenceId = $descriptionArray[1];
            } else {
                $referenceId = $this->safe_string($descriptionArray, 2);
            }
        }
        return array( $type, $referenceId );
    }

    public function parse_ledger_entry_type($type) {
        $types = array(
            'Deposit' => 'transaction',
            'Withdraw' => 'transaction',
            'Order' => 'trade',
        );
        return $this->safe_string($types, $type, $type);
    }

    public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        /**
         * create a trade order
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#a4895a1d-3f50-40ae-8231-6962ef06c771
         *
         * @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 {float} [$params->cost] the quote quantity that can be used alternative for the $amount in $market orders
         * @param {string} [$params->timeInForce] "GTC", "IOC", "FOK", "GTD"
         * @param {number} [$params->expirationTime] timestamp in millisecond, for GTD orders only
         * @param {float} [$params->triggerPrice] the $price at which a trigger order is triggered at
         * @param {float} [$params->stopLossPrice] *margin only* The $price at which a stop loss order is triggered at
         * @param {float} [$params->takeProfitPrice] *margin only* The $price at which a take profit order is triggered at
         * @param {bool} [$params->margin] true for creating a margin order
         * @param {string} [$params->fillStyle] fill style of the limit order => "sell" fulfills selling quantity "buy" fulfills buying quantity "base" fulfills base currency quantity "quote" fulfills quote currency quantity
         * @param {string} [$params->clientOrderId] client's $comment
         * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
         */
        $this->load_markets();
        $market = $this->market($symbol);
        $request = array(
        );
        $request['orderType'] = $type;
        $formattedAmount = null;
        if ($amount !== null) {
            $formattedAmount = $this->amount_to_precision($symbol, $amount);
        }
        $cost = $this->safe_value($params, 'cost');
        $params = $this->omit($params, 'cost');
        if ($type === 'limit') {
            if (($price === null) && ($cost === null)) {
                throw new ArgumentsRequired($this->id . ' createOrder() requires a $price or $params->cost argument for a ' . $type . ' order');
            } elseif (($price !== null) && ($amount !== null)) {
                $costString = Precise::string_mul($this->number_to_string($price), $this->number_to_string($formattedAmount));
                $cost = $this->parse_to_numeric($costString);
            }
        }
        $precisedCost = null;
        if ($cost !== null) {
            $precisedCost = $this->cost_to_precision($symbol, $cost);
        }
        if ($side === 'sell') {
            $request = $this->handle_create_order_side($market['baseId'], $market['quoteId'], $formattedAmount, $precisedCost, $request);
        } elseif ($side === 'buy') {
            $request = $this->handle_create_order_side($market['quoteId'], $market['baseId'], $precisedCost, $formattedAmount, $request);
        }
        $timeInForce = $this->safe_value($params, 'timeInForce');
        if ($timeInForce !== null) {
            $params = $this->omit($params, 'timeInForce');
            $request['timeInForce'] = $this->encode_order_time_in_force($timeInForce);
        }
        $triggerPrice = $this->safe_string_2($params, 'triggerPrice', 'stopPrice');
        if ($triggerPrice !== null) {
            $params = $this->omit($params, array( 'triggerPrice' ));
            $request['stopPrice'] = $this->price_to_precision($symbol, $triggerPrice);
        }
        $userData = $this->safe_value($params, 'userData', array());
        $comment = $this->safe_string_2($params, 'clientOrderId', 'comment');
        if ($comment !== null) {
            $params = $this->omit($params, array( 'clientOrderId' ));
            $userData['comment'] = $comment;
        }
        $stopLossPrice = $this->safe_string($params, 'stopLossPrice');
        if ($stopLossPrice !== null) {
            $params = $this->omit($params, 'stopLossPrice');
            $userData['stopLoss'] = $this->price_to_precision($symbol, $stopLossPrice);
        }
        $takeProfitPrice = $this->safe_string($params, 'takeProfitPrice');
        if ($takeProfitPrice !== null) {
            $params = $this->omit($params, 'takeProfitPrice');
            $userData['takeProfit'] = $this->price_to_precision($symbol, $takeProfitPrice);
        }
        if (!$this->is_empty($userData)) {
            $request['userData'] = $userData;
        }
        $response = $this->privatePostExchangeOrdersCreate ($this->extend($request, $params));
        //
        //     {
        //         "userID" => "65671262d93d9525ac009e36",
        //         "orderID" => "65671262d93d9525ac009e36170257448481749b7ee2893bafec2",
        //         "orderType" => "market",
        //         "buyingCurrency" => "ETH",
        //         "sellingCurrency" => "USDC",
        //         "buyingQty" => 0.002,
        //         "timeInForce" => 4,
        //         "boughtQty" => 0.002,
        //         "soldQty" => 4.587,
        //         "creationTime" => 1702574484829,
        //         "seqNumber" => 10874285330,
        //         "firstFillTime" => 1702574484831,
        //         "lastFillTime" => 1702574484831,
        //         "fills" => array(
        //             {
        //                 "seqNumber" => 10874285329,
        //                 "timestamp" => 1702574484831,
        //                 "qty" => 0.002,
        //                 "price" => 2293.5,
        //                 "side" => "buy"
        //             }
        //         ),
        //         "completionTime" => 1702574484831,
        //         "takerQty" => 0.002
        //     }
        //
        return $this->parse_order($response, $market);
    }

    public function handle_create_order_side($sellingCurrency, $buyingCurrency, $sellingQty, $buyingQty, $request = array ()) {
        $request['sellingCurrency'] = $sellingCurrency;
        $request['buyingCurrency'] = $buyingCurrency;
        if ($sellingQty !== null) {
            $request['sellingQty'] = $sellingQty;
        }
        if ($buyingQty !== null) {
            $request['buyingQty'] = $buyingQty;
        }
        return $request;
    }

    public function encode_order_time_in_force($timeInForce) {
        $timeInForceTypes = array(
            'GTC' => 1,
            'IOC' => 2,
            'GTD' => 3,
            'FOK' => 4,
        );
        return $this->safe_value($timeInForceTypes, $timeInForce, $timeInForce);
    }

    public function cancel_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * cancels an open order
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#eaea86da-16ca-4c56-9f00-5b1cb2ad89f8
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316
         *
         * @param {string} $id order $id
         * @param {string} $symbol not used by coinmetro cancelOrder ()
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @param {string} [$params->margin] true for cancelling a margin order
         * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        $this->load_markets();
        $request = array(
            'orderID' => $id,
        );
        $marginMode = null;
        list($params, $params) = $this->handle_margin_mode_and_params('cancelOrder', $params);
        $isMargin = $this->safe_bool($params, 'margin', false);
        $params = $this->omit($params, 'margin');
        $response = null;
        if ($isMargin || ($marginMode !== null)) {
            $response = $this->privatePostExchangeOrdersCloseOrderID ($this->extend($request, $params));
        } else {
            $response = $this->privatePutExchangeOrdersCancelOrderID ($this->extend($request, $params));
        }
        //
        //     {
        //         "userID" => "65671262d93d9525ac009e36",
        //         "orderID" => "65671262d93d9525ac009e3617026635256739c996fe17d7cd5d4",
        //         "orderType" => "limit",
        //         "buyingCurrency" => "ETH",
        //         "sellingCurrency" => "USDC",
        //         "fillStyle" => "sell",
        //         "orderPlatform" => "trade-v3",
        //         "timeInForce" => 1,
        //         "buyingQty" => 0.005655,
        //         "sellingQty" => 11.31,
        //         "boughtQty" => 0,
        //         "soldQty" => 0,
        //         "creationTime" => 1702663525713,
        //         "seqNumber" => 10915220048,
        //         "completionTime" => 1702928369053
        //     }
        //
        return $this->parse_order($response);
    }

    public function close_position(string $symbol, ?string $side = null, $params = array ()) {
        /**
         * closes an open position
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316
         *
         * @param {string} $symbol not used by coinmetro closePosition ()
         * @param {string} [$side] not used by coinmetro closePosition ()
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @param {string} [$params->orderID] order id
         * @param {number} [$params->fraction] fraction of order to close, between 0 and 1 (defaults to 1)
         * @return {array} An ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
         */
        $this->load_markets();
        $orderId = $this->safe_string($params, 'orderId');
        if ($orderId === null) {
            throw new ArgumentsRequired($this->id . ' closePosition() requires a $orderId parameter');
        }
        $request = array(
            'orderID' => $orderId,
        );
        $response = $this->privatePostExchangeOrdersCloseOrderID ($this->extend($request, $params));
        //
        //     {
        //         "userID" => "65671262d93d9525ac009e36",
        //         "orderID" => "65671262d93d9525ac009e3617030152811996e5b352556d3d7d8_CL",
        //         "orderType" => "market",
        //         "buyingCurrency" => "ETH",
        //         "sellingCurrency" => "EUR",
        //         "margin" => true,
        //         "buyingQty" => 0.03,
        //         "timeInForce" => 4,
        //         "boughtQty" => 0.03,
        //         "soldQty" => 59.375,
        //         "creationTime" => 1703015488482,
        //         "seqNumber" => 10925321179,
        //         "firstFillTime" => 1703015488483,
        //         "lastFillTime" => 1703015488483,
        //         "fills" => array(
        //             {
        //                 "seqNumber" => 10925321178,
        //                 "timestamp" => 1703015488483,
        //                 "qty" => 0.03,
        //                 "price" => 1979.1666666666667,
        //                 "side" => "buy"
        //             }
        //         ),
        //         "completionTime" => 1703015488483,
        //         "takerQty" => 0.03
        //     }
        //
        return $this->parse_order($response);
    }

    public function fetch_open_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetch all unfilled currently open $orders
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#518afd7a-4338-439c-a651-d4fdaa964138
         *
         * @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 $order structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=$order-structure $order structures~
         */
        $this->load_markets();
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
        }
        $response = $this->privateGetExchangeOrdersActive ($params);
        $orders = $this->parse_orders($response, $market, $since, $limit);
        for ($i = 0; $i < count($orders); $i++) {
            $order = $orders[$i];
            $order['status'] = 'open';
        }
        return $orders;
    }

    public function fetch_canceled_and_closed_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): array {
        /**
         * fetches information on multiple canceled and closed orders made by the user
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b
         *
         * @param {string} $symbol unified $market $symbol of the $market orders were made in
         * @param {int} [$since] the earliest time in ms to fetch orders for
         * @param {int} [$limit] the maximum number of order structures to retrieve
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
         */
        $this->load_markets();
        $market = null;
        if ($symbol !== null) {
            $market = $this->market($symbol);
        }
        $request = array();
        if ($since !== null) {
            $request['since'] = $since;
        }
        $response = $this->privateGetExchangeOrdersHistorySince ($this->extend($request, $params));
        return $this->parse_orders($response, $market, $since, $limit);
    }

    public function fetch_order(string $id, ?string $symbol = null, $params = array ()) {
        /**
         * fetches information on an order made by the user
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#95bbed87-db1c-47a7-a03e-aa247e91d5a6
         *
         * @param {int|string} $id order $id
         * @param {string} $symbol not used by coinmetro fetchOrder ()
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} An ~@link https://docs.ccxt.com/#/?$id=order-structure order structure~
         */
        $this->load_markets();
        $request = array(
            'orderID' => $id,
        );
        $response = $this->privateGetExchangeOrdersStatusOrderID ($this->extend($request, $params));
        //
        //     {
        //         "_id" => "657b4e6d60a954244939ac6f",
        //         "userID" => "65671262d93d9525ac009e36",
        //         "orderID" => "65671262d93d9525ac009e361702576531985b78465468b9cc544",
        //         "orderType" => "market",
        //         "buyingCurrency" => "ETH",
        //         "sellingCurrency" => "USDC",
        //         "buyingQty" => 0.004,
        //         "timeInForce" => 4,
        //         "boughtQty" => 0.004,
        //         "soldQty" => 9.236,
        //         "creationTime" => 1702576531995,
        //         "seqNumber" => 10874644062,
        //         "firstFillTime" => 1702576531995,
        //         "lastFillTime" => 1702576531995,
        //         "fills" => array(
        //             {
        //                 "_id" => "657b4e6d60a954244939ac70",
        //                 "seqNumber" => 10874644061,
        //                 "timestamp" => 1702576531995,
        //                 "qty" => 0.004,
        //                 "price" => 2309,
        //                 "side" => "buy"
        //             }
        //         ),
        //         "completionTime" => 1702576531995,
        //         "takerQty" => 0.004,
        //         "fees" => 0.000004,
        //         "isAncillary" => false,
        //         "margin" => false,
        //         "trade" => false,
        //         "canceled" => false
        //     }
        //
        return $this->parse_order($response);
    }

    public function parse_order(array $order, ?array $market = null): array {
        //
        // createOrder $market
        //     {
        //         "userID" => "65671262d93d9525ac009e36",
        //         "orderID" => "65671262d93d9525ac009e36170257448481749b7ee2893bafec2",
        //         "orderType" => "market",
        //         "buyingCurrency" => "ETH",
        //         "sellingCurrency" => "USDC",
        //         "buyingQty" => 0.002,
        //         "timeInForce" => 4,
        //         "boughtQty" => 0.002,
        //         "soldQty" => 4.587,
        //         "creationTime" => 1702574484829,
        //         "seqNumber" => 10874285330,
        //         "firstFillTime" => 1702574484831,
        //         "lastFillTime" => 1702574484831,
        //         "fills" => array(
        //             {
        //                 "seqNumber" => 10874285329,
        //                 "timestamp" => 1702574484831,
        //                 "qty" => 0.002,
        //                 "price" => 2293.5,
        //                 "side" => "buy"
        //             }
        //         ),
        //         "completionTime" => 1702574484831,
        //         "takerQty" => 0.002
        //     }
        //
        // createOrder limit
        //     {
        //         "userID" => "65671262d93d9525ac009e36",
        //         "orderID" => "65671262d93d9525ac009e3617026635256739c996fe17d7cd5d4",
        //         "orderType" => "limit",
        //         "buyingCurrency" => "ETH",
        //         "sellingCurrency" => "USDC",
        //         "fillStyle" => "sell",
        //         "orderPlatform" => "trade-v3",
        //         "timeInForce" => 1,
        //         "buyingQty" => 0.005655,
        //         "sellingQty" => 11.31,
        //         "boughtQty" => 0,
        //         "soldQty" => 0,
        //         "creationTime" => 1702663525713,
        //         "seqNumber" => 10885528683,
        //         "fees" => 0,
        //         "fills" => array(),
        //         "isAncillary" => false,
        //         "margin" => false,
        //         "trade" => false
        //     }
        //
        // fetchOrders $market
        //     {
        //         "userID" => "65671262d93d9525ac009e36",
        //         "orderID" => "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c",
        //         "orderType" => "market",
        //         "buyingCurrency" => "ETH",
        //         "sellingCurrency" => "USDC",
        //         "buyingQty" => 0.002,
        //         "timeInForce" => 4,
        //         "boughtQty" => 0.002,
        //         "soldQty" => 4.564,
        //         "creationTime" => 1702570610746,
        //         "seqNumber" => 10873722344,
        //         "firstFillTime" => 1702570610747,
        //         "lastFillTime" => 1702570610747,
        //         "fills" => array(
        //             {
        //                 "_id" => "657b31d360a9542449381bdc",
        //                 "seqNumber" => 10873722343,
        //                 "timestamp" => 1702570610747,
        //                 "qty" => 0.002,
        //                 "price" => 2282,
        //                 "side" => "buy"
        //             }
        //         ),
        //         "completionTime" => 1702570610747,
        //         "takerQty" => 0.002,
        //         "fees" => 0.000002,
        //         "isAncillary" => false,
        //         "margin" => false,
        //         "trade" => false,
        //         "canceled" => false,
        //         "__v" => 0
        //     }
        //
        // fetchOrders margin
        //     {
        //         "userData" => array(
        //             "takeProfit" => 1700,
        //             "stopLoss" => 2100
        //         ),
        //         "_id" => "658201d060a95424499394a2",
        //         "seqNumber" => 10925300213,
        //         "orderType" => "limit",
        //         "buyingCurrency" => "EUR",
        //         "sellingCurrency" => "ETH",
        //         "userID" => "65671262d93d9525ac009e36",
        //         "closedQty" => 0.03,
        //         "sellingQty" => 0.03,
        //         "buyingQty" => 58.8,
        //         "creationTime" => 1703015281205,
        //         "margin" => true,
        //         "timeInForce" => 1,
        //         "boughtQty" => 59.31,
        //         "orderID" => "65671262d93d9525ac009e3617030152811996e5b352556d3d7d8",
        //         "lastFillTime" => 1703015281206,
        //         "soldQty" => 0.03,
        //         "closedTime" => 1703015488488,
        //         "closedVal" => 59.375,
        //         "trade" => true,
        //         "takerQty" => 59.31,
        //         "firstFillTime" => 1703015281206,
        //         "completionTime" => 1703015281206,
        //         "fills" => array(
        //             array(
        //                 "_id" => "658201d060a95424499394a3",
        //                 "seqNumber" => 10925300212,
        //                 "side" => "sell",
        //                 "price" => 1977,
        //                 "qty" => 0.03,
        //                 "timestamp" => 1703015281206
        //             ),
        //             {
        //                 "_id" => "658201d060a95424499394a4",
        //                 "seqNumber" => 10925321178,
        //                 "timestamp" => 1703015488483,
        //                 "qty" => 0.03,
        //                 "price" => 1979.1666666666667,
        //                 "side" => "buy"
        //             }
        //         ),
        //         "fees" => 0.11875000200000001,
        //         "settledQtys" => array(
        //             "ETH" => -0.000092842104710025
        //         ),
        //         "isAncillary" => false,
        //         "canceled" => false
        //     }
        //
        // fetchOrder
        //     {
        //         "_id" => "657b4e6d60a954244939ac6f",
        //         "userID" => "65671262d93d9525ac009e36",
        //         "orderID" => "65671262d93d9525ac009e361702576531985b78465468b9cc544",
        //         "orderType" => "market",
        //         "buyingCurrency" => "ETH",
        //         "sellingCurrency" => "USDC",
        //         "buyingQty" => 0.004,
        //         "timeInForce" => 4,
        //         "boughtQty" => 0.004,
        //         "soldQty" => 9.236,
        //         "creationTime" => 1702576531995,
        //         "seqNumber" => 10874644062,
        //         "firstFillTime" => 1702576531995,
        //         "lastFillTime" => 1702576531995,
        //         "fills" => array(
        //             {
        //                 "_id" => "657b4e6d60a954244939ac70",
        //                 "seqNumber" => 10874644061,
        //                 "timestamp" => 1702576531995,
        //                 "qty" => 0.004,
        //                 "price" => 2309,
        //                 "side" => "buy"
        //             }
        //         ),
        //         "completionTime" => 1702576531995,
        //         "takerQty" => 0.004,
        //         "fees" => 0.000004,
        //         "isAncillary" => false,
        //         "margin" => false,
        //         "trade" => false,
        //         "canceled" => false
        //     }
        //
        $timestamp = $this->safe_integer($order, 'creationTime');
        $isCanceled = $this->safe_value($order, 'canceled');
        $status = null;
        if ($isCanceled === true) {
            if ($timestamp === null) {
                $timestamp = $this->safe_integer($order, 'completionTime'); // $market orders with bad $price gain IOC - we mark them as 'rejected'?
                $status = 'rejected'; // these orders don't have the 'creationTime` param and have 'canceled' => true
            } else {
                $status = 'canceled';
            }
        } else {
            $status = $this->safe_string($order, 'status');
            $order = $this->omit($order, 'status'); // we mark orders from fetchOpenOrders with param 'status' => 'open'
        }
        $type = $this->safe_string($order, 'orderType');
        $buyingQty = $this->safe_string($order, 'buyingQty');
        $sellingQty = $this->safe_string($order, 'sellingQty');
        $boughtQty = $this->safe_string($order, 'boughtQty');
        $soldQty = $this->safe_string($order, 'soldQty');
        if ($type === 'market') {
            if (($buyingQty === null) && ($boughtQty !== null) && ($boughtQty !== '0')) {
                $buyingQty = $boughtQty;
            }
            if (($sellingQty === null) && ($soldQty !== null) && ($soldQty !== '0')) {
                $sellingQty = $soldQty;
            }
        }
        $buyingCurrencyId = $this->safe_string($order, 'buyingCurrency', '');
        $sellingCurrencyId = $this->safe_string($order, 'sellingCurrency', '');
        $byuingIdPlusSellingId = $buyingCurrencyId . $sellingCurrencyId;
        $sellingIdPlusBuyingId = $sellingCurrencyId . $buyingCurrencyId;
        $side = null;
        $marketId = null;
        $baseAmount = $buyingQty;
        $quoteAmount = $buyingQty;
        $filled = null;
        $cost = null;
        $feeInBaseOrQuote = null;
        $marketsById = $this->index_by($this->markets, 'id');
        if ($this->safe_value($marketsById, $byuingIdPlusSellingId) !== null) {
            $side = 'buy';
            $marketId = $byuingIdPlusSellingId;
            $quoteAmount = $sellingQty;
            $filled = $boughtQty;
            $cost = $soldQty;
            $feeInBaseOrQuote = 'base';
        } elseif ($this->safe_value($marketsById, $sellingIdPlusBuyingId) !== null) {
            $side = 'sell';
            $marketId = $sellingIdPlusBuyingId;
            $baseAmount = $sellingQty;
            $filled = $soldQty;
            $cost = $boughtQty;
            $feeInBaseOrQuote = 'quote';
        }
        $price = null;
        if (($baseAmount !== null) && ($quoteAmount !== null)) {
            $price = Precise::string_div($quoteAmount, $baseAmount);
        }
        $market = $this->safe_market($marketId, $market);
        $fee = null;
        $feeCost = $this->safe_string($order, 'fees');
        if (($feeCost !== null) && ($feeInBaseOrQuote !== null)) {
            $fee = array(
                'currency' => $market[$feeInBaseOrQuote],
                'cost' => $feeCost,
                'rate' => null,
            );
        }
        $trades = $this->safe_value($order, 'fills', array());
        $userData = $this->safe_value($order, 'userData', array());
        $clientOrderId = $this->safe_string($userData, 'comment');
        $takeProfitPrice = $this->safe_string($userData, 'takeProfit');
        $stopLossPrice = $this->safe_string($userData, 'stopLoss');
        return $this->safe_order(array(
            'id' => $this->safe_string($order, 'orderID'),
            'clientOrderId' => $clientOrderId,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastTradeTimestamp' => $this->safe_integer($order, 'lastFillTime'),
            'status' => $status,
            'symbol' => $market['symbol'],
            'type' => $type,
            'timeInForce' => $this->parse_order_time_in_force($this->safe_integer($order, 'timeInForce')),
            'side' => $side,
            'price' => $price,
            'triggerPrice' => $this->safe_string($order, 'stopPrice'),
            'takeProfitPrice' => $takeProfitPrice,
            'stopLossPrice' => $stopLossPrice,
            'average' => null,
            'amount' => $baseAmount,
            'cost' => $cost,
            'filled' => $filled,
            'remaining' => null,
            'fee' => $fee,
            'fees' => null,
            'trades' => $trades,
            'info' => $order,
        ), $market);
    }

    public function parse_order_time_in_force($timeInForce) {
        $timeInForceTypes = array(
            null,
            'GTC',
            'IOC',
            'GTD',
            'FOK',
        );
        return $this->safe_value($timeInForceTypes, $timeInForce, $timeInForce);
    }

    public function borrow_cross_margin(string $code, float $amount, $params = array ()) {
        /**
         * create a loan to borrow margin
         *
         * @see https://documenter.getpostman.com/view/3653795/SVfWN6KS#5b90b3b9-e5db-4d07-ac9d-d680a06fd110
         *
         * @param {string} $code unified $currency $code of the $currency to borrow
         * @param {float} $amount the $amount to borrow
         * @param {array} [$params] extra parameters specific to the exchange API endpoint
         * @return {array} a ~@link https://docs.ccxt.com/#/?id=margin-loan-structure margin loan structure~
         */
        $this->load_markets();
        $currency = $this->currency($code);
        $currencyId = $currency['id'];
        $request = array();
        $request[$currencyId] = $this->currency_to_precision($code, $amount);
        $response = $this->privatePutUsersMarginCollateral ($this->extend($request, $params));
        //
        //     array( "message" => "OK" )
        //
        $result = $this->safe_value($response, 'result', array());
        $transaction = $this->parse_margin_loan($result, $currency);
        return $this->extend($transaction, array(
            'amount' => $amount,
        ));
    }

    public function parse_margin_loan($info, ?array $currency = null) {
        $currencyId = $this->safe_string($info, 'coin');
        return array(
            'id' => null,
            'currency' => $this->safe_currency_code($currencyId, $currency),
            'amount' => null,
            'symbol' => null,
            'timestamp' => null,
            'datetime' => null,
            'info' => $info,
        );
    }

    public function sign($path, $api = 'public', $method = 'GET', $params = array (), $headers = null, $body = null) {
        $request = $this->omit($params, $this->extract_params($path));
        $endpoint = '/' . $this->implode_params($path, $params);
        $url = $this->urls['api'][$api] . $endpoint;
        $query = $this->urlencode($request);
        if ($headers === null) {
            $headers = array();
        }
        $headers['CCXT'] = 'true';
        if ($api === 'private') {
            if (($this->uid === null) && ($this->apiKey !== null)) {
                $this->uid = $this->apiKey;
            }
            if (($this->token === null) && ($this->secret !== null)) {
                $this->token = $this->secret;
            }
            if ($url === 'https://api->coinmetro.com/jwt') { // handle with $headers for login $endpoint
                $headers['X-Device-Id'] = 'bypass';
                if ($this->twofa !== null) {
                    $headers['X-OTP'] = $this->twofa;
                }
            } elseif ($url === 'https://api->coinmetro.com/jwtDevice') { // handle with $headers for long lived token login $endpoint
                $headers['X-Device-Id'] = $this->uid;
                if ($this->twofa !== null) {
                    $headers['X-OTP'] = $this->twofa;
                }
            } else {
                $headers['Authorization'] = 'Bearer ' . $this->token;
                if (str_starts_with(!$url, 'https://api->coinmetro.com/open')) { // if not sandbox $endpoint
                    $this->check_required_credentials();
                    $headers['X-Device-Id'] = $this->uid;
                }
            }
            if (($method === 'POST') || ($method === 'PUT')) {
                $headers['Content-Type'] = 'application/x-www-form-urlencoded';
                $body = $this->urlencode($request);
            }
        } elseif (strlen($query) !== 0) {
            $url .= '?' . $query;
        }
        while (str_ends_with($url, '/')) {
            $url = mb_substr($url, 0, strlen($url) - 1 - 0);
        }
        return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers );
    }

    public function handle_errors(int $code, string $reason, string $url, string $method, array $headers, string $body, $response, $requestHeaders, $requestBody) {
        if ($response === null) {
            return null;
        }
        if (($code !== 200) && ($code !== 201) && ($code !== 202)) {
            $feedback = $this->id . ' ' . $body;
            $message = $this->safe_string($response, 'message');
            $this->throw_broadly_matched_exception($this->exceptions['broad'], $message, $feedback);
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $message, $feedback);
            throw new ExchangeError($feedback);
        }
        return null;
    }
}
