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

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

from ccxt_zagrava.async_support.base.exchange import Exchange
from ccxt_zagrava.abstract.bitopro import ImplicitAPI
import hashlib
import math
from ccxt_zagrava.base.types import Balances, Currencies, Currency, Int, Market, Num, Order, OrderBook, OrderSide, OrderType, Str, Strings, Ticker, Tickers, Trade, TradingFees, Transaction
from typing import List
from ccxt_zagrava.base.errors import ExchangeError
from ccxt_zagrava.base.errors import AuthenticationError
from ccxt_zagrava.base.errors import ArgumentsRequired
from ccxt_zagrava.base.errors import BadRequest
from ccxt_zagrava.base.errors import InsufficientFunds
from ccxt_zagrava.base.errors import InvalidOrder
from ccxt_zagrava.base.decimal_to_precision import TICK_SIZE
from ccxt_zagrava.base.precise import Precise


class bitopro(Exchange, ImplicitAPI):

    def describe(self):
        return self.deep_extend(super(bitopro, self).describe(), {
            'id': 'bitopro',
            'name': 'BitoPro',
            'countries': ['TW'],  # Taiwan
            'version': 'v3',
            'rateLimit': 100,
            'pro': True,
            'has': {
                'CORS': None,
                'spot': True,
                'margin': False,
                'swap': False,
                'future': False,
                'option': False,
                'cancelAllOrders': True,
                'cancelOrder': True,
                'cancelOrders': True,
                'closeAllPositions': False,
                'closePosition': False,
                'createOrder': True,
                'editOrder': False,
                'fetchBalance': True,
                'fetchBorrowRateHistories': False,
                'fetchBorrowRateHistory': False,
                'fetchClosedOrders': True,
                'fetchCrossBorrowRate': False,
                'fetchCrossBorrowRates': False,
                'fetchCurrencies': True,
                'fetchDepositAddress': False,
                'fetchDeposits': True,
                'fetchDepositsWithdrawals': False,
                'fetchDepositWithdrawFee': 'emulated',
                'fetchDepositWithdrawFees': True,
                'fetchFundingHistory': False,
                'fetchFundingRate': False,
                'fetchFundingRateHistory': False,
                'fetchFundingRates': False,
                'fetchIndexOHLCV': False,
                'fetchIsolatedBorrowRate': False,
                'fetchIsolatedBorrowRates': False,
                'fetchMarginMode': False,
                'fetchMarkets': True,
                'fetchMarkOHLCV': False,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenInterestHistory': False,
                'fetchOpenOrders': True,
                'fetchOrder': True,
                'fetchOrderBook': True,
                'fetchOrders': False,
                'fetchOrderTrades': False,
                'fetchPosition': False,
                'fetchPositionHistory': False,
                'fetchPositionMode': False,
                'fetchPositions': False,
                'fetchPositionsForSymbol': False,
                'fetchPositionsHistory': False,
                'fetchPositionsRisk': False,
                'fetchPremiumIndexOHLCV': False,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTime': False,
                'fetchTrades': True,
                'fetchTradingFee': False,
                'fetchTradingFees': True,
                'fetchTransactionFees': False,
                'fetchTransactions': False,
                'fetchTransfer': False,
                'fetchTransfers': False,
                'fetchWithdrawal': True,
                'fetchWithdrawals': True,
                'setLeverage': False,
                'setMarginMode': False,
                'transfer': False,
                'withdraw': True,
            },
            'timeframes': {
                '1m': '1m',
                '5m': '5m',
                '15m': '15m',
                '30m': '30m',
                '1h': '1h',
                '3h': '3h',
                '6h': '6h',
                '12h': '12h',
                '1d': '1d',
                '1w': '1w',
                '1M': '1M',
            },
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/158227251-3a92a220-9222-453c-9277-977c6677fe71.jpg',
                'api': {
                    'rest': 'https://api.bitopro.com/v3',
                },
                'www': 'https://www.bitopro.com',
                'doc': [
                    'https://github.com/bitoex/bitopro-offical-api-docs/blob/master/v3-1/rest-1/rest.md',
                ],
                'fees': 'https://www.bitopro.com/fees',
            },
            'requiredCredentials': {
                'apiKey': True,
                'secret': True,
            },
            'api': {
                'public': {
                    'get': {
                        'order-book/{pair}': 1,
                        'tickers': 1,
                        'tickers/{pair}': 1,
                        'trades/{pair}': 1,
                        'provisioning/currencies': 1,
                        'provisioning/trading-pairs': 1,
                        'provisioning/limitations-and-fees': 1,
                        'trading-history/{pair}': 1,
                        'price/otc/{currency}': 1,
                    },
                },
                'private': {
                    'get': {
                        'accounts/balance': 1,
                        'orders/history': 1,
                        'orders/all/{pair}': 1,
                        'orders/trades/{pair}': 1,
                        'orders/{pair}/{orderId}': 1,
                        'wallet/withdraw/{currency}/{serial}': 1,
                        'wallet/withdraw/{currency}/id/{id}': 1,
                        'wallet/depositHistory/{currency}': 1,
                        'wallet/withdrawHistory/{currency}': 1,
                    },
                    'post': {
                        'orders/{pair}': 1 / 2,  # 1200/m => 20/s => 10/20 = 1/2
                        'orders/batch': 20 / 3,  # 90/m => 1.5/s => 10/1.5 = 20/3
                        'wallet/withdraw/{currency}': 10,  # 60/m => 1/s => 10/1 = 10
                    },
                    'put': {
                        'orders': 5,  # 2/s => 10/2 = 5
                    },
                    'delete': {
                        'orders/{pair}/{id}': 2 / 3,  # 900/m => 15/s => 10/15 = 2/3
                        'orders/all': 5,  # 2/s => 10/2 = 5
                        'orders/{pair}': 5,  # 2/s => 10/2 = 5
                    },
                },
            },
            'fees': {
                'trading': {
                    'tierBased': True,
                    'percentage': True,
                    'maker': self.parse_number('0.001'),
                    'taker': self.parse_number('0.002'),
                    'tiers': {
                        'taker': [
                            [self.parse_number('0'), self.parse_number('0.002')],
                            [self.parse_number('3000000'), self.parse_number('0.00194')],
                            [self.parse_number('5000000'), self.parse_number('0.0015')],
                            [self.parse_number('30000000'), self.parse_number('0.0014')],
                            [self.parse_number('300000000'), self.parse_number('0.0013')],
                            [self.parse_number('550000000'), self.parse_number('0.0012')],
                            [self.parse_number('1300000000'), self.parse_number('0.0011')],
                        ],
                        'maker': [
                            [self.parse_number('0'), self.parse_number('0.001')],
                            [self.parse_number('3000000'), self.parse_number('0.00097')],
                            [self.parse_number('5000000'), self.parse_number('0.0007')],
                            [self.parse_number('30000000'), self.parse_number('0.0006')],
                            [self.parse_number('300000000'), self.parse_number('0.0005')],
                            [self.parse_number('550000000'), self.parse_number('0.0004')],
                            [self.parse_number('1300000000'), self.parse_number('0.0003')],
                        ],
                    },
                },
            },
            'options': {
                'networks': {
                    'ERC20': 'ERC20',
                    'ETH': 'ERC20',
                    'TRX': 'TRX',
                    'TRC20': 'TRX',
                    'BEP20': 'BSC',
                    'BSC': 'BSC',
                },
            },
            'precisionMode': TICK_SIZE,
            'exceptions': {
                'exact': {
                    'Unsupported currency.': BadRequest,  # {"error":"Unsupported currency."}
                    'Unsupported order type': BadRequest,  # {"error":"Unsupported order type"}
                    'Invalid body': BadRequest,  # {"error":"Invalid body"}
                    'Invalid Signature': AuthenticationError,  # {"error":"Invalid Signature"}
                    'Address not in whitelist.': BadRequest,
                },
                'broad': {
                    'Invalid amount': InvalidOrder,  # {"error":"Invalid amount 0.0000000001, decimal limit is 8."}
                    'Balance for ': InsufficientFunds,  # {"error":"Balance for eth not enough, only has 0, but ordered 0.01."}
                    'Invalid ': BadRequest,  # {"error":"Invalid price -1."}
                    'Wrong parameter': BadRequest,  # {"error":"Wrong parameter: from"}
                },
            },
            'commonCurrencies': {
            },
        })

    async def fetch_currencies(self, params={}) -> Currencies:
        """
        fetches all available currencies on an exchange
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_currency_info.md
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: an associative dictionary of currencies
        """
        response = await self.publicGetProvisioningCurrencies(params)
        currencies = self.safe_value(response, 'data', [])
        #
        #     {
        #         "data":[
        #             {
        #                 "currency":"eth",
        #                 "withdrawFee":"0.007",
        #                 "minWithdraw":"0.001",
        #                 "maxWithdraw":"1000",
        #                 "maxDailyWithdraw":"2000",
        #                 "withdraw":true,
        #                 "deposit":true,
        #                 "depositConfirmation":"12"
        #             }
        #         ]
        #     }
        #
        result: dict = {}
        for i in range(0, len(currencies)):
            currency = currencies[i]
            currencyId = self.safe_string(currency, 'currency')
            code = self.safe_currency_code(currencyId)
            deposit = self.safe_value(currency, 'deposit')
            withdraw = self.safe_value(currency, 'withdraw')
            fee = self.safe_number(currency, 'withdrawFee')
            withdrawMin = self.safe_number(currency, 'minWithdraw')
            withdrawMax = self.safe_number(currency, 'maxWithdraw')
            limits: dict = {
                'withdraw': {
                    'min': withdrawMin,
                    'max': withdrawMax,
                },
                'amount': {
                    'min': None,
                    'max': None,
                },
            }
            result[code] = {
                'id': currencyId,
                'code': code,
                'info': currency,
                'type': None,
                'name': None,
                'active': deposit and withdraw,
                'deposit': deposit,
                'withdraw': withdraw,
                'fee': fee,
                'precision': None,
                'limits': limits,
            }
        return result

    async def fetch_markets(self, params={}) -> List[Market]:
        """
        retrieves data on all markets for bitopro
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_trading_pair_info.md
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: an array of objects representing market data
        """
        response = await self.publicGetProvisioningTradingPairs()
        markets = self.safe_value(response, 'data', [])
        #
        #     {
        #         "data":[
        #             {
        #                 "pair":"shib_twd",
        #                 "base":"shib",
        #                 "quote":"twd",
        #                 "basePrecision":"8",
        #                 "quotePrecision":"6",
        #                 "minLimitBaseAmount":"100000",
        #                 "maxLimitBaseAmount":"5500000000",
        #                 "minMarketBuyQuoteAmount":"1000",
        #                 "orderOpenLimit":"200",
        #                 "maintain":false,
        #                 "orderBookQuotePrecision":"6",
        #                 "orderBookQuoteScaleLevel":"5"
        #             }
        #         ]
        #     }
        #
        return self.parse_markets(markets)

    def parse_market(self, market: dict) -> Market:
        active = not self.safe_value(market, 'maintain')
        id = self.safe_string(market, 'pair')
        uppercaseId = id.upper()
        baseId = self.safe_string(market, 'base')
        quoteId = self.safe_string(market, 'quote')
        base = self.safe_currency_code(baseId)
        quote = self.safe_currency_code(quoteId)
        symbol = base + '/' + quote
        limits: dict = {
            'amount': {
                'min': self.safe_number(market, 'minLimitBaseAmount'),
                'max': self.safe_number(market, 'maxLimitBaseAmount'),
            },
            'price': {
                'min': None,
                'max': None,
            },
            'cost': {
                'min': None,
                'max': None,
            },
            'leverage': {
                'min': None,
                'max': None,
            },
        }
        return {
            'id': id,
            'uppercaseId': uppercaseId,
            'symbol': symbol,
            'base': base,
            'quote': quote,
            'baseId': base,
            'quoteId': quote,
            'settle': None,
            'settleId': None,
            'type': 'spot',
            'spot': True,
            'margin': False,
            'swap': False,
            'future': False,
            'option': False,
            'contract': False,
            'linear': None,
            'inverse': None,
            'contractSize': None,
            'expiry': None,
            'expiryDatetime': None,
            'strike': None,
            'optionType': None,
            'limits': limits,
            'precision': {
                'price': self.parse_number(self.parse_precision(self.safe_string(market, 'quotePrecision'))),
                'amount': self.parse_number(self.parse_precision(self.safe_string(market, 'basePrecision'))),
            },
            'active': active,
            'created': None,
            'info': market,
        }

    def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
        #
        #     {
        #         "pair":"btc_twd",
        #         "lastPrice":"1182449.00000000",
        #         "isBuyer":false,
        #         "priceChange24hr":"-1.99",
        #         "volume24hr":"9.13089740",
        #         "high24hr":"1226097.00000000",
        #         "low24hr":"1181000.00000000"
        #     }
        #
        marketId = self.safe_string(ticker, 'pair')
        market = self.safe_market(marketId, market)
        symbol = self.safe_string(market, 'symbol')
        return self.safe_ticker({
            'symbol': symbol,
            'timestamp': None,
            'datetime': None,
            'high': self.safe_string(ticker, 'high24hr'),
            'low': self.safe_string(ticker, 'low24hr'),
            'bid': None,
            'bidVolume': None,
            'ask': None,
            'askVolume': None,
            'vwap': None,
            'open': None,
            'close': self.safe_string(ticker, 'lastPrice'),
            'last': self.safe_string(ticker, 'lastPrice'),
            'previousClose': None,
            'change': None,
            'percentage': self.safe_string(ticker, 'priceChange24hr'),
            'average': None,
            'baseVolume': self.safe_string(ticker, 'volume24hr'),
            'quoteVolume': None,
            'info': ticker,
        }, market)

    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://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_ticker_data.md
        :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_zagrava.com/#/?id=ticker-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'pair': market['id'],
        }
        response = await self.publicGetTickersPair(self.extend(request, params))
        ticker = self.safe_value(response, 'data', {})
        #
        #     {
        #         "data":{
        #             "pair":"btc_twd",
        #             "lastPrice":"1182449.00000000",
        #             "isBuyer":false,
        #             "priceChange24hr":"-1.99",
        #             "volume24hr":"9.13089740",
        #             "high24hr":"1226097.00000000",
        #             "low24hr":"1181000.00000000"
        #         }
        #     }
        #
        return self.parse_ticker(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://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_ticker_data.md
        :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_zagrava.com/#/?id=ticker-structure>`
        """
        await self.load_markets()
        response = await self.publicGetTickers()
        tickers = self.safe_value(response, 'data', [])
        #
        #     {
        #         "data":[
        #             {
        #                 "pair":"xrp_twd",
        #                 "lastPrice":"21.26110000",
        #                 "isBuyer":false,
        #                 "priceChange24hr":"-6.53",
        #                 "volume24hr":"102846.47084802",
        #                 "high24hr":"23.24460000",
        #                 "low24hr":"21.13730000"
        #             }
        #         ]
        #     }
        #
        return self.parse_tickers(tickers, symbols)

    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://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_orderbook_data.md
        :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_zagrava.com/#/?id=order-book-structure>` indexed by market symbols
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'pair': market['id'],
        }
        if limit is not None:
            request['limit'] = limit
        response = await self.publicGetOrderBookPair(self.extend(request, params))
        #
        #     {
        #         "bids":[
        #             {
        #                 "price":"1175271",
        #                 "amount":"0.00022804",
        #                 "count":1,
        #                 "total":"0.00022804"
        #             }
        #         ],
        #         "asks":[
        #             {
        #                 "price":"1176906",
        #                 "amount":"0.0496",
        #                 "count":1,
        #                 "total":"0.0496"
        #             }
        #         ]
        #     }
        #
        return self.parse_order_book(response, market['symbol'], None, 'bids', 'asks', 'price', 'amount')

    def parse_trade(self, trade: dict, market: Market = None) -> Trade:
        #
        # fetchTrades
        #         {
        #                 "timestamp":1644651458,
        #                 "price":"1180785.00000000",
        #                 "amount":"0.00020000",
        #                 "isBuyer":false
        #         }
        #
        # fetchMyTrades
        #         {
        #                 "tradeId":"5685030251",
        #                 "orderId":"9669168142",
        #                 "price":"11821.8",
        #                 "action":"SELL",
        #                 "baseAmount":"0.01",
        #                 "quoteAmount":"118.218",
        #                 "fee":"0.236436",
        #                 "feeSymbol":"BNB",
        #                 "isTaker":true,
        #                 "timestamp":1644905714862,
        #                 "createdTimestamp":1644905714862
        #         }
        #
        id = self.safe_string(trade, 'tradeId')
        orderId = self.safe_string(trade, 'orderId')
        timestamp = None
        if id is None:
            timestamp = self.safe_timestamp(trade, 'timestamp')
        else:
            timestamp = self.safe_integer(trade, 'timestamp')
        marketId = self.safe_string(trade, 'pair')
        market = self.safe_market(marketId, market)
        symbol = self.safe_string(market, 'symbol')
        price = self.safe_string(trade, 'price')
        type = self.safe_string_lower(trade, 'type')
        side = self.safe_string_lower(trade, 'action')
        if side is None:
            isBuyer = self.safe_value(trade, 'isBuyer')
            if isBuyer:
                side = 'buy'
            else:
                side = 'sell'
        amount = self.safe_string(trade, 'amount')
        if amount is None:
            amount = self.safe_string(trade, 'baseAmount')
        fee = None
        feeAmount = self.safe_string(trade, 'fee')
        feeSymbol = self.safe_currency_code(self.safe_string(trade, 'feeSymbol'))
        if feeAmount is not None:
            fee = {
                'cost': feeAmount,
                'currency': feeSymbol,
                'rate': None,
            }
        isTaker = self.safe_value(trade, 'isTaker')
        takerOrMaker = None
        if isTaker is not None:
            if isTaker:
                takerOrMaker = 'taker'
            else:
                takerOrMaker = 'maker'
        return self.safe_trade({
            'id': id,
            'info': trade,
            'order': orderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'takerOrMaker': takerOrMaker,
            'type': type,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': None,
            '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://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_trades_data.md
        :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_zagrava.com/#/?id=public-trades>`
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'pair': market['id'],
        }
        response = await self.publicGetTradesPair(self.extend(request, params))
        trades = self.safe_value(response, 'data', [])
        #
        #     {
        #         "data":[
        #             {
        #                 "timestamp":1644651458,
        #                 "price":"1180785.00000000",
        #                 "amount":"0.00020000",
        #                 "isBuyer":false
        #             }
        #         ]
        #     }
        #
        return self.parse_trades(trades, market, since, limit)

    async def fetch_trading_fees(self, params={}) -> TradingFees:
        """
        fetch the trading fees for multiple markets
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_limitations_and_fees.md
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a dictionary of `fee structures <https://docs.ccxt_zagrava.com/#/?id=fee-structure>` indexed by market symbols
        """
        await self.load_markets()
        response = await self.publicGetProvisioningLimitationsAndFees(params)
        tradingFeeRate = self.safe_value(response, 'tradingFeeRate', {})
        first = self.safe_value(tradingFeeRate, 0)
        #
        #     {
        #         "tradingFeeRate":[
        #             {
        #                 "rank":0,
        #                 "twdVolumeSymbol":"\u003c",
        #                 "twdVolume":"3000000",
        #                 "bitoAmountSymbol":"\u003c",
        #                 "bitoAmount":"7500",
        #                 "makerFee":"0.001",
        #                 "takerFee":"0.002",
        #                 "makerBitoFee":"0.0008",
        #                 "takerBitoFee":"0.0016"
        #             }
        #         ],
        #         "orderFeesAndLimitations":[
        #             {
        #                 "pair":"BTC/TWD",
        #                 "minimumOrderAmount":"0.0001",
        #                 "minimumOrderAmountBase":"BTC",
        #                 "minimumOrderNumberOfDigits":"0"
        #             }
        #         ],
        #         "restrictionsOfWithdrawalFees":[
        #             {
        #                 "currency":"TWD",
        #                 "fee":"15",
        #                 "minimumTradingAmount":"100",
        #                 "maximumTradingAmount":"1000000",
        #                 "dailyCumulativeMaximumAmount":"2000000",
        #                 "remarks":"",
        #                 "protocol":""
        #             }
        #         ],
        #         "cryptocurrencyDepositFeeAndConfirmation":[
        #             {
        #                 "currency":"TWD",
        #                 "generalDepositFees":"0",
        #                 "blockchainConfirmationRequired":""
        #             }
        #         ],
        #         "ttCheckFeesAndLimitationsLevel1":[
        #             {
        #                 "currency":"TWD",
        #                 "redeemDailyCumulativeMaximumAmount":"",
        #                 "generateMinimumTradingAmount":"",
        #                 "generateMaximumTradingAmount":"",
        #                 "generateDailyCumulativeMaximumAmount":""
        #             }
        #         ],
        #         "ttCheckFeesAndLimitationsLevel2":[
        #             {
        #                 "currency":"TWD",
        #                 "redeemDailyCumulativeMaximumAmount":"20000000",
        #                 "generateMinimumTradingAmount":"30",
        #                 "generateMaximumTradingAmount":"10000000",
        #                 "generateDailyCumulativeMaximumAmount":"10000000"
        #             }
        #         ]
        #     }
        #
        result: dict = {}
        maker = self.safe_number(first, 'makerFee')
        taker = self.safe_number(first, 'takerFee')
        for i in range(0, len(self.symbols)):
            symbol = self.symbols[i]
            result[symbol] = {
                'info': first,
                'symbol': symbol,
                'maker': maker,
                'taker': taker,
                'percentage': True,
                'tierBased': True,
            }
        return result

    def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
        return [
            self.safe_integer(ohlcv, 'timestamp'),
            self.safe_number(ohlcv, 'open'),
            self.safe_number(ohlcv, 'high'),
            self.safe_number(ohlcv, 'low'),
            self.safe_number(ohlcv, 'close'),
            self.safe_number(ohlcv, 'volume'),
        ]

    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://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_ohlc_data.md
        :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)
        resolution = self.safe_string(self.timeframes, timeframe, timeframe)
        request: dict = {
            'pair': market['id'],
            'resolution': resolution,
        }
        # we need to have a limit argument because "to" and "from" are required
        if limit is None:
            limit = 500
        else:
            limit = min(limit, 75000)  # supports slightly more than 75k candles atm, but limit here to avoid errors
        timeframeInSeconds = self.parse_timeframe(timeframe)
        alignedSince = None
        if since is None:
            request['to'] = self.seconds()
            request['from'] = request['to'] - (limit * timeframeInSeconds)
        else:
            timeframeInMilliseconds = timeframeInSeconds * 1000
            alignedSince = int(math.floor(since / timeframeInMilliseconds)) * timeframeInMilliseconds
            request['from'] = int(math.floor(since / 1000))
            request['to'] = self.sum(request['from'], limit * timeframeInSeconds)
        response = await self.publicGetTradingHistoryPair(self.extend(request, params))
        data = self.safe_value(response, 'data', [])
        #
        #     {
        #         "data":[
        #             {
        #                 "timestamp":1644581100000,
        #                 "open":"1214737",
        #                 "high":"1215110",
        #                 "low":"1214737",
        #                 "close":"1215110",
        #                 "volume":"0.08423959"
        #             }
        #         ]
        #     }
        #
        sparse = self.parse_ohlcvs(data, market, timeframe, since, limit)
        return self.insert_missing_candles(sparse, timeframeInSeconds, alignedSince, limit)

    def insert_missing_candles(self, candles, distance, since, limit):
        # the exchange doesn't send zero volume candles so we emulate them instead
        # otherwise sending a limit arg leads to unexpected results
        length = len(candles)
        if length == 0:
            return candles
        result = []
        copyFrom = candles[0]
        timestamp = None
        if since is None:
            timestamp = copyFrom[0]
        else:
            timestamp = since
        i = 0
        candleLength = len(candles)
        resultLength = 0
        while((resultLength < limit) and (i < candleLength)):
            candle = candles[i]
            if candle[0] == timestamp:
                result.append(candle)
                i = self.sum(i, 1)
            else:
                copy = self.array_concat([], copyFrom)
                copy[0] = timestamp
                # set open, high, low to close
                copy[1] = copy[4]
                copy[2] = copy[4]
                copy[3] = copy[4]
                copy[5] = self.parse_number('0')
                result.append(copy)
            timestamp = self.sum(timestamp, distance * 1000)
            resultLength = len(result)
            copyFrom = result[resultLength - 1]
        return result

    def parse_balance(self, response) -> Balances:
        #
        #     [{
        #         "currency":"twd",
        #         "amount":"0",
        #         "available":"0",
        #         "stake":"0",
        #         "tradable":true
        #     }]
        #
        result: dict = {
            'info': response,
        }
        for i in range(0, len(response)):
            balance = response[i]
            currencyId = self.safe_string(balance, 'currency')
            code = self.safe_currency_code(currencyId)
            amount = self.safe_string(balance, 'amount')
            available = self.safe_string(balance, 'available')
            account: dict = {
                'free': available,
                'total': amount,
            }
            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://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_account_balance.md
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `balance structure <https://docs.ccxt_zagrava.com/#/?id=balance-structure>`
        """
        await self.load_markets()
        response = await self.privateGetAccountsBalance(params)
        balances = self.safe_value(response, 'data', [])
        #
        #     {
        #         "data":[
        #             {
        #                 "currency":"twd",
        #                 "amount":"0",
        #                 "available":"0",
        #                 "stake":"0",
        #                 "tradable":true
        #             }
        #         ]
        #     }
        #
        return self.parse_balance(balances)

    def parse_order_status(self, status: Str):
        statuses: dict = {
            '-1': 'open',
            '0': 'open',
            '1': 'open',
            '2': 'closed',
            '3': 'closed',
            '4': 'canceled',
            '6': 'canceled',
        }
        return self.safe_string(statuses, status, None)

    def parse_order(self, order: dict, market: Market = None) -> Order:
        #
        # createOrder
        #         {
        #             "orderId": "2220595581",
        #             "timestamp": "1644896744886",
        #             "action": "SELL",
        #             "amount": "0.01",
        #             "price": "15000",
        #             "timeInForce": "GTC"
        #         }
        #
        # fetchOrder
        #         {
        #             "id":"8777138788",
        #             "pair":"bnb_twd",
        #             "price":"16000",
        #             "avgExecutionPrice":"0",
        #             "action":"SELL",
        #             "type":"LIMIT",
        #             "timestamp":1644899002598,
        #             "status":4,
        #             "originalAmount":"0.01",
        #             "remainingAmount":"0.01",
        #             "executedAmount":"0",
        #             "fee":"0",
        #             "feeSymbol":"twd",
        #             "bitoFee":"0",
        #             "total":"0",
        #             "seq":"BNBTWD548774666",
        #             "timeInForce":"GTC",
        #             "createdTimestamp":1644898944074,
        #             "updatedTimestamp":1644899002598
        #         }
        #
        id = self.safe_string_2(order, 'id', 'orderId')
        timestamp = self.safe_integer_2(order, 'timestamp', 'createdTimestamp')
        side = self.safe_string(order, 'action')
        side = side.lower()
        amount = self.safe_string_2(order, 'amount', 'originalAmount')
        price = self.safe_string(order, 'price')
        marketId = self.safe_string(order, 'pair')
        market = self.safe_market(marketId, market, '_')
        symbol = self.safe_string(market, 'symbol')
        orderStatus = self.safe_string(order, 'status')
        status = self.parse_order_status(orderStatus)
        type = self.safe_string_lower(order, 'type')
        average = self.safe_string(order, 'avgExecutionPrice')
        filled = self.safe_string(order, 'executedAmount')
        remaining = self.safe_string(order, 'remainingAmount')
        timeInForce = self.safe_string(order, 'timeInForce')
        postOnly = None
        if timeInForce == 'POST_ONLY':
            postOnly = True
        fee = None
        feeAmount = self.safe_string(order, 'fee')
        feeSymbol = self.safe_currency_code(self.safe_string(order, 'feeSymbol'))
        if Precise.string_gt(feeAmount, '0'):
            fee = {
                'currency': feeSymbol,
                'cost': feeAmount,
            }
        return self.safe_order({
            'id': id,
            'clientOrderId': None,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': self.safe_integer(order, 'updatedTimestamp'),
            'symbol': symbol,
            'type': type,
            'timeInForce': timeInForce,
            'postOnly': postOnly,
            'side': side,
            'price': price,
            'stopPrice': None,
            'triggerPrice': None,
            'amount': amount,
            'cost': None,
            'average': average,
            'filled': filled,
            'remaining': remaining,
            'status': status,
            'fee': fee,
            'trades': None,
            'info': order,
        }, market)

    async def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
        """
        create a trade order
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/create_an_order.md
        :param str symbol: unified symbol of the market to create an order in
        :param str type: 'market' or 'limit'
        :param str side: 'buy' or 'sell'
        :param float amount: how much of currency you want to trade in units of base currency
        :param float [price]: the price at which the order is to be fulfilled, in units of the quote currency, ignored in market orders
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: an `order structure <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'type': type,
            'pair': market['id'],
            'action': side,
            'amount': self.amount_to_precision(symbol, amount),
            'timestamp': self.milliseconds(),
        }
        orderType = type.upper()
        if orderType == 'LIMIT':
            request['price'] = self.price_to_precision(symbol, price)
        if orderType == 'STOP_LIMIT':
            request['price'] = self.price_to_precision(symbol, price)
            stopPrice = self.safe_value_2(params, 'triggerPrice', 'stopPrice')
            params = self.omit(params, ['triggerPrice', 'stopPrice'])
            if stopPrice is None:
                raise InvalidOrder(self.id + ' createOrder() requires a stopPrice parameter for ' + orderType + ' orders')
            else:
                request['stopPrice'] = self.price_to_precision(symbol, stopPrice)
            condition = self.safe_string(params, 'condition')
            if condition is None:
                raise InvalidOrder(self.id + ' createOrder() requires a condition parameter for ' + orderType + ' orders')
            else:
                request['condition'] = condition
        postOnly = self.is_post_only(orderType == 'MARKET', None, params)
        if postOnly:
            request['timeInForce'] = 'POST_ONLY'
        response = await self.privatePostOrdersPair(self.extend(request, params))
        #
        #     {
        #         "orderId": "2220595581",
        #         "timestamp": "1644896744886",
        #         "action": "SELL",
        #         "amount": "0.01",
        #         "price": "15000",
        #         "timeInForce": "GTC"
        #     }
        #
        return self.parse_order(response, market)

    async def cancel_order(self, id: str, symbol: Str = None, params={}):
        """
        cancels an open order
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/cancel_an_order.md
        :param str id: order id
        :param str symbol: unified symbol of the market the order was made in
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: An `order structure <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument')
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'id': id,
            'pair': market['id'],
        }
        response = await self.privateDeleteOrdersPairId(self.extend(request, params))
        #
        #     {
        #         "orderId":"8777138788",
        #         "action":"SELL",
        #         "timestamp":1644899002465,
        #         "price":"16000",
        #         "amount":"0.01"
        #     }
        #
        return self.parse_order(response, market)

    def parse_cancel_orders(self, data):
        dataKeys = list(data.keys())
        orders = []
        for i in range(0, len(dataKeys)):
            marketId = dataKeys[i]
            orderIds = data[marketId]
            for j in range(0, len(orderIds)):
                orders.append(self.safe_order({
                    'info': orderIds[j],
                    'id': orderIds[j],
                    'symbol': self.safe_symbol(marketId),
                }))
        return orders

    async def cancel_orders(self, ids, symbol: Str = None, params={}):
        """
        cancel multiple orders
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/cancel_batch_orders.md
        :param str[] ids: order ids
        :param str symbol: unified market symbol
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: an list of `order structures <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument')
        await self.load_markets()
        market = self.market(symbol)
        id = market['uppercaseId']
        request: dict = {}
        request[id] = ids
        response = await self.privatePutOrders(self.extend(request, params))
        #
        #     {
        #         "data":{
        #             "BNB_TWD":[
        #                 "5236347105",
        #                 "359488711"
        #             ]
        #         }
        #     }
        #
        data = self.safe_dict(response, 'data')
        return self.parse_cancel_orders(data)

    async def cancel_all_orders(self, symbol: Str = None, params={}):
        """
        cancel all open orders
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/cancel_all_orders.md
        :param str symbol: unified market symbol, only orders in the market of self symbol are cancelled when symbol is not None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: a list of `order structures <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        await self.load_markets()
        request: dict = {
            # 'pair': market['id'],  # optional
        }
        response = None
        if symbol is not None:
            market = self.market(symbol)
            request['pair'] = market['id']
            response = await self.privateDeleteOrdersPair(self.extend(request, params))
        else:
            response = await self.privateDeleteOrdersAll(self.extend(request, params))
        data = self.safe_value(response, 'data', {})
        #
        #     {
        #         "data":{
        #             "BNB_TWD":[
        #                 "9515988421",
        #                 "4639130027"
        #             ]
        #         }
        #     }
        #
        return self.parse_cancel_orders(data)

    async def fetch_order(self, id: str, symbol: Str = None, params={}):
        """
        fetches information on an order made by the user
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_an_order_data.md
        :param str symbol: unified symbol of the market the order was made in
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: An `order structure <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' fetchOrder() requires a symbol argument')
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'orderId': id,
            'pair': market['id'],
        }
        response = await self.privateGetOrdersPairOrderId(self.extend(request, params))
        #
        #     {
        #         "id":"8777138788",
        #         "pair":"bnb_twd",
        #         "price":"16000",
        #         "avgExecutionPrice":"0",
        #         "action":"SELL",
        #         "type":"LIMIT",
        #         "timestamp":1644899002598,
        #         "status":4,
        #         "originalAmount":"0.01",
        #         "remainingAmount":"0.01",
        #         "executedAmount":"0",
        #         "fee":"0",
        #         "feeSymbol":"twd",
        #         "bitoFee":"0",
        #         "total":"0",
        #         "seq":"BNBTWD548774666",
        #         "timeInForce":"GTC",
        #         "createdTimestamp":1644898944074,
        #         "updatedTimestamp":1644899002598
        #     }
        #
        return self.parse_order(response, market)

    async def fetch_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        fetches information on multiple orders made by the user
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_orders_data.md
        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns Order[]: a list of `order structures <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' fetchOrders() requires a symbol argument')
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'pair': market['id'],
            # 'startTimestamp': 0,
            # 'endTimestamp': 0,
            # 'statusKind': '',
            # 'orderId': '',
        }
        if since is not None:
            request['startTimestamp'] = since
        if limit is not None:
            request['limit'] = limit
        response = await self.privateGetOrdersAllPair(self.extend(request, params))
        orders = self.safe_value(response, 'data')
        if orders is None:
            orders = []
        #
        #     {
        #         "data":[
        #             {
        #                 "id":"2220595581",
        #                 "pair":"bnb_twd",
        #                 "price":"15000",
        #                 "avgExecutionPrice":"0",
        #                 "action":"SELL",
        #                 "type":"LIMIT",
        #                 "createdTimestamp":1644896744886,
        #                 "updatedTimestamp":1644898706236,
        #                 "status":4,
        #                 "originalAmount":"0.01",
        #                 "remainingAmount":"0.01",
        #                 "executedAmount":"0",
        #                 "fee":"0",
        #                 "feeSymbol":"twd",
        #                 "bitoFee":"0",
        #                 "total":"0",
        #                 "seq":"BNBTWD8540871774",
        #                 "timeInForce":"GTC"
        #             }
        #         ]
        #     }
        #
        return self.parse_orders(orders, market, since, limit)

    async def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
        request: dict = {
            'statusKind': 'OPEN',
        }
        return await self.fetch_orders(symbol, since, limit, self.extend(request, params))

    async def fetch_closed_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        fetches information on multiple closed orders made by the user
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_orders_data.md
        :param str symbol: unified market symbol of the market orders were made in
        :param int [since]: the earliest time in ms to fetch orders for
        :param int [limit]: the maximum number of order structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns Order[]: a list of `order structures <https://docs.ccxt_zagrava.com/#/?id=order-structure>`
        """
        request: dict = {
            'statusKind': 'DONE',
        }
        return self.fetch_orders(symbol, since, limit, self.extend(request, params))

    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://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_trades_data.md
        :param str symbol: unified market symbol
        :param int [since]: the earliest time in ms to fetch trades for
        :param int [limit]: the maximum number of trades structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns Trade[]: a list of `trade structures <https://docs.ccxt_zagrava.com/#/?id=trade-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' fetchMyTrades() requires a symbol argument')
        await self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'pair': market['id'],
        }
        response = await self.privateGetOrdersTradesPair(self.extend(request, params))
        trades = self.safe_value(response, 'data', [])
        #
        #     {
        #         "data":[
        #             {
        #                 "tradeId":"5685030251",
        #                 "orderId":"9669168142",
        #                 "price":"11821.8",
        #                 "action":"SELL",
        #                 "baseAmount":"0.01",
        #                 "quoteAmount":"118.218",
        #                 "fee":"0.236436",
        #                 "feeSymbol":"BNB",
        #                 "isTaker":true,
        #                 "timestamp":1644905714862,
        #                 "createdTimestamp":1644905714862
        #             }
        #         ]
        #     }
        #
        return self.parse_trades(trades, market, since, limit)

    def parse_transaction_status(self, status: Str):
        states: dict = {
            'COMPLETE': 'ok',
            'INVALID': 'failed',
            'PROCESSING': 'pending',
            'WAIT_PROCESS': 'pending',
            'FAILED': 'failed',
            'EXPIRED': 'failed',
            'CANCELLED': 'failed',
            'EMAIL_VERIFICATION': 'pending',
            'WAIT_CONFIRMATION': 'pending',
        }
        return self.safe_string(states, status, status)

    def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
        #
        # fetchDeposits
        #
        #    {
        #        "serial": "20220214X766799",
        #        "timestamp": "1644833015053",
        #        "address": "bnb1xml62k5a9dcewgc542fha75fyxdcp0zv8eqfsh",
        #        "amount": "0.20000000",
        #        "fee": "0.00000000",
        #        "total": "0.20000000",
        #        "status": "COMPLETE",
        #        "txid": "A3CC4F6828CC752B9F3737F48B5826B9EC2857040CB5141D0CC955F7E53DB6D9",
        #        "message": "778553959",
        #        "protocol": "MAIN",
        #        "id": "2905906537"
        #    }
        #
        # fetchWithdrawals or fetchWithdraw
        #
        #    {
        #        "serial": "20220215BW14069838",
        #        "timestamp": "1644907716044",
        #        "address": "TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w",
        #        "amount": "8.00000000",
        #        "fee": "2.00000000",
        #        "total": "10.00000000",
        #        "status": "COMPLETE",
        #        "txid": "50bf250c71a582f40cf699fb58bab978437ea9bdf7259ff8072e669aab30c32b",
        #        "protocol": "TRX",
        #        "id": "9925310345"
        #    }
        #
        # withdraw
        #
        #    {
        #        "serial": "20220215BW14069838",
        #        "currency": "USDT",
        #        "protocol": "TRX",
        #        "address": "TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w",
        #        "amount": "8",
        #        "fee": "2",
        #        "total": "10"
        #    }
        #
        currencyId = self.safe_string(transaction, 'coin')
        code = self.safe_currency_code(currencyId, currency)
        timestamp = self.safe_integer(transaction, 'timestamp')
        address = self.safe_string(transaction, 'address')
        tag = self.safe_string(transaction, 'message')
        status = self.safe_string(transaction, 'status')
        networkId = self.safe_string(transaction, 'protocol')
        if networkId == 'MAIN':
            networkId = code
        return {
            'info': transaction,
            'id': self.safe_string(transaction, 'serial'),
            'txid': self.safe_string(transaction, 'txid'),
            'type': None,
            'currency': code,
            'network': self.network_id_to_code(networkId),
            'amount': self.safe_number(transaction, 'total'),
            'status': self.parse_transaction_status(status),
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'address': address,
            'addressFrom': None,
            'addressTo': address,
            'tag': tag,
            'tagFrom': None,
            'tagTo': tag,
            'updated': None,
            'comment': None,
            'internal': None,
            'fee': {
                'currency': code,
                'cost': self.safe_number(transaction, 'fee'),
                'rate': None,
            },
        }

    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://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_deposit_invoices_data.md
        :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_zagrava.com/#/?id=transaction-structure>`
        """
        if code is None:
            raise ArgumentsRequired(self.id + ' fetchDeposits() requires the code argument')
        await self.load_markets()
        currency = self.safe_currency(code)
        request: dict = {
            'currency': currency['id'],
            # 'endTimestamp': 0,
            # 'id': '',
            # 'statuses': '',  # 'ROCESSING,COMPLETE,INVALID,WAIT_PROCESS,CANCELLED,FAILED'
        }
        if since is not None:
            request['startTimestamp'] = since
        if limit is not None:
            request['limit'] = limit
        response = await self.privateGetWalletDepositHistoryCurrency(self.extend(request, params))
        result = self.safe_value(response, 'data', [])
        #
        #     {
        #         "data":[
        #             {
        #                 "serial":"20220214X766799",
        #                 "timestamp":"1644833015053",
        #                 "address":"bnb1xml62k5a9dcewgc542fha75fyxdcp0zv8eqfsh",
        #                 "amount":"0.20000000",
        #                 "fee":"0.00000000",
        #                 "total":"0.20000000",
        #                 "status":"COMPLETE",
        #                 "txid":"A3CC4F6828CC752B9F3737F48B5826B9EC2857040CB5141D0CC955F7E53DB6D9",
        #                 "message":"778553959",
        #                 "protocol":"MAIN",
        #                 "id":"2905906537"
        #             }
        #         ]
        #     }
        #
        return self.parse_transactions(result, currency, since, limit, {'type': 'deposit'})

    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://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_withdraw_invoices_data.md
        :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_zagrava.com/#/?id=transaction-structure>`
        """
        if code is None:
            raise ArgumentsRequired(self.id + ' fetchWithdrawals() requires the code argument')
        await self.load_markets()
        currency = self.safe_currency(code)
        request: dict = {
            'currency': currency['id'],
            # 'endTimestamp': 0,
            # 'id': '',
            # 'statuses': '',  # 'PROCESSING,COMPLETE,EXPIRED,INVALID,WAIT_PROCESS,WAIT_CONFIRMATION,EMAIL_VERIFICATION,CANCELLED'
        }
        if since is not None:
            request['startTimestamp'] = since
        if limit is not None:
            request['limit'] = limit
        response = await self.privateGetWalletWithdrawHistoryCurrency(self.extend(request, params))
        result = self.safe_value(response, 'data', [])
        #
        #     {
        #         "data":[
        #             {
        #                 "serial":"20220215BW14069838",
        #                 "timestamp":"1644907716044",
        #                 "address":"TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w",
        #                 "amount":"8.00000000",
        #                 "fee":"2.00000000",
        #                 "total":"10.00000000",
        #                 "status":"COMPLETE",
        #                 "txid":"50bf250c71a582f40cf699fb58bab978437ea9bdf7259ff8072e669aab30c32b",
        #                 "protocol":"TRX",
        #                 "id":"9925310345"
        #             }
        #         ]
        #     }
        #
        return self.parse_transactions(result, currency, since, limit, {'type': 'withdrawal'})

    async def fetch_withdrawal(self, id: str, code: Str = None, params={}):
        """
        fetch data on a currency withdrawal via the withdrawal id
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/get_an_withdraw_invoice_data.md
        :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_zagrava.com/#/?id=transaction-structure>`
        """
        if code is None:
            raise ArgumentsRequired(self.id + ' fetchWithdrawal() requires the code argument')
        await self.load_markets()
        currency = self.safe_currency(code)
        request: dict = {
            'serial': id,
            'currency': currency['id'],
        }
        response = await self.privateGetWalletWithdrawCurrencySerial(self.extend(request, params))
        result = self.safe_value(response, 'data', {})
        #
        #     {
        #         "data":{
        #             "serial":"20220215BW14069838",
        #             "address":"TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w",
        #             "amount":"8.00000000",
        #             "fee":"2.00000000",
        #             "total":"10.00000000",
        #             "status":"COMPLETE",
        #             "txid":"50bf250c71a582f40cf699fb58bab978437ea9bdf7259ff8072e669aab30c32b",
        #             "protocol":"TRX",
        #             "id":"9925310345",
        #             "timestamp":"1644907716044"
        #         }
        #     }
        #
        return self.parse_transaction(result, currency)

    async def withdraw(self, code: str, amount: float, address: str, tag=None, params={}):
        """
        make a withdrawal
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/private/create_an_withdraw_invoice.md
        :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_zagrava.com/#/?id=transaction-structure>`
        """
        tag, params = self.handle_withdraw_tag_and_params(tag, params)
        await self.load_markets()
        self.check_address(address)
        currency = self.currency(code)
        request: dict = {
            'currency': currency['id'],
            'amount': self.number_to_string(amount),
            'address': address,
        }
        if 'network' in params:
            networks = self.safe_value(self.options, 'networks', {})
            requestedNetwork = self.safe_string_upper(params, 'network')
            params = self.omit(params, ['network'])
            networkId = self.safe_string(networks, requestedNetwork)
            if networkId is None:
                raise ExchangeError(self.id + ' invalid network ' + requestedNetwork)
            request['protocol'] = networkId
        if tag is not None:
            request['message'] = tag
        response = await self.privatePostWalletWithdrawCurrency(self.extend(request, params))
        result = self.safe_value(response, 'data', {})
        #
        #     {
        #         "data":{
        #             "serial":"20220215BW14069838",
        #             "currency":"USDT",
        #             "protocol":"TRX",
        #             "address":"TKrwMaZaGiAvtXCFT41xHuusNcs4LPWS7w",
        #             "amount":"8",
        #             "fee":"2",
        #             "total":"10"
        #         }
        #     }
        #
        return self.parse_transaction(result, currency)

    def parse_deposit_withdraw_fee(self, fee, currency: Currency = None):
        #    {
        #        "currency":"eth",
        #        "withdrawFee":"0.007",
        #        "minWithdraw":"0.001",
        #        "maxWithdraw":"1000",
        #        "maxDailyWithdraw":"2000",
        #        "withdraw":true,
        #        "deposit":true,
        #        "depositConfirmation":"12"
        #    }
        return {
            'info': fee,
            'withdraw': {
                'fee': self.safe_number(fee, 'withdrawFee'),
                'percentage': False,
            },
            'deposit': {
                'fee': None,
                'percentage': None,
            },
            'networks': {},
        }

    async def fetch_deposit_withdraw_fees(self, codes: Strings = None, params={}):
        """
        fetch deposit and withdraw fees
        :see: https://github.com/bitoex/bitopro-offical-api-docs/blob/master/api/v3/public/get_currency_info.md
        :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 `fee structures <https://docs.ccxt_zagrava.com/#/?id=fee-structure>`
        """
        await self.load_markets()
        response = await self.publicGetProvisioningCurrencies(params)
        #
        #     {
        #         "data":[
        #             {
        #                 "currency":"eth",
        #                 "withdrawFee":"0.007",
        #                 "minWithdraw":"0.001",
        #                 "maxWithdraw":"1000",
        #                 "maxDailyWithdraw":"2000",
        #                 "withdraw":true,
        #                 "deposit":true,
        #                 "depositConfirmation":"12"
        #             }
        #         ]
        #     }
        #
        data = self.safe_list(response, 'data', [])
        return self.parse_deposit_withdraw_fees(data, codes, 'currency')

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        url = '/' + self.implode_params(path, params)
        query = self.omit(params, self.extract_params(path))
        if headers is None:
            headers = {}
        headers['X-BITOPRO-API'] = 'ccxt_zagrava'
        if api == 'private':
            self.check_required_credentials()
            if method == 'POST' or method == 'PUT':
                body = self.json(params)
                payload = self.string_to_base64(body)
                signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384)
                headers['X-BITOPRO-APIKEY'] = self.apiKey
                headers['X-BITOPRO-PAYLOAD'] = payload
                headers['X-BITOPRO-SIGNATURE'] = signature
            elif method == 'GET' or method == 'DELETE':
                if query:
                    url += '?' + self.urlencode(query)
                nonce = self.milliseconds()
                rawData: dict = {
                    'nonce': nonce,
                }
                data = self.json(rawData)
                payload = self.string_to_base64(data)
                signature = self.hmac(self.encode(payload), self.encode(self.secret), hashlib.sha384)
                headers['X-BITOPRO-APIKEY'] = self.apiKey
                headers['X-BITOPRO-PAYLOAD'] = payload
                headers['X-BITOPRO-SIGNATURE'] = signature
        elif api == 'public' and method == 'GET':
            if query:
                url += '?' + self.urlencode(query)
        url = self.urls['api']['rest'] + url
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def handle_errors(self, code: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
        if response is None:
            return None  # fallback to the default error handler
        if code >= 200 and code < 300:
            return None
        feedback = self.id + ' ' + body
        error = self.safe_string(response, 'error')
        self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback)
        self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback)
        raise ExchangeError(feedback)  # unknown message
