<?php

namespace ccxt\async;

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

use Exception; // a common import
use ccxt\async\abstract\blofin as Exchange;
use ccxt\ExchangeError;
use ccxt\ArgumentsRequired;
use ccxt\BadRequest;
use ccxt\Precise;
use \React\Async;
use \React\Promise\PromiseInterface;

class blofin extends Exchange {

    public function describe(): mixed {
        return $this->deep_extend(parent::describe(), array(
            'id' => 'blofin',
            'name' => 'BloFin',
            'countries' => array( 'US' ),
            'version' => 'v1',
            'rateLimit' => 100,
            'pro' => true,
            'has' => array(
                'CORS' => null,
                '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,
                'createTriggerOrder' => true,
                'editOrder' => false,
                'fetchAccounts' => false,
                'fetchBalance' => true,
                'fetchBidsAsks' => null,
                'fetchBorrowInterest' => false,
                'fetchBorrowRateHistories' => false,
                'fetchBorrowRateHistory' => false,
                'fetchCanceledOrders' => false,
                'fetchClosedOrder' => false,
                'fetchClosedOrders' => true,
                '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' => null,
                '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' => null,
                'fetchOpenOrders' => true,
                'fetchOrder' => null,
                'fetchOrderBook' => true,
                'fetchOrderBooks' => false,
                'fetchOrders' => false,
                'fetchOrderTrades' => true,
                'fetchPosition' => true,
                'fetchPositionMode' => 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' => true,
                'setPositionMode' => true,
                'signIn' => false,
                'transfer' => true,
                'withdraw' => false,
            ),
            'timeframes' => array(
                '1m' => '1m',
                '3m' => '3m',
                '5m' => '5m',
                '15m' => '15m',
                '30m' => '30m',
                '1h' => '1H',
                '2h' => '2H',
                '4h' => '4H',
                '6h' => '6H',
                '8h' => '8H',
                '12h' => '12H',
                '1d' => '1D',
                '3d' => '3D',
                '1w' => '1W',
                '1M' => '1M',
            ),
            'hostname' => 'www.blofin.com',
            'urls' => array(
                'logo' => 'https://github.com/user-attachments/assets/518cdf80-f05d-4821-a3e3-d48ceb41d73b',
                'api' => array(
                    'rest' => 'https://openapi.blofin.com',
                ),
                'test' => array(
                    'rest' => 'https://demo-trading-openapi.blofin.com',
                ),
                'referral' => array(
                    'url' => 'https://blofin.com/register?referral_code=f79EsS',
                    'discount' => 0.05,
                ),
                'www' => 'https://www.blofin.com',
                'doc' => 'https://blofin.com/docs',
            ),
            'api' => array(
                'public' => array(
                    'get' => array(
                        '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' => array(
                    'get' => array(
                        '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/position-mode' => 1,
                        'account/batch-leverage-info' => 1,
                        'trade/orders-tpsl-pending' => 1,
                        'trade/orders-algo-pending' => 1,
                        'trade/orders-history' => 1,
                        'trade/orders-tpsl-history' => 1,
                        'trade/orders-algo-history' => 1, // todo new
                        'trade/order/price-range' => 1,
                        'user/query-apikey' => 1,
                        'affiliate/basic' => 1,
                        'copytrading/instruments' => 1,
                        'copytrading/account/balance' => 1,
                        'copytrading/account/positions-by-order' => 1,
                        'copytrading/account/positions-details-by-order' => 1,
                        'copytrading/account/positions-by-contract' => 1,
                        'copytrading/account/position-mode' => 1,
                        'copytrading/account/leverage-info' => 1,
                        'copytrading/trade/orders-pending' => 1,
                        'copytrading/trade/pending-tpsl-by-contract' => 1,
                        'copytrading/trade/position-history-by-order' => 1,
                        'copytrading/trade/orders-history' => 1,
                        'copytrading/trade/pending-tpsl-by-order' => 1,
                    ),
                    'post' => array(
                        'account/set-margin-mode' => 1,
                        'account/set-position-mode' => 1,
                        'trade/order' => 1,
                        'trade/order-algo' => 1,
                        'trade/cancel-order' => 1,
                        'trade/cancel-algo' => 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,
                        'copytrading/account/set-position-mode' => 1,
                        'copytrading/account/set-leverage' => 1,
                        'copytrading/trade/place-order' => 1,
                        'copytrading/trade/cancel-order' => 1,
                        'copytrading/trade/place-tpsl-by-contract' => 1,
                        'copytrading/trade/cancel-tpsl-by-contract' => 1,
                        'copytrading/trade/place-tpsl-by-order' => 1,
                        'copytrading/trade/cancel-tpsl-by-order' => 1,
                        'copytrading/trade/close-position-by-order' => 1,
                        'copytrading/trade/close-position-by-contract' => 1,
                    ),
                ),
            ),
            'fees' => array(
                'swap' => array(
                    'taker' => $this->parse_number('0.00060'),
                    'maker' => $this->parse_number('0.00020'),
                ),
            ),
            'requiredCredentials' => array(
                'apiKey' => true,
                'secret' => true,
                'password' => true,
            ),
            'features' => array(
                'default' => array(
                    'sandbox' => false,
                    'createOrder' => array(
                        'timeInForce' => array(
                            'IOC' => true,
                            'FOK' => true,
                            'PO' => true,
                            'GTD' => false,
                        ),
                        'leverage' => false,
                        'marketBuyRequiresPrice' => false,
                        'marketBuyByCost' => false,
                        'selfTradePrevention' => false,
                        'trailing' => false,
                        'iceberg' => false,
                    ),
                    'createOrders' => array(
                        'max' => 10,
                    ),
                    'fetchMyTrades' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'daysBack' => 100000,
                        'untilDays' => 100000,
                        'symbolRequired' => false,
                    ),
                    'fetchOrder' => null,
                    'fetchOpenOrders' => array(
                        'marginMode' => false,
                        'limit' => 100,
                        'trigger' => true,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOrders' => null,
                    'fetchClosedOrders' => array(
                        'marginMode' => false,
                        'limit' => 1000,
                        'daysBack' => 100000,
                        'daysBackCanceled' => 1,
                        'untilDays' => 100000,
                        'trigger' => true,
                        'trailing' => false,
                        'symbolRequired' => false,
                    ),
                    'fetchOHLCV' => array(
                        'limit' => 1440,
                    ),
                ),
                'spot' => array(
                    'extends' => 'default',
                    'createOrder' => array(
                        'marginMode' => false,
                        'triggerPrice' => false,
                        'triggerPriceType' => null,
                        'triggerDirection' => false,
                        'stopLossPrice' => false,
                        'takeProfitPrice' => false,
                        'attachedStopLossTakeProfit' => null,
                        'hedged' => false,
                    ),
                ),
                'forDerivatives' => array(
                    'extends' => 'default',
                    'createOrder' => array(
                        'marginMode' => true,
                        'triggerPrice' => false, // todo
                        'triggerPriceType' => null,
                        'triggerDirection' => false,
                        'stopLossPrice' => true,
                        'takeProfitPrice' => true,
                        'attachedStopLossTakeProfit' => array(
                            'triggerPriceType' => null,
                            'price' => true,
                        ),
                        'hedged' => true,
                    ),
                ),
                'swap' => array(
                    'linear' => array(
                        'extends' => 'forDerivatives',
                    ),
                    'inverse' => null,
                ),
                'future' => array(
                    'linear' => null,
                    'inverse' => null,
                ),
            ),
            'exceptions' => array(
                'exact' => array(
                    '400' => '\\ccxt\\BadRequest', // Body can not be empty
                    '401' => '\\ccxt\\AuthenticationError',  // Invalid signature
                    '500' => '\\ccxt\\ExchangeError',  // Internal Server Error
                    '404' => '\\ccxt\\BadRequest',  // not found
                    '405' => '\\ccxt\\BadRequest',  // Method Not Allowed
                    '406' => '\\ccxt\\BadRequest',  // Not Acceptable
                    '429' => '\\ccxt\\RateLimitExceeded',  // Too Many Requests
                    '152001' => '\\ccxt\\BadRequest',  // Parameter array() cannot be empty
                    '152002' => '\\ccxt\\BadRequest',  // Parameter array() error
                    '152003' => '\\ccxt\\BadRequest',  // Either parameter array() or array() is required
                    '152004' => '\\ccxt\\BadRequest',  // JSON syntax error
                    '152005' => '\\ccxt\\BadRequest',  // Parameter error => wrong or empty
                    '152006' => '\\ccxt\\InvalidOrder',  // Batch orders can be placed for up to 20 at once
                    '152007' => '\\ccxt\\InvalidOrder',  // Batch orders can only be placed with the same instId and marginMode
                    '152008' => '\\ccxt\\InvalidOrder',  // Only the same field is allowed for bulk cancellation of orders, orderId is preferred
                    '152009' => '\\ccxt\\InvalidOrder',  // array() must be a combination of numbers, letters, or underscores, and the maximum length of characters is 32
                    '150003' => '\\ccxt\\InvalidOrder',  // clientId already exist
                    '150004' => '\\ccxt\\InvalidOrder',  // Insufficient balance. please adjust the amount and try again
                    '542' => '\\ccxt\\InvalidOrder',  // Exceeded the maximum order size limit
                    '102002' => '\\ccxt\\InvalidOrder',  // Duplicate customized order ID
                    '102005' => '\\ccxt\\InvalidOrder',  // Position had been closed
                    '102014' => '\\ccxt\\InvalidOrder',  // Limit order exceeds maximum order size limit
                    '102015' => '\\ccxt\\InvalidOrder',  // Market order exceeds maximum order size limit
                    '102022' => '\\ccxt\\InvalidOrder',  // Failed to place order. You don’t have any positions of this contract. Turn off Reduce-only to continue.
                    '102037' => '\\ccxt\\InvalidOrder',  // TP trigger price should be higher than the latest trading price
                    '102038' => '\\ccxt\\InvalidOrder',  // SL trigger price should be lower than the latest trading price
                    '102039' => '\\ccxt\\InvalidOrder',  // TP trigger price should be lower than the latest trading price
                    '102040' => '\\ccxt\\InvalidOrder',  // SL trigger price should be higher than the latest trading price
                    '102047' => '\\ccxt\\InvalidOrder',  // Stop loss trigger price should be higher than the order price
                    '102048' => '\\ccxt\\InvalidOrder',  // stop loss trigger price must be higher than the best bid price
                    '102049' => '\\ccxt\\InvalidOrder',  // Take profit trigger price should be lower than the order price
                    '102050' => '\\ccxt\\InvalidOrder',  // stop loss trigger price must be lower than the best ask price
                    '102051' => '\\ccxt\\InvalidOrder',  // stop loss trigger price should be lower than the order price
                    '102052' => '\\ccxt\\InvalidOrder',  // take profit trigger price should be higher than the order price
                    '102053' => '\\ccxt\\InvalidOrder',  // take profit trigger price should be lower than the best bid price
                    '102054' => '\\ccxt\\InvalidOrder',  // take profit trigger price should be higher than the best ask price
                    '102055' => '\\ccxt\\InvalidOrder',  // stop loss trigger price should be lower than the best ask price
                    '102064' => '\\ccxt\\BadRequest',  // Buy price is not within the price limit (Minimum => 310.40; Maximum:1,629.40)
                    '102065' => '\\ccxt\\BadRequest',  // Sell price is not within the price limit
                    '102068' => '\\ccxt\\BadRequest',  // Cancel failed order has been filled, triggered, canceled or does not exist
                    '103013' => '\\ccxt\\ExchangeError',  // Internal error; unable to process your request. Please try again.
                    'Order failed. Insufficient USDT margin in account' => '\\ccxt\\InsufficientFunds',  // Insufficient USDT margin in account
                ),
                'broad' => array(
                    'Internal Server Error' => '\\ccxt\\ExchangeNotAvailable', // array("code":500,"data":array(),"detailMsg":"","error_code":"500","error_message":"Internal Server Error","msg":"Internal Server Error")
                    'server error' => '\\ccxt\\ExchangeNotAvailable', // array("code":500,"data":array(),"detailMsg":"","error_code":"500","error_message":"server error 1236805249","msg":"server error 1236805249")
                ),
            ),
            'httpExceptions' => array(
                '429' => '\\ccxt\\ExchangeNotAvailable', // https://github.com/ccxt/ccxt/issues/9612
            ),
            'precisionMode' => TICK_SIZE,
            'options' => array(
                'brokerId' => 'ec6dd3a7dd982d0b',
                'accountsByType' => array(
                    'swap' => 'futures',
                    'funding' => 'funding',
                    'future' => 'futures',
                    'copy_trading' => 'copy_trading',
                    'earn' => 'earn',
                    'spot' => 'spot',
                ),
                'accountsById' => array(
                    'funding' => 'funding',
                    'futures' => 'swap',
                    'copy_trading' => 'copy_trading',
                    'earn' => 'earn',
                    'spot' => 'spot',
                ),
                'defaultNetwork' => 'ERC20',
                'defaultNetworks' => array(
                    'ETH' => 'ERC20',
                    'BTC' => 'BTC',
                    'USDT' => 'TRC20',
                ),
                'networks' => array(
                    'BTC' => 'Bitcoin',
                    'BEP20' => 'BSC',
                    'ERC20' => 'ERC20',
                    'TRC20' => 'TRC20',
                ),
                'fetchOpenInterestHistory' => array(
                    'timeframes' => array(
                        '5m' => '5m',
                        '1h' => '1H',
                        '8h' => '8H',
                        '1d' => '1D',
                        '5M' => '5m',
                        '1H' => '1H',
                        '8H' => '8H',
                        '1D' => '1D',
                    ),
                ),
                'fetchOHLCV' => array(
                    // 'type' => 'Candles', // Candles or HistoryCandles, IndexCandles, MarkPriceCandles
                    'timezone' => 'UTC', // UTC, HK
                ),
                'fetchPositions' => array(
                    'method' => 'privateGetAccountPositions', // privateGetAccountPositions or privateGetAccountPositionsHistory
                ),
                'createOrder' => 'privatePostTradeOrder', // or 'privatePostTradeOrderTpsl'
                'createMarketBuyOrderRequiresPrice' => false,
                'fetchMarkets' => array( 'swap' ),
                'defaultType' => 'swap',
                'fetchLedger' => array(
                    'method' => 'privateGetAssetBills',
                ),
                'fetchOpenOrders' => array(
                    'method' => 'privateGetTradeOrdersPending',
                ),
                'cancelOrders' => array(
                    'method' => 'privatePostTradeCancelBatchOrders',
                ),
                'fetchCanceledOrders' => array(
                    'method' => 'privateGetTradeOrdersHistory', // privateGetTradeOrdersTpslHistory
                ),
                'fetchClosedOrders' => array(
                    'method' => 'privateGetTradeOrdersHistory', // privateGetTradeOrdersTpslHistory
                ),
                'withdraw' => array(
                    // a funding password credential is required by the exchange for the
                    // withdraw call (not to be confused with the api password credential)
                    'password' => null,
                    'pwd' => null, // password or pwd both work
                ),
                'exchangeType' => array(
                    'spot' => 'SPOT',
                    'swap' => 'SWAP',
                    'SPOT' => 'SPOT',
                    'SWAP' => 'SWAP',
                ),
            ),
        ));
    }

    public function fetch_markets($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * retrieves $data on all markets for blofin
             *
             * @see https://blofin.com/docs#get-instruments
             *
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array[]} an array of objects representing market $data
             */
            $response = Async\await($this->publicGetMarketInstruments ($params));
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_markets($data);
        }) ();
    }

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

    public function fetch_order_book(string $symbol, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $limit, $params) {
            /**
             * 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 {string} $symbol unified $symbol of the $market to fetch the order book for
             * @param {int} [$limit] the maximum amount of order book entries to return
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} A dictionary of ~@link https://docs.ccxt.com/#/?id=order-book-structure order book structures~ indexed by $market symbols
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
            );
            $limit = ($limit === null) ? 50 : $limit;
            if ($limit !== null) {
                $request['size'] = $limit; // max 100
            }
            $response = Async\await($this->publicGetMarketBooks ($this->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 = $this->safe_list($response, 'data', array());
            $first = $this->safe_dict($data, 0, array());
            $timestamp = $this->safe_integer($first, 'ts');
            return $this->parse_order_book($first, $symbol, $timestamp);
        }) ();
    }

    public function parse_ticker(array $ticker, ?array $market = null): array {
        //
        // response similar for REST & WS
        //
        //     {
        //         instId => "ADA-USDT",
        //         ts => "1707736811486",
        //         $last => "0.5315",
        //         lastSize => "4",
        //         askPrice => "0.5318",
        //         askSize => "248",
        //         bidPrice => "0.5315",
        //         bidSize => "63",
        //         open24h => "0.5555",
        //         high24h => "0.5563",
        //         low24h => "0.5315",
        //         volCurrency24h => "198560100",
        //         vol24h => "1985601",
        //     }
        //
        $timestamp = $this->safe_integer($ticker, 'ts');
        $marketId = $this->safe_string($ticker, 'instId');
        $market = $this->safe_market($marketId, $market, '-');
        $symbol = $market['symbol'];
        $last = $this->safe_string($ticker, 'last');
        $open = $this->safe_string($ticker, 'open24h');
        $spot = $this->safe_bool($market, 'spot', false);
        $quoteVolume = $spot ? $this->safe_string($ticker, 'volCurrency24h') : null;
        $baseVolume = $this->safe_string($ticker, 'vol24h');
        $high = $this->safe_string($ticker, 'high24h');
        $low = $this->safe_string($ticker, 'low24h');
        return $this->safe_ticker(array(
            'symbol' => $symbol,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'high' => $high,
            'low' => $low,
            'bid' => $this->safe_string($ticker, 'bidPrice'),
            'bidVolume' => $this->safe_string($ticker, 'bidSize'),
            'ask' => $this->safe_string($ticker, 'askPrice'),
            'askVolume' => $this->safe_string($ticker, 'askSize'),
            'vwap' => null,
            'open' => $open,
            'close' => $last,
            'last' => $last,
            'previousClose' => null,
            'change' => null,
            'percentage' => null,
            'average' => null,
            'baseVolume' => $baseVolume,
            'quoteVolume' => $quoteVolume,
            'indexPrice' => $this->safe_string($ticker, 'indexPrice'),
            'markPrice' => $this->safe_string($ticker, 'markPrice'),
            'info' => $ticker,
        ), $market);
    }

    public function fetch_ticker(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * 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 {string} $symbol unified $symbol of the $market to fetch the ticker for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
            );
            $response = Async\await($this->publicGetMarketTickers ($this->extend($request, $params)));
            $data = $this->safe_list($response, 'data', array());
            $first = $this->safe_dict($data, 0, array());
            return $this->parse_ticker($first, $market);
        }) ();
    }

    public function fetch_mark_price(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * fetches mark price for the $market
             *
             * @see https://docs.blofin.com/index.html#get-mark-price
             *
             * @param {string} $symbol unified $market $symbol
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->subType] "linear" or "inverse"
             * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structures~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'symbol' => $market['id'],
            );
            $response = Async\await($this->publicGetMarketMarkPrice ($this->extend($request, $params)));
            $data = $this->safe_list($response, 'data', array());
            $first = $this->safe_dict($data, 0, array());
            return $this->parse_ticker($first, $market);
        }) ();
    }

    public function fetch_tickers(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * 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 {string[]} [$symbols] unified $symbols of the markets to fetch the ticker for, all market $tickers are returned if not assigned
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a dictionary of ~@link https://docs.ccxt.com/#/?id=ticker-structure ticker structures~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $response = Async\await($this->publicGetMarketTickers ($params));
            $tickers = $this->safe_list($response, 'data', array());
            return $this->parse_tickers($tickers, $symbols);
        }) ();
    }

    public function parse_trade(array $trade, ?array $market = null): array {
        //
        // fetch trades (response similar for REST & WS)
        //
        //   {
        //       "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 = $this->safe_string($trade, 'tradeId');
        $marketId = $this->safe_string($trade, 'instId');
        $market = $this->safe_market($marketId, $market, '-');
        $symbol = $market['symbol'];
        $timestamp = $this->safe_integer($trade, 'ts');
        $price = $this->safe_string_2($trade, 'price', 'fillPrice');
        $amount = $this->safe_string_2($trade, 'size', 'fillSize');
        $side = $this->safe_string($trade, 'side');
        $orderId = $this->safe_string($trade, 'orderId');
        $feeCost = $this->safe_string($trade, 'fee');
        $fee = null;
        if ($feeCost !== null) {
            $fee = array(
                'cost' => $feeCost,
                'currency' => $market['settle'],
            );
        }
        return $this->safe_trade(array(
            'info' => $trade,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'symbol' => $symbol,
            'id' => $id,
            'order' => $orderId,
            'type' => null,
            'takerOrMaker' => null,
            'side' => $side,
            'price' => $price,
            'amount' => $amount,
            'cost' => null,
            'fee' => $fee,
        ), $market);
    }

    public function fetch_trades(string $symbol, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * get the list of most recent trades for a particular $symbol
             *
             * @see https://blofin.com/docs#get-trades
             *
             * @param {string} $symbol unified $symbol of the $market to fetch trades for
             * @param {int} [$since] timestamp in ms of the earliest trade to fetch
             * @param {int} [$limit] the maximum amount of trades to fetch
             * @param {array} [$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 this endpoint multiple times
             * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=public-trades trade structures~
             */
            Async\await($this->load_markets());
            $paginate = false;
            list($paginate, $params) = $this->handle_option_and_params($params, 'fetchTrades', 'paginate');
            if ($paginate) {
                return Async\await($this->fetch_paginated_call_cursor('fetchTrades', $symbol, $since, $limit, $params, 'tradeId', 'after', null, 100));
            }
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
            );
            $response = null;
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100
            }
            $method = null;
            list($method, $params) = $this->handle_option_and_params($params, 'fetchTrades', 'method', 'publicGetMarketTrades');
            if ($method === 'publicGetMarketTrades') {
                $response = Async\await($this->publicGetMarketTrades ($this->extend($request, $params)));
            }
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_trades($data, $market, $since, $limit);
        }) ();
    }

    public function parse_ohlcv($ohlcv, ?array $market = null): array {
        //
        //     array(
        //         "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 array(
            $this->safe_integer($ohlcv, 0),
            $this->safe_number($ohlcv, 1),
            $this->safe_number($ohlcv, 2),
            $this->safe_number($ohlcv, 3),
            $this->safe_number($ohlcv, 4),
            $this->safe_number($ohlcv, 6),
        );
    }

    public function fetch_ohlcv(string $symbol, $timeframe = '1m', ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $timeframe, $since, $limit, $params) {
            /**
             * 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 {string} $symbol unified $symbol of the $market to fetch OHLCV $data for
             * @param {string} $timeframe the length of time each candle represents
             * @param {int} [$since] timestamp in ms of the earliest candle to fetch
             * @param {int} [$limit] the maximum amount of candles to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {int} [$params->until] timestamp in ms of the latest candle to fetch
             * @param {boolean} [$params->paginate] default false, when true will automatically $paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params)
             * @return {int[][]} A list of candles ordered, open, high, low, close, volume
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $paginate = false;
            list($paginate, $params) = $this->handle_option_and_params($params, 'fetchOHLCV', 'paginate');
            if ($paginate) {
                return Async\await($this->fetch_paginated_call_deterministic('fetchOHLCV', $symbol, $since, $limit, $timeframe, $params, 100));
            }
            if ($limit === null) {
                $limit = 100; // default 100, max 100
            }
            $request = array(
                'instId' => $market['id'],
                'bar' => $this->safe_string($this->timeframes, $timeframe, $timeframe),
                'limit' => $limit,
            );
            $until = $this->safe_integer($params, 'until');
            if ($until !== null) {
                $request['after'] = $until;
                $params = $this->omit($params, 'until');
            }
            $response = null;
            $response = Async\await($this->publicGetMarketCandles ($this->extend($request, $params)));
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_ohlcvs($data, $market, $timeframe, $since, $limit);
        }) ();
    }

    public function fetch_funding_rate_history(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()) {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * fetches historical funding $rate prices
             *
             * @see https://blofin.com/docs#get-funding-$rate-history
             *
             * @param {string} $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 ~@link https://docs.ccxt.com/#/?id=funding-$rate-history-structure funding $rate structures~ to fetch
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {boolean} [$params->paginate] default false, when true will automatically $paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params)
             * @param {int} [$params->until] $timestamp in ms of the latest funding $rate to fetch
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=funding-$rate-history-structure funding $rate structures~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' fetchFundingRateHistory() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            $paginate = false;
            list($paginate, $params) = $this->handle_option_and_params($params, 'fetchFundingRateHistory', 'paginate');
            if ($paginate) {
                return Async\await($this->fetch_paginated_call_deterministic('fetchFundingRateHistory', $symbol, $since, $limit, '8h', $params, 100));
            }
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
            );
            if ($since !== null) {
                $request['before'] = max ($since - 1, 0);
            }
            if ($limit !== null) {
                $request['limit'] = $limit;
            }
            $until = $this->safe_integer($params, 'until');
            if ($until !== null) {
                $request['after'] = $until;
                $params = $this->omit($params, 'until');
            }
            $response = Async\await($this->publicGetMarketFundingRateHistory ($this->extend($request, $params)));
            $rates = array();
            $data = $this->safe_list($response, 'data', array());
            for ($i = 0; $i < count($data); $i++) {
                $rate = $data[$i];
                $timestamp = $this->safe_integer($rate, 'fundingTime');
                $rates[] = array(
                    'info' => $rate,
                    'symbol' => $market['symbol'],
                    'fundingRate' => $this->safe_number($rate, 'fundingRate'),
                    'timestamp' => $timestamp,
                    'datetime' => $this->iso8601($timestamp),
                );
            }
            $sorted = $this->sort_by($rates, 'timestamp');
            return $this->filter_by_symbol_since_limit($sorted, $market['symbol'], $since, $limit);
        }) ();
    }

    public function parse_funding_rate($contract, ?array $market = null): array {
        //
        //    {
        //        "fundingRate" => "0.00027815",
        //        "fundingTime" => "1634256000000",
        //        "instId" => "BTC-USD-SWAP",
        //    }
        //
        $marketId = $this->safe_string($contract, 'instId');
        $symbol = $this->safe_symbol($marketId, $market);
        $fundingTime = $this->safe_integer($contract, 'fundingTime');
        // > The current interest is 0.
        return array(
            'info' => $contract,
            'symbol' => $symbol,
            'markPrice' => null,
            'indexPrice' => null,
            'interestRate' => $this->parse_number('0'),
            'estimatedSettlePrice' => null,
            'timestamp' => null,
            'datetime' => null,
            'fundingRate' => $this->safe_number($contract, 'fundingRate'),
            'fundingTimestamp' => $fundingTime,
            'fundingDatetime' => $this->iso8601($fundingTime),
            'nextFundingRate' => null,
            'nextFundingTimestamp' => null,
            'nextFundingDatetime' => null,
            'previousFundingRate' => null,
            'previousFundingTimestamp' => null,
            'previousFundingDatetime' => null,
            'interval' => null,
        );
    }

    public function fetch_funding_rate(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * fetch the current funding rate
             *
             * @see https://blofin.com/docs#get-funding-rate
             *
             * @param {string} $symbol unified $market $symbol
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=funding-rate-structure funding rate structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            if (!$market['swap']) {
                throw new ExchangeError($this->id . ' fetchFundingRate() is only valid for swap markets');
            }
            $request = array(
                'instId' => $market['id'],
            );
            $response = Async\await($this->publicGetMarketFundingRate ($this->extend($request, $params)));
            //
            //    {
            //        "code" => "0",
            //        "data" => array(
            //            {
            //                "fundingRate" => "0.00027815",
            //                "fundingTime" => "1634256000000",
            //                "instId" => "BTC-USD-SWAP",
            //            }
            //        ),
            //        "msg" => ""
            //    }
            //
            $data = $this->safe_list($response, 'data', array());
            $entry = $this->safe_dict($data, 0, array());
            return $this->parse_funding_rate($entry, $market);
        }) ();
    }

    public function parse_balance_by_type($response) {
        $data = $this->safe_list($response, 'data');
        if (($data !== null) && gettype($data) === 'array' && array_keys($data) === array_keys(array_keys($data))) {
            return $this->parse_funding_balance($response);
        } else {
            return $this->parse_balance($response);
        }
    }

    public function parse_balance($response) {
        //
        // "data" similar for REST & WS
        //
        // {
        //     "code" => "0",
        //     "msg" => "success",
        //     "data" => {
        //         "ts" => "1697021343571",
        //         "totalEquity" => "10011254.077985990315787910",
        //         "isolatedEquity" => "861.763132108800000000",
        //         "details" => array(
        //             {
        //                 "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" // present only in REST
        //                 "unrealizedPnl" => "0" // present only in WS
        //             }
        //         )
        //     }
        // }
        //
        $result = array( 'info' => $response );
        $data = $this->safe_dict($response, 'data', array());
        $timestamp = $this->safe_integer($data, 'ts');
        $details = $this->safe_list($data, 'details', array());
        for ($i = 0; $i < count($details); $i++) {
            $balance = $details[$i];
            $currencyId = $this->safe_string($balance, 'currency');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            // it may be incorrect to use total, free and used for swap accounts
            $eq = $this->safe_string($balance, 'equity');
            $availEq = $this->safe_string($balance, 'available');
            if (($eq === null) || ($availEq === null)) {
                $account['free'] = $this->safe_string($balance, 'availableEquity');
                $account['used'] = $this->safe_string($balance, 'frozen');
            } else {
                $account['total'] = $eq;
                $account['free'] = $availEq;
            }
            $result[$code] = $account;
        }
        $result['timestamp'] = $timestamp;
        $result['datetime'] = $this->iso8601($timestamp);
        return $this->safe_balance($result);
    }

    public function parse_funding_balance($response) {
        //
        //  {
        //      "code" => "0",
        //      "msg" => "success",
        //      "data" => array(
        //          {
        //              "currency" => "USDT",
        //              "balance" => "10012514.919418081548717298",
        //              "available" => "9872132.414278782284622898",
        //              "frozen" => "138556.471805965930761067",
        //              "bonus" => "0"
        //          }
        //      )
        //  }
        //
        $result = array( 'info' => $response );
        $data = $this->safe_list($response, 'data', array());
        for ($i = 0; $i < count($data); $i++) {
            $balance = $data[$i];
            $currencyId = $this->safe_string($balance, 'currency');
            $code = $this->safe_currency_code($currencyId);
            $account = $this->account();
            // it may be incorrect to use total, free and used for swap accounts
            $account['total'] = $this->safe_string($balance, 'balance');
            $account['free'] = $this->safe_string($balance, 'available');
            $account['used'] = $this->safe_string($balance, 'frozen');
            $result[$code] = $account;
        }
        return $this->safe_balance($result);
    }

    public function parse_trading_fee(array $fee, ?array $market = null): array {
        return array(
            'info' => $fee,
            'symbol' => $this->safe_symbol(null, $market),
            // blofin returns the fees values opposed to other exchanges, so the sign needs to be flipped
            'maker' => $this->parse_number(Precise::string_neg($this->safe_string_2($fee, 'maker', 'makerU'))),
            'taker' => $this->parse_number(Precise::string_neg($this->safe_string_2($fee, 'taker', 'takerU'))),
            'percentage' => null,
            'tierBased' => null,
        );
    }

    public function fetch_balance($params = array ()): PromiseInterface {
        return Async\async(function () use ($params) {
            /**
             * 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 {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->accountType] the type of account to fetch the balance for, either 'funding' or 'futures'  or 'copy_trading' or 'earn'
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=balance-structure balance structure~
             */
            Async\await($this->load_markets());
            $accountType = null;
            list($accountType, $params) = $this->handle_option_and_params_2($params, 'fetchBalance', 'accountType', 'type');
            $request = array(
            );
            $response = null;
            if ($accountType !== null && $accountType !== 'swap') {
                $options = $this->safe_dict($this->options, 'accountsByType', array());
                $parsedAccountType = $this->safe_string($options, $accountType, $accountType);
                $request['accountType'] = $parsedAccountType;
                $response = Async\await($this->privateGetAssetBalances ($this->extend($request, $params)));
            } else {
                $response = Async\await($this->privateGetAccountBalance ($this->extend($request, $params)));
            }
            return $this->parse_balance_by_type($response);
        }) ();
    }

    public function create_order_request(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()) {
        $market = $this->market($symbol);
        $request = array(
            'instId' => $market['id'],
            'side' => $side,
            'orderType' => $type,
            'size' => $this->amount_to_precision($symbol, $amount),
            'brokerId' => $this->safe_string($this->options, 'brokerId', 'ec6dd3a7dd982d0b'),
        );
        $marginMode = null;
        list($marginMode, $params) = $this->handle_margin_mode_and_params('createOrder', $params, 'cross');
        $request['marginMode'] = $marginMode;
        $triggerPrice = $this->safe_string($params, 'triggerPrice');
        $timeInForce = $this->safe_string($params, 'timeInForce', 'GTC');
        $isHedged = $this->safe_bool($params, 'hedged', false);
        if ($isHedged) {
            $request['positionSide'] = ($side === 'buy') ? 'long' : 'short';
        }
        $isMarketOrder = $type === 'market';
        $params = $this->omit($params, array( 'timeInForce' ));
        $ioc = ($timeInForce === 'IOC') || ($type === 'ioc');
        $marketIOC = ($isMarketOrder && $ioc);
        if ($isMarketOrder || $marketIOC) {
            $request['orderType'] = 'market';
        } else {
            $key = ($triggerPrice !== null) ? 'orderPrice' : 'price';
            $request[$key] = $this->price_to_precision($symbol, $price);
        }
        $postOnly = false;
        list($postOnly, $params) = $this->handle_post_only($isMarketOrder, $type === 'post_only', $params);
        if ($postOnly) {
            $request['type'] = 'post_only';
        }
        $stopLoss = $this->safe_dict($params, 'stopLoss');
        $takeProfit = $this->safe_dict($params, 'takeProfit');
        $params = $this->omit($params, array( 'stopLoss', 'takeProfit', 'hedged' ));
        $isStopLoss = $stopLoss !== null;
        $isTakeProfit = $takeProfit !== null;
        if ($isStopLoss || $isTakeProfit) {
            if ($isStopLoss) {
                $slTriggerPrice = $this->safe_string_2($stopLoss, 'triggerPrice', 'stopPrice');
                $request['slTriggerPrice'] = $this->price_to_precision($symbol, $slTriggerPrice);
                $slOrderPrice = $this->safe_string($stopLoss, 'price', '-1');
                $request['slOrderPrice'] = $this->price_to_precision($symbol, $slOrderPrice);
            }
            if ($isTakeProfit) {
                $tpTriggerPrice = $this->safe_string_2($takeProfit, 'triggerPrice', 'stopPrice');
                $request['tpTriggerPrice'] = $this->price_to_precision($symbol, $tpTriggerPrice);
                $tpPrice = $this->safe_string($takeProfit, 'price', '-1');
                $request['tpOrderPrice'] = $this->price_to_precision($symbol, $tpPrice);
            }
        } elseif ($triggerPrice !== null) {
            $request['orderType'] = 'trigger';
            $request['triggerPrice'] = $this->price_to_precision($symbol, $triggerPrice);
            if ($isMarketOrder) {
                $request['orderPrice'] = '-1';
            }
        }
        return $this->extend($request, $params);
    }

    public function parse_order_status(?string $status) {
        $statuses = array(
            'canceled' => 'canceled',
            'order_failed' => 'canceled',
            'live' => 'open',
            'partially_filled' => 'open',
            'filled' => 'closed',
            'effective' => 'closed',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_order(array $order, ?array $market = null): array {
        //
        // response similar for REST & WS
        //
        // {
        //     "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"
        //     "filled_amount" => "1.000000000000000000", // filledAmount in "ws" watchOrders
        //     "cancelSource" => "", // only in WS
        //     "instType" => "SWAP", // only in WS
        // }
        //
        $id = $this->safe_string_n($order, array( 'tpslId', 'orderId', 'algoId' ));
        $timestamp = $this->safe_integer($order, 'createTime');
        $lastUpdateTimestamp = $this->safe_integer($order, 'updateTime');
        $lastTradeTimestamp = $this->safe_integer($order, 'fillTime');
        $side = $this->safe_string($order, 'side');
        $type = $this->safe_string($order, 'orderType');
        $postOnly = null;
        $timeInForce = null;
        if ($type === 'post_only') {
            $postOnly = true;
            $type = 'limit';
        } elseif ($type === 'fok') {
            $timeInForce = 'FOK';
            $type = 'limit';
        } elseif ($type === 'ioc') {
            $timeInForce = 'IOC';
            $type = 'limit';
        }
        $marketId = $this->safe_string($order, 'instId');
        $market = $this->safe_market($marketId, $market);
        $symbol = $this->safe_symbol($marketId, $market, '-');
        $filled = $this->safe_string($order, 'filledSize');
        $price = $this->safe_string_2($order, 'px', 'price');
        $average = $this->safe_string($order, 'averagePrice');
        $status = $this->parse_order_status($this->safe_string($order, 'state'));
        $feeCostString = $this->safe_string($order, 'fee');
        $amount = $this->safe_string($order, 'size');
        $leverage = $this->safe_string($order, 'leverage', '1');
        $contractSize = $this->safe_string($market, 'contractSize');
        $baseAmount = Precise::string_mul($contractSize, $filled);
        $cost = null;
        if ($average !== null) {
            $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 = null;
        if ($feeCostString !== null) {
            $feeCostSigned = Precise::string_abs($feeCostString);
            $feeCurrencyId = $this->safe_string($order, 'feeCcy', 'USDT');
            $feeCurrencyCode = $this->safe_currency_code($feeCurrencyId);
            $fee = array(
                'cost' => $this->parse_number($feeCostSigned),
                'currency' => $feeCurrencyCode,
            );
        }
        $clientOrderId = $this->safe_string($order, 'clientOrderId');
        if (($clientOrderId !== null) && (strlen($clientOrderId) < 1)) {
            $clientOrderId = null; // fix empty $clientOrderId string
        }
        $stopLossTriggerPrice = $this->safe_number($order, 'slTriggerPrice');
        $stopLossPrice = $this->safe_number($order, 'slOrderPrice');
        $takeProfitTriggerPrice = $this->safe_number($order, 'tpTriggerPrice');
        $takeProfitPrice = $this->safe_number($order, 'tpOrderPrice');
        $reduceOnlyRaw = $this->safe_string($order, 'reduceOnly');
        $reduceOnly = ($reduceOnlyRaw === 'true');
        return $this->safe_order(array(
            'info' => $order,
            'id' => $id,
            'clientOrderId' => $clientOrderId,
            'timestamp' => $timestamp,
            'datetime' => $this->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' => null,
            'status' => $status,
            'fee' => $fee,
            'trades' => null,
            'reduceOnly' => $reduceOnly,
        ), $market);
    }

    public function create_order(string $symbol, string $type, string $side, float $amount, ?float $price = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $type, $side, $amount, $price, $params) {
            /**
             * create a trade $order
             *
             * @see https://blofin.com/docs#place-$order
             * @see https://blofin.com/docs#place-$tpsl-$order
             *
             * @param {string} $symbol unified $symbol of the $market to create an $order in
             * @param {string} $type 'market' or 'limit' or 'post_only' or 'ioc' or 'fok'
             * @param {string} $side 'buy' or 'sell'
             * @param {float} $amount how much of currency you want to trade in units of base currency
             * @param {float} [$price] the $price at which the $order is to be fulfilled, in units of the quote currency, ignored in $market orders
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->triggerPrice] the trigger $price for a trigger $order
             * @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 {string} [$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 {string} [$params->positionSide] *stopLossPrice/takeProfitPrice orders only* 'long' or 'short' or 'net' default is 'net'
             * @param {boolean} [$params->hedged] if true, the positionSide will be set to long/short instead of net, default is false
             * @param {string} [$params->clientOrderId] a unique id for the $order
             * @param {array} [$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 {array} [$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)
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=$order-structure $order structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $tpsl = $this->safe_bool($params, 'tpsl', false);
            $params = $this->omit($params, 'tpsl');
            $method = null;
            list($method, $params) = $this->handle_option_and_params($params, 'createOrder', 'method', 'privatePostTradeOrder');
            $isStopLossPriceDefined = $this->safe_string($params, 'stopLossPrice') !== null;
            $isTakeProfitPriceDefined = $this->safe_string($params, 'takeProfitPrice') !== null;
            $isTriggerOrder = $this->safe_string($params, 'triggerPrice') !== null;
            $isType2Order = ($isStopLossPriceDefined || $isTakeProfitPriceDefined);
            $response = null;
            $reduceOnly = $this->safe_bool($params, 'reduceOnly');
            if ($reduceOnly !== null) {
                $params['reduceOnly'] = $reduceOnly ? 'true' : 'false';
            }
            if ($tpsl || ($method === 'privatePostTradeOrderTpsl') || $isType2Order) {
                $tpslRequest = $this->create_tpsl_order_request($symbol, $type, $side, $amount, $price, $params);
                $response = Async\await($this->privatePostTradeOrderTpsl ($tpslRequest));
            } elseif ($isTriggerOrder || ($method === 'privatePostTradeOrderAlgo')) {
                $triggerRequest = $this->create_order_request($symbol, $type, $side, $amount, $price, $params);
                $response = Async\await($this->privatePostTradeOrderAlgo ($triggerRequest));
            } else {
                $request = $this->create_order_request($symbol, $type, $side, $amount, $price, $params);
                $response = Async\await($this->privatePostTradeOrder ($request));
            }
            if ($isTriggerOrder || ($method === 'privatePostTradeOrderAlgo')) {
                $dataDict = $this->safe_dict($response, 'data', array());
                $triggerOrder = $this->parse_order($dataDict, $market);
                return $triggerOrder;
            }
            $data = $this->safe_list($response, 'data', array());
            $first = $this->safe_dict($data, 0);
            $order = $this->parse_order($first, $market);
            $order['type'] = $type;
            $order['side'] = $side;
            return $order;
        }) ();
    }

    public function create_tpsl_order_request(string $symbol, string $type, string $side, ?float $amount = null, ?float $price = null, $params = array ()) {
        $market = $this->market($symbol);
        $positionSide = $this->safe_string($params, 'positionSide', 'net');
        $request = array(
            'instId' => $market['id'],
            'side' => $side,
            'positionSide' => $positionSide,
            'brokerId' => $this->safe_string($this->options, 'brokerId', 'ec6dd3a7dd982d0b'),
        );
        if ($amount !== null) {
            $request['size'] = $this->amount_to_precision($symbol, $amount);
        }
        $marginMode = $this->safe_string($params, 'marginMode', 'cross'); // cross or isolated
        if ($marginMode !== 'cross' && $marginMode !== 'isolated') {
            throw new BadRequest($this->id . ' createTpslOrder() requires a $marginMode parameter that must be either cross or isolated');
        }
        $stopLossPrice = $this->safe_string($params, 'stopLossPrice');
        $takeProfitPrice = $this->safe_string($params, 'takeProfitPrice');
        if ($stopLossPrice !== null) {
            $request['slTriggerPrice'] = $this->price_to_precision($symbol, $stopLossPrice);
            if ($type === 'market') {
                $request['slOrderPrice'] = '-1';
            } else {
                $request['slOrderPrice'] = $this->price_to_precision($symbol, $price);
            }
        } elseif ($takeProfitPrice !== null) {
            $request['tpTriggerPrice'] = $this->price_to_precision($symbol, $takeProfitPrice);
            if ($type === 'market') {
                $request['tpOrderPrice'] = '-1';
            } else {
                $request['tpOrderPrice'] = $this->price_to_precision($symbol, $price);
            }
        }
        $request['marginMode'] = $marginMode;
        $params = $this->omit($params, array( 'stopLossPrice', 'takeProfitPrice' ));
        return $this->extend($request, $params);
    }

    public function cancel_order(string $id, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($id, $symbol, $params) {
            /**
             * cancels an open $order
             *
             * @see https://blofin.com/docs#cancel-$order
             * @see https://blofin.com/docs#cancel-tpsl-$order
             *
             * @param {string} $id $order $id
             * @param {string} $symbol unified $symbol of the $market the $order was made in
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {boolean} [$params->trigger] True if cancelling a trigger/conditional
             * @param {boolean} [$params->tpsl] True if cancelling a tpsl $order
             * @return {array} An ~@link https://docs.ccxt.com/#/?$id=$order-structure $order structure~
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' cancelOrder() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
            );
            $isTrigger = $this->safe_bool_n($params, array( 'trigger' ), false);
            $isTpsl = $this->safe_bool_2($params, 'tpsl', 'TPSL', false);
            $clientOrderId = $this->safe_string($params, 'clientOrderId');
            if ($clientOrderId !== null) {
                $request['clientOrderId'] = $clientOrderId;
            } else {
                if (!$isTrigger && !$isTpsl) {
                    $request['orderId'] = (string) $id;
                } elseif ($isTpsl) {
                    $request['tpslId'] = (string) $id;
                } elseif ($isTrigger) {
                    $request['algoId'] = (string) $id;
                }
            }
            $query = $this->omit($params, array( 'orderId', 'clientOrderId', 'stop', 'trigger', 'tpsl' ));
            if ($isTpsl) {
                $tpslResponse = Async\await($this->cancel_orders(array( $id ), $symbol, $params));
                $first = $this->safe_dict($tpslResponse, 0);
                return $first;
            } elseif ($isTrigger) {
                $triggerResponse = Async\await($this->privatePostTradeCancelAlgo ($this->extend($request, $query)));
                $triggerData = $this->safe_dict($triggerResponse, 'data');
                return $this->parse_order($triggerData, $market);
            }
            $response = Async\await($this->privatePostTradeCancelOrder ($this->extend($request, $query)));
            $data = $this->safe_list($response, 'data', array());
            $order = $this->safe_dict($data, 0);
            return $this->parse_order($order, $market);
        }) ();
    }

    public function create_orders(array $orders, $params = array ()): PromiseInterface {
        return Async\async(function () use ($orders, $params) {
            /**
             * 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
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an ~@link https://docs.ccxt.com/#/?id=order-structure order structure~
             */
            Async\await($this->load_markets());
            $ordersRequests = array();
            for ($i = 0; $i < count($orders); $i++) {
                $rawOrder = $orders[$i];
                $marketId = $this->safe_string($rawOrder, 'symbol');
                $type = $this->safe_string($rawOrder, 'type');
                $side = $this->safe_string($rawOrder, 'side');
                $amount = $this->safe_value($rawOrder, 'amount');
                $price = $this->safe_value($rawOrder, 'price');
                $orderParams = $this->safe_dict($rawOrder, 'params', array());
                $extendedParams = $this->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 = $this->create_order_request($marketId, $type, $side, $amount, $price, $extendedParams);
                $ordersRequests[] = $orderRequest;
            }
            $response = Async\await($this->privatePostTradeBatchOrders ($ordersRequests));
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_orders($data);
        }) ();
    }

    public function fetch_open_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * Fetch orders that are still open
             *
             * @see https://blofin.com/docs#get-active-orders
             * @see https://blofin.com/docs#get-active-tpsl-orders
             * @see https://docs.blofin.com/index.html#get-active-algo-orders
             *
             * @param {string} $symbol unified $market $symbol
             * @param {int} [$since] the earliest time in ms to fetch open orders for
             * @param {int} [$limit] the maximum number of  open orders structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {bool} [$params->trigger] True if fetching trigger or conditional orders
             * @param {boolean} [$params->paginate] default false, when true will automatically $paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params)
             * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $paginate = false;
            list($paginate, $params) = $this->handle_option_and_params($params, 'fetchOpenOrders', 'paginate');
            if ($paginate) {
                return Async\await($this->fetch_paginated_call_dynamic('fetchOpenOrders', $symbol, $since, $limit, $params));
            }
            $request = array(
            );
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $request['instId'] = $market['id'];
            }
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100, max 100
            }
            $isTrigger = $this->safe_bool_n($params, array( 'stop', 'trigger' ), false);
            $isTpSl = $this->safe_bool_2($params, 'tpsl', 'TPSL', false);
            $method = null;
            list($method, $params) = $this->handle_option_and_params($params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersPending');
            $query = $this->omit($params, array( 'method', 'stop', 'trigger', 'tpsl', 'TPSL' ));
            $response = null;
            if ($isTpSl || ($method === 'privateGetTradeOrdersTpslPending')) {
                $response = Async\await($this->privateGetTradeOrdersTpslPending ($this->extend($request, $query)));
            } elseif ($isTrigger || ($method === 'privateGetTradeOrdersAlgoPending')) {
                $request['orderType'] = 'trigger';
                $response = Async\await($this->privateGetTradeOrdersAlgoPending ($this->extend($request, $query)));
            } else {
                $response = Async\await($this->privateGetTradeOrdersPending ($this->extend($request, $query)));
            }
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_orders($data, $market, $since, $limit);
        }) ();
    }

    public function fetch_my_trades(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * fetch all trades made by the user
             *
             * @see https://blofin.com/docs#get-trade-history
             *
             * @param {string} $symbol unified $market $symbol
             * @param {int} [$since] the earliest time in ms to fetch trades for
             * @param {int} [$limit] the maximum number of trades structures to retrieve
             * @param {array} [$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 this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params)
             * @return {Trade[]} a list of ~@link https://docs.ccxt.com/#/?id=trade-structure trade structures~
             */
            Async\await($this->load_markets());
            $paginate = false;
            list($paginate, $params) = $this->handle_option_and_params($params, 'fetchMyTrades', 'paginate');
            if ($paginate) {
                return Async\await($this->fetch_paginated_call_dynamic('fetchMyTrades', $symbol, $since, $limit, $params));
            }
            $request = array(
            );
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $request['instId'] = $market['id'];
            }
            list($request, $params) = $this->handle_until_option('end', $request, $params);
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100, max 100
            }
            $response = Async\await($this->privateGetTradeFillsHistory ($this->extend($request, $params)));
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_trades($data, $market, $since, $limit);
        }) ();
    }

    public function fetch_deposits(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $since, $limit, $params) {
            /**
             * fetch all deposits made to an account
             *
             * @see https://blofin.com/docs#get-deposite-history
             *
             * @param {string} $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 {array} [$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 this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params)
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structures~
             */
            Async\await($this->load_markets());
            $paginate = false;
            list($paginate, $params) = $this->handle_option_and_params($params, 'fetchDeposits', 'paginate');
            if ($paginate) {
                return Async\await($this->fetch_paginated_call_dynamic('fetchDeposits', $code, $since, $limit, $params));
            }
            $request = array(
            );
            $currency = null;
            if ($code !== null) {
                $currency = $this->currency($code);
                $request['currency'] = $currency['id'];
            }
            if ($since !== null) {
                $request['before'] = max ($since - 1, 0);
            }
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100, max 100
            }
            list($request, $params) = $this->handle_until_option('after', $request, $params);
            $response = Async\await($this->privateGetAssetDepositHistory ($this->extend($request, $params)));
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_transactions($data, $currency, $since, $limit, $params);
        }) ();
    }

    public function fetch_withdrawals(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $since, $limit, $params) {
            /**
             * fetch all withdrawals made from an account
             *
             * @see https://blofin.com/docs#get-withdraw-history
             *
             * @param {string} $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 {array} [$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 this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params)
             * @return {array[]} a list of ~@link https://docs.ccxt.com/#/?id=transaction-structure transaction structures~
             */
            Async\await($this->load_markets());
            $paginate = false;
            list($paginate, $params) = $this->handle_option_and_params($params, 'fetchWithdrawals', 'paginate');
            if ($paginate) {
                return Async\await($this->fetch_paginated_call_dynamic('fetchWithdrawals', $code, $since, $limit, $params));
            }
            $request = array(
            );
            $currency = null;
            if ($code !== null) {
                $currency = $this->currency($code);
                $request['currency'] = $currency['id'];
            }
            if ($since !== null) {
                $request['before'] = max ($since - 1, 0);
            }
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100, max 100
            }
            list($request, $params) = $this->handle_until_option('after', $request, $params);
            $response = Async\await($this->privateGetAssetWithdrawalHistory ($this->extend($request, $params)));
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_transactions($data, $currency, $since, $limit, $params);
        }) ();
    }

    public function fetch_ledger(?string $code = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $since, $limit, $params) {
            /**
             * fetch the history of changes, actions done by the user or operations that altered the balance of the user
             *
             * @see https://blofin.com/docs#get-funds-transfer-history
             *
             * @param {string} [$code] unified $currency $code, default is null
             * @param {int} [$since] timestamp in ms of the earliest ledger entry, default is null
             * @param {int} [$limit] max number of ledger entries to return, default is null
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$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 this endpoint multiple times. See in the docs all the [available parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params)
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=ledger ledger structure~
             */
            Async\await($this->load_markets());
            $paginate = false;
            list($paginate, $params) = $this->handle_option_and_params($params, 'fetchLedger', 'paginate');
            if ($paginate) {
                return Async\await($this->fetch_paginated_call_dynamic('fetchLedger', $code, $since, $limit, $params));
            }
            $request = array(
            );
            if ($limit !== null) {
                $request['limit'] = $limit;
            }
            $currency = null;
            if ($code !== null) {
                $currency = $this->currency($code);
                $request['currency'] = $currency['id'];
            }
            list($request, $params) = $this->handle_until_option('end', $request, $params);
            $response = null;
            $response = Async\await($this->privateGetAssetBills ($this->extend($request, $params)));
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_ledger($data, $currency, $since, $limit);
        }) ();
    }

    public function parse_transaction(array $transaction, ?array $currency = null): array {
        //
        //
        // 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 = null;
        $id = null;
        $withdrawalId = $this->safe_string($transaction, 'withdrawId');
        $depositId = $this->safe_string($transaction, 'depositId');
        $addressTo = $this->safe_string($transaction, 'address');
        $address = $addressTo;
        $tagTo = $this->safe_string($transaction, 'tag');
        if ($withdrawalId !== null) {
            $type = 'withdrawal';
            $id = $withdrawalId;
        } else {
            $id = $depositId;
            $type = 'deposit';
        }
        $currencyId = $this->safe_string($transaction, 'currency');
        $code = $this->safe_currency_code($currencyId);
        $amount = $this->safe_number($transaction, 'amount');
        $status = $this->parse_transaction_status($this->safe_string($transaction, 'state'));
        $txid = $this->safe_string($transaction, 'txId');
        $timestamp = $this->safe_integer($transaction, 'ts');
        $feeCurrencyId = $this->safe_string($transaction, 'feeCurrency');
        $feeCode = $this->safe_currency_code($feeCurrencyId);
        $feeCost = $this->safe_number($transaction, 'fee');
        return array(
            'info' => $transaction,
            'id' => $id,
            'currency' => $code,
            'amount' => $amount,
            'network' => null,
            'addressFrom' => null,
            'addressTo' => $addressTo,
            'address' => $address,
            'tagFrom' => null,
            'tagTo' => $tagTo,
            'tag' => $tagTo,
            'status' => $status,
            'type' => $type,
            'updated' => null,
            'txid' => $txid,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'internal' => null,
            'comment' => null,
            'fee' => array(
                'currency' => $feeCode,
                'cost' => $feeCost,
            ),
        );
    }

    public function parse_transaction_status(?string $status) {
        $statuses = array(
            '0' => 'pending',
            '1' => 'ok',
            '2' => 'failed',
            '3' => 'pending',
        );
        return $this->safe_string($statuses, $status, $status);
    }

    public function parse_ledger_entry_type($type) {
        $types = array(
            '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 $this->safe_string($types, $type, $type);
    }

    public function parse_ledger_entry(array $item, ?array $currency = null): array {
        $currencyId = $this->safe_string($item, 'currency');
        $code = $this->safe_currency_code($currencyId, $currency);
        $currency = $this->safe_currency($currencyId, $currency);
        $timestamp = $this->safe_integer($item, 'ts');
        return $this->safe_ledger_entry(array(
            'info' => $item,
            'id' => $this->safe_string($item, 'transferId'),
            'direction' => null,
            'account' => null,
            'referenceId' => $this->safe_string($item, 'clientId'),
            'referenceAccount' => null,
            'type' => $this->parse_ledger_entry_type($this->safe_string($item, 'type')),
            'currency' => $code,
            'amount' => $this->safe_number($item, 'amount'),
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'before' => null,
            'after' => null,
            'status' => 'ok',
            'fee' => null,
        ), $currency);
    }

    public function parse_ids($ids) {
        /**
         * @ignore
         * @param {string[]|string} $ids order $ids
         * @return {string[]} list of order $ids
         */
        if (gettype($ids) === 'string') {
            return explode(',', $ids);
        } else {
            return $ids;
        }
    }

    public function cancel_orders($ids, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($ids, $symbol, $params) {
            /**
             * cancel multiple orders
             *
             * @see https://blofin.com/docs#cancel-multiple-orders
             *
             * @param {string[]} $ids order $ids
             * @param {string} $symbol unified $market $symbol
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {boolean} [$params->trigger] whether the order is a stop/trigger order
             * @return {array} an list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            // TODO : the original endpoint signature differs, according to that you can skip individual $symbol and assign $ids in batch. At this moment, `$params` is not being used too.
            if ($symbol === null) {
                throw new ArgumentsRequired($this->id . ' cancelOrders() requires a $symbol argument');
            }
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array();
            $options = $this->safe_dict($this->options, 'cancelOrders', array());
            $defaultMethod = $this->safe_string($options, 'method', 'privatePostTradeCancelBatchOrders');
            $method = $this->safe_string($params, 'method', $defaultMethod);
            $clientOrderIds = $this->parse_ids($this->safe_value($params, 'clientOrderId'));
            $tpslIds = $this->parse_ids($this->safe_value($params, 'tpslId'));
            $trigger = $this->safe_bool_n($params, array( 'stop', 'trigger', 'tpsl' ));
            if ($trigger) {
                $method = 'privatePostTradeCancelTpsl';
            }
            if ($clientOrderIds === null) {
                $ids = $this->parse_ids($ids);
                if ($tpslIds !== null) {
                    for ($i = 0; $i < count($tpslIds); $i++) {
                        $request[] = array(
                            'tpslId' => $tpslIds[$i],
                            'instId' => $market['id'],
                        );
                    }
                }
                for ($i = 0; $i < count($ids); $i++) {
                    if ($trigger) {
                        $request[] = array(
                            'tpslId' => $ids[$i],
                            'instId' => $market['id'],
                        );
                    } else {
                        $request[] = array(
                            'orderId' => $ids[$i],
                            'instId' => $market['id'],
                        );
                    }
                }
            } else {
                for ($i = 0; $i < count($clientOrderIds); $i++) {
                    $request[] = array(
                        'instId' => $market['id'],
                        'clientOrderId' => $clientOrderIds[$i],
                    );
                }
            }
            $response = null;
            if ($method === 'privatePostTradeCancelTpsl') {
                $response = Async\await($this->privatePostTradeCancelTpsl ($request)); // * dont extend with $params, otherwise ARRAY will be turned into OBJECT
            } else {
                $response = Async\await($this->privatePostTradeCancelBatchOrders ($request)); // * dont extend with $params, otherwise ARRAY will be turned into OBJECT
            }
            $ordersData = $this->safe_list($response, 'data', array());
            return $this->parse_orders($ordersData, $market, null, null, $params);
        }) ();
    }

    public function transfer(string $code, float $amount, string $fromAccount, string $toAccount, $params = array ()): PromiseInterface {
        return Async\async(function () use ($code, $amount, $fromAccount, $toAccount, $params) {
            /**
             * transfer $currency internally between wallets on the same account
             *
             * @see https://blofin.com/docs#funds-transfer
             *
             * @param {string} $code unified $currency $code
             * @param {float} $amount amount to transfer
             * @param {string} $fromAccount account to transfer from (funding, swap, copy_trading, earn)
             * @param {string} $toAccount account to transfer to (funding, swap, copy_trading, earn)
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=transfer-structure transfer structure~
             */
            Async\await($this->load_markets());
            $currency = $this->currency($code);
            $accountsByType = $this->safe_dict($this->options, 'accountsByType', array());
            $fromId = $this->safe_string($accountsByType, $fromAccount, $fromAccount);
            $toId = $this->safe_string($accountsByType, $toAccount, $toAccount);
            $request = array(
                'currency' => $currency['id'],
                'amount' => $this->currency_to_precision($code, $amount),
                'fromAccount' => $fromId,
                'toAccount' => $toId,
            );
            $response = Async\await($this->privatePostAssetTransfer ($this->extend($request, $params)));
            $data = $this->safe_dict($response, 'data', array());
            return $this->parse_transfer($data, $currency);
        }) ();
    }

    public function parse_transfer(array $transfer, ?array $currency = null): array {
        $id = $this->safe_string($transfer, 'transferId');
        return array(
            'info' => $transfer,
            'id' => $id,
            'timestamp' => null,
            'datetime' => null,
            'currency' => null,
            'amount' => null,
            'fromAccount' => null,
            'toAccount' => null,
            'status' => null,
        );
    }

    public function fetch_position(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * fetch $data on a single open contract trade $position
             *
             * @see https://blofin.com/docs#get-positions
             *
             * @param {string} $symbol unified $market $symbol of the $market the $position is held in, default is null
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->instType] MARGIN, SWAP, FUTURES, OPTION
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=$position-structure $position structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
            );
            $response = Async\await($this->privateGetAccountPositions ($this->extend($request, $params)));
            $data = $this->safe_list($response, 'data', array());
            $position = $this->safe_dict($data, 0);
            if ($position === null) {
                return null;
            }
            return $this->parse_position($position, $market);
        }) ();
    }

    public function fetch_positions(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * fetch $data on a single open contract trade position
             *
             * @see https://blofin.com/docs#get-positions
             *
             * @param {string[]} [$symbols] list of unified market $symbols
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->instType] MARGIN, SWAP, FUTURES, OPTION
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=position-structure position structure~
             */
            Async\await($this->load_markets());
            $symbols = $this->market_symbols($symbols);
            $response = Async\await($this->privateGetAccountPositions ($params));
            $data = $this->safe_list($response, 'data', array());
            $result = $this->parse_positions($data);
            return $this->filter_by_array_positions($result, 'symbol', $symbols, false);
        }) ();
    }

    public function parse_position(array $position, ?array $market = null) {
        //
        // response similar for REST & WS
        //
        //     {
        //         instType => 'SWAP',
        //         instId => 'LTC-USDT',
        //         $marginMode => 'cross',
        //         positionId => '644159',
        //         positionSide => 'net',
        //         positions => '1',
        //         availablePositions => '1',
        //         averagePrice => '68.16',
        //         unrealizedPnl => '0.80631223',
        //         unrealizedPnlRatio => '0.03548909463028169',
        //         leverage => '3',
        //         $liquidationPrice => '10.116655172370356435',
        //         markPrice => '68.96',
        //         initialMargin => '22.988770743333333333',
        //         margin => '', // this field might not exist in rest response
        //         $marginRatio => '152.523509620342499273',
        //         $maintenanceMargin => '0.34483156115',
        //         adl => '4',
        //         createTime => '1707235776528',
        //         updateTime => '1707235776528'
        //     }
        //
        $marketId = $this->safe_string($position, 'instId');
        $market = $this->safe_market($marketId, $market);
        $symbol = $market['symbol'];
        $pos = $this->safe_string($position, 'positions');
        $contractsAbs = Precise::string_abs($pos);
        $side = $this->safe_string($position, 'positionSide');
        $hedged = $side !== 'net';
        $contracts = $this->parse_number($contractsAbs);
        if ($pos !== null) {
            if ($side === 'net') {
                if (Precise::string_gt($pos, '0')) {
                    $side = 'long';
                } elseif (Precise::string_lt($pos, '0')) {
                    $side = 'short';
                } else {
                    $side = null;
                }
            }
        }
        $contractSize = $this->safe_number($market, 'contractSize');
        $contractSizeString = $this->number_to_string($contractSize);
        $markPriceString = $this->safe_string($position, 'markPrice');
        $notionalString = $this->safe_string($position, 'notionalUsd');
        if ($market['inverse']) {
            $notionalString = Precise::string_div(Precise::string_mul($contractsAbs, $contractSizeString), $markPriceString);
        }
        $notional = $this->parse_number($notionalString);
        $marginMode = $this->safe_string($position, 'marginMode');
        $initialMarginString = null;
        $entryPriceString = $this->safe_string($position, 'averagePrice');
        $unrealizedPnlString = $this->safe_string($position, 'unrealizedPnl');
        $leverageString = $this->safe_string($position, 'leverage');
        $initialMarginPercentage = null;
        $collateralString = null;
        if ($marginMode === 'cross') {
            $initialMarginString = $this->safe_string($position, 'initialMargin');
            $collateralString = Precise::string_add($initialMarginString, $unrealizedPnlString);
        } elseif ($marginMode === 'isolated') {
            $initialMarginPercentage = Precise::string_div('1', $leverageString);
            $collateralString = $this->safe_string($position, 'margin');
        }
        $maintenanceMarginString = $this->safe_string($position, 'maintenanceMargin');
        $maintenanceMargin = $this->parse_number($maintenanceMarginString);
        $maintenanceMarginPercentageString = Precise::string_div($maintenanceMarginString, $notionalString);
        if ($initialMarginPercentage === null) {
            $initialMarginPercentage = $this->parse_number(Precise::string_div($initialMarginString, $notionalString, 4));
        } elseif ($initialMarginString === null) {
            $initialMarginString = Precise::string_mul($initialMarginPercentage, $notionalString);
        }
        $rounder = '0.00005'; // round to closest 0.01%
        $maintenanceMarginPercentage = $this->parse_number(Precise::string_div(Precise::string_add($maintenanceMarginPercentageString, $rounder), '1', 4));
        $liquidationPrice = $this->safe_number($position, 'liquidationPrice');
        $percentageString = $this->safe_string($position, 'unrealizedPnlRatio');
        $percentage = $this->parse_number(Precise::string_mul($percentageString, '100'));
        $timestamp = $this->safe_integer($position, 'updateTime');
        $marginRatio = $this->parse_number(Precise::string_div($maintenanceMarginString, $collateralString, 4));
        return $this->safe_position(array(
            'info' => $position,
            'id' => null,
            'symbol' => $symbol,
            'notional' => $notional,
            'marginMode' => $marginMode,
            'liquidationPrice' => $liquidationPrice,
            'entryPrice' => $this->parse_number($entryPriceString),
            'unrealizedPnl' => $this->parse_number($unrealizedPnlString),
            'percentage' => $percentage,
            'contracts' => $contracts,
            'contractSize' => $contractSize,
            'markPrice' => $this->parse_number($markPriceString),
            'lastPrice' => null,
            'side' => $side,
            'hedged' => $hedged,
            'timestamp' => $timestamp,
            'datetime' => $this->iso8601($timestamp),
            'lastUpdateTimestamp' => null,
            'maintenanceMargin' => $maintenanceMargin,
            'maintenanceMarginPercentage' => $maintenanceMarginPercentage,
            'collateral' => $this->parse_number($collateralString),
            'initialMargin' => $this->parse_number($initialMarginString),
            'initialMarginPercentage' => $this->parse_number($initialMarginPercentage),
            'leverage' => $this->parse_number($leverageString),
            'marginRatio' => $marginRatio,
            'stopLossPrice' => null,
            'takeProfitPrice' => null,
        ));
    }

    public function fetch_leverages(?array $symbols = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbols, $params) {
            /**
             * fetch the set leverage for all contract markets
             *
             * @see https://docs.blofin.com/index.html#get-multiple-leverage
             *
             * @param {string[]} $symbols a list of unified market $symbols, required on blofin
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->marginMode] 'cross' or 'isolated'
             * @return {array} a list of ~@link https://docs.ccxt.com/#/?id=leverage-structure leverage structures~
             */
            Async\await($this->load_markets());
            if ($symbols === null) {
                throw new ArgumentsRequired($this->id . ' fetchLeverages() requires a $symbols argument');
            }
            $marginMode = null;
            list($marginMode, $params) = $this->handle_margin_mode_and_params('fetchLeverages', $params);
            if ($marginMode === null) {
                $marginMode = $this->safe_string($params, 'marginMode', 'cross'); // cross $marginMode
            }
            if (($marginMode !== 'cross') && ($marginMode !== 'isolated')) {
                throw new BadRequest($this->id . ' fetchLeverages() requires a $marginMode parameter that must be either cross or isolated');
            }
            $symbols = $this->market_symbols($symbols);
            $instIds = '';
            for ($i = 0; $i < count($symbols); $i++) {
                $entry = $symbols[$i];
                $entryMarket = $this->market($entry);
                if ($i > 0) {
                    $instIds = $instIds . ',' . $entryMarket['id'];
                } else {
                    $instIds = $instIds . $entryMarket['id'];
                }
            }
            $request = array(
                'instId' => $instIds,
                'marginMode' => $marginMode,
            );
            $response = Async\await($this->privateGetAccountBatchLeverageInfo ($this->extend($request, $params)));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "success",
            //         "data" => array(
            //             array(
            //                 "leverage" => "3",
            //                 "marginMode" => "cross",
            //                 "instId" => "BTC-USDT"
            //             ),
            //         )
            //     }
            //
            $leverages = $this->safe_list($response, 'data', array());
            return $this->parse_leverages($leverages, $symbols, 'instId');
        }) ();
    }

    public function fetch_leverage(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * fetch the set leverage for a $market
             *
             * @see https://docs.blofin.com/index.html#get-leverage
             *
             * @param {string} $symbol unified $market $symbol
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->marginMode] 'cross' or 'isolated'
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=leverage-structure leverage structure~
             */
            Async\await($this->load_markets());
            $marginMode = null;
            list($marginMode, $params) = $this->handle_margin_mode_and_params('fetchLeverage', $params);
            if ($marginMode === null) {
                $marginMode = $this->safe_string($params, 'marginMode', 'cross'); // cross $marginMode
            }
            if (($marginMode !== 'cross') && ($marginMode !== 'isolated')) {
                throw new BadRequest($this->id . ' fetchLeverage() requires a $marginMode parameter that must be either cross or isolated');
            }
            $market = $this->market($symbol);
            $request = array(
                'instId' => $market['id'],
                'marginMode' => $marginMode,
            );
            $response = Async\await($this->privateGetAccountLeverageInfo ($this->extend($request, $params)));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "success",
            //         "data" => {
            //             "leverage" => "3",
            //             "marginMode" => "cross",
            //             "instId" => "BTC-USDT"
            //         }
            //     }
            //
            $data = $this->safe_dict($response, 'data', array());
            return $this->parse_leverage($data, $market);
        }) ();
    }

    public function parse_leverage(array $leverage, ?array $market = null): array {
        $marketId = $this->safe_string($leverage, 'instId');
        $leverageValue = $this->safe_integer($leverage, 'leverage');
        return array(
            'info' => $leverage,
            'symbol' => $this->safe_symbol($marketId, $market),
            'marginMode' => $this->safe_string_lower($leverage, 'marginMode'),
            'longLeverage' => $leverageValue,
            'shortLeverage' => $leverageValue,
        );
    }

    public function set_leverage(int $leverage, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($leverage, $symbol, $params) {
            /**
             * set the level of $leverage for a $market
             *
             * @see https://blofin.com/docs#set-$leverage
             *
             * @param {int} $leverage the rate of $leverage
             * @param {string} $symbol unified $market $symbol
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {string} [$params->marginMode] 'cross' or 'isolated'
             * @param {string} [$params->positionSide] 'long' or 'short' - required for hedged mode in isolated margin
             * @return {array} $response from the exchange
             */
            if ($symbol === null) {
                throw new ArgumentsRequired($this->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) || ($leverage > 125)) {
                throw new BadRequest($this->id . ' setLeverage() $leverage should be between 1 and 125');
            }
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $marginMode = null;
            list($marginMode, $params) = $this->handle_margin_mode_and_params('setLeverage', $params, 'cross');
            if (($marginMode !== 'cross') && ($marginMode !== 'isolated')) {
                throw new BadRequest($this->id . ' setLeverage() requires a $marginMode parameter that must be either cross or isolated');
            }
            $request = array(
                'leverage' => $leverage,
                'marginMode' => $marginMode,
                'instId' => $market['id'],
            );
            $response = Async\await($this->privatePostAccountSetLeverage ($this->extend($request, $params)));
            return $response;
        }) ();
    }

    public function close_position(string $symbol, ?string $side = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $side, $params) {
            /**
             * closes open positions for a $market
             *
             * @see https://blofin.com/docs#close-positions
             *
             * @param {string} $symbol Unified CCXT $market $symbol
             * @param {string} [$side] 'buy' or 'sell', leave in net mode
             * @param {array} [$params] extra parameters specific to the blofin api endpoint
             * @param {string} [$params->clientOrderId] a unique identifier for the order
             * @param {string} [$params->marginMode] 'cross' or 'isolated', default is 'cross;
             * @param {string} [$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 {string} [$params->tag] order tag a combination of case-sensitive alphanumerics, all numbers, or all letters of up to 16 characters
             * @return {array[]} ~@link https://docs.ccxt.com/#/?id=position-structure A list of position structures~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $clientOrderId = $this->safe_string($params, 'clientOrderId');
            $marginMode = null;
            list($marginMode, $params) = $this->handle_margin_mode_and_params('closePosition', $params, 'cross');
            $request = array(
                'instId' => $market['id'],
                'marginMode' => $marginMode,
            );
            if ($clientOrderId !== null) {
                $request['clientOrderId'] = $clientOrderId;
            }
            $response = Async\await($this->privatePostTradeClosePosition ($this->extend($request, $params)));
            return $this->safe_dict($response, 'data');
        }) ();
    }

    public function fetch_closed_orders(?string $symbol = null, ?int $since = null, ?int $limit = null, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $since, $limit, $params) {
            /**
             * 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 {string} $symbol unified $market $symbol of the $market orders were made in
             * @param {int} [$since] the earliest time in ms to fetch orders for
             * @param {int} [$limit] the maximum number of  orde structures to retrieve
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @param {bool} [$params->trigger] True if fetching trigger or conditional orders
             * @param {boolean} [$params->paginate] default false, when true will automatically $paginate by calling this endpoint multiple times. See in the docs all the [availble parameters](https://github.com/ccxt/ccxt/wiki/Manual#pagination-$params)
             * @return {Order[]} a list of ~@link https://docs.ccxt.com/#/?id=order-structure order structures~
             */
            Async\await($this->load_markets());
            $paginate = false;
            list($paginate, $params) = $this->handle_option_and_params($params, 'fetchClosedOrders', 'paginate');
            if ($paginate) {
                return Async\await($this->fetch_paginated_call_dynamic('fetchClosedOrders', $symbol, $since, $limit, $params));
            }
            $request = array(
            );
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
                $request['instId'] = $market['id'];
            }
            if ($limit !== null) {
                $request['limit'] = $limit; // default 100, max 100
            }
            if ($since !== null) {
                $request['begin'] = $since;
            }
            $isTrigger = $this->safe_bool_n($params, array( 'stop', 'trigger', 'tpsl', 'TPSL' ), false);
            $method = null;
            list($method, $params) = $this->handle_option_and_params($params, 'fetchOpenOrders', 'method', 'privateGetTradeOrdersHistory');
            $query = $this->omit($params, array( 'method', 'stop', 'trigger', 'tpsl', 'TPSL' ));
            $response = null;
            if (($isTrigger) || ($method === 'privateGetTradeOrdersTpslHistory')) {
                $response = Async\await($this->privateGetTradeOrdersTpslHistory ($this->extend($request, $query)));
            } else {
                $response = Async\await($this->privateGetTradeOrdersHistory ($this->extend($request, $query)));
            }
            $data = $this->safe_list($response, 'data', array());
            return $this->parse_orders($data, $market, $since, $limit);
        }) ();
    }

    public function fetch_margin_mode(string $symbol, $params = array ()): PromiseInterface {
        return Async\async(function () use ($symbol, $params) {
            /**
             * fetches the margin mode of a trading pair
             *
             * @see https://docs.blofin.com/index.html#get-margin-mode
             *
             * @param {string} $symbol unified $symbol of the $market to fetch the margin mode for
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} a ~@link https://docs.ccxt.com/#/?id=margin-mode-structure margin mode structure~
             */
            Async\await($this->load_markets());
            $market = $this->market($symbol);
            $response = Async\await($this->privateGetAccountMarginMode ($params));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "success",
            //         "data" => {
            //             "marginMode" => "cross"
            //         }
            //     }
            //
            $data = $this->safe_dict($response, 'data', array());
            return $this->parse_margin_mode($data, $market);
        }) ();
    }

    public function parse_margin_mode(array $marginMode, ?array $market = null): array {
        return array(
            'info' => $marginMode,
            'symbol' => $this->safe_string($market, 'symbol'),
            'marginMode' => $this->safe_string($marginMode, 'marginMode'),
        );
    }

    public function set_margin_mode(string $marginMode, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($marginMode, $symbol, $params) {
            /**
             * set margin mode to 'cross' or 'isolated'
             *
             * @see https://docs.blofin.com/index.html#set-margin-mode
             *
             * @param {string} $marginMode 'cross' or 'isolated'
             * @param {string} [$symbol] unified $market $symbol (not used in blofin setMarginMode)
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} $response from the exchange
             */
            $this->check_required_argument('setMarginMode', $marginMode, 'marginMode', array( 'cross', 'isolated' ));
            Async\await($this->load_markets());
            $market = null;
            if ($symbol !== null) {
                $market = $this->market($symbol);
            }
            $request = array(
                'marginMode' => $marginMode,
            );
            $response = Async\await($this->privatePostAccountSetMarginMode ($this->extend($request, $params)));
            //
            //     {
            //         "code" => "0",
            //         "msg" => "success",
            //         "data" => {
            //             "marginMode" => "isolated"
            //         }
            //     }
            //
            $data = $this->safe_dict($response, 'data', array());
            return $this->parse_margin_mode($data, $market);
        }) ();
    }

    public function fetch_position_mode(?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($symbol, $params) {
            /**
             * fetchs the position mode, hedged or one way
             *
             * @see https://docs.blofin.com/index.html#get-position-mode
             *
             * @param {string} [$symbol] unified $symbol of the market to fetch the position mode for (not used in blofin fetchPositionMode)
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} an object detailing whether the market is in hedged or one-way mode
             */
            $response = Async\await($this->privateGetAccountPositionMode ($params));
            $data = $this->safe_dict($response, 'data', array());
            $positionMode = $this->safe_string($data, 'positionMode');
            //
            //     {
            //         "code" => "0",
            //         "msg" => "success",
            //         "data" => {
            //             "positionMode" => "long_short_mode"
            //         }
            //     }
            //
            return array(
                'info' => $data,
                'hedged' => $positionMode === 'long_short_mode',
            );
        }) ();
    }

    public function set_position_mode(bool $hedged, ?string $symbol = null, $params = array ()) {
        return Async\async(function () use ($hedged, $symbol, $params) {
            /**
             * set $hedged to true or false for a market
             *
             * @see https://docs.blofin.com/index.html#set-position-mode
             *
             * @param {bool} $hedged set to true to use $hedged mode, false for one-way mode
             * @param {string} [$symbol] not used by blofin setPositionMode ()
             * @param {array} [$params] extra parameters specific to the exchange API endpoint
             * @return {array} response from the exchange
             */
            $request = array(
                'positionMode' => $hedged ? 'long_short_mode' : 'net_mode',
            );
            //
            //     {
            //         "code" => "0",
            //         "msg" => "success",
            //         "data" => {
            //             "positionMode" => "net_mode"
            //         }
            //     }
            //
            return Async\await($this->privatePostAccountSetPositionMode ($this->extend($request, $params)));
        }) ();
    }

    public function handle_errors(int $httpCode, string $reason, string $url, string $method, array $headers, string $body, $response, $requestHeaders, $requestBody) {
        if ($response === null) {
            return null; // fallback to default error handler
        }
        //
        // array("code":"152002","msg":"Parameter bar error.")
        //
        $code = $this->safe_string($response, 'code');
        $message = $this->safe_string($response, 'msg');
        $feedback = $this->id . ' ' . $body;
        if ($code !== null && $code !== '0') {
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $message, $feedback);
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $code, $feedback);
            $this->throw_broadly_matched_exception($this->exceptions['broad'], $message, $feedback);
            throw new ExchangeError($feedback); // unknown $message
        }
        //
        //  {
        //      orderId => null,
        //      clientOrderId => '',
        //      msg => 'Order failed. Insufficient USDT margin in account',
        //      $code => '103003'
        //  }
        //
        $data = $this->safe_list($response, 'data');
        $first = $this->safe_dict($data, 0);
        $insideMsg = $this->safe_string($first, 'msg');
        $insideCode = $this->safe_string($first, 'code');
        if ($insideCode !== null && $insideCode !== '0') {
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $insideCode, $feedback);
            $this->throw_exactly_matched_exception($this->exceptions['exact'], $insideMsg, $feedback);
            $this->throw_broadly_matched_exception($this->exceptions['broad'], $insideMsg, $feedback);
        }
        return null;
    }

    public function sign($path, $api = 'public', $method = 'GET', $params = array (), $headers = null, $body = null) {
        $request = '/api/' . $this->version . '/' . $this->implode_params($path, $params);
        $query = $this->omit($params, $this->extract_params($path));
        $url = $this->implode_hostname($this->urls['api']['rest']) . $request;
        // $type = $this->getPathAuthenticationType ($path);
        if ($api === 'public') {
            if (!$this->is_empty($query)) {
                $url .= '?' . $this->urlencode($query);
            }
        } elseif ($api === 'private') {
            $this->check_required_credentials();
            $timestamp = (string) $this->milliseconds();
            $headers = array(
                'ACCESS-KEY' => $this->apiKey,
                'ACCESS-PASSPHRASE' => $this->password,
                'ACCESS-TIMESTAMP' => $timestamp,
                'ACCESS-NONCE' => $timestamp,
            );
            $sign_body = '';
            if ($method === 'GET') {
                if (!$this->is_empty($query)) {
                    $urlencodedQuery = '?' . $this->urlencode($query);
                    $url .= $urlencodedQuery;
                    $request .= $urlencodedQuery;
                }
            } else {
                if (!$this->is_empty($query)) {
                    $body = $this->json($query);
                    $sign_body = $body;
                }
                $headers['Content-Type'] = 'application/json';
            }
            $auth = $request . $method . $timestamp . $timestamp . $sign_body;
            $signature = base64_encode($this->hmac($this->encode($auth), $this->encode($this->secret), 'sha256'));
            $headers['ACCESS-SIGN'] = $signature;
        }
        return array( 'url' => $url, 'method' => $method, 'body' => $body, 'headers' => $headers );
    }
}
