# -*- 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.async_support.base.exchange import Exchange
from ccxt.abstract.exmo import ImplicitAPI
import hashlib
from ccxt.base.types import Balances, Currencies, Currency, Int, MarginModification, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction
from typing import List
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import PermissionDenied
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import BadRequest
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.errors import OnMaintenance
from ccxt.base.errors import InvalidNonce
from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise


class exmo(Exchange, ImplicitAPI):

    def describe(self):
        return self.deep_extend(super(exmo, self).describe(), {
            'id': 'exmo',
            'name': 'EXMO',
            'countries': ['LT'],  # Lithuania
            'rateLimit': 350,  # once every 350 ms ≈ 180 requests per minute ≈ 3 requests per second
            'version': 'v1.1',
            'has': {
                'CORS': None,
                'spot': True,
                'margin': True,
                'swap': False,
                'future': False,
                'option': False,
                'addMargin': True,
                'cancelOrder': True,
                'cancelOrders': False,
                'createDepositAddress': False,
                'createOrder': True,
                'createStopLimitOrder': True,
                'createStopMarketOrder': True,
                'createStopOrder': True,
                'editOrder': True,  # margin only
                'fetchAccounts': False,
                'fetchBalance': True,
                'fetchCanceledOrders': True,
                'fetchCurrencies': True,
                'fetchDeposit': True,
                'fetchDepositAddress': True,
                'fetchDeposits': True,
                'fetchDepositsWithdrawals': True,
                'fetchDepositWithdrawFee': 'emulated',
                'fetchDepositWithdrawFees': True,
                'fetchFundingHistory': False,
                'fetchFundingRate': False,
                'fetchFundingRateHistory': False,
                'fetchFundingRates': False,
                'fetchIndexOHLCV': False,
                'fetchMarginMode': False,
                'fetchMarkets': True,
                'fetchMarkOHLCV': False,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenInterestHistory': False,
                'fetchOpenOrders': True,
                'fetchOrder': 'emulated',
                'fetchOrderBook': True,
                'fetchOrderBooks': True,
                'fetchOrderTrades': True,
                'fetchPosition': False,
                'fetchPositionHistory': False,
                'fetchPositionMode': False,
                'fetchPositions': False,
                'fetchPositionsHistory': False,
                'fetchPositionsRisk': False,
                'fetchPremiumIndexOHLCV': False,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTrades': True,
                'fetchTradingFee': False,
                'fetchTradingFees': True,
                'fetchTransactionFees': True,
                'fetchTransactions': 'emulated',
                'fetchTransfer': False,
                'fetchTransfers': False,
                'fetchWithdrawal': True,
                'fetchWithdrawals': True,
                'reduceMargin': True,
                'setMargin': False,
                'transfer': False,
                'withdraw': True,
            },
            'timeframes': {
                '1m': '1',
                '5m': '5',
                '15m': '15',
                '30m': '30',
                '45m': '45',
                '1h': '60',
                '2h': '120',
                '3h': '180',
                '4h': '240',
                '1d': 'D',
                '1w': 'W',
                '1M': 'M',
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/27766491-1b0ea956-5eda-11e7-9225-40d67b481b8d.jpg',
                'api': {
                    'public': 'https://api.exmo.com',
                    'private': 'https://api.exmo.com',
                    'web': 'https://exmo.me',
                },
                'www': 'https://exmo.me',
                'referral': 'https://exmo.me/?ref=131685',
                'doc': [
                    'https://exmo.me/en/api_doc?ref=131685',
                ],
                'fees': 'https://exmo.com/en/docs/fees',
            },
            'api': {
                'web': {
                    'get': [
                        'ctrl/feesAndLimits',
                        'en/docs/fees',
                    ],
                },
                'public': {
                    'get': [
                        'currency',
                        'currency/list/extended',
                        'order_book',
                        'pair_settings',
                        'ticker',
                        'trades',
                        'candles_history',
                        'required_amount',
                        'payments/providers/crypto/list',
                    ],
                },
                'private': {
                    'post': [
                        'user_info',
                        'order_create',
                        'order_cancel',
                        'stop_market_order_create',
                        'stop_market_order_cancel',
                        'user_open_orders',
                        'user_trades',
                        'user_cancelled_orders',
                        'order_trades',
                        'deposit_address',
                        'withdraw_crypt',
                        'withdraw_get_txid',
                        'excode_create',
                        'excode_load',
                        'code_check',
                        'wallet_history',
                        'wallet_operations',
                        'margin/user/order/create',
                        'margin/user/order/update',
                        'margin/user/order/cancel',
                        'margin/user/position/close',
                        'margin/user/position/margin_add',
                        'margin/user/position/margin_remove',
                        'margin/currency/list',
                        'margin/pair/list',
                        'margin/settings',
                        'margin/funding/list',
                        'margin/user/info',
                        'margin/user/order/list',
                        'margin/user/order/history',
                        'margin/user/order/trades',
                        'margin/user/order/max_quantity',
                        'margin/user/position/list',
                        'margin/user/position/margin_remove_info',
                        'margin/user/position/margin_add_info',
                        'margin/user/wallet/list',
                        'margin/user/wallet/history',
                        'margin/user/trade/list',
                        'margin/trades',
                        'margin/liquidation/feed',
                    ],
                },
            },
            'fees': {
                'trading': {
                    'feeSide': 'get',
                    'tierBased': True,
                    'percentage': True,
                    'maker': self.parse_number('0.004'),
                    'taker': self.parse_number('0.004'),
                },
                'transaction': {
                    'tierBased': False,
                    'percentage': False,  # fixed transaction fees for crypto, see fetchDepositWithdrawFees below
                },
            },
            'options': {
                'networks': {
                    'ETH': 'ERC20',
                    'TRX': 'TRC20',
                },
                'fetchTradingFees': {
                    'method': 'fetchPrivateTradingFees',  # or 'fetchPublicTradingFees'
                },
                'margin': {
                    'fillResponseFromRequest': True,
                },
            },
            'commonCurrencies': {
                'GMT': 'GMT Token',
            },
            'precisionMode': TICK_SIZE,
            'exceptions': {
                'exact': {
                    '140434': BadRequest,
                    '40005': AuthenticationError,  # Authorization error, incorrect signature
                    '40009': InvalidNonce,  #
                    '40015': ExchangeError,  # API function do not exist
                    '40016': OnMaintenance,  # {"result":false,"error":"Error 40016: Maintenance work in progress"}
                    '40017': AuthenticationError,  # Wrong API Key
                    '40032': PermissionDenied,  # {"result":false,"error":"Error 40032: Access is denied for self API key"}
                    '40033': PermissionDenied,  # {"result":false,"error":"Error 40033: Access is denied, self resources are temporarily blocked to user"}
                    '40034': RateLimitExceeded,  # {"result":false,"error":"Error 40034: Access is denied, rate limit is exceeded"}
                    '50052': InsufficientFunds,
                    '50054': InsufficientFunds,
                    '50304': OrderNotFound,  # "Order was not found '123456789'"(fetching order trades for an order that does not have trades yet)
                    '50173': OrderNotFound,  # "Order with id X was not found."(cancelling non-existent, closed and cancelled order)
                    '50277': InvalidOrder,
                    '50319': InvalidOrder,  # Price by order is less than permissible minimum for self pair
                    '50321': InvalidOrder,  # Price by order is more than permissible maximum for self pair
                    '50381': InvalidOrder,  # {"result":false,"error":"Error 50381: More than 2 decimal places are not permitted for pair BTC_USD"}
                },
                'broad': {
                    'range period is too long': BadRequest,
                    'invalid syntax': BadRequest,
                    'API rate limit exceeded': RateLimitExceeded,  # {"result":false,"error":"API rate limit exceeded for x.x.x.x. Retry after 60 sec.","history":[],"begin":1579392000,"end":1579478400}
                },
            },
        })

    async def modify_margin_helper(self, symbol: str, amount, type, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'position_id': market['id'],
            'quantity': amount,
        }
        response = None
        if type == 'add':
            response = await self.privatePostMarginUserPositionMarginAdd(self.extend(request, params))
        elif type == 'reduce':
            response = await self.privatePostMarginUserPositionMarginRemove(self.extend(request, params))
        #
        #      {}
        #
        margin = self.parse_margin_modification(response, market)
        options = self.safe_value(self.options, 'margin', {})
        fillResponseFromRequest = self.safe_bool(options, 'fillResponseFromRequest', True)
        if fillResponseFromRequest:
            margin['type'] = type
            margin['amount'] = amount
        return margin

    def parse_margin_modification(self, data: dict, market: Market = None) -> MarginModification:
        #
        #      {}
        #
        return {
            'info': data,
            'symbol': self.safe_symbol(None, market),
            'type': None,
            'marginMode': 'isolated',
            'amount': None,
            'total': None,
            'code': self.safe_value(market, 'quote'),
            'status': 'ok',
            'timestamp': None,
            'datetime': None,
        }

    async def reduce_margin(self, symbol: str, amount: float, params={}) -> MarginModification:
        """
        remove margin from a position
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#eebf9f25-0289-4946-9482-89872c738449
        :param str symbol: unified market symbol
        :param float amount: the amount of margin to remove
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `margin structure <https://docs.ccxt.com/#/?id=reduce-margin-structure>`
        """
        return await self.modify_margin_helper(symbol, amount, 'reduce', params)

    async def add_margin(self, symbol: str, amount: float, params={}) -> MarginModification:
        """
        add margin
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#143ef808-79ca-4e49-9e79-a60ea4d8c0e3
        :param str symbol: unified market symbol
        :param float amount: amount of margin to add
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `margin structure <https://docs.ccxt.com/#/?id=add-margin-structure>`
        """
        return await self.modify_margin_helper(symbol, amount, 'add', params)

    async def fetch_trading_fees(self, params={}) -> TradingFees:
        """
        fetch the trading fees for multiple markets
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#90927062-256c-4b03-900f-2b99131f9a54
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#7de7e75c-5833-45a8-b937-c2276d235aaa
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a dictionary of `fee structures <https://docs.ccxt.com/#/?id=fee-structure>` indexed by market symbols
        """
        options = self.safe_value(self.options, 'fetchTradingFees', {})
        defaultMethod = self.safe_string(options, 'method', 'fetchPrivateTradingFees')
        method = self.safe_string(params, 'method', defaultMethod)
        params = self.omit(params, 'method')
        if method == 'fetchPrivateTradingFees':
            return await self.fetch_private_trading_fees(params)
        else:
            return await self.fetch_public_trading_fees(params)

    async def fetch_private_trading_fees(self, params={}):
        await self.load_markets()
        response = await self.privatePostMarginPairList(params)
        #
        #     {
        #         "pairs": [{
        #             "name": "EXM_USD",
        #             "buy_price": "0.02728391",
        #             "sell_price": "0.0276",
        #             "last_trade_price": "0.0276",
        #             "ticker_updated": "1646956050056696046",
        #             "is_fair_price": True,
        #             "max_price_precision": "8",
        #             "min_order_quantity": "1",
        #             "max_order_quantity": "50000",
        #             "min_order_price": "0.00000001",
        #             "max_order_price": "1000",
        #             "max_position_quantity": "50000",
        #             "trade_taker_fee": "0.05",
        #             "trade_maker_fee": "0",
        #             "liquidation_fee": "0.5",
        #             "max_leverage": "3",
        #             "default_leverage": "3",
        #             "liquidation_level": "5",
        #             "margin_call_level": "7.5",
        #             "position": "1",
        #             "updated": "1638976144797807397"
        #         }
        #         ...
        #         ]
        #     }
        #
        pairs = self.safe_value(response, 'pairs', [])
        result = {}
        for i in range(0, len(pairs)):
            pair = pairs[i]
            marketId = self.safe_string(pair, 'name')
            symbol = self.safe_symbol(marketId, None, '_')
            makerString = self.safe_string(pair, 'trade_maker_fee')
            takerString = self.safe_string(pair, 'trade_taker_fee')
            maker = self.parse_number(Precise.string_div(makerString, '100'))
            taker = self.parse_number(Precise.string_div(takerString, '100'))
            result[symbol] = {
                'info': pair,
                'symbol': symbol,
                'maker': maker,
                'taker': taker,
                'percentage': True,
                'tierBased': True,
            }
        return result

    async def fetch_public_trading_fees(self, params={}):
        await self.load_markets()
        response = await self.publicGetPairSettings(params)
        #
        #     {
        #         "BTC_USD": {
        #             "min_quantity": "0.00002",
        #             "max_quantity": "1000",
        #             "min_price": "1",
        #             "max_price": "150000",
        #             "max_amount": "500000",
        #             "min_amount": "1",
        #             "price_precision": "2",
        #             "commission_taker_percent": "0.3",
        #             "commission_maker_percent": "0.3"
        #         },
        #     }
        #
        result = {}
        for i in range(0, len(self.symbols)):
            symbol = self.symbols[i]
            market = self.market(symbol)
            fee = self.safe_value(response, market['id'], {})
            makerString = self.safe_string(fee, 'commission_maker_percent')
            takerString = self.safe_string(fee, 'commission_taker_percent')
            maker = self.parse_number(Precise.string_div(makerString, '100'))
            taker = self.parse_number(Precise.string_div(takerString, '100'))
            result[symbol] = {
                'info': fee,
                'symbol': symbol,
                'maker': maker,
                'taker': taker,
                'percentage': True,
                'tierBased': True,
            }
        return result

    def parse_fixed_float_value(self, input):
        if (input is None) or (input == '-'):
            return None
        if input == '':
            return 0
        isPercentage = (input.find('%') >= 0)
        parts = input.split(' ')
        value = parts[0].replace('%', '')
        result = float(value)
        if (result > 0) and isPercentage:
            raise ExchangeError(self.id + ' parseFixedFloatValue() detected an unsupported non-zero percentage-based fee ' + input)
        return result

    async def fetch_transaction_fees(self, codes: List[str] = None, params={}):
        """
         * @deprecated
        please use fetchDepositWithdrawFees instead
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2
        :param str[]|None codes: list of unified currency codes
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a list of `transaction fees structures <https://docs.ccxt.com/#/?id=fees-structure>`
        """
        await self.load_markets()
        cryptoList = await self.publicGetPaymentsProvidersCryptoList(params)
        #
        #     {
        #         "BTC":[
        #             {"type":"deposit", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1},
        #             {"type":"withdraw", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"350", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.0005 BTC", "currency_confirmations":6}
        #         ],
        #         "ETH":[
        #             {"type":"withdraw", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"500", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.004 ETH", "currency_confirmations":4},
        #             {"type":"deposit", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.01 ETH. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}
        #         ],
        #         "USDT":[
        #             {"type":"deposit", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":false,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2},
        #             {"type":"withdraw", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":false,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"5 USDT", "currency_confirmations":6},
        #             {"type":"deposit", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2},
        #             {
        #                 "type":"withdraw",
        #                 "name":"USDT(ERC20)",
        #                 "currency_name":"USDT",
        #                 "min":"55",
        #                 "max":"200000",
        #                 "enabled":true,
        #                 "comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Recommendation: Due to the high load of ERC20 network, using TRC20 address for withdrawal is recommended.",
        #                 "commission_desc":"10 USDT",
        #                 "currency_confirmations":6
        #             },
        #             {"type":"deposit", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":true,"comment":"Minimum deposit amount is 10 USDT. Only TRON main network supported", "commission_desc":"0%", "currency_confirmations":2},
        #             {"type":"withdraw", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"150000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Only TRON main network supported.", "commission_desc":"1 USDT", "currency_confirmations":6}
        #         ],
        #         "XLM":[
        #             {"type":"deposit", "name":"XLM", "currency_name":"XLM", "min":"1", "max":"1000000", "enabled":true,"comment":"Attention! A deposit without memo(invoice) will not be credited. Minimum deposit amount is 1 XLM. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1},
        #             {"type":"withdraw", "name":"XLM", "currency_name":"XLM", "min":"21", "max":"1000000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales.", "commission_desc":"0.01 XLM", "currency_confirmations":1}
        #         ],
        #     }
        #
        result = {}
        cryptoListKeys = list(cryptoList.keys())
        for i in range(0, len(cryptoListKeys)):
            code = cryptoListKeys[i]
            if codes is not None and not self.in_array(code, codes):
                continue
            result[code] = {
                'deposit': None,
                'withdraw': None,
            }
            currency = self.currency(code)
            currencyId = self.safe_string(currency, 'id')
            providers = self.safe_value(cryptoList, currencyId, [])
            for j in range(0, len(providers)):
                provider = providers[j]
                typeInner = self.safe_string(provider, 'type')
                commissionDesc = self.safe_string(provider, 'commission_desc')
                fee = self.parse_fixed_float_value(commissionDesc)
                result[code][typeInner] = fee
            result[code]['info'] = providers
        # cache them for later use
        self.options['transactionFees'] = result
        return result

    async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}):
        """
        fetch deposit and withdraw fees
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2
        :param str[]|None codes: list of unified currency codes
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a list of `transaction fees structures <https://docs.ccxt.com/#/?id=fees-structure>`
        """
        await self.load_markets()
        response = await self.publicGetPaymentsProvidersCryptoList(params)
        #
        #    {
        #        "USDT": [
        #            {
        #                "type": "deposit",  # or "withdraw"
        #                "name": "USDT(ERC20)",
        #                "currency_name": "USDT",
        #                "min": "10",
        #                "max": "0",
        #                "enabled": True,
        #                "comment": "Minimum deposit amount is 10 USDT",
        #                "commission_desc": "0%",
        #                "currency_confirmations": 2
        #            },
        #            ...
        #        ],
        #        ...
        #    }
        #
        result = self.parse_deposit_withdraw_fees(response, codes)
        # cache them for later use
        self.options['transactionFees'] = result
        return result

    def parse_deposit_withdraw_fee(self, fee, currency: Currency = None):
        #
        #    [
        #        {
        #            "type": "deposit",  # or "withdraw"
        #            "name": "BTC",
        #            "currency_name": "BTC",
        #            "min": "0.001",
        #            "max": "0",
        #            "enabled": True,
        #            "comment": "Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds",
        #            "commission_desc": "0%",
        #            "currency_confirmations": 1
        #        },
        #        ...
        #    ]
        #
        result = self.deposit_withdraw_fee(fee)
        for i in range(0, len(fee)):
            provider = fee[i]
            type = self.safe_string(provider, 'type')
            networkId = self.safe_string(provider, 'name')
            networkCode = self.network_id_to_code(networkId, self.safe_string(currency, 'code'))
            commissionDesc = self.safe_string(provider, 'commission_desc')
            splitCommissionDesc = []
            percentage = None
            if commissionDesc is not None:
                splitCommissionDesc = commissionDesc.split('%')
                splitCommissionDescLength = len(splitCommissionDesc)
                percentage = splitCommissionDescLength >= 2
            network = self.safe_value(result['networks'], networkCode)
            if network is None:
                result['networks'][networkCode] = {
                    'withdraw': {
                        'fee': None,
                        'percentage': None,
                    },
                    'deposit': {
                        'fee': None,
                        'percentage': None,
                    },
                }
            result['networks'][networkCode][type] = {
                'fee': self.parse_fixed_float_value(self.safe_string(splitCommissionDesc, 0)),
                'percentage': percentage,
            }
        return self.assign_default_deposit_withdraw_fees(result)

    async def fetch_currencies(self, params={}) -> Currencies:
        """
        fetches all available currencies on an exchange
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#7cdf0ca8-9ff6-4cf3-aa33-bcec83155c49
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#4190035d-24b1-453d-833b-37e0a52f88e2
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: an associative dictionary of currencies
        """
        #
        currencyList = await self.publicGetCurrencyListExtended(params)
        #
        #     [
        #         {"name":"VLX","description":"Velas"},
        #         {"name":"RUB","description":"Russian Ruble"},
        #         {"name":"BTC","description":"Bitcoin"},
        #         {"name":"USD","description":"US Dollar"}
        #     ]
        #
        cryptoList = await self.publicGetPaymentsProvidersCryptoList(params)
        #
        #     {
        #         "BTC":[
        #             {"type":"deposit", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.001 BTC. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1},
        #             {"type":"withdraw", "name":"BTC", "currency_name":"BTC", "min":"0.001", "max":"350", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.0005 BTC", "currency_confirmations":6}
        #         ],
        #         "ETH":[
        #             {"type":"withdraw", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"500", "enabled":true,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"0.004 ETH", "currency_confirmations":4},
        #             {"type":"deposit", "name":"ETH", "currency_name":"ETH", "min":"0.01", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 0.01 ETH. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1}
        #         ],
        #         "USDT":[
        #             {"type":"deposit", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":false,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2},
        #             {"type":"withdraw", "name":"USDT(OMNI)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":false,"comment":"Do not withdraw directly to the Crowdfunding or ICO address account will not be credited with tokens from such sales.", "commission_desc":"5 USDT", "currency_confirmations":6},
        #             {"type":"deposit", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"10", "max":"0", "enabled":true,"comment":"Minimum deposit amount is 10 USDT", "commission_desc":"0%", "currency_confirmations":2},
        #             {"type":"withdraw", "name":"USDT(ERC20)", "currency_name":"USDT", "min":"55", "max":"200000", "enabled":true, "comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Recommendation: Due to the high load of ERC20 network, using TRC20 address for withdrawal is recommended.",  "commission_desc":"10 USDT", "currency_confirmations":6},
        #             {"type":"deposit", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"100000", "enabled":true,"comment":"Minimum deposit amount is 10 USDT. Only TRON main network supported", "commission_desc":"0%", "currency_confirmations":2},
        #             {"type":"withdraw", "name":"USDT(TRC20)", "currency_name":"USDT", "min":"10", "max":"150000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales. Only TRON main network supported.", "commission_desc":"1 USDT", "currency_confirmations":6}
        #         ],
        #         "XLM":[
        #             {"type":"deposit", "name":"XLM", "currency_name":"XLM", "min":"1", "max":"1000000", "enabled":true,"comment":"Attention! A deposit without memo(invoice) will not be credited. Minimum deposit amount is 1 XLM. We do not support BSC and BEP20 network, please consider self when sending funds", "commission_desc":"0%", "currency_confirmations":1},
        #             {"type":"withdraw", "name":"XLM", "currency_name":"XLM", "min":"21", "max":"1000000", "enabled":true,"comment":"Caution! Do not withdraw directly to a crowdfund or ICO address, account will not be credited with tokens from such sales.", "commission_desc":"0.01 XLM", "currency_confirmations":1}
        #         ],
        #     }
        #
        result = {}
        for i in range(0, len(currencyList)):
            currency = currencyList[i]
            currencyId = self.safe_string(currency, 'name')
            name = self.safe_string(currency, 'description')
            providers = self.safe_value(cryptoList, currencyId)
            active = False
            type = 'crypto'
            limits = {
                'deposit': {
                    'min': None,
                    'max': None,
                },
                'withdraw': {
                    'min': None,
                    'max': None,
                },
            }
            fee = None
            depositEnabled = None
            withdrawEnabled = None
            if providers is None:
                active = True
                type = 'fiat'
            else:
                for j in range(0, len(providers)):
                    provider = providers[j]
                    typeInner = self.safe_string(provider, 'type')
                    minValue = self.safe_string(provider, 'min')
                    maxValue = self.safe_string(provider, 'max')
                    if Precise.string_eq(maxValue, '0.0'):
                        maxValue = None
                    activeProvider = self.safe_value(provider, 'enabled')
                    if typeInner == 'deposit':
                        if activeProvider and not depositEnabled:
                            depositEnabled = True
                        elif not activeProvider:
                            depositEnabled = False
                    elif typeInner == 'withdraw':
                        if activeProvider and not withdrawEnabled:
                            withdrawEnabled = True
                        elif not activeProvider:
                            withdrawEnabled = False
                    if activeProvider:
                        active = True
                        limitMin = self.number_to_string(limits[typeInner]['min'])
                        if (limits[typeInner]['min'] is None) or (Precise.string_lt(minValue, limitMin)):
                            limits[typeInner]['min'] = minValue
                            limits[typeInner]['max'] = maxValue
                            if typeInner == 'withdraw':
                                commissionDesc = self.safe_string(provider, 'commission_desc')
                                fee = self.parse_fixed_float_value(commissionDesc)
            code = self.safe_currency_code(currencyId)
            result[code] = {
                'id': currencyId,
                'code': code,
                'name': name,
                'type': type,
                'active': active,
                'deposit': depositEnabled,
                'withdraw': withdrawEnabled,
                'fee': fee,
                'precision': self.parse_number('1e-8'),
                'limits': limits,
                'info': providers,
                'networks': {},
            }
        return result

    async def fetch_markets(self, params={}) -> List[Market]:
        """
        retrieves data on all markets for exmo
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#7de7e75c-5833-45a8-b937-c2276d235aaa
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: an array of objects representing market data
        """
        response = await self.publicGetPairSettings(params)
        #
        #     {
        #         "BTC_USD":{
        #             "min_quantity":"0.0001",
        #             "max_quantity":"1000",
        #             "min_price":"1",
        #             "max_price":"30000",
        #             "max_amount":"500000",
        #             "min_amount":"1",
        #             "price_precision":8,
        #             "commission_taker_percent":"0.4",
        #             "commission_maker_percent":"0.4"
        #         },
        #     }
        #
        marginPairsDict = {}
        if self.check_required_credentials(False):
            marginPairs = await self.privatePostMarginPairList(params)
            #
            #    {
            #        "pairs": [
            #            {
            #                "buy_price": "55978.85",
            #                "default_leverage": "3",
            #                "is_fair_price": True,
            #                "last_trade_price": "55999.23",
            #                "liquidation_fee": "2",
            #                "liquidation_level": "10",
            #                "margin_call_level": "15",
            #                "max_leverage": "3",
            #                "max_order_price": "150000",
            #                "max_order_quantity": "1",
            #                "max_position_quantity": "1",
            #                "max_price_precision": 2,
            #                "min_order_price": "1",
            #                "min_order_quantity": "0.00002",
            #                "name": "BTC_USD",
            #                "position": 1,
            #                "sell_price": "55985.51",
            #                "ticker_updated": "1619019818936107989",
            #                "trade_maker_fee": "0",
            #                "trade_taker_fee": "0.05",
            #                "updated": "1619008608955599013"
            #            }
            #        ]
            #    }
            #
            pairs = self.safe_value(marginPairs, 'pairs')
            marginPairsDict = self.index_by(pairs, 'name')
        keys = list(response.keys())
        result = []
        for i in range(0, len(keys)):
            id = keys[i]
            market = response[id]
            marginMarket = self.safe_value(marginPairsDict, id)
            symbol = id.replace('_', '/')
            baseId, quoteId = symbol.split('/')
            base = self.safe_currency_code(baseId)
            quote = self.safe_currency_code(quoteId)
            takerString = self.safe_string(market, 'commission_taker_percent')
            makerString = self.safe_string(market, 'commission_maker_percent')
            maxQuantity = self.safe_string(market, 'max_quantity')
            marginMaxQuantity = self.safe_string(marginMarket, 'max_order_quantity')
            result.append({
                'id': id,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'settle': None,
                'baseId': baseId,
                'quoteId': quoteId,
                'settleId': None,
                'type': 'spot',
                'spot': True,
                'margin': marginMarket is not None,
                'swap': False,
                'future': False,
                'option': False,
                'active': None,
                'contract': False,
                'linear': None,
                'inverse': None,
                'taker': self.parse_number(Precise.string_div(takerString, '100')),
                'maker': self.parse_number(Precise.string_div(makerString, '100')),
                'contractSize': None,
                'expiry': None,
                'expiryDatetime': None,
                'strike': None,
                'optionType': None,
                'precision': {
                    'amount': self.parse_number('1e-8'),
                    'price': self.parse_number(self.parse_precision(self.safe_string(market, 'price_precision'))),
                },
                'limits': {
                    'leverage': {
                        'min': None,
                        'max': self.safe_number(market, 'leverage'),
                    },
                    'amount': {
                        'min': self.safe_number(market, 'min_quantity'),
                        'max': self.parse_number(Precise.string_max(maxQuantity, marginMaxQuantity)),
                    },
                    'price': {
                        'min': self.safe_number(market, 'min_price'),
                        'max': self.safe_number(market, 'max_price'),
                    },
                    'cost': {
                        'min': self.safe_number(market, 'min_amount'),
                        'max': self.safe_number(market, 'max_amount'),
                    },
                },
                'created': None,
                'info': market,
            })
        return result

    async 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
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#65eeb949-74e5-4631-9184-c38387fe53e8
        :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
        :returns int[][]: A list of candles ordered, open, high, low, close, volume
        """
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'symbol': market['id'],
            'resolution': self.safe_string(self.timeframes, timeframe, timeframe),
        }
        maxLimit = 3000
        duration = self.parse_timeframe(timeframe)
        now = self.milliseconds()
        if since is None:
            if limit is None:
                limit = 1000  # cap default at generous amount
            else:
                limit = min(limit, maxLimit)
            request['from'] = self.parse_to_int(now / 1000) - limit * duration - 1
            request['to'] = self.parse_to_int(now / 1000)
        else:
            request['from'] = self.parse_to_int(since / 1000) - 1
            if limit is None:
                limit = maxLimit
            else:
                limit = min(limit, maxLimit)
            to = self.sum(since, limit * duration * 1000)
            request['to'] = self.parse_to_int(to / 1000)
        response = await self.publicGetCandlesHistory(self.extend(request, params))
        #
        #     {
        #         "candles":[
        #             {"t":1584057600000,"o":0.02235144,"c":0.02400233,"h":0.025171,"l":0.02221,"v":5988.34031761},
        #             {"t":1584144000000,"o":0.0240373,"c":0.02367413,"h":0.024399,"l":0.0235,"v":2027.82522329},
        #             {"t":1584230400000,"o":0.02363458,"c":0.02319242,"h":0.0237948,"l":0.02223196,"v":1707.96944997},
        #         ]
        #     }
        #
        candles = self.safe_list(response, 'candles', [])
        return self.parse_ohlcvs(candles, market, timeframe, since, limit)

    def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
        #
        #     {
        #         "t":1584057600000,
        #         "o":0.02235144,
        #         "c":0.02400233,
        #         "h":0.025171,
        #         "l":0.02221,
        #         "v":5988.34031761
        #     }
        #
        return [
            self.safe_integer(ohlcv, 't'),
            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 parse_balance(self, response) -> Balances:
        result = {'info': response}
        wallets = self.safe_value(response, 'wallets')
        if wallets is not None:
            currencyIds = list(wallets.keys())
            for i in range(0, len(currencyIds)):
                currencyId = currencyIds[i]
                item = wallets[currencyId]
                currency = self.safe_currency_code(currencyId)
                account = self.account()
                account['used'] = self.safe_string(item, 'used')
                account['free'] = self.safe_string(item, 'free')
                account['total'] = self.safe_string(item, 'balance')
                result[currency] = account
        else:
            free = self.safe_value(response, 'balances', {})
            used = self.safe_value(response, 'reserved', {})
            currencyIds = list(free.keys())
            for i in range(0, len(currencyIds)):
                currencyId = currencyIds[i]
                code = self.safe_currency_code(currencyId)
                account = self.account()
                if currencyId in free:
                    account['free'] = self.safe_string(free, currencyId)
                if currencyId in used:
                    account['used'] = self.safe_string(used, currencyId)
                result[code] = account
        return self.safe_balance(result)

    async def fetch_balance(self, params={}) -> Balances:
        """
        query for balance and get the amount of funds available for trading or funds locked in orders
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#59c5160f-27a1-4d9a-8cfb-7979c7ffaac6
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#c8388df7-1f9f-4d41-81c4-5a387d171dc6
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.marginMode]: *isolated* fetches the isolated margin balance
        :returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
        """
        await self.load_markets()
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('fetchBalance', params)
        if marginMode == 'cross':
            raise BadRequest(self.id + ' does not support cross margin')
        response = None
        if marginMode == 'isolated':
            response = await self.privatePostMarginUserWalletList(params)
            #
            #    {
            #        "wallets": {
            #            "USD": {
            #                "balance": "1000",
            #                "free": "600",
            #                "used": "400"
            #            }
            #        }
            #    }
            #
        else:
            response = await self.privatePostUserInfo(params)
            #
            #     {
            #         "uid":131685,
            #         "server_date":1628999600,
            #         "balances":{
            #             "EXM":"0",
            #             "USD":"0",
            #             "EUR":"0",
            #             "GBP":"0",
            #         },
            #     }
            #
        return self.parse_balance(response)

    async 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
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#c60c51a8-e683-4f45-a000-820723d37871
        :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
        :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
        """
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'pair': market['id'],
        }
        if limit is not None:
            request['limit'] = limit
        response = await self.publicGetOrderBook(self.extend(request, params))
        result = self.safe_dict(response, market['id'])
        return self.parse_order_book(result, market['symbol'], None, 'bid', 'ask')

    async def fetch_order_books(self, symbols: Strings = None, limit: Int = None, params={}):
        """
        fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data for multiple markets
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#c60c51a8-e683-4f45-a000-820723d37871
        :param str[]|None symbols: list of unified market symbols, all symbols fetched if None, default is None
        :param int [limit]: max number of entries per orderbook to return, default is None
        :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 symbol
        """
        await self.load_markets()
        ids = None
        if symbols is None:
            ids = ','.join(self.ids)
            # max URL length is 2083 symbols, including http schema, hostname, tld, etc...
            if len(ids) > 2048:
                numIds = len(self.ids)
                raise ExchangeError(self.id + ' fetchOrderBooks() has ' + str(numIds) + ' symbols exceeding max URL length, you are required to specify a list of symbols in the first argument to fetchOrderBooks')
        else:
            ids = self.market_ids(symbols)
            ids = ','.join(ids)
        request = {
            'pair': ids,
        }
        if limit is not None:
            request['limit'] = limit
        response = await self.publicGetOrderBook(self.extend(request, params))
        result = {}
        marketIds = list(response.keys())
        for i in range(0, len(marketIds)):
            marketId = marketIds[i]
            symbol = self.safe_symbol(marketId)
            result[symbol] = self.parse_order_book(response[marketId], symbol, None, 'bid', 'ask')
        return result

    def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
        #
        #     {
        #         "buy_price":"0.00002996",
        #         "sell_price":"0.00003002",
        #         "last_trade":"0.00002992",
        #         "high":"0.00003028",
        #         "low":"0.00002935",
        #         "avg":"0.00002963",
        #         "vol":"1196546.3163222",
        #         "vol_curr":"35.80066578",
        #         "updated":1642291733
        #     }
        #
        timestamp = self.safe_timestamp(ticker, 'updated')
        market = self.safe_market(None, market)
        last = self.safe_string(ticker, 'last_trade')
        return self.safe_ticker({
            'symbol': market['symbol'],
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': self.safe_string(ticker, 'high'),
            'low': self.safe_string(ticker, 'low'),
            'bid': self.safe_string(ticker, 'buy_price'),
            'bidVolume': None,
            'ask': self.safe_string(ticker, 'sell_price'),
            'askVolume': None,
            'vwap': None,
            'open': None,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': None,
            'percentage': None,
            'average': self.safe_string(ticker, 'avg'),
            'baseVolume': self.safe_string(ticker, 'vol'),
            'quoteVolume': self.safe_string(ticker, 'vol_curr'),
            'info': ticker,
        }, market)

    async 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
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#4c8e6459-3503-4361-b012-c34bb9f7e385
        :param str[]|None 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>`
        """
        await self.load_markets()
        symbols = self.market_symbols(symbols)
        response = await self.publicGetTicker(params)
        #
        #     {
        #         "ADA_BTC":{
        #             "buy_price":"0.00002996",
        #             "sell_price":"0.00003002",
        #             "last_trade":"0.00002992",
        #             "high":"0.00003028",
        #             "low":"0.00002935",
        #             "avg":"0.00002963",
        #             "vol":"1196546.3163222",
        #             "vol_curr":"35.80066578",
        #             "updated":1642291733
        #         }
        #     }
        #
        result = {}
        marketIds = list(response.keys())
        for i in range(0, len(marketIds)):
            marketId = marketIds[i]
            market = self.safe_market(marketId, None, '_')
            symbol = market['symbol']
            ticker = self.safe_value(response, marketId)
            result[symbol] = self.parse_ticker(ticker, market)
        return self.filter_by_array_tickers(result, 'symbol', symbols)

    async def fetch_ticker(self, symbol: str, params={}) -> Ticker:
        """
        fetches a price ticker, a statistical calculation with the information calculated over the past 24 hours for a specific market
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#4c8e6459-3503-4361-b012-c34bb9f7e385
        :param str symbol: unified symbol of the market to fetch the ticker for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `ticker structure <https://docs.ccxt.com/#/?id=ticker-structure>`
        """
        await self.load_markets()
        response = await self.publicGetTicker(params)
        market = self.market(symbol)
        return self.parse_ticker(response[market['id']], market)

    def parse_trade(self, trade, market: Market = None) -> Trade:
        #
        # fetchTrades(public)
        #
        #     {
        #         "trade_id":165087520,
        #         "date":1587470005,
        #         "type":"buy",
        #         "quantity":"1.004",
        #         "price":"0.02491461",
        #         "amount":"0.02501426"
        #     },
        #
        # fetchMyTrades, fetchOrderTrades
        #
        #     {
        #         "trade_id": 3,
        #         "date": 1435488248,
        #         "type": "buy",
        #         "pair": "BTC_USD",
        #         "order_id": 12345,
        #         "quantity": 1,
        #         "price": 100,
        #         "amount": 100,
        #         "exec_type": "taker",
        #         "commission_amount": "0.02",
        #         "commission_currency": "BTC",
        #         "commission_percent": "0.2"
        #     }
        #
        # fetchMyTrades(margin)
        #
        #    {
        #        "trade_id": "692861757015952517",
        #        "trade_dt": "1693951853197811824",
        #        "trade_type": "buy",
        #        "pair": "ADA_USDT",
        #        "quantity": "1.96607879",
        #        "price": "0.2568",
        #        "amount": "0.50488903"
        #    }
        #
        timestamp = self.safe_timestamp(trade, 'date')
        id = self.safe_string(trade, 'trade_id')
        orderId = self.safe_string(trade, 'order_id')
        priceString = self.safe_string(trade, 'price')
        amountString = self.safe_string(trade, 'quantity')
        costString = self.safe_string(trade, 'amount')
        side = self.safe_string_2(trade, 'type', 'trade_type')
        type = None
        marketId = self.safe_string(trade, 'pair')
        market = self.safe_market(marketId, market, '_')
        symbol = market['symbol']
        isMaker = self.safe_value(trade, 'is_maker')
        takerOrMakerDefault = None
        if isMaker is not None:
            takerOrMakerDefault = 'maker' if isMaker else 'taker'
        takerOrMaker = self.safe_string(trade, 'exec_type', takerOrMakerDefault)
        fee = None
        feeCostString = self.safe_string(trade, 'commission_amount')
        if feeCostString is not None:
            feeCurrencyId = self.safe_string(trade, 'commission_currency')
            feeCurrencyCode = self.safe_currency_code(feeCurrencyId)
            feeRateString = self.safe_string(trade, 'commission_percent')
            if feeRateString is not None:
                feeRateString = Precise.string_div(feeRateString, '1000', 18)
            fee = {
                'cost': feeCostString,
                'currency': feeCurrencyCode,
                'rate': feeRateString,
            }
        return self.safe_trade({
            'id': id,
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'order': orderId,
            'type': type,
            'side': side,
            'takerOrMaker': takerOrMaker,
            'price': priceString,
            'amount': amountString,
            'cost': costString,
            'fee': fee,
        }, market)

    async 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
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#5a5a9c0d-cf17-47f6-9d62-6d4404ebd5ac
        :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
        :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>`
        """
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'pair': market['id'],
        }
        response = await self.publicGetTrades(self.extend(request, params))
        #
        #     {
        #         "ETH_BTC":[
        #             {
        #                 "trade_id":165087520,
        #                 "date":1587470005,
        #                 "type":"buy",
        #                 "quantity":"1.004",
        #                 "price":"0.02491461",
        #                 "amount":"0.02501426"
        #             },
        #             {
        #                 "trade_id":165087369,
        #                 "date":1587469938,
        #                 "type":"buy",
        #                 "quantity":"0.94",
        #                 "price":"0.02492348",
        #                 "amount":"0.02342807"
        #             }
        #         ]
        #     }
        #
        data = self.safe_list(response, market['id'], [])
        return self.parse_trades(data, market, since, limit)

    async def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        fetch all trades made by the user
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#b8d8d9af-4f46-46a1-939b-ad261d79f452  # spot
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#f4b1aaf8-399f-403b-ab5e-4926d967a106  # margin
        :param str symbol: a symbol is required but it can be a single string, or a non-empty array
        :param int [since]: the earliest time in ms to fetch trades for
        :param int [limit]: *required for margin orders* the maximum number of trades structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
         *
         * EXCHANGE SPECIFIC PARAMETERS
        :param int [params.offset]: last deal offset, default = 0
        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument')
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('fetchMyTrades', params)
        if marginMode == 'cross':
            raise BadRequest(self.id + 'only isolated margin is supported')
        await self.load_markets()
        market = self.market(symbol)
        pair = market['id']
        isSpot = marginMode != 'isolated'
        if limit is None:
            limit = 100
        request = {}
        if isSpot:
            request['pair'] = pair
        else:
            request['pair_name'] = pair
        if limit is not None:
            request['limit'] = limit
        offset = self.safe_integer(params, 'offset', 0)
        request['offset'] = offset
        response = None
        if isSpot:
            response = await self.privatePostUserTrades(self.extend(request, params))
            #
            #    {
            #        "BTC_USD": [
            #            {
            #                "trade_id": 20056872,
            #                "client_id": 100500,
            #                "date": 1435488248,
            #                "type": "buy",
            #                "pair": "BTC_USD",
            #                "quantity": "1",
            #                "price": "100",
            #                "amount": "100",
            #                "order_id": 7,
            #                "parent_order_id": 117684023830293,
            #                "exec_type": "taker",
            #                "commission_amount": "0.02",
            #                "commission_currency": "BTC",
            #                "commission_percent": "0.2"
            #            }
            #        ],
            #        ...
            #    }
            #
        else:
            responseFromExchange = await self.privatePostMarginTrades(self.extend(request, params))
            #
            #    {
            #        "trades": {
            #            "ADA_USDT": [
            #                {
            #                    "trade_id": "692861757015952517",
            #                    "trade_dt": "1693951853197811824",
            #                    "trade_type": "buy",
            #                    "pair": "ADA_USDT",
            #                    "quantity": "1.96607879",
            #                    "price": "0.2568",
            #                    "amount": "0.50488903"
            #                },
            #            ]
            #            ...
            #        }
            #    }
            #
            response = self.safe_value(responseFromExchange, 'trades')
        result = []
        marketIdsInner = list(response.keys())
        for i in range(0, len(marketIdsInner)):
            marketId = marketIdsInner[i]
            resultMarket = self.safe_market(marketId, None, '_')
            items = response[marketId]
            trades = self.parse_trades(items, resultMarket, since, limit)
            result = self.array_concat(result, trades)
        return self.filter_by_since_limit(result, since, limit)

    async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
        """
        create a trade order
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#80daa469-ec59-4d0a-b229-6a311d8dd1cd
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#de6f4321-eeac-468c-87f7-c4ad7062e265  # stop market
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#3561b86c-9ff1-436e-8e68-ac926b7eb523  # margin
        :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 fullfilled, in units of the quote currency, ignored in market orders
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param float [params.stopPrice]: the price at which a trigger order is triggered at
        :param str [params.timeInForce]: *spot only* 'fok', 'ioc' or 'post_only'
        :param boolean [params.postOnly]: *spot only* True for post only orders
        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        isMarket = (type == 'market') and (price is None)
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('createOrder', params)
        if marginMode == 'cross':
            raise BadRequest(self.id + ' only supports isolated margin')
        isSpot = (marginMode != 'isolated')
        triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop_price'])
        request = {
            'pair': market['id'],
            # 'leverage': 2,
            'quantity': self.amount_to_precision(market['symbol'], amount),
            # spot - buy, sell, market_buy, market_sell, market_buy_total, market_sell_total
            # margin - limit_buy, limit_sell, market_buy, market_sell, stop_buy, stop_sell, stop_limit_buy, stop_limit_sell, trailing_stop_buy, trailing_stop_sell
            # 'stop_price': self.price_to_precision(symbol, stopPrice),
            # 'distance': 0,  # distance for trailing stop orders
            # 'expire': 0,  # expiration timestamp in UTC timezone for the order, unless expire is 0
            # 'client_id': 123,  # optional, must be a positive integer
            # 'comment': '',  # up to 50 latin symbols, whitespaces, underscores
        }
        clientOrderId = self.safe_value_2(params, 'client_id', 'clientOrderId')
        if clientOrderId is not None:
            clientOrderId = self.safe_integer_2(params, 'client_id', 'clientOrderId')
            if clientOrderId is None:
                raise BadRequest(self.id + ' createOrder() client order id must be an integer / numeric literal')
            else:
                request['client_id'] = clientOrderId
        leverage = self.safe_number(params, 'leverage')
        if not isSpot and (leverage is None):
            raise ArgumentsRequired(self.id + ' createOrder requires an extra param params["leverage"] for margin orders')
        params = self.omit(params, ['stopPrice', 'stop_price', 'triggerPrice', 'timeInForce', 'client_id', 'clientOrderId'])
        if price is not None:
            request['price'] = self.price_to_precision(market['symbol'], price)
        response = None
        if isSpot:
            if triggerPrice is not None:
                if type == 'limit':
                    raise BadRequest(self.id + ' createOrder() cannot create stop limit orders for spot, only stop market')
                else:
                    request['type'] = side
                    request['trigger_price'] = self.price_to_precision(symbol, triggerPrice)
                response = await self.privatePostStopMarketOrderCreate(self.extend(request, params))
            else:
                execType = self.safe_string(params, 'exec_type')
                isPostOnly = None
                isPostOnly, params = self.handle_post_only(type == 'market', execType == 'post_only', params)
                timeInForce = self.safe_string(params, 'timeInForce')
                request['price'] = 0 if isMarket else self.price_to_precision(market['symbol'], price)
                if type == 'limit':
                    request['type'] = side
                elif type == 'market':
                    request['type'] = 'market_' + side
                if isPostOnly:
                    request['exec_type'] = 'post_only'
                elif timeInForce is not None:
                    request['exec_type'] = timeInForce
                response = await self.privatePostOrderCreate(self.extend(request, params))
        else:
            if triggerPrice is not None:
                request['stop_price'] = self.price_to_precision(symbol, triggerPrice)
                if type == 'limit':
                    request['type'] = 'stop_limit_' + side
                elif type == 'market':
                    request['type'] = 'stop_' + side
                else:
                    request['type'] = type
            else:
                if type == 'limit' or type == 'market':
                    request['type'] = type + '_' + side
                else:
                    request['type'] = type
            response = await self.privatePostMarginUserOrderCreate(self.extend(request, params))
        return self.parse_order(response, market)

    async def cancel_order(self, id: str, symbol: Str = None, params={}):
        """
        cancels an open order
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#1f710d4b-75bc-4b65-ad68-006f863a3f26
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#a4d0aae8-28f7-41ac-94fd-c4030130453d  # stop market
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#705dfec5-2b35-4667-862b-faf54eca6209  # margin
        :param str id: order id
        :param str symbol: not used by exmo cancelOrder()
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param boolean [params.trigger]: True to cancel a trigger order
        :param str [params.marginMode]: set to 'cross' or 'isolated' to cancel a margin order
        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        await self.load_markets()
        request = {}
        stop = self.safe_value_2(params, 'trigger', 'stop')
        params = self.omit(params, ['trigger', 'stop'])
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('cancelOrder', params)
        if marginMode == 'cross':
            raise BadRequest(self.id + ' only supports isolated margin')
        response = None
        if (marginMode == 'isolated'):
            request['order_id'] = id
            response = await self.privatePostMarginUserOrderCancel(self.extend(request, params))
            #
            #    {}
            #
        else:
            if stop:
                request['parent_order_id'] = id
                response = await self.privatePostStopMarketOrderCancel(self.extend(request, params))
                #
                #    {}
                #
            else:
                request['order_id'] = id
                response = await self.privatePostOrderCancel(self.extend(request, params))
                #
                #    {
                #        "error": '',
                #        "result": True
                #    }
                #
        return self.parse_order(response)

    async def fetch_order(self, id: str, symbol: Str = None, params={}):
        """
        *spot only* fetches information on an order made by the user
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#cf27781e-28e5-4b39-a52d-3110f5d22459  # spot
        :param str symbol: not used by exmo fetchOrder
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        await self.load_markets()
        request = {
            'order_id': str(id),
        }
        response = await self.privatePostOrderTrades(self.extend(request, params))
        #
        #     {
        #         "type": "buy",
        #         "in_currency": "BTC",
        #         "in_amount": "1",
        #         "out_currency": "USD",
        #         "out_amount": "100",
        #         "trades": [
        #             {
        #                 "trade_id": 3,
        #                 "date": 1435488248,
        #                 "type": "buy",
        #                 "pair": "BTC_USD",
        #                 "order_id": 12345,
        #                 "quantity": 1,
        #                 "price": 100,
        #                 "amount": 100
        #             }
        #         ]
        #     }
        #
        order = self.parse_order(response)
        order['id'] = str(id)
        return order

    async def fetch_order_trades(self, id: str, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        fetch all the trades made from a single order
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#cf27781e-28e5-4b39-a52d-3110f5d22459  # spot
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#00810661-9119-46c5-aec5-55abe9cb42c7  # margin
        :param str id: order id
        :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 to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.marginMode]: set to "isolated" to fetch trades for a margin order
        :returns dict[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
        """
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('fetchOrderTrades', params)
        if marginMode == 'cross':
            raise BadRequest(self.id + ' only supports isolated margin')
        market = None
        if symbol is not None:
            market = self.market(symbol)
        request = {
            'order_id': str(id),
        }
        response = None
        if marginMode == 'isolated':
            response = await self.privatePostMarginUserOrderTrades(self.extend(request, params))
            #
            #    {
            #        "trades": [
            #            {
            #                "is_maker": False,
            #                "order_id": "123",
            #                "pair": "BTC_USD",
            #                "price": "54122.25",
            #                "quantity": "0.00069994",
            #                "trade_dt": "1619069561718824428",
            #                "trade_id": "692842802860135010",
            #                "type": "sell"
            #            }
            #        ]
            #    }
            #
        else:
            response = await self.privatePostOrderTrades(self.extend(request, params))
            #
            #     {
            #         "type": "buy",
            #         "in_currency": "BTC",
            #         "in_amount": "1",
            #         "out_currency": "USD",
            #         "out_amount": "100",
            #         "trades": [
            #             {
            #                 "trade_id": 3,
            #                 "date": 1435488248,
            #                 "type": "buy",
            #                 "pair": "BTC_USD",
            #                 "order_id": 12345,
            #                 "quantity": 1,
            #                 "price": 100,
            #                 "amount": 100,
            #                 "exec_type": "taker",
            #                 "commission_amount": "0.02",
            #                 "commission_currency": "BTC",
            #                 "commission_percent": "0.2"
            #             }
            #         ]
            #     }
            #
        trades = self.safe_list(response, 'trades')
        return self.parse_trades(trades, market, since, limit)

    async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        fetch all unfilled currently open orders
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#0e135370-daa4-4689-8acd-b6876dee9ba1  # spot open orders
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#a7cfd4f0-476e-4675-b33f-22a46902f245  # margin
        :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 orders structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.marginMode]: set to "isolated" for margin orders
        :returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
        """
        await self.load_markets()
        market = None
        if symbol is not None:
            market = self.market(symbol)
            symbol = market['symbol']
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('fetchOpenOrders', params)
        isMargin = ((marginMode == 'cross') or (marginMode == 'isolated'))
        response = None
        orders = []
        if isMargin:
            response = await self.privatePostMarginUserOrderList(params)
            #
            #    {
            #        "orders": [
            #            {
            #                "client_id": "0",
            #                "comment": "",
            #                "created": "1619068707985325495",
            #                "distance": "0",
            #                "expire": 0,
            #                "funding_currency": "BTC",
            #                "funding_quantity": "0.01",
            #                "funding_rate": "0.02",
            #                "leverage": "2",
            #                "order_id": "123",
            #                "pair": "BTC_USD",
            #                "previous_type": "limit_sell",
            #                "price": "58000",
            #                "quantity": "0.01",
            #                "src": 0,
            #                "stop_price": "0",
            #                "trigger_price": "58000",
            #                "type": "limit_sell",
            #                "updated": 1619068707989411800
            #            }
            #        ]
            #    }
            #
            params = self.extend(params, {
                'status': 'open',
            })
            responseOrders = self.safe_value(response, 'orders')
            orders = self.parse_orders(responseOrders, market, since, limit, params)
        else:
            response = await self.privatePostUserOpenOrders(params)
            #
            #    {
            #        "USDT_USD": [
            #            {
            #                "parent_order_id": "507061384740151010",
            #                "client_id": "100500",
            #                "created": "1589547391",
            #                "type": "stop_market_buy",
            #                "pair": "USDT_USD",
            #                "quantity": "1",
            #                "trigger_price": "5",
            #                "amount": "5"
            #            }
            #        ],
            #        ...
            #    }
            #
            marketIds = list(response.keys())
            for i in range(0, len(marketIds)):
                marketId = marketIds[i]
                marketInner = self.safe_market(marketId)
                params = self.extend(params, {
                    'status': 'open',
                })
                parsedOrders = self.parse_orders(response[marketId], marketInner, since, limit, params)
                orders = self.array_concat(orders, parsedOrders)
        return orders

    def parse_status(self, status):
        if status is None:
            return None
        statuses = {
            'cancel_started': 'canceled',
        }
        if status.find('cancel') >= 0:
            status = 'canceled'
        return self.safe_string(statuses, status, status)

    def parse_side(self, orderType):
        side = {
            'limit_buy': 'buy',
            'limit_sell': 'sell',
            'market_buy': 'buy',
            'market_sell': 'sell',
            'stop_buy': 'buy',
            'stop_sell': 'sell',
            'stop_limit_buy': 'buy',
            'stop_limit_sell': 'sell',
            'trailing_stop_buy': 'buy',
            'trailing_stop_sell': 'sell',
            'stop_market_sell': 'sell',
            'stop_market_buy': 'buy',
            'buy': 'buy',
            'sell': 'sell',
        }
        return self.safe_string(side, orderType, orderType)

    def parse_order(self, order, market: Market = None) -> Order:
        #
        # fetchOrders, fetchOpenOrders, fetchClosedOrders, fetchCanceledOrders
        #
        #     {
        #         "order_id": "14",
        #         "created": "1435517311",
        #         "type": "buy",
        #         "pair": "BTC_USD",
        #         "price": "100",
        #         "quantity": "1",
        #         "amount": "100"
        #     }
        #
        # fetchOrder
        #
        #     {
        #         "type": "buy",
        #         "in_currency": "BTC",
        #         "in_amount": "1",
        #         "out_currency": "USD",
        #         "out_amount": "100",
        #         "trades": [
        #             {
        #                 "trade_id": 3,
        #                 "date": 1435488248,
        #                 "type": "buy",
        #                 "pair": "BTC_USD",
        #                 "order_id": 12345,
        #                 "quantity": 1,
        #                 "price": 100,
        #                 "amount": 100
        #             }
        #         ]
        #     }
        #
        # Margin fetchOpenOrders
        #
        #    {
        #        "client_id": "0",
        #        "comment": "",
        #        "created": "1619068707985325495",
        #        "distance": "0",
        #        "expire": 0,
        #        "funding_currency": "BTC",
        #        "funding_quantity": "0.01",
        #        "funding_rate": "0.02",
        #        "leverage": "2",
        #        "order_id": "123",
        #        "pair": "BTC_USD",
        #        "previous_type": "limit_sell",
        #        "price": "58000",
        #        "quantity": "0.01",
        #        "src": 0,
        #        "stop_price": "0",
        #        "trigger_price": "58000",
        #        "type": "limit_sell",
        #        "updated": 1619068707989411800
        #    }
        #
        # Margin fetchClosedOrders
        #
        #    {
        #        "distance": "0",
        #        "event_id": "692842802860022508",
        #        "event_time": "1619069531190173720",
        #        "event_type": "OrderCancelStarted",
        #        "order_id": "123",
        #        "order_status": "cancel_started",
        #        "order_type": "limit_sell",
        #        "pair": "BTC_USD",
        #        "price": "54115",
        #        "quantity": "0.001",
        #        "stop_price": "0",
        #        "trade_id": "0",
        #        "trade_price": "0",
        #        "trade_quantity": "0",
        #        "trade_type": ""
        #    },
        #
        id = self.safe_string_2(order, 'order_id', 'parent_order_id')
        eventTime = self.safe_integer_product_2(order, 'event_time', 'created', 0.000001)
        timestamp = self.safe_timestamp(order, 'created', eventTime)
        orderType = self.safe_string_2(order, 'type', 'order_type')
        side = self.parse_side(orderType)
        marketId = None
        if 'pair' in order:
            marketId = order['pair']
        elif ('in_currency' in order) and ('out_currency' in order):
            if side == 'buy':
                marketId = order['in_currency'] + '_' + order['out_currency']
            else:
                marketId = order['out_currency'] + '_' + order['in_currency']
        market = self.safe_market(marketId, market)
        symbol = market['symbol']
        amount = self.safe_string(order, 'quantity')
        if amount is None:
            amountField = 'in_amount' if (side == 'buy') else 'out_amount'
            amount = self.safe_string(order, amountField)
        price = self.safe_string(order, 'price')
        cost = self.safe_string(order, 'amount')
        transactions = self.safe_value(order, 'trades', [])
        clientOrderId = self.safe_integer(order, 'client_id')
        triggerPrice = self.safe_string(order, 'stop_price')
        if triggerPrice == '0':
            triggerPrice = None
        type = None
        if (orderType != 'buy') and (orderType != 'sell'):
            type = orderType
        return self.safe_order({
            'id': id,
            'clientOrderId': clientOrderId,
            'datetime': self.iso8601(timestamp),
            'timestamp': timestamp,
            'lastTradeTimestamp': self.safe_integer_product(order, 'updated', 0.000001),
            'status': self.parse_status(self.safe_string(order, 'order_status')),
            'symbol': symbol,
            'type': type,
            'timeInForce': None,
            'postOnly': None,
            'side': side,
            'price': price,
            'stopPrice': triggerPrice,
            'triggerPrice': triggerPrice,
            'cost': cost,
            'amount': amount,
            'filled': None,
            'remaining': None,
            'average': None,
            'trades': transactions,
            'fee': None,
            'info': order,
        }, market)

    async def fetch_canceled_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        fetches information on multiple canceled orders made by the user
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#1d2524dd-ae6d-403a-a067-77b50d13fbe5  # margin
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#a51be1d0-af5f-44e4-99d7-f7b04c6067d0  # spot canceled orders
        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: timestamp in ms of the earliest order, default is None
        :param int [limit]: max number of orders to return, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.marginMode]: set to "isolated" for margin orders
        :returns dict: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
        """
        await self.load_markets()
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('fetchOrders', params)
        if marginMode == 'cross':
            raise BadRequest(self.id + ' only supports isolated margin')
        if limit is None:
            limit = 100
        isSpot = (marginMode != 'isolated')
        if symbol is not None:
            marketInner = self.market(symbol)
            symbol = marketInner['symbol']
        request = {
            'limit': limit,
        }
        request['offset'] = limit if (since is not None) else 0
        request['limit'] = limit
        market = None
        if symbol is not None:
            market = self.market(symbol)
        response = None
        if isSpot:
            response = await self.privatePostUserCancelledOrders(self.extend(request, params))
            #
            #    [
            #        {
            #            "order_id": "27056153840",
            #            "client_id": "0",
            #            "created": "1653428646",
            #            "type": "buy",
            #            "pair": "BTC_USDT",
            #            "quantity": "0.1",
            #            "price": "10",
            #            "amount": "1"
            #        }
            #    ]
            #
            params = self.extend(params, {
                'status': 'canceled',
            })
            return self.parse_orders(response, market, since, limit, params)
        else:
            responseSwap = await self.privatePostMarginUserOrderHistory(self.extend(request, params))
            #
            #    {
            #        "items": [
            #            {
            #                "event_id": "692862104574106858",
            #                "event_time": "1694116400173489405",
            #                "event_type": "OrderCancelStarted",
            #                "order_id": "692862104561289319",
            #                "order_type": "stop_limit_sell",
            #                "order_status": "cancel_started",
            #                "trade_id": "0",
            #                "trade_type":"",
            #                "trade_quantity": "0",
            #                "trade_price": "0",
            #                "pair": "ADA_USDT",
            #                "quantity": "12",
            #                "price": "0.23",
            #                "stop_price": "0.22",
            #                "distance": "0"
            #            }
            #            ...
            #        ]
            #    }
            #
            items = self.safe_value(responseSwap, 'items')
            orders = self.parse_orders(items, market, since, limit, params)
            result = []
            for i in range(0, len(orders)):
                order = orders[i]
                if order['status'] == 'canceled':
                    result.append(order)
            return result

    async def edit_order(self, id: str, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}):
        """
        *margin only* edit a trade order
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#f27ee040-c75f-4b59-b608-d05bd45b7899  # margin
        :param str id: order id
        :param str symbol: unified CCXT market symbol
        :param str type: not used by exmo editOrder
        :param str side: not used by exmo editOrder
        :param float [amount]: how much of the currency you want to trade in units of the base currency
        :param float [price]: the price at which the order is to be fullfilled, in units of the quote currency, ignored in market orders
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param float [params.triggerPrice]: stop price for stop-market and stop-limit orders
        :param str params['marginMode']: must be set to isolated
         *
         * EXCHANGE SPECIFIC PARAMETERS
        :param int [params.distance]: distance for trailing stop orders
        :param int [params.expire]: expiration timestamp in UTC timezone for the order. order will not be expired if expire is 0
        :param str [params.comment]: optional comment for order. up to 50 latin symbols, whitespaces, underscores
        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('editOrder', params)
        if marginMode != 'isolated':
            raise BadRequest(self.id + ' editOrder() can only be used for isolated margin orders')
        triggerPrice = self.safe_number_n(params, ['triggerPrice', 'stopPrice', 'stop_price'])
        params = self.omit(params, ['triggerPrice', 'stopPrice'])
        request = {
            'order_id': id,  # id of the open order
        }
        if amount is not None:
            request['quantity'] = amount
        if price is not None:
            request['price'] = self.price_to_precision(market['symbol'], price)
        if triggerPrice is not None:
            request['stop_price'] = self.price_to_precision(market['symbol'], triggerPrice)
        response = await self.privatePostMarginUserOrderUpdate(self.extend(request, params))
        return self.parse_order(response)

    async def fetch_deposit_address(self, code: str, params={}):
        """
        fetch the deposit address for a currency associated with self account
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#c8f9ced9-7ab6-4383-a6a4-bc54469ba60e
        :param str code: unified currency code
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: an `address structure <https://docs.ccxt.com/#/?id=address-structure>`
        """
        await self.load_markets()
        response = await self.privatePostDepositAddress(params)
        #
        #     {
        #         "TRX":"TBnwrf4ZdoYXE3C8L2KMs7YPSL3fg6q6V9",
        #         "USDTTRC20":"TBnwrf4ZdoYXE3C8L2KMs7YPSL3fg6q6V9"
        #     }
        #
        depositAddress = self.safe_string(response, code)
        address = None
        tag = None
        if depositAddress:
            addressAndTag = depositAddress.split(',')
            address = addressAndTag[0]
            numParts = len(addressAndTag)
            if numParts > 1:
                tag = addressAndTag[1]
        self.check_address(address)
        return {
            'currency': code,
            'address': address,
            'tag': tag,
            'network': None,
            'info': response,
        }

    def get_market_from_trades(self, trades):
        tradesBySymbol = self.index_by(trades, 'pair')
        symbols = list(tradesBySymbol.keys())
        numSymbols = len(symbols)
        if numSymbols == 1:
            return self.markets[symbols[0]]
        return None

    async def withdraw(self, code: str, amount: float, address: str, tag=None, params={}):
        """
        make a withdrawal
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#3ab9c34d-ad58-4f87-9c57-2e2ea88a8325
        :param str code: unified currency code
        :param float amount: the amount to withdraw
        :param str address: the address to withdraw to
        :param str tag:
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
        """
        tag, params = self.handle_withdraw_tag_and_params(tag, params)
        await self.load_markets()
        currency = self.currency(code)
        request = {
            'amount': amount,
            'currency': currency['id'],
            'address': address,
        }
        if tag is not None:
            request['invoice'] = tag
        networks = self.safe_value(self.options, 'networks', {})
        network = self.safe_string_upper(params, 'network')  # self line allows the user to specify either ERC20 or ETH
        network = self.safe_string(networks, network, network)  # handle ERC20>ETH alias
        if network is not None:
            request['transport'] = network
            params = self.omit(params, 'network')
        response = await self.privatePostWithdrawCrypt(self.extend(request, params))
        return self.parse_transaction(response, currency)

    def parse_transaction_status(self, status):
        statuses = {
            'transferred': 'ok',
            'paid': 'ok',
            'pending': 'pending',
            'processing': 'pending',
            'verifying': 'pending',
        }
        return self.safe_string(statuses, status, status)

    def parse_transaction(self, transaction, currency: Currency = None) -> Transaction:
        #
        # fetchDepositsWithdrawals
        #
        #    {
        #        "dt": 1461841192,
        #        "type": "deposit",
        #        "curr": "RUB",
        #        "status": "processing",
        #        "provider": "Qiwi(LA) [12345]",
        #        "amount": "1",
        #        "account": "",
        #        "txid": "ec46f784ad976fd7f7539089d1a129fe46...",
        #    }
        #
        # fetchWithdrawals
        #
        #    {
        #        "operation_id": 47412538520634344,
        #        "created": 1573760013,
        #        "updated": 1573760013,
        #        "type": "withdraw",
        #        "currency": "DOGE",
        #        "status": "Paid",
        #        "amount": "300",
        #        "provider": "DOGE",
        #        "commission": "0",
        #        "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
        #        "order_id": 69670170,
        #        "provider_type": "crypto",
        #        "crypto_address": "DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
        #        "card_number": "",
        #        "wallet_address": "",
        #        "email": "",
        #        "phone": "",
        #        "extra": {
        #            "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
        #            "confirmations": null,
        #            "excode": "",
        #            "invoice": ""
        #        },
        #        "error": ""
        #    }
        #
        # withdraw
        #
        #    {
        #        "result": True,
        #        "error": "",
        #        "task_id": 11775077
        #    }
        #
        timestamp = self.safe_timestamp_2(transaction, 'dt', 'created')
        amountString = self.safe_string(transaction, 'amount')
        if amountString is not None:
            amountString = Precise.string_abs(amountString)
        txid = self.safe_string(transaction, 'txid')
        if txid is None:
            extra = self.safe_value(transaction, 'extra', {})
            extraTxid = self.safe_string(extra, 'txid')
            if extraTxid != '':
                txid = extraTxid
        type = self.safe_string(transaction, 'type')
        currencyId = self.safe_string_2(transaction, 'curr', 'currency')
        code = self.safe_currency_code(currencyId, currency)
        address = None
        comment = None
        account = self.safe_string(transaction, 'account')
        if type == 'deposit':
            comment = account
        elif type == 'withdrawal':
            address = account
            if address is not None:
                parts = address.split(':')
                numParts = len(parts)
                if numParts == 2:
                    address = self.safe_string(parts, 1)
                    address = address.replace(' ', '')
        fee = {
            'currency': None,
            'cost': None,
            'rate': None,
        }
        # fixed funding fees only(for now)
        if not self.fees['transaction']['percentage']:
            key = 'withdraw' if (type == 'withdrawal') else 'deposit'
            feeCost = self.safe_string(transaction, 'commission')
            if feeCost is None:
                transactionFees = self.safe_value(self.options, 'transactionFees', {})
                codeFees = self.safe_value(transactionFees, code, {})
                feeCost = self.safe_string(codeFees, key)
            # users don't pay for cashbacks, no fees for that
            provider = self.safe_string(transaction, 'provider')
            if provider == 'cashback':
                feeCost = '0'
            if feeCost is not None:
                # withdrawal amount includes the fee
                if type == 'withdrawal':
                    amountString = Precise.string_sub(amountString, feeCost)
                fee['cost'] = self.parse_number(feeCost)
                fee['currency'] = code
        return {
            'info': transaction,
            'id': self.safe_string_2(transaction, 'order_id', 'task_id'),
            'txid': txid,
            'type': type,
            'currency': code,
            'network': self.safe_string(transaction, 'provider'),
            'amount': self.parse_number(amountString),
            'status': self.parse_transaction_status(self.safe_string_lower(transaction, 'status')),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'address': address,
            'addressFrom': None,
            'addressTo': address,
            'tag': None,
            'tagFrom': None,
            'tagTo': None,
            'updated': self.safe_timestamp(transaction, 'updated'),
            'comment': comment,
            'internal': None,
            'fee': fee,
        }

    async def fetch_deposits_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
        """
        fetch history of deposits and withdrawals
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#31e69a33-4849-4e6a-b4b4-6d574238f6a7
        :param str [code]: unified currency code for the currency of the deposit/withdrawals, default is None
        :param int [since]: timestamp in ms of the earliest deposit/withdrawal, default is None
        :param int [limit]: max number of deposit/withdrawals to return, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a list of `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
        """
        await self.load_markets()
        request = {}
        if since is not None:
            request['date'] = self.parse_to_int(since / 1000)
        currency = None
        if code is not None:
            currency = self.currency(code)
        response = await self.privatePostWalletHistory(self.extend(request, params))
        #
        #     {
        #       "result": True,
        #       "error": "",
        #       "begin": "1493942400",
        #       "end": "1494028800",
        #       "history": [
        #          {
        #            "dt": 1461841192,
        #            "type": "deposit",
        #            "curr": "RUB",
        #            "status": "processing",
        #            "provider": "Qiwi(LA) [12345]",
        #            "amount": "1",
        #            "account": "",
        #            "txid": "ec46f784ad976fd7f7539089d1a129fe46...",
        #          },
        #          {
        #            "dt": 1463414785,
        #            "type": "withdrawal",
        #            "curr": "USD",
        #            "status": "paid",
        #            "provider": "EXCODE",
        #            "amount": "-1",
        #            "account": "EX-CODE_19371_USDda...",
        #            "txid": "",
        #          },
        #       ],
        #     }
        #
        return self.parse_transactions(response['history'], currency, since, limit)

    async def fetch_withdrawals(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
        """
        fetch all withdrawals made from an account
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706
        :param str code: unified currency code
        :param int [since]: the earliest time in ms to fetch withdrawals for
        :param int [limit]: the maximum number of withdrawals structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
        """
        await self.load_markets()
        currency = None
        request = {
            'type': 'withdraw',
        }
        if limit is not None:
            request['limit'] = limit  # default: 100, maximum: 100
        if code is not None:
            currency = self.currency(code)
            request['currency'] = currency['id']
        response = await self.privatePostWalletOperations(self.extend(request, params))
        #
        #     {
        #         "items": [
        #         {
        #             "operation_id": 47412538520634344,
        #             "created": 1573760013,
        #             "updated": 1573760013,
        #             "type": "withdraw",
        #             "currency": "DOGE",
        #             "status": "Paid",
        #             "amount": "300",
        #             "provider": "DOGE",
        #             "commission": "0",
        #             "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
        #             "order_id": 69670170,
        #             "extra": {
        #                 "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
        #                 "excode": "",
        #                 "invoice": ""
        #             },
        #             "error": ""
        #         },
        #     ],
        #         "count": 23
        #     }
        #
        items = self.safe_list(response, 'items', [])
        return self.parse_transactions(items, currency, since, limit)

    async def fetch_withdrawal(self, id: str, code: Str = None, params={}):
        """
        fetch data on a currency withdrawal via the withdrawal id
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706
        :param str id: withdrawal id
        :param str code: unified currency code of the currency withdrawn, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
        """
        await self.load_markets()
        currency = None
        request = {
            'order_id': id,
            'type': 'withdraw',
        }
        if code is not None:
            currency = self.currency(code)
            request['currency'] = currency['id']
        response = await self.privatePostWalletOperations(self.extend(request, params))
        #
        #     {
        #         "items": [
        #         {
        #             "operation_id": 47412538520634344,
        #             "created": 1573760013,
        #             "updated": 1573760013,
        #             "type": "deposit",
        #             "currency": "DOGE",
        #             "status": "Paid",
        #             "amount": "300",
        #             "provider": "DOGE",
        #             "commission": "0",
        #             "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
        #             "order_id": 69670170,
        #             "extra": {
        #                 "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
        #                 "excode": "",
        #                 "invoice": ""
        #             },
        #             "error": ""
        #         },
        #     ],
        #         "count": 23
        #     }
        #
        items = self.safe_value(response, 'items', [])
        first = self.safe_dict(items, 0, {})
        return self.parse_transaction(first, currency)

    async def fetch_deposit(self, id=None, code: Str = None, params={}):
        """
        fetch information on a deposit
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706
        :param str id: deposit id
        :param str code: unified currency code, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `transaction structure <https://docs.ccxt.com/#/?id=transaction-structure>`
        """
        await self.load_markets()
        currency = None
        request = {
            'order_id': id,
            'type': 'deposit',
        }
        if code is not None:
            currency = self.currency(code)
            request['currency'] = currency['id']
        response = await self.privatePostWalletOperations(self.extend(request, params))
        #
        #     {
        #         "items": [
        #         {
        #             "operation_id": 47412538520634344,
        #             "created": 1573760013,
        #             "updated": 1573760013,
        #             "type": "deposit",
        #             "currency": "DOGE",
        #             "status": "Paid",
        #             "amount": "300",
        #             "provider": "DOGE",
        #             "commission": "0",
        #             "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
        #             "order_id": 69670170,
        #             "extra": {
        #                 "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
        #                 "excode": "",
        #                 "invoice": ""
        #             },
        #             "error": ""
        #         },
        #     ],
        #         "count": 23
        #     }
        #
        items = self.safe_value(response, 'items', [])
        first = self.safe_dict(items, 0, {})
        return self.parse_transaction(first, currency)

    async def fetch_deposits(self, code: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Transaction]:
        """
        fetch all deposits made to an account
        :see: https://documenter.getpostman.com/view/10287440/SzYXWKPi#97f1becd-7aad-4e0e-babe-7bbe09e33706
        :param str code: unified currency code
        :param int [since]: the earliest time in ms to fetch deposits for
        :param int [limit]: the maximum number of deposits structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
        """
        await self.load_markets()
        currency = None
        request = {
            'type': 'deposit',
        }
        if limit is not None:
            request['limit'] = limit  # default: 100, maximum: 100
        if code is not None:
            currency = self.currency(code)
            request['currency'] = currency['id']
        response = await self.privatePostWalletOperations(self.extend(request, params))
        #
        #     {
        #         "items": [
        #         {
        #             "operation_id": 47412538520634344,
        #             "created": 1573760013,
        #             "updated": 1573760013,
        #             "type": "deposit",
        #             "currency": "DOGE",
        #             "status": "Paid",
        #             "amount": "300",
        #             "provider": "DOGE",
        #             "commission": "0",
        #             "account": "DOGE: DBVy8pF1f8yxaCVEHqHeR7kkcHecLQ8nRS",
        #             "order_id": 69670170,
        #             "extra": {
        #                 "txid": "f2b66259ae1580f371d38dd27e31a23fff8c04122b65ee3ab5a3f612d579c792",
        #                 "excode": "",
        #                 "invoice": ""
        #             },
        #             "error": ""
        #         },
        #     ],
        #         "count": 23
        #     }
        #
        items = self.safe_list(response, 'items', [])
        return self.parse_transactions(items, currency, since, limit)

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        url = self.urls['api'][api] + '/'
        if api != 'web':
            url += self.version + '/'
        url += path
        if (api == 'public') or (api == 'web'):
            if params:
                url += '?' + self.urlencode(params)
        elif api == 'private':
            self.check_required_credentials()
            nonce = self.nonce()
            body = self.urlencode(self.extend({'nonce': nonce}, params))
            headers = {
                'Content-Type': 'application/x-www-form-urlencoded',
                'Key': self.apiKey,
                'Sign': self.hmac(self.encode(body), self.encode(self.secret), hashlib.sha512),
            }
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def nonce(self):
        return self.milliseconds()

    def handle_errors(self, httpCode, reason, url, method, headers, body, response, requestHeaders, requestBody):
        if response is None:
            return None  # fallback to default error handler
        if ('error' in response) and not ('result' in response):
            # error: {
            #     "code": "140434",
            #     "msg": "Your margin balance is not sufficient to place the order for '5 TON'. Please top up your margin wallet by "2.5 USDT"."
            # }
            #
            errorCode = self.safe_value(response, 'error', {})
            messageError = self.safe_string(errorCode, 'msg')
            code = self.safe_string(errorCode, 'code')
            feedback = self.id + ' ' + body
            self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
            self.throw_broadly_matched_exception(self.exceptions['broad'], messageError, feedback)
            raise ExchangeError(feedback)
        if ('result' in response) or ('errmsg' in response):
            #
            #     {"result":false,"error":"Error 50052: Insufficient funds"}
            #     {"s":"error","errmsg":"strconv.ParseInt: parsing \"\": invalid syntax"}
            #
            success = self.safe_bool(response, 'result', False)
            if isinstance(success, str):
                if (success == 'true') or (success == '1'):
                    success = True
                else:
                    success = False
            if not success:
                code = None
                message = self.safe_string_2(response, 'error', 'errmsg')
                errorParts = message.split(':')
                numParts = len(errorParts)
                if numParts > 1:
                    errorSubParts = errorParts[0].split(' ')
                    numSubParts = len(errorSubParts)
                    code = errorSubParts[1] if (numSubParts > 1) else errorSubParts[0]
                feedback = self.id + ' ' + body
                self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
                self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
                raise ExchangeError(feedback)
        return None
