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

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

from ccxt.base.exchange import Exchange
from ccxt.abstract.blofin import ImplicitAPI
import hashlib
from ccxt.base.types import Balances, Currency, Int, Leverage, Leverages, MarginMode, Market, Num, Order, OrderBook, OrderRequest, OrderSide, OrderType, Position, Str, Strings, Ticker, Tickers, Trade, TradingFeeInterface, Transaction, TransferEntry
from typing import List
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
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 RateLimitExceeded
from ccxt.base.errors import ExchangeNotAvailable
from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise


class blofin(Exchange, ImplicitAPI):

    def describe(self):
        return self.deep_extend(super(blofin, self).describe(), {
            'id': 'blofin',
            'name': 'BloFin',
            'countries': ['US'],
            'version': 'v1',
            'rateLimit': 100,
            'has': {
                'CORS': None,
                'spot': False,
                'margin': False,
                'swap': True,
                'future': False,
                'option': False,
                'addMargin': False,
                'borrowMargin': False,
                'cancelAllOrders': False,
                'cancelOrder': True,
                'cancelOrders': True,
                'closeAllPositions': False,
                'closePosition': True,
                'createDepositAddress': False,
                'createMarketBuyOrderWithCost': False,
                'createMarketSellOrderWithCost': False,
                'createOrder': True,
                'createOrders': True,
                'createOrderWithTakeProfitAndStopLoss': True,
                'createPostOnlyOrder': False,
                'createReduceOnlyOrder': False,
                'createStopLimitOrder': False,
                'createStopLossOrder': True,
                'createStopMarketOrder': False,
                'createStopOrder': False,
                'createTakeProfitOrder': True,
                'editOrder': False,
                'fetchAccounts': False,
                'fetchBalance': True,
                'fetchBidsAsks': None,
                'fetchBorrowInterest': False,
                'fetchBorrowRateHistories': False,
                'fetchBorrowRateHistory': False,
                'fetchCanceledOrders': False,
                'fetchClosedOrder': False,
                'fetchClosedOrders': False,
                'fetchCrossBorrowRate': False,
                'fetchCrossBorrowRates': False,
                'fetchCurrencies': False,
                'fetchDeposit': False,
                'fetchDepositAddress': False,
                'fetchDepositAddresses': False,
                'fetchDepositAddressesByNetwork': False,
                'fetchDeposits': True,
                'fetchDepositsWithdrawals': False,
                'fetchDepositWithdrawFee': 'emulated',
                'fetchDepositWithdrawFees': False,
                'fetchFundingHistory': True,
                'fetchFundingRate': True,
                'fetchFundingRateHistory': True,
                'fetchFundingRates': False,
                'fetchGreeks': False,
                'fetchIndexOHLCV': False,
                'fetchIsolatedBorrowRate': False,
                'fetchIsolatedBorrowRates': False,
                'fetchL3OrderBook': False,
                'fetchLedger': True,
                'fetchLedgerEntry': None,
                'fetchLeverage': True,
                'fetchLeverages': True,
                'fetchLeverageTiers': False,
                'fetchMarginMode': True,
                'fetchMarginModes': False,
                'fetchMarketLeverageTiers': False,
                'fetchMarkets': True,
                'fetchMarkOHLCV': False,
                'fetchMySettlementHistory': False,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenInterest': False,
                'fetchOpenInterestHistory': False,
                'fetchOpenOrder': None,
                'fetchOpenOrders': True,
                'fetchOrder': True,
                'fetchOrderBook': True,
                'fetchOrderBooks': False,
                'fetchOrders': False,
                'fetchOrderTrades': True,
                'fetchPermissions': None,
                'fetchPosition': True,
                'fetchPositions': True,
                'fetchPositionsForSymbol': False,
                'fetchPositionsRisk': False,
                'fetchPremiumIndexOHLCV': False,
                'fetchSettlementHistory': False,
                'fetchStatus': False,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTime': False,
                'fetchTrades': True,
                'fetchTradingFee': False,
                'fetchTradingFees': False,
                'fetchTradingLimits': False,
                'fetchTransactionFee': False,
                'fetchTransactionFees': False,
                'fetchTransactions': False,
                'fetchTransfer': False,
                'fetchTransfers': False,
                'fetchUnderlyingAssets': False,
                'fetchVolatilityHistory': False,
                'fetchWithdrawal': False,
                'fetchWithdrawals': True,
                'fetchWithdrawalWhitelist': False,
                'reduceMargin': False,
                'repayCrossMargin': False,
                'setLeverage': True,
                'setMargin': False,
                'setMarginMode': False,
                'setPositionMode': False,
                'signIn': False,
                'transfer': True,
                'withdraw': False,
            },
            'timeframes': {
                '1m': '1m',
                '3m': '3m',
                '5m': '5m',
                '15m': '15m',
                '30m': '30m',
                '1h': '1H',
                '2h': '2H',
                '4h': '4H',
                '6h': '6H',
                '12h': '12H',
                '1d': '1D',
                '1w': '1W',
                '1M': '1M',
                '3M': '3M',
            },
            'hostname': 'www.blofin.com',
            'urls': {
                'logo': 'https://github.com/ccxt/ccxt/assets/43336371/255a7b29-341f-4d20-8342-fbfae4932807',
                'api': {
                    'rest': 'https://openapi.blofin.com',
                },
                'referral': {
                    'url': 'https://blofin.com/register?referral_code=jBd8U1',
                    'discount': 0.05,
                },
                'www': 'https://www.blofin.com',
                'doc': 'https://blofin.com/docs',
            },
            'api': {
                'public': {
                    'get': {
                        'market/instruments': 1,
                        'market/tickers': 1,
                        'market/books': 1,
                        'market/trades': 1,
                        'market/candles': 1,
                        'market/mark-price': 1,
                        'market/funding-rate': 1,
                        'market/funding-rate-history': 1,
                    },
                },
                'private': {
                    'get': {
                        'asset/balances': 1,
                        'trade/orders-pending': 1,
                        'trade/fills-history': 1,
                        'asset/deposit-history': 1,
                        'asset/withdrawal-history': 1,
                        'asset/bills': 1,
                        'account/balance': 1,
                        'account/positions': 1,
                        'account/leverage-info': 1,
                        'account/margin-mode': 1,
                        'account/batch-leverage-info': 1,
                        'trade/orders-tpsl-pending': 1,
                        'trade/orders-history': 1,
                        'trade/orders-tpsl-history': 1,
                        'user/query-apikey': 1,
                        'affiliate/basic': 1,
                    },
                    'post': {
                        'trade/order': 1,
                        'trade/cancel-order': 1,
                        'account/set-leverage': 1,
                        'trade/batch-orders': 1,
                        'trade/order-tpsl': 1,
                        'trade/cancel-batch-orders': 1,
                        'trade/cancel-tpsl': 1,
                        'trade/close-position': 1,
                        'asset/transfer': 1,
                    },
                },
            },
            'fees': {
                'swap': {
                    'taker': self.parse_number('0.00060'),
                    'maker': self.parse_number('0.00020'),
                },
            },
            'requiredCredentials': {
                'apiKey': True,
                'secret': True,
                'password': True,
            },
            'exceptions': {
                'exact': {
                    '400': BadRequest,  # Body can not be empty
                    '401': AuthenticationError,  # Invalid signature
                    '500': ExchangeError,  # Internal Server Error
                    '404': BadRequest,  # not found
                    '405': BadRequest,  # Method Not Allowed
                    '406': BadRequest,  # Not Acceptable
                    '429': RateLimitExceeded,  # Too Many Requests
                    '152001': BadRequest,  # Parameter {} cannot be empty
                    '152002': BadRequest,  # Parameter {} error
                    '152003': BadRequest,  # Either parameter {} or {} is required
                    '152004': BadRequest,  # JSON syntax error
                    '152005': BadRequest,  # Parameter error: wrong or empty
                    '152006': InvalidOrder,  # Batch orders can be placed for up to 20 at once
                    '152007': InvalidOrder,  # Batch orders can only be placed with the same instId and marginMode
                    '152008': InvalidOrder,  # Only the same field is allowed for bulk cancellation of orders, orderId is preferred
                    '152009': InvalidOrder,  # {} must be a combination of numbers, letters, or underscores, and the maximum length of characters is 32
                    '150003': InvalidOrder,  # clientId already exist
                    '150004': InvalidOrder,  # Insufficient balance. please adjust the amount and try again
                    '542': InvalidOrder,  # Exceeded the maximum order size limit
                    '102002': InvalidOrder,  # Duplicate customized order ID
                    '102005': InvalidOrder,  # Position had been closed
                    '102014': InvalidOrder,  # Limit order exceeds maximum order size limit
                    '102015': InvalidOrder,  # Market order exceeds maximum order size limit
                    '102022': InvalidOrder,  # Failed to place order. You don’t have any positions of self contract. Turn off Reduce-only to continue.
                    '102037': InvalidOrder,  # TP trigger price should be higher than the latest trading price
                    '102038': InvalidOrder,  # SL trigger price should be lower than the latest trading price
                    '102039': InvalidOrder,  # TP trigger price should be lower than the latest trading price
                    '102040': InvalidOrder,  # SL trigger price should be higher than the latest trading price
                    '102047': InvalidOrder,  # Stop loss trigger price should be higher than the order price
                    '102048': InvalidOrder,  # stop loss trigger price must be higher than the best bid price
                    '102049': InvalidOrder,  # Take profit trigger price should be lower than the order price
                    '102050': InvalidOrder,  # stop loss trigger price must be lower than the best ask price
                    '102051': InvalidOrder,  # stop loss trigger price should be lower than the order price
                    '102052': InvalidOrder,  # take profit trigger price should be higher than the order price
                    '102053': InvalidOrder,  # take profit trigger price should be lower than the best bid price
                    '102054': InvalidOrder,  # take profit trigger price should be higher than the best ask price
                    '102055': InvalidOrder,  # stop loss trigger price should be lower than the best ask price
                    '102064': BadRequest,  # Buy price is not within the price limit(Minimum: 310.40; Maximum:1,629.40)
                    '102065': BadRequest,  # Sell price is not within the price limit
                    '102068': BadRequest,  # Cancel failed order has been filled, triggered, canceled or does not exist
                    '103013': ExchangeError,  # Internal error; unable to process your request. Please try again.
                    'Order failed. Insufficient USDT margin in account': InsufficientFunds,  # Insufficient USDT margin in account
                },
                'broad': {
                    'Internal Server Error': ExchangeNotAvailable,  # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"Internal Server Error","msg":"Internal Server Error"}
                    'server error': ExchangeNotAvailable,  # {"code":500,"data":{},"detailMsg":"","error_code":"500","error_message":"server error 1236805249","msg":"server error 1236805249"}
                },
            },
            'httpExceptions': {
                '429': ExchangeNotAvailable,  # https://github.com/ccxt/ccxt/issues/9612
            },
            'precisionMode': TICK_SIZE,
            'options': {
                'brokerId': 'ec6dd3a7dd982d0b',
                'accountsByType': {
                    'swap': 'futures',
                    'future': 'futures',
                },
                'accountsById': {
                    'futures': 'swap',
                },
                'sandboxMode': False,
                'defaultNetwork': 'ERC20',
                'defaultNetworks': {
                    'ETH': 'ERC20',
                    'BTC': 'BTC',
                    'USDT': 'TRC20',
                },
                'networks': {
                    'BTC': 'Bitcoin',
                    'BEP20': 'BSC',
                    'ERC20': 'ERC20',
                    'TRC20': 'TRC20',
                },
                'fetchOpenInterestHistory': {
                    'timeframes': {
                        '5m': '5m',
                        '1h': '1H',
                        '8h': '8H',
                        '1d': '1D',
                        '5M': '5m',
                        '1H': '1H',
                        '8H': '8H',
                        '1D': '1D',
                    },
                },
                'fetchOHLCV': {
                    # 'type': 'Candles',  # Candles or HistoryCandles, IndexCandles, MarkPriceCandles
                    'timezone': 'UTC',  # UTC, HK
                },
                'fetchPositions': {
                    'method': 'privateGetAccountPositions',  # privateGetAccountPositions or privateGetAccountPositionsHistory
                },
                'createOrder': 'privatePostTradeOrder',  # or 'privatePostTradeOrderTpsl'
                'createMarketBuyOrderRequiresPrice': False,
                'fetchMarkets': ['swap'],
                'defaultType': 'swap',
                'fetchLedger': {
                    'method': 'privateGetAssetBills',
                },
                'fetchOpenOrders': {
                    'method': 'privateGetTradeOrdersPending',
                },
                'cancelOrders': {
                    'method': 'privatePostTradeCancelBatchOrders',
                },
                'fetchCanceledOrders': {
                    'method': 'privateGetTradeOrdersHistory',  # privateGetTradeOrdersTpslHistory
                },
                'fetchClosedOrders': {
                    'method': 'privateGetTradeOrdersHistory',  # privateGetTradeOrdersTpslHistory
                },
                'withdraw': {
                    # a funding password credential is required by the exchange for the
                    # withdraw call(not to be confused with the api password credential)
                    'password': None,
                    'pwd': None,  # password or pwd both work
                },
                'exchangeType': {
                    'spot': 'SPOT',
                    'swap': 'SWAP',
                    'SPOT': 'SPOT',
                    'SWAP': 'SWAP',
                },
            },
        })

    def fetch_markets(self, params={}) -> List[Market]:
        """
        retrieves data on all markets for blofin
        :see: https://blofin.com/docs#get-instruments
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict[]: an array of objects representing market data
        """
        response = self.publicGetMarketInstruments(params)
        data = self.safe_list(response, 'data', [])
        return self.parse_markets(data)

    def parse_market(self, market: dict) -> Market:
        id = self.safe_string(market, 'instId')
        type = self.safe_string_lower(market, 'instType')
        spot = (type == 'spot')
        future = (type == 'future')
        swap = (type == 'swap')
        option = (type == 'option')
        contract = swap or future
        baseId = self.safe_string(market, 'baseCurrency')
        quoteId = self.safe_string(market, 'quoteCurrency')
        settleId = self.safe_string(market, 'quoteCurrency')
        settle = self.safe_currency_code(settleId)
        base = self.safe_currency_code(baseId)
        quote = self.safe_currency_code(quoteId)
        symbol = base + '/' + quote
        if swap:
            symbol = symbol + ':' + settle
        expiry = None
        strikePrice = None
        optionType = None
        tickSize = self.safe_string(market, 'tickSize')
        fees = self.safe_dict_2(self.fees, type, 'trading', {})
        taker = self.safe_number(fees, 'taker')
        maker = self.safe_number(fees, 'maker')
        maxLeverage = self.safe_string(market, 'maxLeverage', '100')
        maxLeverage = Precise.string_max(maxLeverage, '1')
        isActive = (self.safe_string(market, 'state') == 'live')
        return self.safe_market_structure({
            'id': id,
            'symbol': symbol,
            'base': base,
            'quote': quote,
            'baseId': baseId,
            'quoteId': quoteId,
            'settle': settle,
            'settleId': settleId,
            'type': type,
            'spot': spot,
            'option': option,
            'margin': spot and (Precise.string_gt(maxLeverage, '1')),
            'swap': swap,
            'future': future,
            'active': isActive,
            'taker': taker,
            'maker': maker,
            'contract': contract,
            'linear': (quoteId == settleId) if contract else None,
            'inverse': (baseId == settleId) if contract else None,
            'contractSize': self.safe_number(market, 'contractValue') if contract else None,
            'expiry': expiry,
            'expiryDatetime': expiry,
            'strike': strikePrice,
            'optionType': optionType,
            'created': self.safe_integer(market, 'listTime'),
            'precision': {
                'amount': self.safe_number(market, 'lotSize'),
                'price': self.parse_number(tickSize),
            },
            'limits': {
                'leverage': {
                    'min': self.parse_number('1'),
                    'max': self.parse_number(maxLeverage),
                },
                'amount': {
                    'min': self.safe_number(market, 'minSize'),
                    'max': None,
                },
                'price': {
                    'min': None,
                    'max': None,
                },
                'cost': {
                    'min': None,
                    'max': None,
                },
            },
            'info': market,
        })

    def fetch_order_book(self, symbol: str, limit: Int = None, params={}) -> OrderBook:
        """
        fetches information on open orders with bid(buy) and ask(sell) prices, volumes and other data
        :see: https://blofin.com/docs#get-order-book
        :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
        """
        self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'instId': market['id'],
        }
        limit = 50 if (limit is None) else limit
        if limit is not None:
            request['size'] = limit  # max 100
        response = self.publicGetMarketBooks(self.extend(request, params))
        #
        #     {
        #         "code": "0",
        #         "msg": "",
        #         "data": [
        #             {
        #                 "asks": [
        #                     ["0.07228","4.211619","0","2"],  # price, amount, liquidated orders, total open orders
        #                     ["0.0723","299.880364","0","2"],
        #                     ["0.07231","3.72832","0","1"],
        #                 ],
        #                 "bids": [
        #                     ["0.07221","18.5","0","1"],
        #                     ["0.0722","18.5","0","1"],
        #                     ["0.07219","0.505407","0","1"],
        #                 ],
        #                 "ts": "1621438475342"
        #             }
        #         ]
        #     }
        #
        data = self.safe_list(response, 'data', [])
        first = self.safe_dict(data, 0, {})
        timestamp = self.safe_integer(first, 'ts')
        return self.parse_order_book(first, symbol, timestamp)

    def parse_ticker(self, ticker: dict, market: Market = None) -> Ticker:
        timestamp = self.safe_integer(ticker, 'ts')
        marketId = self.safe_string(ticker, 'instId')
        market = self.safe_market(marketId, market, '-')
        symbol = market['symbol']
        last = self.safe_string(ticker, 'last')
        open = self.safe_string(ticker, 'open24h')
        spot = self.safe_bool(market, 'spot', False)
        quoteVolume = self.safe_string(ticker, 'volCurrency24h') if spot else None
        baseVolume = self.safe_string(ticker, 'vol24h')
        high = self.safe_string(ticker, 'high24h')
        low = self.safe_string(ticker, 'low24h')
        return self.safe_ticker({
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': high,
            'low': low,
            'bid': self.safe_string(ticker, 'bidPrice'),
            'bidVolume': self.safe_string(ticker, 'bidSize'),
            'ask': self.safe_string(ticker, 'askPrice'),
            'askVolume': self.safe_string(ticker, 'askSize'),
            'vwap': None,
            'open': open,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': None,
            'percentage': None,
            'average': None,
            'baseVolume': baseVolume,
            'quoteVolume': quoteVolume,
            'info': ticker,
        }, market)

    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://blofin.com/docs#get-tickers
        :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>`
        """
        self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'instId': market['id'],
        }
        response = self.publicGetMarketTickers(self.extend(request, params))
        data = self.safe_list(response, 'data', [])
        first = self.safe_dict(data, 0, {})
        return self.parse_ticker(first, market)

    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://blofin.com/docs#get-tickers
        :param str[] [symbols]: unified symbols of the markets to fetch the ticker for, all market tickers are returned if not assigned
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a dictionary of `ticker structures <https://docs.ccxt.com/#/?id=ticker-structure>`
        """
        self.load_markets()
        symbols = self.market_symbols(symbols)
        response = self.publicGetMarketTickers(params)
        tickers = self.safe_list(response, 'data', [])
        return self.parse_tickers(tickers, symbols)

    def parse_trade(self, trade: dict, market: Market = None) -> Trade:
        #
        # fetch trades
        #   {
        #       "tradeId": "3263934920",
        #       "instId": "LTC-USDT",
        #       "price": "67.87",
        #       "size": "1",
        #       "side": "buy",
        #       "ts": "1707232020854"
        #   }
        # my trades
        #   {
        #       "instId": "LTC-USDT",
        #       "tradeId": "1440847",
        #       "orderId": "2075705202",
        #       "fillPrice": "67.850000000000000000",
        #       "fillSize": "1.000000000000000000",
        #       "fillPnl": "0.000000000000000000",
        #       "side": "buy",
        #       "positionSide": "net",
        #       "fee": "0.040710000000000000",
        #       "ts": "1707224678878",
        #       "brokerId": ""
        #   }
        #
        id = self.safe_string(trade, 'tradeId')
        marketId = self.safe_string(trade, 'instId')
        market = self.safe_market(marketId, market, '-')
        symbol = market['symbol']
        timestamp = self.safe_integer(trade, 'ts')
        price = self.safe_string_2(trade, 'price', 'fillPrice')
        amount = self.safe_string_2(trade, 'size', 'fillSize')
        side = self.safe_string(trade, 'side')
        orderId = self.safe_string(trade, 'orderId')
        feeCost = self.safe_string(trade, 'fee')
        fee = None
        if feeCost is not None:
            fee = {
                'cost': feeCost,
                'currency': market['settle'],
            }
        return self.safe_trade({
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'id': id,
            'order': orderId,
            'type': None,
            'takerOrMaker': None,
            'side': side,
            'price': price,
            'amount': amount,
            'cost': None,
            'fee': fee,
        }, market)

    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://blofin.com/docs#get-trades
        :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
        :param boolean [params.paginate]: *only applies to publicGetMarketHistoryTrades* default False, when True will automatically paginate by calling self endpoint multiple times
        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=public-trades>`
        """
        self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchTrades', 'paginate')
        if paginate:
            return self.fetch_paginated_call_cursor('fetchTrades', symbol, since, limit, params, 'tradeId', 'after', None, 100)
        market = self.market(symbol)
        request: dict = {
            'instId': market['id'],
        }
        response = None
        if limit is not None:
            request['limit'] = limit  # default 100
        method = None
        method, params = self.handle_option_and_params(params, 'fetchTrades', 'method', 'publicGetMarketTrades')
        if method == 'publicGetMarketTrades':
            response = self.publicGetMarketTrades(self.extend(request, params))
        data = self.safe_list(response, 'data', [])
        return self.parse_trades(data, market, since, limit)

    def parse_ohlcv(self, ohlcv, market: Market = None) -> list:
        #
        #     [
        #         "1678928760000",  # timestamp
        #         "24341.4",  # open
        #         "24344",  # high
        #         "24313.2",  # low
        #         "24323",  # close
        #         "628",  # contract volume
        #         "2.5819",  # base volume
        #         "62800",  # quote volume
        #         "0"  # candlestick state
        #     ]
        #
        return [
            self.safe_integer(ohlcv, 0),
            self.safe_number(ohlcv, 1),
            self.safe_number(ohlcv, 2),
            self.safe_number(ohlcv, 3),
            self.safe_number(ohlcv, 4),
            self.safe_number(ohlcv, 6),
        ]

    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://blofin.com/docs#get-candlesticks
        :param str symbol: unified symbol of the market to fetch OHLCV data for
        :param str timeframe: the length of time each candle represents
        :param int [since]: timestamp in ms of the earliest candle to fetch
        :param int [limit]: the maximum amount of candles to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param int [params.until]: timestamp in ms of the latest candle to fetch
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
        :returns int[][]: A list of candles ordered, open, high, low, close, volume
        """
        self.load_markets()
        market = self.market(symbol)
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchOHLCV', 'paginate')
        if paginate:
            return self.fetch_paginated_call_deterministic('fetchOHLCV', symbol, since, limit, timeframe, params, 100)
        if limit is None:
            limit = 100  # default 100, max 100
        request: dict = {
            'instId': market['id'],
            'bar': self.safe_string(self.timeframes, timeframe, timeframe),
            'limit': limit,
        }
        until = self.safe_integer(params, 'until')
        if until is not None:
            request['after'] = until
            params = self.omit(params, 'until')
        response = None
        response = self.publicGetMarketCandles(self.extend(request, params))
        data = self.safe_list(response, 'data', [])
        return self.parse_ohlcvs(data, market, timeframe, since, limit)

    def fetch_funding_rate_history(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        fetches historical funding rate prices
        :see: https://blofin.com/docs#get-funding-rate-history
        :param str symbol: unified symbol of the market to fetch the funding rate history for
        :param int [since]: timestamp in ms of the earliest funding rate to fetch
        :param int [limit]: the maximum amount of `funding rate structures <https://docs.ccxt.com/#/?id=funding-rate-history-structure>` to fetch
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
        :returns dict[]: a list of `funding rate structures <https://docs.ccxt.com/#/?id=funding-rate-history-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' fetchFundingRateHistory() requires a symbol argument')
        self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchFundingRateHistory', 'paginate')
        if paginate:
            return self.fetch_paginated_call_deterministic('fetchFundingRateHistory', symbol, since, limit, '8h', params)
        market = self.market(symbol)
        request: dict = {
            'instId': market['id'],
        }
        if since is not None:
            request['before'] = max(since - 1, 0)
        if limit is not None:
            request['limit'] = limit
        response = self.publicGetMarketFundingRateHistory(self.extend(request, params))
        rates = []
        data = self.safe_list(response, 'data', [])
        for i in range(0, len(data)):
            rate = data[i]
            timestamp = self.safe_integer(rate, 'fundingTime')
            rates.append({
                'info': rate,
                'symbol': market['symbol'],
                'fundingRate': self.safe_number(rate, 'fundingRate'),
                'timestamp': timestamp,
                'datetime': self.iso8601(timestamp),
            })
        sorted = self.sort_by(rates, 'timestamp')
        return self.filter_by_symbol_since_limit(sorted, market['symbol'], since, limit)

    def parse_funding_rate(self, contract, market: Market = None):
        #
        #    {
        #        "fundingRate": "0.00027815",
        #        "fundingTime": "1634256000000",
        #        "instId": "BTC-USD-SWAP",
        #        "instType": "SWAP",
        #        "nextFundingRate": "0.00017",
        #        "nextFundingTime": "1634284800000"
        #    }
        #
        # in the response above nextFundingRate is actually two funding rates from now
        #
        nextFundingRateTimestamp = self.safe_integer(contract, 'nextFundingTime')
        marketId = self.safe_string(contract, 'instId')
        symbol = self.safe_symbol(marketId, market)
        nextFundingRate = self.safe_number(contract, 'nextFundingRate')
        fundingTime = self.safe_integer(contract, 'fundingTime')
        # > The current interest is 0.
        return {
            'info': contract,
            'symbol': symbol,
            'markPrice': None,
            'indexPrice': None,
            'interestRate': self.parse_number('0'),
            'estimatedSettlePrice': None,
            'timestamp': None,
            'datetime': None,
            'fundingRate': self.safe_number(contract, 'fundingRate'),
            'fundingTimestamp': fundingTime,
            'fundingDatetime': self.iso8601(fundingTime),
            'nextFundingRate': nextFundingRate,
            'nextFundingTimestamp': nextFundingRateTimestamp,
            'nextFundingDatetime': self.iso8601(nextFundingRateTimestamp),
            'previousFundingRate': None,
            'previousFundingTimestamp': None,
            'previousFundingDatetime': None,
        }

    def fetch_funding_rate(self, symbol: str, params={}):
        """
        fetch the current funding rate
        :see: https://blofin.com/docs#get-funding-rate
        :param str symbol: unified market symbol
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `funding rate structure <https://docs.ccxt.com/#/?id=funding-rate-structure>`
        """
        self.load_markets()
        market = self.market(symbol)
        if not market['swap']:
            raise ExchangeError(self.id + ' fetchFundingRate() is only valid for swap markets')
        request: dict = {
            'instId': market['id'],
        }
        response = self.publicGetMarketFundingRate(self.extend(request, params))
        #
        #    {
        #        "code": "0",
        #        "data": [
        #            {
        #                "fundingRate": "0.00027815",
        #                "fundingTime": "1634256000000",
        #                "instId": "BTC-USD-SWAP",
        #                "instType": "SWAP",
        #                "nextFundingRate": "0.00017",
        #                "nextFundingTime": "1634284800000"
        #            }
        #        ],
        #        "msg": ""
        #    }
        #
        data = self.safe_list(response, 'data', [])
        entry = self.safe_dict(data, 0, {})
        return self.parse_funding_rate(entry, market)

    def parse_balance_by_type(self, type, response):
        if type:
            return self.parse_funding_balance(response)
        else:
            return self.parse_trading_balance(response)

    def parse_trading_balance(self, response):
        #
        # {
        #     "code": "0",
        #     "msg": "success",
        #     "data": {
        #         "ts": "1697021343571",
        #         "totalEquity": "10011254.077985990315787910",
        #         "isolatedEquity": "861.763132108800000000",
        #         "details": [
        #             {
        #                 "currency": "USDT",
        #                 "equity": "10014042.988958415234430699548",
        #                 "balance": "10013119.885958415234430699",
        #                 "ts": "1697021343571",
        #                 "isolatedEquity": "862.003200000000000000048",
        #                 "available": "9996399.4708691159703362725",
        #                 "availableEquity": "9996399.4708691159703362725",
        #                 "frozen": "15805.149672632597427761",
        #                 "orderFrozen": "14920.994472632597427761",
        #                 "equityUsd": "10011254.077985990315787910",
        #                 "isolatedUnrealizedPnl": "-22.151999999999999999952",
        #                 "bonus": "0"
        #             }
        #         ]
        #     }
        # }
        #
        result: dict = {'info': response}
        data = self.safe_dict(response, 'data', {})
        timestamp = self.safe_integer(data, 'ts')
        details = self.safe_list(data, 'details', [])
        for i in range(0, len(details)):
            balance = details[i]
            currencyId = self.safe_string(balance, 'currency')
            code = self.safe_currency_code(currencyId)
            account = self.account()
            # it may be incorrect to use total, free and used for swap accounts
            eq = self.safe_string(balance, 'equity')
            availEq = self.safe_string(balance, 'available')
            if (eq is None) or (availEq is None):
                account['free'] = self.safe_string(balance, 'availableEquity')
                account['used'] = self.safe_string(balance, 'frozen')
            else:
                account['total'] = eq
                account['free'] = availEq
            result[code] = account
        result['timestamp'] = timestamp
        result['datetime'] = self.iso8601(timestamp)
        return self.safe_balance(result)

    def parse_funding_balance(self, response):
        #
        #  {
        #      "code": "0",
        #      "msg": "success",
        #      "data": [
        #          {
        #              "currency": "USDT",
        #              "balance": "10012514.919418081548717298",
        #              "available": "9872132.414278782284622898",
        #              "frozen": "138556.471805965930761067",
        #              "bonus": "0"
        #          }
        #      ]
        #  }
        #
        result: dict = {'info': response}
        data = self.safe_list(response, 'data', [])
        for i in range(0, len(data)):
            balance = data[i]
            currencyId = self.safe_string(balance, 'currency')
            code = self.safe_currency_code(currencyId)
            account = self.account()
            # it may be incorrect to use total, free and used for swap accounts
            account['total'] = self.safe_string(balance, 'balance')
            account['free'] = self.safe_string(balance, 'available')
            account['used'] = self.safe_string(balance, 'frozen')
            result[code] = account
        return self.safe_balance(result)

    def parse_trading_fee(self, fee: dict, market: Market = None) -> TradingFeeInterface:
        return {
            'info': fee,
            'symbol': self.safe_symbol(None, market),
            # blofin returns the fees values opposed to other exchanges, so the sign needs to be flipped
            'maker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'maker', 'makerU'))),
            'taker': self.parse_number(Precise.string_neg(self.safe_string_2(fee, 'taker', 'takerU'))),
            'percentage': None,
            'tierBased': None,
        }

    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://blofin.com/docs#get-balance
        :see: https://blofin.com/docs#get-futures-account-balance
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.accountType]: the type of account to fetch the balance for, either 'funding' or 'futures'  or 'copy_trading' or 'earn'
        :returns dict: a `balance structure <https://docs.ccxt.com/#/?id=balance-structure>`
        """
        self.load_markets()
        accountType = self.safe_string_2(params, 'accountType', 'type')
        params = self.omit(params, ['accountType', 'type'])
        request: dict = {
        }
        response = None
        if accountType is not None:
            options = self.safe_dict(self.options, 'accountsByType', {})
            parsedAccountType = self.safe_string(options, accountType, accountType)
            request['accountType'] = parsedAccountType
            response = self.privateGetAssetBalances(self.extend(request, params))
        else:
            response = self.privateGetAccountBalance(self.extend(request, params))
        return self.parse_balance_by_type(accountType, response)

    def create_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}):
        market = self.market(symbol)
        request: dict = {
            'instId': market['id'],
            'side': side,
            'orderType': type,
            'size': self.amount_to_precision(symbol, amount),
            'brokerId': self.safe_string(self.options, 'brokerId', 'ec6dd3a7dd982d0b'),
        }
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('createOrder', params, 'cross')
        request['marginMode'] = marginMode
        timeInForce = self.safe_string(params, 'timeInForce', 'GTC')
        isMarketOrder = type == 'market'
        params = self.omit(params, ['timeInForce'])
        ioc = (timeInForce == 'IOC') or (type == 'ioc')
        marketIOC = (isMarketOrder and ioc)
        if isMarketOrder or marketIOC:
            request['orderType'] = 'market'
        else:
            request['price'] = self.price_to_precision(symbol, price)
        postOnly = False
        postOnly, params = self.handle_post_only(isMarketOrder, type == 'post_only', params)
        if postOnly:
            request['type'] = 'post_only'
        stopLoss = self.safe_dict(params, 'stopLoss')
        takeProfit = self.safe_dict(params, 'takeProfit')
        params = self.omit(params, ['stopLoss', 'takeProfit'])
        isStopLoss = stopLoss is not None
        isTakeProfit = takeProfit is not None
        if isStopLoss or isTakeProfit:
            if isStopLoss:
                slTriggerPrice = self.safe_string_2(stopLoss, 'triggerPrice', 'stopPrice')
                request['slTriggerPrice'] = self.price_to_precision(symbol, slTriggerPrice)
                slOrderPrice = self.safe_string(stopLoss, 'price', '-1')
                request['slOrderPrice'] = self.price_to_precision(symbol, slOrderPrice)
            if isTakeProfit:
                tpTriggerPrice = self.safe_string_2(takeProfit, 'triggerPrice', 'stopPrice')
                request['tpTriggerPrice'] = self.price_to_precision(symbol, tpTriggerPrice)
                tpPrice = self.safe_string(takeProfit, 'price', '-1')
                request['tpOrderPrice'] = self.price_to_precision(symbol, tpPrice)
        return self.extend(request, params)

    def parse_order_status(self, status: Str):
        statuses: dict = {
            'canceled': 'canceled',
            'order_failed': 'canceled',
            'live': 'open',
            'partially_filled': 'open',
            'filled': 'closed',
            'effective': 'closed',
        }
        return self.safe_string(statuses, status, status)

    def parse_order(self, order: dict, market: Market = None) -> Order:
        #
        # {
        #     "orderId": "2075628533",
        #     "clientOrderId": "",
        #     "instId": "LTC-USDT",
        #     "marginMode": "cross",
        #     "positionSide": "net",
        #     "side": "buy",
        #     "orderType": "market",
        #     "price": "0.000000000000000000",
        #     "size": "1.000000000000000000",
        #     "reduceOnly": "true",
        #     "leverage": "3",
        #     "state": "filled",
        #     "filledSize": "1.000000000000000000",
        #     "pnl": "-0.050000000000000000",
        #     "averagePrice": "68.110000000000000000",
        #     "fee": "0.040866000000000000",
        #     "createTime": "1706891359010",
        #     "updateTime": "1706891359098",
        #     "orderCategory": "normal",
        #     "tpTriggerPrice": null,
        #     "tpOrderPrice": null,
        #     "slTriggerPrice": null,
        #     "slOrderPrice": null,
        #     "cancelSource": "not_canceled",
        #     "cancelSourceReason": null,
        #     "brokerId": "ec6dd3a7dd982d0b"
        # }
        #
        id = self.safe_string_2(order, 'tpslId', 'orderId')
        timestamp = self.safe_integer(order, 'createTime')
        lastUpdateTimestamp = self.safe_integer(order, 'updateTime')
        lastTradeTimestamp = self.safe_integer(order, 'fillTime')
        side = self.safe_string(order, 'side')
        type = self.safe_string(order, 'orderType')
        postOnly = None
        timeInForce = None
        if type == 'post_only':
            postOnly = True
            type = 'limit'
        elif type == 'fok':
            timeInForce = 'FOK'
            type = 'limit'
        elif type == 'ioc':
            timeInForce = 'IOC'
            type = 'limit'
        marketId = self.safe_string(order, 'instId')
        market = self.safe_market(marketId, market)
        symbol = self.safe_symbol(marketId, market, '-')
        filled = self.safe_string(order, 'filledSize')
        price = self.safe_string_2(order, 'px', 'price')
        average = self.safe_string(order, 'averagePrice')
        status = self.parse_order_status(self.safe_string(order, 'state'))
        feeCostString = self.safe_string(order, 'fee')
        amount = self.safe_string(order, 'size')
        leverage = self.safe_string(order, 'leverage', '1')
        contractSize = self.safe_string(market, 'contractSize')
        baseAmount = Precise.string_mul(contractSize, filled)
        cost: Str = None
        if average is not None:
            cost = Precise.string_mul(average, baseAmount)
            cost = Precise.string_div(cost, leverage)
        # spot market buy: "sz" can refer either to base currency units or to quote currency units
        fee = None
        if feeCostString is not None:
            feeCostSigned = Precise.string_abs(feeCostString)
            feeCurrencyId = self.safe_string(order, 'feeCcy', 'USDT')
            feeCurrencyCode = self.safe_currency_code(feeCurrencyId)
            fee = {
                'cost': self.parse_number(feeCostSigned),
                'currency': feeCurrencyCode,
            }
        clientOrderId = self.safe_string(order, 'clientOrderId')
        if (clientOrderId is not None) and (len(clientOrderId) < 1):
            clientOrderId = None  # fix empty clientOrderId string
        stopLossTriggerPrice = self.safe_number(order, 'slTriggerPrice')
        stopLossPrice = self.safe_number(order, 'slOrderPrice')
        takeProfitTriggerPrice = self.safe_number(order, 'tpTriggerPrice')
        takeProfitPrice = self.safe_number(order, 'tpOrderPrice')
        reduceOnlyRaw = self.safe_string(order, 'reduceOnly')
        reduceOnly = (reduceOnlyRaw == 'true')
        return self.safe_order({
            'info': order,
            'id': id,
            'clientOrderId': clientOrderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'lastUpdateTimestamp': lastUpdateTimestamp,
            'symbol': symbol,
            'type': type,
            'timeInForce': timeInForce,
            'postOnly': postOnly,
            'side': side,
            'price': price,
            'stopLossTriggerPrice': stopLossTriggerPrice,
            'takeProfitTriggerPrice': takeProfitTriggerPrice,
            'stopLossPrice': stopLossPrice,
            'takeProfitPrice': takeProfitPrice,
            'average': average,
            'cost': cost,
            'amount': amount,
            'filled': filled,
            'remaining': None,
            'status': status,
            'fee': fee,
            'trades': None,
            'reduceOnly': reduceOnly,
        }, market)

    def create_order(self, symbol: str, type: OrderType, side: OrderSide, amount: float, price: Num = None, params={}) -> Order:
        """
        create a trade order
        :see: https://blofin.com/docs#place-order
        :see: https://blofin.com/docs#place-tpsl-order
        :param str symbol: unified symbol of the market to create an order in
        :param str type: 'market' or 'limit' or 'post_only' or 'ioc' or 'fok'
        :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 bool [params.reduceOnly]: a mark to reduce the position size for margin, swap and future orders
        :param bool [params.postOnly]: True to place a post only order
        :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross'
        :param float [params.stopLossPrice]: stop loss trigger price(will use privatePostTradeOrderTpsl)
        :param float [params.takeProfitPrice]: take profit trigger price(will use privatePostTradeOrderTpsl)
        :param str [param.positionSide]: *stopLossPrice/takeProfitPrice orders only* 'long' or 'short' or 'net' default is 'net'
        :param str [params.clientOrderId]: a unique id for the order
        :param dict [params.takeProfit]: *takeProfit object in params* containing the triggerPrice at which the attached take profit order will be triggered
        :param float [params.takeProfit.triggerPrice]: take profit trigger price
        :param float [params.takeProfit.price]: take profit order price(if not provided the order will be a market order)
        :param dict [params.stopLoss]: *stopLoss object in params* containing the triggerPrice at which the attached stop loss order will be triggered
        :param float [params.stopLoss.triggerPrice]: stop loss trigger price
        :param float [params.stopLoss.price]: stop loss order price(if not provided the order will be a market order)
        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        market = self.market(symbol)
        tpsl = self.safe_bool(params, 'tpsl', False)
        params = self.omit(params, 'tpsl')
        method = None
        method, params = self.handle_option_and_params(params, 'createOrder', 'method', 'privatePostTradeOrder')
        isStopLossPriceDefined = self.safe_string(params, 'stopLossPrice') is not None
        isTakeProfitPriceDefined = self.safe_string(params, 'takeProfitPrice') is not None
        isType2Order = (isStopLossPriceDefined or isTakeProfitPriceDefined)
        response = None
        if tpsl or (method == 'privatePostTradeOrderTpsl') or isType2Order:
            tpslRequest = self.create_tpsl_order_request(symbol, type, side, amount, price, params)
            response = self.privatePostTradeOrderTpsl(tpslRequest)
        else:
            request = self.create_order_request(symbol, type, side, amount, price, params)
            response = self.privatePostTradeOrder(request)
        data = self.safe_list(response, 'data', [])
        first = self.safe_dict(data, 0)
        order = self.parse_order(first, market)
        order['type'] = type
        order['side'] = side
        return order

    def create_tpsl_order_request(self, symbol: str, type: OrderType, side: OrderSide, amount: Num = None, price: Num = None, params={}):
        market = self.market(symbol)
        positionSide = self.safe_string(params, 'positionSide', 'net')
        request: dict = {
            'instId': market['id'],
            'side': side,
            'positionSide': positionSide,
            'brokerId': self.safe_string(self.options, 'brokerId', 'ec6dd3a7dd982d0b'),
        }
        if amount is not None:
            request['size'] = self.amount_to_precision(symbol, amount)
        marginMode = self.safe_string(params, 'marginMode', 'cross')  # cross or isolated
        if marginMode != 'cross' and marginMode != 'isolated':
            raise BadRequest(self.id + ' createTpslOrder() requires a marginMode parameter that must be either cross or isolated')
        stopLossPrice = self.safe_string(params, 'stopLossPrice')
        takeProfitPrice = self.safe_string(params, 'takeProfitPrice')
        if stopLossPrice is not None:
            request['slTriggerPrice'] = self.price_to_precision(symbol, stopLossPrice)
            if type == 'market':
                request['slOrderPrice'] = '-1'
            else:
                request['slOrderPrice'] = self.price_to_precision(symbol, price)
        elif takeProfitPrice is not None:
            request['tpTriggerPrice'] = self.price_to_precision(symbol, takeProfitPrice)
            if type == 'market':
                request['tpOrderPrice'] = '-1'
            else:
                request['tpOrderPrice'] = self.price_to_precision(symbol, price)
        request['marginMode'] = marginMode
        params = self.omit(params, ['stopLossPrice', 'takeProfitPrice'])
        return self.extend(request, params)

    def cancel_order(self, id: str, symbol: Str = None, params={}):
        """
        cancels an open order
        :see: https://blofin.com/docs#cancel-order
        :see: https://blofin.com/docs#cancel-tpsl-order
        :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
        :param boolean [params.trigger]: True if cancelling a trigger/conditional order/tp sl orders
        :returns dict: An `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' cancelOrder() requires a symbol argument')
        self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'instId': market['id'],
        }
        isTrigger = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl'], False)
        clientOrderId = self.safe_string(params, 'clientOrderId')
        if clientOrderId is not None:
            request['clientOrderId'] = clientOrderId
        else:
            if not isTrigger:
                request['orderId'] = str(id)
            else:
                request['tpslId'] = str(id)
        query = self.omit(params, ['orderId', 'clientOrderId', 'stop', 'trigger', 'tpsl'])
        if isTrigger:
            tpslResponse = self.cancel_orders([id], symbol, params)
            first = self.safe_dict(tpslResponse, 0)
            return first
        response = self.privatePostTradeCancelOrder(self.extend(request, query))
        data = self.safe_list(response, 'data', [])
        order = self.safe_dict(data, 0)
        return self.parse_order(order, market)

    def create_orders(self, orders: List[OrderRequest], params={}) -> List[Order]:
        """
        create a list of trade orders
        :see: https://blofin.com/docs#place-multiple-orders
        :param Array orders: list of orders to create, each object should contain the parameters required by createOrder, namely symbol, type, side, amount, price and params
        :returns dict: an `order structure <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        ordersRequests = []
        for i in range(0, len(orders)):
            rawOrder = orders[i]
            marketId = self.safe_string(rawOrder, 'symbol')
            type = self.safe_string(rawOrder, 'type')
            side = self.safe_string(rawOrder, 'side')
            amount = self.safe_value(rawOrder, 'amount')
            price = self.safe_value(rawOrder, 'price')
            orderParams = self.safe_dict(rawOrder, 'params', {})
            extendedParams = self.extend(orderParams, params)  # the request does not accept extra params since it's a list, so we're extending each order with the common params
            orderRequest = self.create_order_request(marketId, type, side, amount, price, extendedParams)
            ordersRequests.append(orderRequest)
        response = self.privatePostTradeBatchOrders(ordersRequests)
        data = self.safe_list(response, 'data', [])
        return self.parse_orders(data)

    def fetch_open_orders(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Order]:
        """
        Fetch orders that are still open
        :see: https://blofin.com/docs#get-active-orders
        :see: https://blofin.com/docs#get-active-tpsl-orders
        :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 bool [params.stop]: True if fetching trigger or conditional orders
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
        :returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'paginate')
        if paginate:
            return self.fetch_paginated_call_dynamic('fetchOpenOrders', symbol, since, limit, params)
        request: dict = {
        }
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['instId'] = market['id']
        if limit is not None:
            request['limit'] = limit  # default 100, max 100
        isStop = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl', 'TPSL'], False)
        method: Str = None
        method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersPending')
        query = self.omit(params, ['method', 'stop', 'trigger', 'tpsl', 'TPSL'])
        response = None
        if isStop or (method == 'privateGetTradeOrdersTpslPending'):
            response = self.privateGetTradeOrdersTpslPending(self.extend(request, query))
        else:
            response = self.privateGetTradeOrdersPending(self.extend(request, query))
        data = self.safe_list(response, 'data', [])
        return self.parse_orders(data, market, since, limit)

    def fetch_my_trades(self, symbol: Str = None, since: Int = None, limit: Int = None, params={}) -> List[Trade]:
        """
        fetch all trades made by the user
        :see: https://blofin.com/docs#get-trade-history
        :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
        :param int [params.until]: Timestamp in ms of the latest time to retrieve trades for
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
        :returns Trade[]: a list of `trade structures <https://docs.ccxt.com/#/?id=trade-structure>`
        """
        self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchMyTrades', 'paginate')
        if paginate:
            return self.fetch_paginated_call_dynamic('fetchMyTrades', symbol, since, limit, params)
        request: dict = {
        }
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['instId'] = market['id']
        request, params = self.handle_until_option('end', request, params)
        if limit is not None:
            request['limit'] = limit  # default 100, max 100
        response = self.privateGetTradeFillsHistory(self.extend(request, params))
        data = self.safe_list(response, 'data', [])
        return self.parse_trades(data, market, since, limit)

    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://blofin.com/docs#get-deposite-history
        :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
        :param int [params.until]: the latest time in ms to fetch entries for
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
        :returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
        """
        self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchDeposits', 'paginate')
        if paginate:
            return self.fetch_paginated_call_dynamic('fetchDeposits', code, since, limit, params)
        request: dict = {
        }
        currency = None
        if code is not None:
            currency = self.currency(code)
            request['currency'] = currency['id']
        if since is not None:
            request['before'] = max(since - 1, 0)
        if limit is not None:
            request['limit'] = limit  # default 100, max 100
        request, params = self.handle_until_option('after', request, params)
        response = self.privateGetAssetDepositHistory(self.extend(request, params))
        data = self.safe_list(response, 'data', [])
        return self.parse_transactions(data, currency, since, limit, params)

    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://blofin.com/docs#get-withdraw-history
        :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
        :param int [params.until]: the latest time in ms to fetch entries for
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
        :returns dict[]: a list of `transaction structures <https://docs.ccxt.com/#/?id=transaction-structure>`
        """
        self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchWithdrawals', 'paginate')
        if paginate:
            return self.fetch_paginated_call_dynamic('fetchWithdrawals', code, since, limit, params)
        request: dict = {
        }
        currency = None
        if code is not None:
            currency = self.currency(code)
            request['currency'] = currency['id']
        if since is not None:
            request['before'] = max(since - 1, 0)
        if limit is not None:
            request['limit'] = limit  # default 100, max 100
        request, params = self.handle_until_option('after', request, params)
        response = self.privateGetAssetWithdrawalHistory(self.extend(request, params))
        data = self.safe_list(response, 'data', [])
        return self.parse_transactions(data, currency, since, limit, params)

    def fetch_ledger(self, code: Str = None, since: Int = None, limit: Int = None, params={}):
        """
        fetch the history of changes, actions done by the user or operations that altered balance of the user
        :see: https://blofin.com/docs#get-funds-transfer-history
        :param str code: unified currency code, default is None
        :param int [since]: timestamp in ms of the earliest ledger entry, default is None
        :param int [limit]: max number of ledger entrys to return, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.marginMode]: 'cross' or 'isolated'
        :param int [params.until]: the latest time in ms to fetch entries for
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
        :returns dict: a `ledger structure <https://docs.ccxt.com/#/?id=ledger-structure>`
        """
        self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchLedger', 'paginate')
        if paginate:
            return self.fetch_paginated_call_dynamic('fetchLedger', code, since, limit, params)
        request: dict = {
        }
        if limit is not None:
            request['limit'] = limit
        currency = None
        if code is not None:
            currency = self.currency(code)
            request['currency'] = currency['id']
        request, params = self.handle_until_option('end', request, params)
        response = None
        response = self.privateGetAssetBills(self.extend(request, params))
        data = self.safe_list(response, 'data', [])
        return self.parse_ledger(data, currency, since, limit)

    def parse_transaction(self, transaction: dict, currency: Currency = None) -> Transaction:
        #
        #
        # fetchDeposits
        #
        #     {
        #         "currency": "USDT",
        #         "chain": "TRC20",
        #         "address": "TGfJLtnsh3B9EqekFEBZ1nR14QanBUf5Bi",
        #         "txId": "892f4e0c32268b29b2e541ef30d32a30bbf10f902adcc4b1428319ed7c3758fd",
        #         "type": "0",
        #         "amount": "86.975843",
        #         "state": "1",
        #         "ts": "1703163304153",
        #         "tag": null,
        #         "confirm": "16",
        #         "depositId": "36c8e2a7ea184a219de72215a696acaf"
        #     }
        # fetchWithdrawals
        #    {
        #       "currency": "USDT",
        #        "chain": "TRC20",
        #        "address": "TYgB3sVXHPEDQUu288EG1uMFh9Pk2swLgW",
        #        "txId": "1fd5ac52df414d7ea66194cadd9a5b4d2422c2b9720037f66d98207f9858fd96",
        #        "type": "0",
        #        "amount": "9",
        #        "fee": "1",
        #        "feeCurrency": "USDT",
        #        "state": "3",
        #        "clientId": null,
        #        "ts": "1707217439351",
        #        "tag": null,
        #        "memo": null,
        #        "withdrawId": "e0768698cfdf4aee8e54654c3775914b"
        #    }
        #
        type = None
        id = None
        withdrawalId = self.safe_string(transaction, 'withdrawId')
        depositId = self.safe_string(transaction, 'depositId')
        addressTo = self.safe_string(transaction, 'address')
        address = addressTo
        tagTo = self.safe_string(transaction, 'tag')
        if withdrawalId is not None:
            type = 'withdrawal'
            id = withdrawalId
        else:
            id = depositId
            type = 'deposit'
        currencyId = self.safe_string(transaction, 'currency')
        code = self.safe_currency_code(currencyId)
        amount = self.safe_number(transaction, 'amount')
        status = self.parse_transaction_status(self.safe_string(transaction, 'state'))
        txid = self.safe_string(transaction, 'txId')
        timestamp = self.safe_integer(transaction, 'ts')
        feeCurrencyId = self.safe_string(transaction, 'feeCurrency')
        feeCode = self.safe_currency_code(feeCurrencyId)
        feeCost = self.safe_number(transaction, 'fee')
        return {
            'info': transaction,
            'id': id,
            'currency': code,
            'amount': amount,
            'network': None,
            'addressFrom': None,
            'addressTo': addressTo,
            'address': address,
            'tagFrom': None,
            'tagTo': tagTo,
            'tag': tagTo,
            'status': status,
            'type': type,
            'updated': None,
            'txid': txid,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'internal': None,
            'comment': None,
            'fee': {
                'currency': feeCode,
                'cost': feeCost,
            },
        }

    def parse_transaction_status(self, status: Str):
        statuses: dict = {
            '0': 'pending',
            '1': 'ok',
            '2': 'failed',
            '3': 'pending',
        }
        return self.safe_string(statuses, status, status)

    def parse_ledger_entry_type(self, type):
        types: dict = {
            '1': 'transfer',  # transfer
            '2': 'trade',  # trade
            '3': 'trade',  # delivery
            '4': 'rebate',  # auto token conversion
            '5': 'trade',  # liquidation
            '6': 'transfer',  # margin transfer
            '7': 'trade',  # interest deduction
            '8': 'fee',  # funding rate
            '9': 'trade',  # adl
            '10': 'trade',  # clawback
            '11': 'trade',  # system token conversion
        }
        return self.safe_string(types, type, type)

    def parse_ledger_entry(self, item: dict, currency: Currency = None):
        id = self.safe_string(item, 'transferId')
        referenceId = self.safe_string(item, 'clientId')
        fromAccount = self.safe_string(item, 'fromAccount')
        toAccount = self.safe_string(item, 'toAccount')
        type = self.parse_ledger_entry_type(self.safe_string(item, 'type'))
        code = self.safe_currency_code(self.safe_string(item, 'currency'), currency)
        amountString = self.safe_string(item, 'amount')
        amount = self.parse_number(amountString)
        timestamp = self.safe_integer(item, 'ts')
        status = 'ok'
        return {
            'id': id,
            'info': item,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'fromAccount': fromAccount,
            'toAccount': toAccount,
            'type': type,
            'currency': code,
            'amount': amount,
            'clientId': referenceId,  # balance before
            'status': status,
        }

    def parse_ids(self, ids):
        """
         * @ignore
        :param string[]|str ids: order ids
        :returns str[]: list of order ids
        """
        if isinstance(ids, str):
            return ids.split(',')
        else:
            return ids

    def cancel_orders(self, ids, symbol: Str = None, params={}):
        """
        cancel multiple orders
        :see: https://blofin.com/docs#cancel-multiple-orders
        :param str[] ids: order ids
        :param str symbol: unified market symbol
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param boolean [params.trigger]: whether the order is a stop/trigger order
        :returns dict: an list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
        """
        # TODO : the original endpoint signature differs, according to that you can skip individual symbol and assign ids in batch. At self moment, `params` is not being used too.
        if symbol is None:
            raise ArgumentsRequired(self.id + ' cancelOrders() requires a symbol argument')
        self.load_markets()
        market = self.market(symbol)
        request = []
        options = self.safe_dict(self.options, 'cancelOrders', {})
        defaultMethod = self.safe_string(options, 'method', 'privatePostTradeCancelBatchOrders')
        method = self.safe_string(params, 'method', defaultMethod)
        clientOrderIds = self.parse_ids(self.safe_value(params, 'clientOrderId'))
        tpslIds = self.parse_ids(self.safe_value(params, 'tpslId'))
        stop = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl'])
        if stop:
            method = 'privatePostTradeCancelTpsl'
        if clientOrderIds is None:
            ids = self.parse_ids(ids)
            if tpslIds is not None:
                for i in range(0, len(tpslIds)):
                    request.append({
                        'tpslId': tpslIds[i],
                        'instId': market['id'],
                    })
            for i in range(0, len(ids)):
                if stop:
                    request.append({
                        'tpslId': ids[i],
                        'instId': market['id'],
                    })
                else:
                    request.append({
                        'orderId': ids[i],
                        'instId': market['id'],
                    })
        else:
            for i in range(0, len(clientOrderIds)):
                request.append({
                    'instId': market['id'],
                    'clientOrderId': clientOrderIds[i],
                })
        response = None
        if method == 'privatePostTradeCancelTpsl':
            response = self.privatePostTradeCancelTpsl(request)  # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT
        else:
            response = self.privatePostTradeCancelBatchOrders(request)  # * dont self.extend with params, otherwise ARRAY will be turned into OBJECT
        ordersData = self.safe_list(response, 'data', [])
        return self.parse_orders(ordersData, market, None, None, params)

    def transfer(self, code: str, amount: float, fromAccount: str, toAccount: str, params={}) -> TransferEntry:
        """
        transfer currency internally between wallets on the same account
        :see: https://blofin.com/docs#funds-transfer
        :param str code: unified currency code
        :param float amount: amount to transfer
        :param str fromAccount: account to transfer from(funding, swap, copy_trading, earn)
        :param str toAccount: account to transfer to(funding, swap, copy_trading, earn)
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `transfer structure <https://docs.ccxt.com/#/?id=transfer-structure>`
        """
        self.load_markets()
        currency = self.currency(code)
        accountsByType = self.safe_dict(self.options, 'accountsByType', {})
        fromId = self.safe_string(accountsByType, fromAccount, fromAccount)
        toId = self.safe_string(accountsByType, toAccount, toAccount)
        request: dict = {
            'currency': currency['id'],
            'amount': self.currency_to_precision(code, amount),
            'fromAccount': fromId,
            'toAccount': toId,
        }
        response = self.privatePostAssetTransfer(self.extend(request, params))
        data = self.safe_dict(response, 'data', {})
        return self.parse_transfer(data, currency)

    def parse_transfer(self, transfer: dict, currency: Currency = None) -> TransferEntry:
        id = self.safe_string(transfer, 'transferId')
        return {
            'info': transfer,
            'id': id,
            'timestamp': None,
            'datetime': None,
            'currency': None,
            'amount': None,
            'fromAccount': None,
            'toAccount': None,
            'status': None,
        }

    def fetch_position(self, symbol: str, params={}) -> Position:
        """
        fetch data on a single open contract trade position
        :see: https://blofin.com/docs#get-positions
        :param str symbol: unified market symbol of the market the position is held in, default is None
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION
        :returns dict: a `position structure <https://docs.ccxt.com/#/?id=position-structure>`
        """
        self.load_markets()
        market = self.market(symbol)
        request: dict = {
            'instId': market['id'],
        }
        response = self.privateGetAccountPositions(self.extend(request, params))
        data = self.safe_list(response, 'data', [])
        position = self.safe_dict(data, 0)
        if position is None:
            return None
        return self.parse_position(position, market)

    def fetch_positions(self, symbols: Strings = None, params={}) -> List[Position]:
        """
        fetch data on a single open contract trade position
        :see: https://blofin.com/docs#get-positions
        :param str[] [symbols]: list of unified market symbols
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.instType]: MARGIN, SWAP, FUTURES, OPTION
        :returns dict: a `position structure <https://docs.ccxt.com/#/?id=position-structure>`
        """
        self.load_markets()
        symbols = self.market_symbols(symbols)
        response = self.privateGetAccountPositions(params)
        data = self.safe_list(response, 'data', [])
        result = self.parse_positions(data)
        return self.filter_by_array_positions(result, 'symbol', symbols, False)

    def parse_position(self, position: dict, market: Market = None):
        marketId = self.safe_string(position, 'instId')
        market = self.safe_market(marketId, market)
        symbol = market['symbol']
        pos = self.safe_string(position, 'positions')
        contractsAbs = Precise.string_abs(pos)
        side = self.safe_string(position, 'positionSide')
        hedged = side != 'net'
        contracts = self.parse_number(contractsAbs)
        if pos is not None:
            if side == 'net':
                if Precise.string_gt(pos, '0'):
                    side = 'long'
                elif Precise.string_lt(pos, '0'):
                    side = 'short'
                else:
                    side = None
        contractSize = self.safe_number(market, 'contractSize')
        contractSizeString = self.number_to_string(contractSize)
        markPriceString = self.safe_string(position, 'markPrice')
        notionalString = self.safe_string(position, 'notionalUsd')
        if market['inverse']:
            notionalString = Precise.string_div(Precise.string_mul(contractsAbs, contractSizeString), markPriceString)
        notional = self.parse_number(notionalString)
        marginMode = self.safe_string(position, 'marginMode')
        initialMarginString = None
        entryPriceString = self.safe_string(position, 'averagePrice')
        unrealizedPnlString = self.safe_string(position, 'unrealizedPnl')
        leverageString = self.safe_string(position, 'leverage')
        initialMarginPercentage = None
        collateralString = None
        if marginMode == 'cross':
            initialMarginString = self.safe_string(position, 'initialMargin')
            collateralString = Precise.string_add(initialMarginString, unrealizedPnlString)
        elif marginMode == 'isolated':
            initialMarginPercentage = Precise.string_div('1', leverageString)
            collateralString = self.safe_string(position, 'margin')
        maintenanceMarginString = self.safe_string(position, 'maintenanceMargin')
        maintenanceMargin = self.parse_number(maintenanceMarginString)
        maintenanceMarginPercentageString = Precise.string_div(maintenanceMarginString, notionalString)
        if initialMarginPercentage is None:
            initialMarginPercentage = self.parse_number(Precise.string_div(initialMarginString, notionalString, 4))
        elif initialMarginString is None:
            initialMarginString = Precise.string_mul(initialMarginPercentage, notionalString)
        rounder = '0.00005'  # round to closest 0.01%
        maintenanceMarginPercentage = self.parse_number(Precise.string_div(Precise.string_add(maintenanceMarginPercentageString, rounder), '1', 4))
        liquidationPrice = self.safe_number(position, 'liquidationPrice')
        percentageString = self.safe_string(position, 'unrealizedPnlRatio')
        percentage = self.parse_number(Precise.string_mul(percentageString, '100'))
        timestamp = self.safe_integer(position, 'updateTime')
        marginRatio = self.parse_number(Precise.string_div(maintenanceMarginString, collateralString, 4))
        return self.safe_position({
            'info': position,
            'id': None,
            'symbol': symbol,
            'notional': notional,
            'marginMode': marginMode,
            'liquidationPrice': liquidationPrice,
            'entryPrice': self.parse_number(entryPriceString),
            'unrealizedPnl': self.parse_number(unrealizedPnlString),
            'percentage': percentage,
            'contracts': contracts,
            'contractSize': contractSize,
            'markPrice': self.parse_number(markPriceString),
            'lastPrice': None,
            'side': side,
            'hedged': hedged,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastUpdateTimestamp': None,
            'maintenanceMargin': maintenanceMargin,
            'maintenanceMarginPercentage': maintenanceMarginPercentage,
            'collateral': self.parse_number(collateralString),
            'initialMargin': self.parse_number(initialMarginString),
            'initialMarginPercentage': self.parse_number(initialMarginPercentage),
            'leverage': self.parse_number(leverageString),
            'marginRatio': marginRatio,
            'stopLossPrice': None,
            'takeProfitPrice': None,
        })

    def fetch_leverages(self, symbols: Strings = None, params={}) -> Leverages:
        """
        fetch the set leverage for all contract markets
        :see: https://docs.blofin.com/index.html#get-multiple-leverage
        :param str[] symbols: a list of unified market symbols, required on blofin
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.marginMode]: 'cross' or 'isolated'
        :returns dict: a list of `leverage structures <https://docs.ccxt.com/#/?id=leverage-structure>`
        """
        self.load_markets()
        if symbols is None:
            raise ArgumentsRequired(self.id + ' fetchLeverages() requires a symbols argument')
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('fetchLeverages', params)
        if marginMode is None:
            marginMode = self.safe_string(params, 'marginMode', 'cross')  # cross marginMode
        if (marginMode != 'cross') and (marginMode != 'isolated'):
            raise BadRequest(self.id + ' fetchLeverages() requires a marginMode parameter that must be either cross or isolated')
        symbols = self.market_symbols(symbols)
        instIds = ''
        for i in range(0, len(symbols)):
            entry = symbols[i]
            entryMarket = self.market(entry)
            if i > 0:
                instIds = instIds + ',' + entryMarket['id']
            else:
                instIds = instIds + entryMarket['id']
        request: dict = {
            'instId': instIds,
            'marginMode': marginMode,
        }
        response = self.privateGetAccountBatchLeverageInfo(self.extend(request, params))
        #
        #     {
        #         "code": "0",
        #         "msg": "success",
        #         "data": [
        #             {
        #                 "leverage": "3",
        #                 "marginMode": "cross",
        #                 "instId": "BTC-USDT"
        #             },
        #         ]
        #     }
        #
        leverages = self.safe_list(response, 'data', [])
        return self.parse_leverages(leverages, symbols, 'instId')

    def fetch_leverage(self, symbol: str, params={}) -> Leverage:
        """
        fetch the set leverage for a market
        :see: https://docs.blofin.com/index.html#get-leverage
        :param str symbol: unified market symbol
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.marginMode]: 'cross' or 'isolated'
        :returns dict: a `leverage structure <https://docs.ccxt.com/#/?id=leverage-structure>`
        """
        self.load_markets()
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('fetchLeverage', params)
        if marginMode is None:
            marginMode = self.safe_string(params, 'marginMode', 'cross')  # cross marginMode
        if (marginMode != 'cross') and (marginMode != 'isolated'):
            raise BadRequest(self.id + ' fetchLeverage() requires a marginMode parameter that must be either cross or isolated')
        market = self.market(symbol)
        request: dict = {
            'instId': market['id'],
            'marginMode': marginMode,
        }
        response = self.privateGetAccountLeverageInfo(self.extend(request, params))
        #
        #     {
        #         "code": "0",
        #         "msg": "success",
        #         "data": {
        #             "leverage": "3",
        #             "marginMode": "cross",
        #             "instId": "BTC-USDT"
        #         }
        #     }
        #
        data = self.safe_dict(response, 'data', {})
        return self.parse_leverage(data, market)

    def parse_leverage(self, leverage: dict, market: Market = None) -> Leverage:
        marketId = self.safe_string(leverage, 'instId')
        leverageValue = self.safe_integer(leverage, 'leverage')
        return {
            'info': leverage,
            'symbol': self.safe_symbol(marketId, market),
            'marginMode': self.safe_string_lower(leverage, 'marginMode'),
            'longLeverage': leverageValue,
            'shortLeverage': leverageValue,
        }

    def set_leverage(self, leverage: Int, symbol: Str = None, params={}):
        """
        set the level of leverage for a market
        :see: https://blofin.com/docs#set-leverage
        :param int leverage: the rate of leverage
        :param str symbol: unified market symbol
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param str [params.marginMode]: 'cross' or 'isolated'
        :returns dict: response from the exchange
        """
        if symbol is None:
            raise ArgumentsRequired(self.id + ' setLeverage() requires a symbol argument')
        # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS
        # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS
        if (leverage < 1) or (leverage > 125):
            raise BadRequest(self.id + ' setLeverage() leverage should be between 1 and 125')
        self.load_markets()
        market = self.market(symbol)
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('setLeverage', params, 'cross')
        if (marginMode != 'cross') and (marginMode != 'isolated'):
            raise BadRequest(self.id + ' setLeverage() requires a marginMode parameter that must be either cross or isolated')
        request: dict = {
            'leverage': leverage,
            'marginMode': marginMode,
            'instId': market['id'],
        }
        response = self.privatePostAccountSetLeverage(self.extend(request, params))
        return response

    def close_position(self, symbol: str, side: OrderSide = None, params={}) -> Order:
        """
        closes open positions for a market
        :see: https://blofin.com/docs#close-positions
        :param str symbol: Unified CCXT market symbol
        :param str [side]: 'buy' or 'sell', leave in net mode
        :param dict [params]: extra parameters specific to the blofin api endpoint
        :param str [params.clientOrderId]: a unique identifier for the order
        :param str [params.marginMode]: 'cross' or 'isolated', default is 'cross
        :param str [params.code]: *required in the case of closing cross MARGIN position for Single-currency margin* margin currency
         *
         * EXCHANGE SPECIFIC PARAMETERS
        :param boolean [params.autoCxl]: whether any pending orders for closing out needs to be automatically canceled when close position via a market order. False or True, the default is False
        :param str [params.tag]: order tag a combination of case-sensitive alphanumerics, all numbers, or all letters of up to 16 characters
        :returns dict[]: `A list of position structures <https://docs.ccxt.com/#/?id=position-structure>`
        """
        self.load_markets()
        market = self.market(symbol)
        clientOrderId = self.safe_string(params, 'clientOrderId')
        marginMode = None
        marginMode, params = self.handle_margin_mode_and_params('closePosition', params, 'cross')
        request: dict = {
            'instId': market['id'],
            'marginMode': marginMode,
        }
        if clientOrderId is not None:
            request['clientOrderId'] = clientOrderId
        response = self.privatePostTradeClosePosition(self.extend(request, params))
        return self.safe_dict(response, 'data')

    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://blofin.com/docs#get-order-history
        :see: https://blofin.com/docs#get-tpsl-order-history
        :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  orde structures to retrieve
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :param bool [params.stop]: True if fetching trigger or conditional orders
        :param boolean [params.paginate]: default False, when True will automatically paginate by calling self endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-params)
        :returns Order[]: a list of `order structures <https://docs.ccxt.com/#/?id=order-structure>`
        """
        self.load_markets()
        paginate = False
        paginate, params = self.handle_option_and_params(params, 'fetchClosedOrders', 'paginate')
        if paginate:
            return self.fetch_paginated_call_dynamic('fetchClosedOrders', symbol, since, limit, params)
        request: dict = {
        }
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['instId'] = market['id']
        if limit is not None:
            request['limit'] = limit  # default 100, max 100
        if since is not None:
            request['begin'] = since
        isStop = self.safe_bool_n(params, ['stop', 'trigger', 'tpsl', 'TPSL'], False)
        method: Str = None
        method, params = self.handle_option_and_params(params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersHistory')
        query = self.omit(params, ['method', 'stop', 'trigger', 'tpsl', 'TPSL'])
        response = None
        if (isStop) or (method == 'privateGetTradeOrdersTpslHistory'):
            response = self.privateGetTradeOrdersTpslHistory(self.extend(request, query))
        else:
            response = self.privateGetTradeOrdersHistory(self.extend(request, query))
        data = self.safe_list(response, 'data', [])
        return self.parse_orders(data, market, since, limit)

    def fetch_margin_mode(self, symbol: str, params={}) -> MarginMode:
        """
        fetches the margin mode of a trading pair
        :see: https://docs.blofin.com/index.html#get-margin-mode
        :param str symbol: unified symbol of the market to fetch the margin mode for
        :param dict [params]: extra parameters specific to the exchange API endpoint
        :returns dict: a `margin mode structure <https://docs.ccxt.com/#/?id=margin-mode-structure>`
        """
        self.load_markets()
        market = self.market(symbol)
        response = self.privateGetAccountMarginMode(params)
        #
        #     {
        #         "code": "0",
        #         "msg": "success",
        #         "data": {
        #             "marginMode": "cross"
        #         }
        #     }
        #
        data = self.safe_dict(response, 'data', {})
        return self.parse_margin_mode(data, market)

    def parse_margin_mode(self, marginMode: dict, market=None) -> MarginMode:
        return {
            'info': marginMode,
            'symbol': market['symbol'],
            'marginMode': self.safe_string(marginMode, 'marginMode'),
        }

    def handle_errors(self, httpCode: int, reason: str, url: str, method: str, headers: dict, body: str, response, requestHeaders, requestBody):
        if response is None:
            return None  # fallback to default error handler
        #
        # {"code":"152002","msg":"Parameter bar error."}
        #
        code = self.safe_string(response, 'code')
        message = self.safe_string(response, 'msg')
        feedback = self.id + ' ' + body
        if code is not None and code != '0':
            self.throw_exactly_matched_exception(self.exceptions['exact'], message, feedback)
            self.throw_exactly_matched_exception(self.exceptions['exact'], code, feedback)
            self.throw_broadly_matched_exception(self.exceptions['broad'], message, feedback)
            raise ExchangeError(feedback)  # unknown message
        #
        #  {
        #      orderId: null,
        #      clientOrderId: '',
        #      msg: 'Order failed. Insufficient USDT margin in account',
        #      code: '103003'
        #  }
        #
        data = self.safe_list(response, 'data')
        first = self.safe_dict(data, 0)
        insideMsg = self.safe_string(first, 'msg')
        insideCode = self.safe_string(first, 'code')
        if insideCode is not None and insideCode != '0':
            self.throw_exactly_matched_exception(self.exceptions['exact'], insideCode, feedback)
            self.throw_exactly_matched_exception(self.exceptions['exact'], insideMsg, feedback)
            self.throw_broadly_matched_exception(self.exceptions['broad'], insideMsg, feedback)
        return None

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        request = '/api/' + self.version + '/' + self.implode_params(path, params)
        query = self.omit(params, self.extract_params(path))
        url = self.implode_hostname(self.urls['api']['rest']) + request
        # type = self.getPathAuthenticationType(path)
        if api == 'public':
            if not self.is_empty(query):
                url += '?' + self.urlencode(query)
        elif api == 'private':
            self.check_required_credentials()
            timestamp = str(self.milliseconds())
            headers = {
                'ACCESS-KEY': self.apiKey,
                'ACCESS-PASSPHRASE': self.password,
                'ACCESS-TIMESTAMP': timestamp,
                'ACCESS-NONCE': timestamp,
            }
            sign_body = ''
            if method == 'GET':
                if not self.is_empty(query):
                    urlencodedQuery = '?' + self.urlencode(query)
                    url += urlencodedQuery
                    request += urlencodedQuery
            else:
                if not self.is_empty(query):
                    body = self.json(query)
                    sign_body = body
                headers['Content-Type'] = 'application/json'
            auth = request + method + timestamp + timestamp + sign_body
            signature = self.string_to_base64(self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256))
            headers['ACCESS-SIGN'] = signature
        return {'url': url, 'method': method, 'body': body, 'headers': headers}
