# -*- coding: utf-8 -*-

# 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

from ccxt.base.exchange import Exchange
from ccxt.abstract.coinmetro import ImplicitAPI
from ccxt.base.types import Any, Balances, Currencies, Currency, IndexType, Int, LedgerEntry, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade
from typing import List
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import PermissionDenied
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import BadRequest
from ccxt.base.errors import BadSymbol
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import OrderNotFound
from ccxt.base.errors import RateLimitExceeded
from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise


class coinmetro(Exchange, ImplicitAPI):

    def describe(self) -> Any:
        return self.deep_extend(super(coinmetro, self).describe(), {
            'id': 'coinmetro',
            'name': 'Coinmetro',
            'countries': ['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': {
                'CORS': None,
                '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': {
                '1m': '60000',
                '5m': '300000',
                '30m': '1800000',
                '4h': '14400000',
                '1d': '86400000',
            },
            'urls': {
                'logo': 'https://github.com/ccxt/ccxt/assets/43336371/e86f87ec-6ba3-4410-962b-f7988c5db539',
                'api': {
                    'public': 'https://api.coinmetro.com',
                    'private': 'https://api.coinmetro.com',
                },
                'test': {
                    'public': 'https://api.coinmetro.com/open',
                    'private': 'https://api.coinmetro.com/open',
                },
                'www': 'https://coinmetro.com/',
                'doc': [
                    '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': {
                'public': {
                    'get': {
                        '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': {
                    'get': {
                        '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': {
                        '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': {
                        'jwt': 1,  # not unified
                        'exchange/orders/cancel/{orderID}': 1,
                        'users/margin/collateral': 1,
                        'users/margin/primary/{currency}': 1,  # not unified
                    },
                },
            },
            'requiredCredentials': {
                'apiKey': False,
                'secret': False,
                'uid': True,
                'token': True,
            },
            'fees': {
                'trading': {
                    'feeSide': 'get',
                    'tierBased': False,
                    'percentage': True,
                    'taker': self.parse_number('0.001'),
                    'maker': self.parse_number('0'),
                },
            },
            'precisionMode': TICK_SIZE,
            # exchange-specific options
            'options': {
                'currenciesByIdForParseMarket': None,
                'currencyIdsListForParseMarket': ['QRDO'],
            },
            'features': {
                'spot': {
                    'sandbox': True,
                    'createOrder': {
                        'marginMode': True,  # todo implement
                        'triggerPrice': True,
                        'triggerPriceType': None,
                        'triggerDirection': False,
                        'stopLossPrice': False,  # todo
                        'takeProfitPrice': False,  # todo
                        'attachedStopLossTakeProfit': {
                            'triggerPriceType': None,
                            'price': False,
                        },
                        'timeInForce': {
                            'IOC': True,
                            'FOK': True,
                            'PO': False,
                            'GTD': True,
                        },
                        'hedged': False,
                        'trailing': False,
                        'leverage': False,
                        'marketBuyByCost': True,
                        'marketBuyRequiresPrice': False,
                        'selfTradePrevention': False,
                        'iceberg': True,
                    },
                    'createOrders': None,
                    'fetchMyTrades': {
                        'marginMode': False,
                        'limit': None,
                        'daysBack': 100000,
                        'untilDays': None,
                        'symbolRequired': False,
                    },
                    'fetchOrder': {
                        'marginMode': False,
                        'trigger': False,
                        'trailing': False,
                        'symbolRequired': False,
                    },
                    'fetchOpenOrders': {
                        'marginMode': False,
                        'limit': None,
                        'trigger': False,
                        'trailing': False,
                        'symbolRequired': False,
                    },
                    'fetchOrders': {
                        'marginMode': False,
                        'limit': None,
                        'daysBack': 100000,
                        'untilDays': None,
                        'trigger': False,
                        'trailing': False,
                        'symbolRequired': False,
                    },
                    'fetchClosedOrders': None,
                    'fetchOHLCV': {
                        'limit': 1000,
                    },
                },
                'swap': {
                    'linear': None,
                    'inverse': None,
                },
                'future': {
                    'linear': None,
                    'inverse': None,
                },
            },
            'exceptions': {
                # https://trade-docs.coinmetro.co/?javascript--nodejs#message-codes
                'exact': {
                    'Both buyingCurrency and sellingCurrency are required': InvalidOrder,  # 422 - "Both buyingCurrency and sellingCurrency are required"
                    'One and only one of buyingQty and sellingQty is required': InvalidOrder,  # 422 - "One and only one of buyingQty and sellingQty is required"
                    'Invalid buyingCurrency': InvalidOrder,  # 422 - "Invalid buyingCurrency"
                    'Invalid \'from\'': BadRequest,  # 422 Unprocessable Entity {"message":"Invalid 'from'"}
                    'Invalid sellingCurrency': InvalidOrder,  # 422 - "Invalid sellingCurrency"
                    'Invalid buyingQty': InvalidOrder,  # 422 - "Invalid buyingQty"
                    'Invalid sellingQty': InvalidOrder,  # 422 - "Invalid sellingQty"
                    'Insufficient balance': InsufficientFunds,  # 422 - "Insufficient balance"
                    'Expiration date is in the past or too near in the future': InvalidOrder,  # 422 Unprocessable Entity {"message":"Expiration date is in the past or too near in the future"}
                    'Forbidden': PermissionDenied,  # 403 Forbidden {"message":"Forbidden"}
                    'Order Not Found': OrderNotFound,  # 404 Not Found {"message":"Order Not Found"}
                    'since must be a millisecond timestamp': BadRequest,  # 422 Unprocessable Entity {"message":"since must be a millisecond timestamp"}
                    'This pair is disabled on margin': BadSymbol,  # 422 Unprocessable Entity {"message":"This pair is disabled on margin"}
                },
                'broad': {
                    'accessing from a new IP': PermissionDenied,  # 403 Forbidden {"message":"You're accessing from a new IP. Please check your email."}
                    'available to allocate': InsufficientFunds,  # 403 Forbidden {"message":"Insufficient EUR available to allocate"}
                    'At least': BadRequest,  # 422 Unprocessable Entity {"message":"At least 5 EUR per operation"}
                    'collateral is not allowed': BadRequest,  # 422 Unprocessable Entity {"message":"DOGE collateral is not allowed"}
                    'Insufficient liquidity': InvalidOrder,  # 503 Service Unavailable {"message":"Insufficient liquidity to fill the FOK order completely."}
                    'Insufficient order size': InvalidOrder,  # 422 Unprocessable Entity {"message":"Insufficient order size - min 0.002 ETH"}
                    'Invalid quantity': InvalidOrder,  # 422 Unprocessable Entity {"message":"Invalid quantity!"}
                    'Invalid Stop Loss': InvalidOrder,  # 422 Unprocessable Entity {"message":"Invalid Stop Loss!"}
                    'Invalid stop price!': InvalidOrder,  # 422 Unprocessable Entity {"message":"Invalid stop price!"}
                    'Not enough balance': InsufficientFunds,  # 422 Unprocessable Entity {"message":"Not enough balance!"}
                    'Not enough margin': InsufficientFunds,  # Unprocessable Entity {"message":"Not enough margin!"}
                    'orderType missing': BadRequest,  # 422 Unprocessable Entity {"message":"orderType missing!"}
                    'Server Timeout': ExchangeError,  # 503 Service Unavailable {"message":"Server Timeout!"}
                    'Time in force has to be IOC or FOK for market orders': InvalidOrder,  # 422 Unprocessable Entity {"message":"Time in force has to be IOC or FOK for market orders!"}
                    'Too many attempts': RateLimitExceeded,  # 429 Too Many Requests {"message":"Too many attempts. Try again in 3 seconds"}
                },
            },
        })

    def fetch_currencies(self, params={}) -> Currencies:
        """
        fetches all available currencies on an exchange

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#d5876d43-a3fe-4479-8c58-24d0f044edfb

        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: an associative dictionary of currencies
        """
        response = self.publicGetAssets(params)
        #
        #     [
        #         {
        #             "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": {
        #                 "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: dict = {}
        for i in range(0, len(response)):
            currency = response[i]
            id = self.safe_string(currency, 'symbol')
            code = self.safe_currency_code(id)
            typeRaw = self.safe_string(currency, 'type')
            type = None
            if typeRaw == 'coin' or typeRaw == 'token' or typeRaw == 'erc20':
                type = 'crypto'
            elif typeRaw == 'fiat':
                type = 'fiat'
            precisionDigits = self.safe_string_2(currency, 'digits', 'notabeneDecimals')
            result[code] = self.safe_currency_structure({
                'id': id,
                'code': code,
                'name': code,
                'type': type,
                'info': currency,
                'active': self.safe_bool(currency, 'canTrade'),
                'deposit': self.safe_bool(currency, 'canDeposit'),
                'withdraw': self.safe_bool(currency, 'canWithdraw'),
                'fee': None,
                'precision': self.parse_number(self.parse_precision(precisionDigits)),
                'limits': {
                    'amount': {
                        'min': self.safe_number(currency, 'minQty'),
                        'max': None,
                    },
                    'withdraw': {
                        'min': None,
                        'max': None,
                    },
                },
                'networks': {},
            })
        if self.safe_value(self.options, 'currenciesByIdForParseMarket') is None:
            currenciesById = self.index_by(result, 'id')
            self.options['currenciesByIdForParseMarket'] = currenciesById
            currentCurrencyIdsList = self.safe_list(self.options, 'currencyIdsListForParseMarket', [])
            currencyIdsList = list(currenciesById.keys())
            for i in range(0, len(currencyIdsList)):
                currentCurrencyIdsList.append(currencyIdsList[i])
            self.options['currencyIdsListForParseMarket'] = currentCurrencyIdsList
        return result

    def fetch_markets(self, params={}) -> List[Market]:
        """
        retrieves data on all markets for coinmetro

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#9fd18008-338e-4863-b07d-722878a46832

        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: an array of objects representing market data
        """
        response = self.publicGetMarkets(params)
        if self.safe_value(self.options, 'currenciesByIdForParseMarket') is None:
            self.fetch_currencies()
        #
        #     [
        #         {
        #             "pair": "YFIEUR",
        #             "precision": 5,
        #             "margin": False
        #         },
        #         {
        #             "pair": "BTCEUR",
        #             "precision": 2,
        #             "margin": True
        #         },
        #         ...
        #     ]
        #
        return self.parse_markets(response)

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

    def parse_market_id(self, marketId):
        baseId = None
        quoteId = None
        currencyIds = self.safe_value(self.options, 'currencyIdsListForParseMarket', [])
        # Bubble sort by length(longest first)
        currencyIdsLength = len(currencyIds)
        for i in range(0, currencyIdsLength):
            for j in range(0, currencyIdsLength - i - 1):
                a = currencyIds[j]
                b = currencyIds[j + 1]
                if len(a) < len(b):
                    currencyIds[j] = b
                    currencyIds[j + 1] = a
        for i in range(0, len(currencyIds)):
            currencyId = currencyIds[i]
            entryIndex = marketId.find(currencyId)
            if entryIndex == 0:
                restId = marketId.replace(currencyId, '')
                if self.in_array(restId, currencyIds):
                    if entryIndex == 0:
                        baseId = currencyId
                        quoteId = restId
                    else:
                        baseId = restId
                        quoteId = currencyId
                    break
        result: dict = {
            'baseId': baseId,
            'quoteId': quoteId,
        }
        return result

    def parse_market_precision_and_limits(self, currencyId):
        currencies = self.safe_value(self.options, 'currenciesByIdForParseMarket', {})
        currency = self.safe_value(currencies, currencyId, {})
        limits = self.safe_value(currency, 'limits', {})
        amountLimits = self.safe_value(limits, 'amount', {})
        minLimit = self.safe_number(amountLimits, 'min')
        result: dict = {
            'precision': self.safe_number(currency, 'precision'),
            'minLimit': minLimit,
        }
        return result

    def fetch_ohlcv(self, symbol: str, timeframe='1m', since: Int = None, limit: Int = None, params={}) -> List[list]:
        """
        fetches historical candlestick data containing the open, high, low, and close price, and the volume of a market

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#13cfb5bc-7bfb-4847-85e1-e0f35dfb3573

        :param str symbol: unified symbol of the market to fetch OHLCV data for
        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: the latest time in ms to fetch entries for
        :returns int[][]: A list of candles ordered, open, high, low, close, volume
        """
        self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'pair': market['id'],
            'timeframe': self.safe_string(self.timeframes, timeframe, timeframe),
        }
        until = None
        if since is not None:
            request['from'] = since
            if limit is not None:
                duration = self.parse_timeframe(timeframe) * 1000
                until = self.sum(since, duration * (limit))
        else:
            request['from'] = ':from'  # self endpoint doesn't accept empty from and to params(setting them into the value described in the documentation)
        until = self.safe_integer(params, 'until', until)
        if until is not None:
            params = self.omit(params, ['until'])
            request['to'] = until
        else:
            request['to'] = ':to'
        response = self.publicGetExchangeCandlesPairTimeframeFromTo(self.extend(request, params))
        #
        #     {
        #         "candleHistory": [
        #             {
        #                 "pair": "ETHUSDT",
        #                 "timeframe": 86400000,
        #                 "timestamp": 1697673600000,
        #                 "c": 1567.4409353098604,
        #                 "h": 1566.7514068472303,
        #                 "l": 1549.4563666936847,
        #                 "o": 1563.4490341395904,
        #                 "v": 0
        #             },
        #             {
        #                 "pair": "ETHUSDT",
        #                 "timeframe": 86400000,
        #                 "timestamp": 1697760000000,
        #                 "c": 1603.7831363339324,
        #                 "h": 1625.0356823666407,
        #                 "l": 1565.4629390011505,
        #                 "o": 1566.8387619426028,
        #                 "v": 0
        #             },
        #             ...
        #         ]
        #     }
        #
        candleHistory = self.safe_list(response, 'candleHistory', [])
        return self.parse_ohlcvs(candleHistory, market, timeframe, since, limit)

    def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
        return [
            self.safe_integer(ohlcv, 'timestamp'),
            self.safe_number(ohlcv, 'o'),
            self.safe_number(ohlcv, 'h'),
            self.safe_number(ohlcv, 'l'),
            self.safe_number(ohlcv, 'c'),
            self.safe_number(ohlcv, 'v'),
        ]

    def fetch_trades(self, symbol: str, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        get the list of most recent trades for a particular symbol

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ee5d698-06da-4570-8c84-914185e05065

        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
        """
        self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'pair': market['id'],
        }
        if since is not None:
            request['from'] = since
        else:
            # self endpoint accepts empty from param
            request['from'] = ''
        response = self.publicGetExchangeTicksPairFrom(self.extend(request, params))
        #
        #     {
        #         "tickHistory": [
        #             {
        #                 "pair": "ETHUSDT",
        #                 "price": 2077.5623,
        #                 "qty": 0.002888,
        #                 "timestamp": 1700684689420,
        #                 "seqNum": 10644554718
        #             },
        #             {
        #                 "pair": "ETHUSDT",
        #                 "price": 2078.3848,
        #                 "qty": 0.003368,
        #                 "timestamp": 1700684738410,
        #                 "seqNum": 10644559561
        #             },
        #             {
        #                 "pair": "ETHUSDT",
        #                 "price": 2077.1513,
        #                 "qty": 0.00337,
        #                 "timestamp": 1700684816853,
        #                 "seqNum": 10644567113
        #             },
        #             ...
        #         ]
        #     }
        #
        tickHistory = self.safe_list(response, 'tickHistory', [])
        return self.parse_trades(tickHistory, market, since, limit)

    def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        fetch all trades made by the user

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b

        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
        """
        self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        request: dict = {}
        if since is not None:
            request['since'] = since
        else:
            # the exchange requires a value for the since param
            request['since'] = 0
        response = self.privateGetExchangeFillsSince(self.extend(request, params))
        #
        #     [
        #         {
        #             "pair": "ETHUSDC",
        #             "seqNumber": 10873722343,
        #             "timestamp": 1702570610747,
        #             "qty": 0.002,
        #             "price": 2282,
        #             "side": "buy",
        #             "orderID": "65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c"
        #         },
        #         ...
        #     ]
        #
        return self.parse_trades(response, market, since, limit)

    def parse_trade(self, trade: dict, market: Market = None) -> Trade:
        #
        # fetchTrades
        #     {
        #         "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 = self.safe_string_2(trade, 'symbol', 'pair')
        market = self.safe_market(marketId, market)
        symbol = market['symbol']
        id = self.safe_string_n(trade, ['_id', 'seqNum', 'seqNumber'])
        timestamp = self.safe_integer(trade, 'timestamp')
        priceString = self.safe_string(trade, 'price')
        amountString = self.safe_string(trade, 'qty')
        order = self.safe_string(trade, 'orderID')
        side = self.safe_string(trade, 'side')
        return self.safe_trade({
            'id': id,
            'order': order,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'type': None,
            'side': side,
            'takerOrMaker': None,
            'price': priceString,
            'amount': amountString,
            'cost': None,
            'fee': None,
            'info': trade,
        }, market)

    def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        """
        fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#26ad80d7-8c46-41b5-9208-386f439a8b87

        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: A dictionary of `order book structures <https://docs.ccxt.com/#/?id=order-book-structure>` indexed by market symbols
        """
        self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'pair': market['id'],
        }
        response = self.publicGetExchangeBookPair(self.extend(request, params))
        #
        #     {
        #         "book": {
        #             "pair": "ETHUSDT",
        #             "seqNumber": 10800409239,
        #             "ask": {
        #                 "2354.2861": 3.75,
        #                 "2354.3138": 19,
        #                 "2354.7538": 80,
        #                 "2355.5430": 260,
        #                 "2356.4611": 950,
        #                 "2361.7150": 1500,
        #                 "206194.0000": 0.01
        #             },
        #             "bid": {
        #                 "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 = self.safe_value(response, 'book', {})
        rawBids = self.safe_value(book, 'bid', {})
        rawAsks = self.safe_value(book, 'ask', {})
        rawOrderbook: dict = {
            'bids': rawBids,
            'asks': rawAsks,
        }
        orderbook = self.parse_order_book(rawOrderbook, symbol)
        orderbook['nonce'] = self.safe_integer(book, 'seqNumber')
        return orderbook

    def parse_bids_asks(self, bidasks, priceKey: IndexType = 0, amountKey: IndexType = 1, countOrIdKey: IndexType = 2):
        prices = list(bidasks.keys())
        result = []
        for i in range(0, len(prices)):
            priceString = self.safe_string(prices, i)
            price = self.safe_number(prices, i)
            volume = self.safe_number(bidasks, priceString)
            result.append([price, volume])
        return result

    def fetch_tickers(self, symbols: Strings = None, params={}) -> Tickers:
        """
        fetches price tickers for multiple markets, statistical information calculated over the past 24 hours for each market

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485

        :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`
        """
        self.load_markets()
        response = self.publicGetExchangePrices(params)
        #
        #     {
        #         "latestPrices": [
        #             {
        #                 "pair": "PERPEUR",
        #                 "timestamp": 1702549840393,
        #                 "price": 0.7899997816001223,
        #                 "qty": 1e-12,
        #                 "ask": 0.8,
        #                 "bid": 0.7799995632002446
        #             },
        #             {
        #                 "pair": "PERPUSD",
        #                 "timestamp": 1702549841973,
        #                 "price": 0.8615317721366659,
        #                 "qty": 1e-12,
        #                 "ask": 0.8742333599999257,
        #                 "bid": 0.8490376365388491
        #             },
        #             ...
        #         ],
        #         "24hInfo": [
        #             {
        #                 "delta": 0.25396444229149906,
        #                 "h": 0.78999978160012,
        #                 "l": 0.630001740844,
        #                 "v": 54.910000002833996,
        #                 "pair": "PERPEUR",
        #                 "sentimentData": {
        #                     "sentiment": 36.71333333333333,
        #                     "interest": 0.47430830039525695
        #                     }
        #                 },
        #             {
        #                 "delta": 0.26915154078134096,
        #                 "h": 0.86220315458898,
        #                 "l": 0.67866757035154,
        #                 "v": 2.835000000000001e-9,
        #                 "pair": "PERPUSD",
        #                 "sentimentData": {
        #                     "sentiment": 36.71333333333333,
        #                     "interest": 0.47430830039525695
        #                 }
        #             },
        #             ...
        #         ]
        #     }
        #
        latestPrices = self.safe_value(response, 'latestPrices', [])
        twentyFourHInfos = self.safe_value(response, '24hInfo', [])
        tickersObject: dict = {}
        # merging info from two lists into one
        for i in range(0, len(latestPrices)):
            latestPrice = latestPrices[i]
            marketId = self.safe_string(latestPrice, 'pair')
            if marketId is not None:
                tickersObject[marketId] = latestPrice
        for i in range(0, len(twentyFourHInfos)):
            twentyFourHInfo = twentyFourHInfos[i]
            marketId = self.safe_string(twentyFourHInfo, 'pair')
            if marketId is not None:
                latestPrice = self.safe_value(tickersObject, marketId, {})
                tickersObject[marketId] = self.extend(twentyFourHInfo, latestPrice)
        tickers = list(tickersObject.values())
        return self.parse_tickers(tickers, symbols)

    def fetch_bids_asks(self, symbols: Strings = None, params={}):
        """
        fetches the bid and ask price and volume for multiple markets

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#6ecd1cd1-f162-45a3-8b3b-de690332a485

        :param str[] [symbols]: unified symbols of the markets to fetch the bids and asks for, all markets are returned if not assigned
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`
        """
        self.load_markets()
        response = self.publicGetExchangePrices(params)
        latestPrices = self.safe_list(response, 'latestPrices', [])
        return self.parse_tickers(latestPrices, symbols)

    def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
        #
        #     {
        #         "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 = self.safe_string(ticker, 'pair')
        market = self.safe_market(marketId, market)
        timestamp = self.safe_integer(ticker, 'timestamp')
        bid = self.safe_string(ticker, 'bid')
        ask = self.safe_string(ticker, 'ask')
        high = self.safe_string(ticker, 'h')
        low = self.safe_string(ticker, 'l')
        last = self.safe_string(ticker, 'price')
        baseVolume = self.safe_string(ticker, 'v')
        delta = self.safe_string(ticker, 'delta')
        percentage = Precise.string_mul(delta, '100')
        return self.safe_ticker({
            'symbol': market['symbol'],
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'open': None,
            'high': high,
            'low': low,
            'close': None,
            'last': last,
            'bid': bid,
            'bidVolume': None,
            'ask': ask,
            'askVolume': None,
            'vwap': None,
            'previousClose': None,
            'change': None,
            'percentage': percentage,
            'average': None,
            'baseVolume': baseVolume,
            'quoteVolume': None,
            'info': ticker,
        }, market)

    def fetch_balance(self, params={}) -> Balances:
        """
        query for balance and get the amount of funds available for trading or funds locked in orders

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#741a1dcc-7307-40d0-acca-28d003d1506a

        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
        """
        self.load_markets()
        response = self.privateGetUsersWallets(params)
        list = self.safe_list(response, 'list', [])
        return self.parse_balance(list)

    def parse_balance(self, balances) -> Balances:
        #
        #     [
        #         {
        #             "xcmLocks": [],
        #             "xcmLockAmounts": [],
        #             "refList": [],
        #             "balanceHistory": [],
        #             "_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: dict = {
            'info': balances,
        }
        for i in range(0, len(balances)):
            balanceEntry = self.safe_dict(balances, i, {})
            currencyId = self.safe_string(balanceEntry, 'currency')
            code = self.safe_currency_code(currencyId)
            account = self.account()
            account['total'] = self.safe_string(balanceEntry, 'balance')
            account['used'] = self.safe_string(balanceEntry, 'reserved')
            result[code] = account
        return self.safe_balance(result)

    def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[LedgerEntry]:
        """
        fetch the history of changes, actions done by the user or operations that altered the balance of the user

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#4e7831f7-a0e7-4c3e-9336-1d0e5dcb15cf

        :param str [code]: unified currency code, default is None
        :param int [since]: timestamp in ms of the earliest ledger entry, default is None
        :param int [limit]: max number of ledger entries to return(default 200, max 500)
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: the latest time in ms to fetch entries for
        :returns dict: a `ledger structure <https://docs.ccxt.com/#/?id=ledger>`
        """
        self.load_markets()
        request: dict = {}
        if since is not None:
            request['since'] = since
        else:
            # self endpoint accepts empty since param
            request['since'] = ''
        currency = None
        if code is not None:
            currency = self.currency(code)
        response = self.privateGetUsersWalletsHistorySince(self.extend(request, params))
        #
        #     {
        #         "list": [
        #             {
        #                 "currency": "USDC",
        #                 "label": "USDC",
        #                 "userId": "65671262d93d9525ac009e36",
        #                 "balance": 0,
        #                 "disabled": False,
        #                 "balanceHistory": [
        #                     {
        #                         "description": "Deposit - 657973a9b6eadf0f33d70100",
        #                         "JSONdata": {
        #                             "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
        #                     },
        #                     {
        #                         "description": "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342",
        #                         "JSONdata": {
        #                             "price": "2282.00 ETH/USDC",
        #                             "fees": 0,
        #                             "notes": "Order 3a8c5b4d6c"
        #                         },
        #                         "amount": -4.564,
        #                         "timestamp": "2023-12-14T16:16:50.760Z",
        #                         "amountEUR": -4.150043849187587
        #                     },
        #                     ...
        #                 ]
        #             },
        #             {
        #                 "currency": "ETH",
        #                 "label": "ETH",
        #                 "userId": "65671262d93d9525ac009e36",
        #                 "balance": 0,
        #                 "disabled": False,
        #                 "balanceHistory": [
        #                     {
        #                         "description": "Order 65671262d93d9525ac009e36170257061073952c6423a8c5b4d6c SeqNum 10873722342",
        #                         "JSONdata": {
        #                             "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": [
        #                     {
        #                         "description": "Order 65671262d93d9525ac009e361702905785319b5d9016dc20736034d13ca6a - Swap",
        #                         "JSONdata": {
        #                             "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 = self.safe_value(response, 'list', [])
        ledger = []
        for i in range(0, len(ledgerByCurrencies)):
            currencyLedger = ledgerByCurrencies[i]
            currencyId = self.safe_string(currencyLedger, 'currency')
            balanceHistory = self.safe_value(currencyLedger, 'balanceHistory', [])
            for j in range(0, len(balanceHistory)):
                rawLedgerEntry = balanceHistory[j]
                rawLedgerEntry['currencyId'] = currencyId
                ledger.append(rawLedgerEntry)
        return self.parse_ledger(ledger, currency, since, limit)

    def parse_ledger_entry(self, item: dict, currency: Currency = None) -> LedgerEntry:
        datetime = self.safe_string(item, 'timestamp')
        currencyId = self.safe_string(item, 'currencyId')
        item = self.omit(item, 'currencyId')
        currency = self.safe_currency(currencyId, currency)
        description = self.safe_string(item, 'description', '')
        type, referenceId = self.parse_ledger_entry_description(description)
        JSONdata = self.safe_value(item, 'JSONdata', {})
        feeCost = self.safe_string(JSONdata, 'fees')
        fee = {
            'cost': feeCost,
            'currency': None,
        }
        amount = self.safe_string(item, 'amount')
        direction = None
        if amount is not None:
            if Precise.string_lt(amount, '0'):
                direction = 'out'
                amount = Precise.string_abs(amount)
            elif Precise.string_gt(amount, '0'):
                direction = 'in'
        return self.safe_ledger_entry({
            'info': item,
            'id': None,
            'timestamp': self.parse8601(datetime),
            'datetime': datetime,
            'direction': direction,
            'account': None,
            'referenceId': referenceId,
            'referenceAccount': None,
            'type': type,
            'currency': currency,
            'amount': amount,
            'before': None,
            'after': None,
            'status': None,
            'fee': fee,
        }, currency)

    def parse_ledger_entry_description(self, description):
        descriptionArray = []
        if description is not None:
            descriptionArray = description.split(' ')
        type = None
        referenceId = None
        length = len(descriptionArray)
        if length > 1:
            type = self.parse_ledger_entry_type(descriptionArray[0])
            if descriptionArray[1] != '-':
                referenceId = descriptionArray[1]
            else:
                referenceId = self.safe_string(descriptionArray, 2)
        return [type, referenceId]

    def parse_ledger_entry_type(self, type):
        types: dict = {
            'Deposit': 'transaction',
            'Withdraw': 'transaction',
            'Order': 'trade',
        }
        return self.safe_string(types, type, type)

    def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
        """
        create a trade order

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#a4895a1d-3f50-40ae-8231-6962ef06c771

        :param str symbol: unified symbol of the market to create an order in
        :param str type: 'market' or 'limit'
        :param str 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 dict [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 str [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 str [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 str [params.clientOrderId]: client's comment
        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        market = self.market(symbol)
        request: dict = {
        }
        request['orderType'] = type
        formattedAmount = None
        if amount is not None:
            formattedAmount = self.amount_to_precision(symbol, amount)
        cost = self.safe_value(params, 'cost')
        params = self.omit(params, 'cost')
        if type == 'limit':
            if (price is None) and (cost is None):
                raise ArgumentsRequired(self.id + ' createOrder() requires a price or params.cost argument for a ' + type + ' order')
            elif (price is not None) and (amount is not None):
                costString = Precise.string_mul(self.number_to_string(price), self.number_to_string(formattedAmount))
                cost = self.parse_to_numeric(costString)
        precisedCost = None
        if cost is not None:
            precisedCost = self.cost_to_precision(symbol, cost)
        if side == 'sell':
            request = self.handle_create_order_side(market['baseId'], market['quoteId'], formattedAmount, precisedCost, request)
        elif side == 'buy':
            request = self.handle_create_order_side(market['quoteId'], market['baseId'], precisedCost, formattedAmount, request)
        timeInForce = self.safe_value(params, 'timeInForce')
        if timeInForce is not None:
            params = self.omit(params, 'timeInForce')
            request['timeInForce'] = self.encode_order_time_in_force(timeInForce)
        triggerPrice = self.safe_string_2(params, 'triggerPrice', 'stopPrice')
        if triggerPrice is not None:
            params = self.omit(params, ['triggerPrice'])
            request['stopPrice'] = self.price_to_precision(symbol, triggerPrice)
        userData = self.safe_value(params, 'userData', {})
        comment = self.safe_string_2(params, 'clientOrderId', 'comment')
        if comment is not None:
            params = self.omit(params, ['clientOrderId'])
            userData['comment'] = comment
        stopLossPrice = self.safe_string(params, 'stopLossPrice')
        if stopLossPrice is not None:
            params = self.omit(params, 'stopLossPrice')
            userData['stopLoss'] = self.price_to_precision(symbol, stopLossPrice)
        takeProfitPrice = self.safe_string(params, 'takeProfitPrice')
        if takeProfitPrice is not None:
            params = self.omit(params, 'takeProfitPrice')
            userData['takeProfit'] = self.price_to_precision(symbol, takeProfitPrice)
        if not self.is_empty(userData):
            request['userData'] = userData
        response = self.privatePostExchangeOrdersCreate(self.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": [
        #             {
        #                 "seqNumber": 10874285329,
        #                 "timestamp": 1702574484831,
        #                 "qty": 0.002,
        #                 "price": 2293.5,
        #                 "side": "buy"
        #             }
        #         ],
        #         "completionTime": 1702574484831,
        #         "takerQty": 0.002
        #     }
        #
        return self.parse_order(response, market)

    def handle_create_order_side(self, sellingCurrency, buyingCurrency, sellingQty, buyingQty, request={}):
        request['sellingCurrency'] = sellingCurrency
        request['buyingCurrency'] = buyingCurrency
        if sellingQty is not None:
            request['sellingQty'] = sellingQty
        if buyingQty is not None:
            request['buyingQty'] = buyingQty
        return request

    def encode_order_time_in_force(self, timeInForce):
        timeInForceTypes: dict = {
            'GTC': 1,
            'IOC': 2,
            'GTD': 3,
            'FOK': 4,
        }
        return self.safe_value(timeInForceTypes, timeInForce, timeInForce)

    def cancel_order(self, id: str, symbol: Str = None, params={}):
        """
        cancels an open order

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#eaea86da-16ca-4c56-9f00-5b1cb2ad89f8
        https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316

        :param str id: order id
        :param str symbol: not used by coinmetro cancelOrder()
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.margin]: True for cancelling a margin order
        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        request: dict = {
            'orderID': id,
        }
        marginMode = None
        params, params = self.handle_margin_mode_and_params('cancelOrder', params)
        isMargin = self.safe_bool(params, 'margin', False)
        params = self.omit(params, 'margin')
        response = None
        if isMargin or (marginMode is not None):
            response = self.privatePostExchangeOrdersCloseOrderID(self.extend(request, params))
        else:
            response = self.privatePutExchangeOrdersCancelOrderID(self.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 self.parse_order(response)

    def close_position(self, symbol: str, side: OrderSide = None, params={}):
        """
        closes an open position

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#47f913fb-8cab-49f4-bc78-d980e6ced316

        :param str symbol: not used by coinmetro closePosition()
        :param str [side]: not used by coinmetro closePosition()
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.orderID]: order id
        :param number [params.fraction]: fraction of order to close, between 0 and 1(defaults to 1)
        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        orderId = self.safe_string(params, 'orderId')
        if orderId is None:
            raise ArgumentsRequired(self.id + ' closePosition() requires a orderId parameter')
        request: dict = {
            'orderID': orderId,
        }
        response = self.privatePostExchangeOrdersCloseOrderID(self.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": [
        #             {
        #                 "seqNumber": 10925321178,
        #                 "timestamp": 1703015488483,
        #                 "qty": 0.03,
        #                 "price": 1979.1666666666667,
        #                 "side": "buy"
        #             }
        #         ],
        #         "completionTime": 1703015488483,
        #         "takerQty": 0.03
        #     }
        #
        return self.parse_order(response)

    def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        fetch all unfilled currently open orders

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#518afd7a-4338-439c-a651-d4fdaa964138

        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        response = self.privateGetExchangeOrdersActive(params)
        orders = self.parse_orders(response, market, since, limit)
        for i in range(0, len(orders)):
            order = orders[i]
            order['status'] = 'open'
        return orders

    def fetch_canceled_and_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        fetches information on multiple canceled and closed orders made by the user

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#4d48ae69-8ee2-44d1-a268-71f84e557b7b

        :param str 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 dict [params]: extra parameters specific to the exchange API endpoint
        :returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
        request: dict = {}
        if since is not None:
            request['since'] = since
        response = self.privateGetExchangeOrdersHistorySince(self.extend(request, params))
        return self.parse_orders(response, market, since, limit)

    def fetch_order(self, id: str, symbol: Str = None, params={}):
        """
        fetches information on an order made by the user

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#95bbed87-db1c-47a7-a03e-aa247e91d5a6

        :param int|str id: order id
        :param str symbol: not used by coinmetro fetchOrder()
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        request: dict = {
            'orderID': id,
        }
        response = self.privateGetExchangeOrdersStatusOrderID(self.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": [
        #             {
        #                 "_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 self.parse_order(response)

    def parse_order(self, order: dict, market: Market = None) -> Order:
        #
        # 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": [
        #             {
        #                 "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": [],
        #         "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": [
        #             {
        #                 "_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": {
        #             "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": [
        #             {
        #                 "_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": {
        #             "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": [
        #             {
        #                 "_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 = self.safe_integer(order, 'creationTime')
        isCanceled = self.safe_value(order, 'canceled')
        status = None
        if isCanceled is True:
            if timestamp is None:
                timestamp = self.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 = self.safe_string(order, 'status')
            order = self.omit(order, 'status')  # we mark orders from fetchOpenOrders with param 'status': 'open'
        type = self.safe_string(order, 'orderType')
        buyingQty = self.safe_string(order, 'buyingQty')
        sellingQty = self.safe_string(order, 'sellingQty')
        boughtQty = self.safe_string(order, 'boughtQty')
        soldQty = self.safe_string(order, 'soldQty')
        if type == 'market':
            if (buyingQty is None) and (boughtQty is not None) and (boughtQty != '0'):
                buyingQty = boughtQty
            if (sellingQty is None) and (soldQty is not None) and (soldQty != '0'):
                sellingQty = soldQty
        buyingCurrencyId = self.safe_string(order, 'buyingCurrency', '')
        sellingCurrencyId = self.safe_string(order, 'sellingCurrency', '')
        byuingIdPlusSellingId = buyingCurrencyId + sellingCurrencyId
        sellingIdPlusBuyingId = sellingCurrencyId + buyingCurrencyId
        side = None
        marketId = None
        baseAmount = buyingQty
        quoteAmount = buyingQty
        filled = None
        cost = None
        feeInBaseOrQuote = None
        marketsById = self.index_by(self.markets, 'id')
        if self.safe_value(marketsById, byuingIdPlusSellingId) is not None:
            side = 'buy'
            marketId = byuingIdPlusSellingId
            quoteAmount = sellingQty
            filled = boughtQty
            cost = soldQty
            feeInBaseOrQuote = 'base'
        elif self.safe_value(marketsById, sellingIdPlusBuyingId) is not None:
            side = 'sell'
            marketId = sellingIdPlusBuyingId
            baseAmount = sellingQty
            filled = soldQty
            cost = boughtQty
            feeInBaseOrQuote = 'quote'
        price = None
        if (baseAmount is not None) and (quoteAmount is not None):
            price = Precise.string_div(quoteAmount, baseAmount)
        market = self.safe_market(marketId, market)
        fee = None
        feeCost = self.safe_string(order, 'fees')
        if (feeCost is not None) and (feeInBaseOrQuote is not None):
            fee = {
                'currency': market[feeInBaseOrQuote],
                'cost': feeCost,
                'rate': None,
            }
        trades = self.safe_value(order, 'fills', [])
        userData = self.safe_value(order, 'userData', {})
        clientOrderId = self.safe_string(userData, 'comment')
        takeProfitPrice = self.safe_string(userData, 'takeProfit')
        stopLossPrice = self.safe_string(userData, 'stopLoss')
        return self.safe_order({
            'id': self.safe_string(order, 'orderID'),
            'clientOrderId': clientOrderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': self.safe_integer(order, 'lastFillTime'),
            'status': status,
            'symbol': market['symbol'],
            'type': type,
            'timeInForce': self.parse_order_time_in_force(self.safe_integer(order, 'timeInForce')),
            'side': side,
            'price': price,
            'triggerPrice': self.safe_string(order, 'stopPrice'),
            'takeProfitPrice': takeProfitPrice,
            'stopLossPrice': stopLossPrice,
            'average': None,
            'amount': baseAmount,
            'cost': cost,
            'filled': filled,
            'remaining': None,
            'fee': fee,
            'fees': None,
            'trades': trades,
            'info': order,
        }, market)

    def parse_order_time_in_force(self, timeInForce):
        timeInForceTypes = [
            None,
            'GTC',
            'IOC',
            'GTD',
            'FOK',
        ]
        return self.safe_value(timeInForceTypes, timeInForce, timeInForce)

    def borrow_cross_margin(self, code: str, amount: float, params={}):
        """
        create a loan to borrow margin

        https://documenter.getpostman.com/view/3653795/SVfWN6KS#5b90b3b9-e5db-4d07-ac9d-d680a06fd110

        :param str code: unified currency code of the currency to borrow
        :param float amount: the amount to borrow
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `margin loan structure <https://docs.ccxt.com/#/?id=margin-loan-structure>`
        """
        self.load_markets()
        currency = self.currency(code)
        currencyId = currency['id']
        request: dict = {}
        request[currencyId] = self.currency_to_precision(code, amount)
        response = self.privatePutUsersMarginCollateral(self.extend(request, params))
        #
        #     {"message": "OK"}
        #
        result = self.safe_value(response, 'result', {})
        transaction = self.parse_margin_loan(result, currency)
        return self.extend(transaction, {
            'amount': amount,
        })

    def parse_margin_loan(self, info, currency: Currency = None):
        currencyId = self.safe_string(info, 'coin')
        return {
            'id': None,
            'currency': self.safe_currency_code(currencyId, currency),
            'amount': None,
            'symbol': None,
            'timestamp': None,
            'datetime': None,
            'info': info,
        }

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        request = self.omit(params, self.extract_params(path))
        endpoint = '/' + self.implode_params(path, params)
        url = self.urls['api'][api] + endpoint
        query = self.urlencode(request)
        if headers is None:
            headers = {}
        headers['CCXT'] = 'true'
        if api == 'private':
            if (self.uid is None) and (self.apiKey is not None):
                self.uid = self.apiKey
            if (self.token is None) and (self.secret is not None):
                self.token = self.secret
            if url == 'https://api.coinmetro.com/jwt':  # handle with headers for login endpoint
                headers['X-Device-Id'] = 'bypass'
                if self.twofa is not None:
                    headers['X-OTP'] = self.twofa
            elif url == 'https://api.coinmetro.com/jwtDevice':  # handle with headers for long lived token login endpoint
                headers['X-Device-Id'] = self.uid
                if self.twofa is not None:
                    headers['X-OTP'] = self.twofa
            else:
                headers['Authorization'] = 'Bearer ' + self.token
                if not url.startswith('https://api.coinmetro.com/open'):  # if not sandbox endpoint
                    self.check_required_credentials()
                    headers['X-Device-Id'] = self.uid
            if (method == 'POST') or (method == 'PUT'):
                headers['Content-Type'] = 'application/x-www-form-urlencoded'
                body = self.urlencode(request)
        elif len(query) != 0:
            url += '?' + query
        while(url.endswith('/')):
            url = url[0:len(url) - 1]
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
        if response is None:
            return None
        if (code != 200) and (code != 201) and (code != 202):
            feedback = self.id + ' ' + body
            message = self.safe_string(response, 'message')
            self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
            self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
            raise ExchangeError(feedback)
        return None
