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

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

from ccxt.async_support.base.exchange import Exchange

# -----------------------------------------------------------------------------

try:
    basestring  # Python 3
except NameError:
    basestring = str  # Python 2
import hashlib
from ccxt.base.errors import ExchangeError
from ccxt.base.errors import AuthenticationError
from ccxt.base.errors import PermissionDenied
from ccxt.base.errors import ArgumentsRequired
from ccxt.base.errors import BadRequest
from ccxt.base.errors import BadSymbol
from ccxt.base.errors import InsufficientFunds
from ccxt.base.errors import InvalidOrder
from ccxt.base.errors import OrderNotFound
from ccxt.base.errors import CancelPending
from ccxt.base.errors import DuplicateOrderId
from ccxt.base.errors import RateLimitExceeded
from ccxt.base.errors import ExchangeNotAvailable
from ccxt.base.decimal_to_precision import TICK_SIZE
from ccxt.base.precise import Precise


class ftx(Exchange):

    def describe(self):
        return self.deep_extend(super(ftx, self).describe(), {
            'id': 'ftx',
            'name': 'FTX',
            'countries': ['BS'],  # Bahamas
            # hard limit of 6 requests per 200ms => 30 requests per 1000ms => 1000ms / 30 = 33.3333 ms between requests
            # 10 withdrawal requests per 30 seconds = (1000ms / rateLimit) / (1/3) = 90.1
            # cancels do not count towards rateLimit
            # only 'order-making' requests count towards ratelimit
            'rateLimit': 33.34,
            'certified': True,
            'pro': True,
            'hostname': 'ftx.com',  # or ftx.us
            'urls': {
                'logo': 'https://user-images.githubusercontent.com/1294454/67149189-df896480-f2b0-11e9-8816-41593e17f9ec.jpg',
                'www': 'https://ftx.com',
                'api': {
                    'public': 'https://{hostname}',
                    'private': 'https://{hostname}',
                },
                'doc': 'https://github.com/ftexchange/ftx',
                'fees': 'https://ftexchange.zendesk.com/hc/en-us/articles/360024479432-Fees',
                'referral': {
                    'url': 'https://ftx.com/#a=ccxt',
                    'discount': 0.05,
                },
            },
            'has': {
                'spot': True,
                'margin': True,
                'swap': True,
                'future': True,
                'option': False,
                'cancelAllOrders': True,
                'cancelOrder': True,
                'createOrder': True,
                'editOrder': True,
                'fetchBalance': True,
                'fetchBorrowRate': True,
                'fetchBorrowRateHistory': False,
                'fetchBorrowRates': True,
                'fetchClosedOrders': None,
                'fetchCurrencies': True,
                'fetchDepositAddress': True,
                'fetchDeposits': True,
                'fetchFundingFees': None,
                'fetchFundingRate': True,
                'fetchFundingHistory': True,
                'fetchFundingRateHistory': True,
                'fetchFundingRates': None,
                'fetchIndexOHLCV': True,
                'fetchMarkets': True,
                'fetchMarkOHLCV': False,
                'fetchMyTrades': True,
                'fetchOHLCV': True,
                'fetchOpenOrders': True,
                'fetchOrder': True,
                'fetchOrderBook': True,
                'fetchOrders': True,
                'fetchOrderTrades': True,
                'fetchPositions': True,
                'fetchPremiumIndexOHLCV': False,
                'fetchTicker': True,
                'fetchTickers': True,
                'fetchTime': False,
                'fetchTrades': True,
                'fetchTradingFees': True,
                'fetchWithdrawals': True,
                'setLeverage': True,
                'setMarginMode': False,  # FTX only supports cross margin
                'withdraw': True,
            },
            'timeframes': {
                '15s': '15',
                '1m': '60',
                '5m': '300',
                '15m': '900',
                '1h': '3600',
                '4h': '14400',
                '1d': '86400',
                '3d': '259200',
                '1w': '604800',
                '2w': '1209600',
                '1M': '2592000',
            },
            'api': {
                'public': {
                    'get': {
                        'coins': 1,
                        # markets
                        'markets': 1,
                        'markets/{market_name}': 1,
                        'markets/{market_name}/orderbook': 1,  # ?depth={depth}
                        'markets/{market_name}/trades': 1,  # ?limit={limit}&start_time={start_time}&end_time={end_time}
                        'markets/{market_name}/candles': 1,  # ?resolution={resolution}&limit={limit}&start_time={start_time}&end_time={end_time}
                        # futures
                        'futures': 1,
                        'futures/{future_name}': 1,
                        'futures/{future_name}/stats': 1,
                        'funding_rates': 1,
                        'indexes/{index_name}/weights': 1,
                        'expired_futures': 1,
                        'indexes/{market_name}/candles': 1,  # ?resolution={resolution}&limit={limit}&start_time={start_time}&end_time={end_time}
                        # wallet
                        'wallet/coins': 1,
                        # leverage tokens
                        'lt/tokens': 1,
                        'lt/{token_name}': 1,
                        # etfs
                        'etfs/rebalance_info': 1,
                        # options
                        'options/requests': 1,
                        'options/trades': 1,
                        'options/historical_volumes/BTC': 1,
                        'stats/24h_options_volume': 1,
                        'options/open_interest/BTC': 1,
                        'options/historical_open_interest/BTC': 1,
                        # spot margin
                        'spot_margin/history': 1,
                        'spot_margin/borrow_summary': 1,
                        # nfts
                        'nft/nfts': 1,
                        'nft/{nft_id}': 1,
                        'nft/{nft_id}/trades': 1,
                        'nft/all_trades': 1,
                        'nft/{nft_id}/account_info': 1,
                        'nft/collections': 1,
                        # ftx pay
                        'ftxpay/apps/{user_specific_id}/details': 1,
                    },
                    'post': {
                        'ftxpay/apps/{user_specific_id}/orders': 1,
                    },
                },
                'private': {
                    'get': {
                        # subaccounts
                        'subaccounts': 1,
                        'subaccounts/{nickname}/balances': 1,
                        # account
                        'account': 1,
                        'positions': 1,
                        # wallet
                        'wallet/balances': 1,
                        'wallet/all_balances': 1,
                        'wallet/deposit_address/{coin}': 1,  # ?method={method}
                        'wallet/deposits': 1,
                        'wallet/withdrawals': 1,
                        'wallet/airdrops': 1,
                        'wallet/withdrawal_fee': 1,
                        'wallet/saved_addresses': 1,
                        # orders
                        'orders': 1,  # ?market={market}
                        'orders/history': 1,  # ?market={market}
                        'orders/{order_id}': 1,
                        'orders/by_client_id/{client_order_id}': 1,
                        # conditional orders
                        'conditional_orders': 1,  # ?market={market}
                        'conditional_orders/{conditional_order_id}/triggers': 1,
                        'conditional_orders/history': 1,  # ?market={market}
                        'fills': 1,  # ?market={market}
                        'funding_payments': 1,
                        # leverage tokens
                        'lt/balances': 1,
                        'lt/creations': 1,
                        'lt/redemptions': 1,
                        # options
                        'options/my_requests': 1,
                        'options/requests/{request_id}/quotes': 1,
                        'options/my_quotes': 1,
                        'options/account_info': 1,
                        'options/positions': 1,
                        'options/fills': 1,
                        # staking
                        'staking/stakes': 1,
                        'staking/unstake_requests': 1,
                        'staking/balances': 1,
                        'staking/staking_rewards': 1,
                        # otc
                        'otc/quotes/{quoteId}': 1,
                        # spot margin
                        'spot_margin/borrow_rates': 1,
                        'spot_margin/lending_rates': 1,
                        'spot_margin/market_info': 1,  # ?market={market}
                        'spot_margin/borrow_history': 1,
                        'spot_margin/lending_history': 1,
                        'spot_margin/offers': 1,
                        'spot_margin/lending_info': 1,
                        # nfts
                        'nft/balances': 1,
                        'nft/bids': 1,
                        'nft/deposits': 1,
                        'nft/withdrawals': 1,
                        'nft/fills': 1,
                        'nft/gallery/{gallery_id}': 1,
                        'nft/gallery_settings': 1,
                        # latency statistics
                        'stats/latency_stats': 1,
                        # pnl
                        'pnl/historical_changes': 1,
                    },
                    'post': {
                        # subaccounts
                        'subaccounts': 1,
                        'subaccounts/update_name': 1,
                        'subaccounts/transfer': 1,
                        # account
                        'account/leverage': 1,
                        # wallet
                        'wallet/withdrawals': 90,
                        'wallet/saved_addresses': 1,
                        # orders
                        'orders': 1,
                        'conditional_orders': 1,
                        'orders/{order_id}/modify': 1,
                        'orders/by_client_id/{client_order_id}/modify': 1,
                        'conditional_orders/{order_id}/modify': 1,
                        # leverage tokens
                        'lt/{token_name}/create': 1,
                        'lt/{token_name}/redeem': 1,
                        # options
                        'options/requests': 1,
                        'options/requests/{request_id}/quotes': 1,
                        'options/quotes/{quote_id}/accept': 1,
                        # staking
                        'staking/unstake_requests': 1,
                        'srm_stakes/stakes': 1,
                        # otc
                        'otc/quotes/{quote_id}/accept': 1,
                        'otc/quotes': 1,
                        # spot margin
                        'spot_margin/offers': 1,
                        # nfts
                        'nft/offer': 1,
                        'nft/buy': 1,
                        'nft/auction': 1,
                        'nft/edit_auction': 1,
                        'nft/cancel_auction': 1,
                        'nft/bids': 1,
                        'nft/redeem': 1,
                        'nft/gallery_settings': 1,
                        # ftx pay
                        'ftxpay/apps/{user_specific_id}/orders': 1,
                    },
                    'delete': {
                        # subaccounts
                        'subaccounts': 1,
                        # wallet
                        'wallet/saved_addresses/{saved_address_id}': 1,
                        # orders
                        'orders/{order_id}': 1,
                        'orders/by_client_id/{client_order_id}': 1,
                        'orders': 1,
                        'conditional_orders/{order_id}': 1,
                        # options
                        'options/requests/{request_id}': 1,
                        'options/quotes/{quote_id}': 1,
                        # staking
                        'staking/unstake_requests/{request_id}': 1,
                    },
                },
            },
            'fees': {
                'trading': {
                    'tierBased': True,
                    'percentage': True,
                    'maker': self.parse_number('0.0002'),
                    'taker': self.parse_number('0.0007'),
                    'tiers': {
                        'taker': [
                            [self.parse_number('0'), self.parse_number('0.0007')],
                            [self.parse_number('2000000'), self.parse_number('0.0006')],
                            [self.parse_number('5000000'), self.parse_number('0.00055')],
                            [self.parse_number('10000000'), self.parse_number('0.0005')],
                            [self.parse_number('25000000'), self.parse_number('0.0045')],
                            [self.parse_number('50000000'), self.parse_number('0.0004')],
                        ],
                        'maker': [
                            [self.parse_number('0'), self.parse_number('0.0002')],
                            [self.parse_number('2000000'), self.parse_number('0.00015')],
                            [self.parse_number('5000000'), self.parse_number('0.0001')],
                            [self.parse_number('10000000'), self.parse_number('0.00005')],
                            [self.parse_number('25000000'), self.parse_number('0')],
                            [self.parse_number('50000000'), self.parse_number('0')],
                        ],
                    },
                },
                'funding': {
                    'withdraw': {},
                },
            },
            'exceptions': {
                'exact': {
                    'Please slow down': RateLimitExceeded,  # {"error":"Please slow down","success":false}
                    'Size too small for provide': InvalidOrder,  # {"error":"Size too small for provide","success":false}
                    'Not enough balances': InsufficientFunds,  # {"error":"Not enough balances","success":false}
                    'InvalidPrice': InvalidOrder,  # {"error":"Invalid price","success":false}
                    'Size too small': InvalidOrder,  # {"error":"Size too small","success":false}
                    'Size too large': InvalidOrder,  # {"error":"Size too large","success":false}
                    'Invalid price': InvalidOrder,  # {"success":false,"error":"Invalid price"}
                    'Missing parameter price': InvalidOrder,  # {"error":"Missing parameter price","success":false}
                    'Order not found': OrderNotFound,  # {"error":"Order not found","success":false}
                    'Order already closed': InvalidOrder,  # {"error":"Order already closed","success":false}
                    'Trigger price too high': InvalidOrder,  # {"error":"Trigger price too high","success":false}
                    'Trigger price too low': InvalidOrder,  # {"error":"Trigger price too low","success":false}
                    'Order already queued for cancellation': CancelPending,  # {"error":"Order already queued for cancellation","success":false}
                    'Duplicate client order ID': DuplicateOrderId,  # {"error":"Duplicate client order ID","success":false}
                    'Spot orders cannot be reduce-only': InvalidOrder,  # {"error":"Spot orders cannot be reduce-only","success":false}
                    'Invalid reduce-only order': InvalidOrder,  # {"error":"Invalid reduce-only order","success":false}
                    'Account does not have enough balances': InsufficientFunds,  # {"success":false,"error":"Account does not have enough balances"}
                    'Not authorized for subaccount-specific access': PermissionDenied,  # {"success":false,"error":"Not authorized for subaccount-specific access"}
                    'Not approved to trade self product': PermissionDenied,  # {"success":false,"error":"Not approved to trade self product"}
                },
                'broad': {
                    # {"error":"Not logged in","success":false}
                    # {"error":"Not logged in: Invalid API key","success":false}
                    'Not logged in': AuthenticationError,
                    'Account does not have enough margin for order': InsufficientFunds,
                    'Invalid parameter': BadRequest,  # {"error":"Invalid parameter start_time","success":false}
                    'The requested URL was not found on the server': BadRequest,
                    'No such coin': BadRequest,
                    'No such subaccount': BadRequest,
                    'No such future': BadSymbol,
                    'No such market': BadSymbol,
                    'Do not send more than': RateLimitExceeded,
                    'An unexpected error occurred': ExchangeNotAvailable,  # {"error":"An unexpected error occurred, please try again later(58BC21C795).","success":false}
                    'Please retry request': ExchangeNotAvailable,  # {"error":"Please retry request","success":false}
                    'Please try again': ExchangeNotAvailable,  # {"error":"Please try again","success":false}
                    'Try again': ExchangeNotAvailable,  # {"error":"Try again","success":false}
                    'Only have permissions for subaccount': PermissionDenied,  # {"success":false,"error":"Only have permissions for subaccount *sub_name*"}
                },
            },
            'precisionMode': TICK_SIZE,
            'options': {
                # support for canceling conditional orders
                # https://github.com/ccxt/ccxt/issues/6669
                'cancelOrder': {
                    'method': 'privateDeleteOrdersOrderId',  # privateDeleteConditionalOrdersOrderId
                },
                'fetchOpenOrders': {
                    'method': 'privateGetOrders',  # privateGetConditionalOrders
                },
                'fetchOrders': {
                    'method': 'privateGetOrdersHistory',  # privateGetConditionalOrdersHistory
                },
                'sign': {
                    'ftx.com': 'FTX',
                    'ftx.us': 'FTXUS',
                },
                'networks': {
                    'SOL': 'sol',
                    'SPL': 'sol',
                    'TRX': 'trx',
                    'TRC20': 'trx',
                    'ETH': 'erc20',
                    'ERC20': 'erc20',
                    'OMNI': 'omni',
                    'BEP2': 'bep2',
                    'BNB': 'bep2',
                    'BEP20': 'bsc',
                    'BSC': 'bsc',
                },
            },
        })

    async def fetch_currencies(self, params={}):
        response = await self.publicGetCoins(params)
        currencies = self.safe_value(response, 'result', [])
        #
        #     {
        #         "success":true,
        #         "result": [
        #             {"id":"BTC","name":"Bitcoin"},
        #             {"id":"ETH","name":"Ethereum"},
        #             {"id":"ETHMOON","name":"10X Long Ethereum Token","underlying":"ETH"},
        #             {"id":"EOSBULL","name":"3X Long EOS Token","underlying":"EOS"},
        #         ],
        #     }
        #
        result = {}
        for i in range(0, len(currencies)):
            currency = currencies[i]
            id = self.safe_string(currency, 'id')
            code = self.safe_currency_code(id)
            name = self.safe_string(currency, 'name')
            result[code] = {
                'id': id,
                'code': code,
                'info': currency,
                'type': None,
                'name': name,
                'active': None,
                'deposit': None,
                'withdraw': None,
                'fee': None,
                'precision': None,
                'limits': {
                    'withdraw': {'min': None, 'max': None},
                    'amount': {'min': None, 'max': None},
                },
            }
        return result

    async def fetch_markets(self, params={}):
        response = await self.publicGetMarkets(params)
        #
        #     {
        #         'success': True,
        #         "result": [
        #             {
        #                 "ask":170.37,
        #                 "baseCurrency":null,
        #                 "bid":170.31,
        #                 "change1h":-0.019001554672655036,
        #                 "change24h":-0.024841165359738997,
        #                 "changeBod":-0.03816406029469881,
        #                 "enabled":true,
        #                 "last":170.37,
        #                 "name":"ETH-PERP",
        #                 "price":170.37,
        #                 "priceIncrement":0.01,
        #                 "quoteCurrency":null,
        #                 "quoteVolume24h":7742164.59889,
        #                 "sizeIncrement":0.001,
        #                 "type":"future",
        #                 "underlying":"ETH",
        #                 "volumeUsd24h":7742164.59889
        #             },
        #             {
        #                 "ask":170.44,
        #                 "baseCurrency":"ETH",
        #                 "bid":170.41,
        #                 "change1h":-0.018485459257126403,
        #                 "change24h":-0.023825887743413515,
        #                 "changeBod":-0.037605872388481086,
        #                 "enabled":true,
        #                 "last":172.72,
        #                 "name":"ETH/USD",
        #                 "price":170.44,
        #                 "priceIncrement":0.01,
        #                 "quoteCurrency":"USD",
        #                 "quoteVolume24h":382802.0252,
        #                 "sizeIncrement":0.001,
        #                 "type":"spot",
        #                 "underlying":null,
        #                 "volumeUsd24h":382802.0252
        #             },
        #         ],
        #     }
        #
        #     {
        #         name: "BTC-PERP",
        #         enabled:  True,
        #         postOnly:  False,
        #         priceIncrement: "1.0",
        #         sizeIncrement: "0.0001",
        #         minProvideSize: "0.001",
        #         last: "60397.0",
        #         bid: "60387.0",
        #         ask: "60388.0",
        #         price: "60388.0",
        #         type: "future",
        #         baseCurrency:  null,
        #         quoteCurrency:  null,
        #         underlying: "BTC",
        #         restricted:  False,
        #         highLeverageFeeExempt:  True,
        #         change1h: "-0.0036463231533270636",
        #         change24h: "-0.01844838515677064",
        #         changeBod: "-0.010130151132675475",
        #         quoteVolume24h: "2892083192.6099",
        #         volumeUsd24h: "2892083192.6099"
        #     }
        #
        allFuturesResponse = None
        if self.has['future']:
            allFuturesResponse = await self.publicGetFutures()
        #
        #    {
        #        success: True,
        #        result: [
        #            {
        #                name: "1INCH-PERP",
        #                underlying: "1INCH",
        #                description: "1INCH Token Perpetual Futures",
        #                type: "perpetual",
        #                expiry: null,
        #                perpetual: True,
        #                expired: False,
        #                enabled: True,
        #                postOnly: False,
        #                priceIncrement: "0.0001",
        #                sizeIncrement: "1.0",
        #                last: "2.5556",
        #                bid: "2.5555",
        #                ask: "2.5563",
        #                index: "2.5612449804010833",
        #                mark: "2.5587",
        #                imfFactor: "0.0005",
        #                lowerBound: "2.4315",
        #                upperBound: "2.6893",
        #                underlyingDescription: "1INCH Token",
        #                expiryDescription: "Perpetual",
        #                moveStart: null,
        #                marginPrice: "2.5587",
        #                positionLimitWeight: "20.0",
        #                group: "perpetual",
        #                change1h: "0.00799716356760164",
        #                change24h: "0.004909276569004792",
        #                changeBod: "0.008394419484511705",
        #                volumeUsd24h: "17834492.0818",
        #                volume: "7224898.0",
        #                openInterest: "5597917.0",
        #                openInterestUsd: "14323390.2279",
        #            },
        #            ...
        #        ],
        #    }
        #
        result = []
        markets = self.safe_value(response, 'result', [])
        allFutures = self.safe_value(allFuturesResponse, 'result', [])
        allFuturesDict = self.index_by(allFutures, 'name')
        for i in range(0, len(markets)):
            market = markets[i]
            id = self.safe_string(market, 'name')
            future = self.safe_value(allFuturesDict, id)
            marketType = self.safe_string(market, 'type')
            contract = (marketType == 'future')
            baseId = self.safe_string_2(market, 'baseCurrency', 'underlying')
            quoteId = self.safe_string(market, 'quoteCurrency', 'USD')
            settleId = 'USD' if contract else None
            base = self.safe_currency_code(baseId)
            quote = self.safe_currency_code(quoteId)
            settle = self.safe_currency_code(settleId)
            spot = not contract
            margin = not contract
            perpetual = self.safe_value(future, 'perpetual')
            swap = perpetual
            option = False
            isFuture = contract and not swap
            expiry = None
            expiryDatetime = self.safe_string(future, 'expiry')
            type = 'spot'
            symbol = base + '/' + quote
            if swap:
                type = 'swap'
                symbol = base + '/' + quote + ':' + settle
            elif isFuture:
                type = 'future'
                expiry = self.parse8601(expiryDatetime)
                parsedId = id.split('-')
                length = len(parsedId)
                if length > 2:
                    # handling for MOVE contracts
                    # BTC-MOVE-2022Q1
                    # BTC-MOVE-0106
                    # BTC-MOVE-WK-0121
                    parsedId.pop()
                    # remove expiry
                    # ['BTC', 'MOVE']
                    # ['BTC', 'MOVE']
                    # ['BTC', 'MOVE', 'WK']
                    base = '-'.join(parsedId)
                symbol = base + '/' + quote + ':' + settle + '-' + self.yymmdd(expiry, '')
            # check if a market is a spot or future market
            sizeIncrement = self.safe_number(market, 'sizeIncrement')
            priceIncrement = self.safe_number(market, 'priceIncrement')
            result.append({
                'id': id,
                'symbol': symbol,
                'base': base,
                'quote': quote,
                'settle': settle,
                'baseId': baseId,
                'quoteId': quoteId,
                'settleId': settleId,
                'type': type,
                'spot': spot,
                'margin': margin,
                'swap': swap,
                'future': isFuture,
                'option': option,
                'active': self.safe_value(market, 'enabled'),
                'derivative': contract,
                'contract': contract,
                'linear': True,
                'inverse': False,
                'contractSize': self.parse_number('1'),
                'expiry': expiry,
                'expiryDatetime': self.iso8601(expiry),
                'strike': None,
                'optionType': None,
                'precision': {
                    'amount': sizeIncrement,
                    'price': priceIncrement,
                },
                'limits': {
                    'amount': {
                        'min': sizeIncrement,
                        'max': None,
                    },
                    'price': {
                        'min': priceIncrement,
                        'max': None,
                    },
                    'cost': {
                        'min': None,
                        'max': None,
                    },
                    'leverage': {
                        'min': self.parse_number('1'),
                        'max': self.parse_number('20'),
                    },
                },
                'info': market,
            })
        return result

    def parse_ticker(self, ticker, market=None):
        #
        #     {
        #         "ask":171.29,
        #         "baseCurrency":null,  # base currency for spot markets
        #         "bid":171.24,
        #         "change1h":-0.0012244897959183673,
        #         "change24h":-0.031603346901854366,
        #         "changeBod":-0.03297013492914808,
        #         "enabled":true,
        #         "last":171.44,
        #         "name":"ETH-PERP",
        #         "price":171.29,
        #         "priceIncrement":0.01,
        #         "quoteCurrency":null,  # quote currency for spot markets
        #         "quoteVolume24h":8570651.12113,
        #         "sizeIncrement":0.001,
        #         "type":"future",
        #         "underlying":"ETH",  # null for spot markets
        #         "volumeUsd24h":8570651.12113,
        #     }
        #
        marketId = self.safe_string(ticker, 'name')
        if marketId in self.markets_by_id:
            market = self.markets_by_id[marketId]
        symbol = self.safe_symbol(marketId, market)
        last = self.safe_number(ticker, 'last')
        timestamp = self.safe_timestamp(ticker, 'time', self.milliseconds())
        percentage = self.safe_number(ticker, 'change24h')
        if percentage is not None:
            percentage *= 100
        return self.safe_ticker({
            'symbol': symbol,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'high': self.safe_number(ticker, 'high'),
            'low': self.safe_number(ticker, 'low'),
            'bid': self.safe_number(ticker, 'bid'),
            'bidVolume': self.safe_number(ticker, 'bidSize'),
            'ask': self.safe_number(ticker, 'ask'),
            'askVolume': self.safe_number(ticker, 'askSize'),
            'vwap': None,
            'open': None,
            'close': last,
            'last': last,
            'previousClose': None,
            'change': None,
            'percentage': percentage,
            'average': None,
            'baseVolume': None,
            'quoteVolume': self.safe_number(ticker, 'quoteVolume24h'),
            'info': ticker,
        }, market)

    async def fetch_ticker(self, symbol, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market_name': market['id'],
        }
        response = await self.publicGetMarketsMarketName(self.extend(request, params))
        #
        #     {
        #         "success":true,
        #         "result":{
        #             "ask":171.29,
        #             "baseCurrency":null,  # base currency for spot markets
        #             "bid":171.24,
        #             "change1h":-0.0012244897959183673,
        #             "change24h":-0.031603346901854366,
        #             "changeBod":-0.03297013492914808,
        #             "enabled":true,
        #             "last":171.44,
        #             "name":"ETH-PERP",
        #             "price":171.29,
        #             "priceIncrement":0.01,
        #             "quoteCurrency":null,  # quote currency for spot markets
        #             "quoteVolume24h":8570651.12113,
        #             "sizeIncrement":0.001,
        #             "type":"future",
        #             "underlying":"ETH",  # null for spot markets
        #             "volumeUsd24h":8570651.12113,
        #         }
        #     }
        #
        result = self.safe_value(response, 'result', {})
        return self.parse_ticker(result, market)

    async def fetch_tickers(self, symbols=None, params={}):
        await self.load_markets()
        response = await self.publicGetMarkets(params)
        #
        #     {
        #         'success': True,
        #         "result": [
        #             {
        #                 "ask":170.44,
        #                 "baseCurrency":"ETH",
        #                 "bid":170.41,
        #                 "change1h":-0.018485459257126403,
        #                 "change24h":-0.023825887743413515,
        #                 "changeBod":-0.037605872388481086,
        #                 "enabled":true,
        #                 "last":172.72,
        #                 "name":"ETH/USD",
        #                 "price":170.44,
        #                 "priceIncrement":0.01,
        #                 "quoteCurrency":"USD",
        #                 "quoteVolume24h":382802.0252,
        #                 "sizeIncrement":0.001,
        #                 "type":"spot",
        #                 "underlying":null,
        #                 "volumeUsd24h":382802.0252
        #             },
        #         ],
        #     }
        #
        tickers = self.safe_value(response, 'result', [])
        return self.parse_tickers(tickers, symbols)

    async def fetch_order_book(self, symbol, limit=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market_name': market['id'],
        }
        if limit is not None:
            request['depth'] = limit  # max 100, default 20
        response = await self.publicGetMarketsMarketNameOrderbook(self.extend(request, params))
        #
        #     {
        #         "success":true,
        #         "result":{
        #             "asks":[
        #                 [171.95,279.865],
        #                 [171.98,102.42],
        #                 [171.99,124.11],
        #             ],
        #             "bids":[
        #                 [171.93,69.749],
        #                 [171.9,288.325],
        #                 [171.88,87.47],
        #             ],
        #         }
        #     }
        #
        result = self.safe_value(response, 'result', {})
        return self.parse_order_book(result, symbol)

    def parse_ohlcv(self, ohlcv, market=None):
        #
        #     {
        #         "close":177.23,
        #         "high":177.45,
        #         "low":177.2,
        #         "open":177.43,
        #         "startTime":"2019-10-17T13:27:00+00:00",
        #         "time":1571318820000.0,
        #         "volume":0.0
        #     }
        #
        return [
            self.safe_integer(ohlcv, 'time'),
            self.safe_number(ohlcv, 'open'),
            self.safe_number(ohlcv, 'high'),
            self.safe_number(ohlcv, 'low'),
            self.safe_number(ohlcv, 'close'),
            self.safe_number(ohlcv, 'volume'),
        ]

    def get_market_id(self, symbol, key, params={}):
        parts = self.get_market_params(symbol, key, params)
        return self.safe_string(parts, 1, symbol)

    def get_market_params(self, symbol, key, params={}):
        market = None
        marketId = None
        if symbol in self.markets:
            market = self.market(symbol)
            marketId = market['id']
        else:
            marketId = self.safe_string(params, key, symbol)
        return [market, marketId]

    async def fetch_ohlcv(self, symbol, timeframe='1m', since=None, limit=None, params={}):
        await self.load_markets()
        market, marketId = self.get_market_params(symbol, 'market_name', params)
        request = {
            'resolution': self.timeframes[timeframe],
            'market_name': marketId,
        }
        price = self.safe_string(params, 'price')
        params = self.omit(params, 'price')
        # max 1501 candles, including the current candle when since is not specified
        limit = 1501 if (limit is None) else limit
        if since is None:
            request['end_time'] = self.seconds()
            request['limit'] = limit
            request['start_time'] = request['end_time'] - limit * self.parse_timeframe(timeframe)
        else:
            request['start_time'] = int(since / 1000)
            request['limit'] = limit
            request['end_time'] = self.sum(request['start_time'], limit * self.parse_timeframe(timeframe))
        method = 'publicGetMarketsMarketNameCandles'
        if price == 'index':
            if symbol in self.markets:
                request['market_name'] = market['baseId']
            method = 'publicGetIndexesMarketNameCandles'
        response = await getattr(self, method)(self.extend(request, params))
        #
        #     {
        #         "success": True,
        #         "result":[
        #             {
        #                 "close":177.23,
        #                 "high":177.45,
        #                 "low":177.2,
        #                 "open":177.43,
        #                 "startTime":"2019-10-17T13:27:00+00:00",
        #                 "time":1571318820000.0,
        #                 "volume":0.0
        #             },
        #             {
        #                 "close":177.26,
        #                 "high":177.33,
        #                 "low":177.23,
        #                 "open":177.23,
        #                 "startTime":"2019-10-17T13:28:00+00:00",
        #                 "time":1571318880000.0,
        #                 "volume":0.0
        #             },
        #         ],
        #     }
        #
        result = self.safe_value(response, 'result', [])
        return self.parse_ohlcvs(result, market, timeframe, since, limit)

    async def fetch_index_ohlcv(self, symbol, timeframe='1m', since=None, limit=None, params={}):
        request = {
            'price': 'index',
        }
        return await self.fetch_ohlcv(symbol, timeframe, since, limit, self.extend(request, params))

    def parse_trade(self, trade, market=None):
        #
        # fetchTrades(public)
        #
        #     {
        #         "id":1715826,
        #         "liquidation":false,
        #         "price":171.62,
        #         "side":"buy",
        #         "size":2.095,
        #         "time":"2019-10-18T12:59:54.288166+00:00"
        #     }
        #
        # fetchMyTrades(private)
        #
        #     {
        #         "fee": 20.1374935,
        #         "feeRate": 0.0005,
        #         "feeCurrency": "USD",
        #         "future": "EOS-0329",
        #         "id": 11215,
        #         "liquidity": "taker",
        #         "market": "EOS-0329",
        #         "baseCurrency": null,
        #         "quoteCurrency": null,
        #         "orderId": 8436981,
        #         "price": 4.201,
        #         "side": "buy",
        #         "size": 9587,
        #         "time": "2019-03-27T19:15:10.204619+00:00",
        #         "type": "order"
        #     }
        #
        #     {
        #         "baseCurrency": "BTC",
        #         "fee": 0,
        #         "feeCurrency": "USD",
        #         "feeRate": 0,
        #         "future": null,
        #         "id": 664079556,
        #         "liquidity": "taker",
        #         "market": null,
        #         "orderId": null,
        #         "price": 34830.61359,
        #         "quoteCurrency": "USD",
        #         "side": "sell",
        #         "size": 0.0005996,
        #         "time": "2021-01-15T16:05:29.246135+00:00",
        #         "tradeId": null,
        #         "type": "otc"
        #     }
        #
        #     with -ve fee
        #     {
        #         "id": 1171258927,
        #         "fee": -0.0000713875,
        #         "side": "sell",
        #         "size": 1,
        #         "time": "2021-03-11T13:34:35.523627+00:00",
        #         "type": "order",
        #         "price": 14.2775,
        #         "future": null,
        #         "market": "SOL/USD",
        #         "feeRate": -0.000005,
        #         "orderId": 33182929044,
        #         "tradeId": 582936801,
        #         "liquidity": "maker",
        #         "feeCurrency": "USD",
        #         "baseCurrency": "SOL",
        #         "quoteCurrency": "USD"
        #     }
        #
        #     # from OTC order
        #     {
        #         "id": 1172129651,
        #         "fee": 0,
        #         "side": "sell",
        #         "size": 1.47568846,
        #         "time": "2021-03-11T15:04:46.893383+00:00",
        #         "type": "otc",
        #         "price": 14.60932598,
        #         "future": null,
        #         "market": null,
        #         "feeRate": 0,
        #         "orderId": null,
        #         "tradeId": null,
        #         "liquidity": "taker",
        #         "feeCurrency": "USD",
        #         "baseCurrency": "BCHA",
        #         "quoteCurrency": "USD"
        #     }
        id = self.safe_string(trade, 'id')
        takerOrMaker = self.safe_string(trade, 'liquidity')
        marketId = self.safe_string(trade, 'market')
        market = self.safe_market(marketId, market)
        symbol = market['symbol']
        timestamp = self.parse8601(self.safe_string(trade, 'time'))
        priceString = self.safe_string(trade, 'price')
        amountString = self.safe_string(trade, 'size')
        side = self.safe_string(trade, 'side')
        fee = None
        feeCostString = self.safe_string(trade, 'fee')
        if feeCostString is not None:
            feeCurrencyId = self.safe_string(trade, 'feeCurrency')
            feeCurrencyCode = self.safe_currency_code(feeCurrencyId)
            fee = {
                'cost': feeCostString,
                'currency': feeCurrencyCode,
                'rate': self.safe_string(trade, 'feeRate'),
            }
        orderId = self.safe_string(trade, 'orderId')
        return self.safe_trade({
            'info': trade,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'symbol': symbol,
            'id': id,
            'order': orderId,
            'type': None,
            'takerOrMaker': takerOrMaker,
            'side': side,
            'price': priceString,
            'amount': amountString,
            'cost': None,
            'fee': fee,
        }, market)

    async def fetch_trades(self, symbol, since=None, limit=None, params={}):
        await self.load_markets()
        market, marketId = self.get_market_params(symbol, 'market_name', params)
        request = {
            'market_name': marketId,
        }
        if since is not None:
            request['start_time'] = int(since / 1000)
            # start_time doesn't work without end_time
            request['end_time'] = self.seconds()
        if limit is not None:
            request['limit'] = limit
        response = await self.publicGetMarketsMarketNameTrades(self.extend(request, params))
        #
        #     {
        #         "success":true,
        #         "result":[
        #             {
        #                 "id":1715826,
        #                 "liquidation":false,
        #                 "price":171.62,
        #                 "side":"buy",
        #                 "size":2.095,
        #                 "time":"2019-10-18T12:59:54.288166+00:00"
        #             },
        #             {
        #                 "id":1715763,
        #                 "liquidation":false,
        #                 "price":171.89,
        #                 "side":"sell",
        #                 "size":1.477,
        #                 "time":"2019-10-18T12:58:38.443734+00:00"
        #             },
        #         ],
        #     }
        #
        result = self.safe_value(response, 'result', [])
        return self.parse_trades(result, market, since, limit)

    async def fetch_trading_fees(self, params={}):
        await self.load_markets()
        response = await self.privateGetAccount(params)
        #
        #     {
        #         "success": True,
        #         "result": {
        #             "backstopProvider": True,
        #             "collateral": 3568181.02691129,
        #             "freeCollateral": 1786071.456884368,
        #             "initialMarginRequirement": 0.12222384240257728,
        #             "liquidating": False,
        #             "maintenanceMarginRequirement": 0.07177992558058484,
        #             "makerFee": 0.0002,
        #             "marginFraction": 0.5588433331419503,
        #             "openMarginFraction": 0.2447194090423075,
        #             "takerFee": 0.0005,
        #             "totalAccountValue": 3568180.98341129,
        #             "totalPositionSize": 6384939.6992,
        #             "username": "user@domain.com",
        #             "positions": [
        #                 {
        #                     "cost": -31.7906,
        #                     "entryPrice": 138.22,
        #                     "future": "ETH-PERP",
        #                     "initialMarginRequirement": 0.1,
        #                     "longOrderSize": 1744.55,
        #                     "maintenanceMarginRequirement": 0.04,
        #                     "netSize": -0.23,
        #                     "openSize": 1744.32,
        #                     "realizedPnl": 3.39441714,
        #                     "shortOrderSize": 1732.09,
        #                     "side": "sell",
        #                     "size": 0.23,
        #                     "unrealizedPnl": 0,
        #                 },
        #             ],
        #         },
        #     }
        #
        result = self.safe_value(response, 'result', {})
        return {
            'info': response,
            'maker': self.safe_number(result, 'makerFee'),
            'taker': self.safe_number(result, 'takerFee'),
        }

    async def fetch_funding_rate_history(self, symbol=None, since=None, limit=None, params={}):
        #
        # Gets a history of funding rates with their timestamps
        #  (param) symbol: Future currency pair(e.g. "BTC-PERP")
        #  (param) limit: Not used by ftx
        #  (param) since: Unix timestamp in miliseconds for the time of the earliest requested funding rate
        #  (param) params: Object containing more params for the request
        #             - until: Unix timestamp in miliseconds for the time of the earliest requested funding rate
        #  return: [{symbol, fundingRate, timestamp}]
        #
        await self.load_markets()
        request = {}
        if symbol is not None:
            market = self.market(symbol)
            request['future'] = market['id']
        if since is not None:
            request['start_time'] = int(since / 1000)
        till = self.safe_integer(params, 'till')  # unified in milliseconds
        endTime = self.safe_string(params, 'end_time')  # exchange-specific in seconds
        params = self.omit(params, ['end_time', 'till'])
        if till is not None:
            request['end_time'] = int(till / 1000)
        elif endTime is not None:
            request['end_time'] = endTime
        response = await self.publicGetFundingRates(self.extend(request, params))
        #
        #     {
        #        "success": True,
        #        "result": [
        #          {
        #            "future": "BTC-PERP",
        #            "rate": 0.0025,
        #            "time": "2019-06-02T08:00:00+00:00"
        #          }
        #        ]
        #      }
        #
        result = self.safe_value(response, 'result')
        rates = []
        for i in range(0, len(result)):
            entry = result[i]
            marketId = self.safe_string(entry, 'future')
            timestamp = self.parse8601(self.safe_string(result[i], 'time'))
            rates.append({
                'info': entry,
                'symbol': self.safe_symbol(marketId),
                'fundingRate': self.safe_number(entry, 'rate'),
                'timestamp': timestamp,
                'datetime': self.iso8601(timestamp),
            })
        sorted = self.sort_by(rates, 'timestamp')
        return self.filter_by_symbol_since_limit(sorted, symbol, since, limit)

    def parse_balance(self, response):
        result = {
            'info': response,
        }
        balances = self.safe_value(response, 'result', [])
        for i in range(0, len(balances)):
            balance = balances[i]
            code = self.safe_currency_code(self.safe_string(balance, 'coin'))
            account = self.account()
            account['free'] = self.safe_string_2(balance, 'availableWithoutBorrow', 'free')
            account['total'] = self.safe_string(balance, 'total')
            result[code] = account
        return self.safe_balance(result)

    async def fetch_balance(self, params={}):
        await self.load_markets()
        response = await self.privateGetWalletBalances(params)
        #
        #     {
        #         "success": True,
        #         "result": [
        #             {
        #                 "coin": "USDTBEAR",
        #                 "free": 2320.2,
        #                 "total": 2340.2
        #             },
        #         ],
        #     }
        #
        return self.parse_balance(response)

    def parse_order_status(self, status):
        statuses = {
            'new': 'open',
            'open': 'open',
            'closed': 'closed',  # filled or canceled
            'triggered': 'closed',
        }
        return self.safe_string(statuses, status, status)

    def parse_order(self, order, market=None):
        #
        # limit orders - fetchOrder, fetchOrders, fetchOpenOrders, createOrder, editOrder
        #
        #     {
        #         "createdAt": "2019-03-05T09:56:55.728933+00:00",
        #         "filledSize": 0,
        #         "future": "XRP-PERP",
        #         "id": 9596912,
        #         "market": "XRP-PERP",
        #         "price": 0.306525,
        #         "remainingSize": 31431,
        #         "side": "sell",
        #         "size": 31431,
        #         "status": "open",
        #         "type": "limit",
        #         "reduceOnly": False,
        #         "ioc": False,
        #         "postOnly": False,
        #         "clientId": null,
        #     }
        #
        # market orders - fetchOrder, fetchOrders, fetchOpenOrders, createOrder
        #
        #     {
        #         "avgFillPrice": 2666.0,
        #         "clientId": None,
        #         "createdAt": "2020-02-12T00: 53: 49.009726+00: 00",
        #         "filledSize": 0.0007,
        #         "future": None,
        #         "id": 3109208514,
        #         "ioc": True,
        #         "market": "BNBBULL/USD",
        #         "postOnly": False,
        #         "price": None,
        #         "reduceOnly": False,
        #         "remainingSize": 0.0,
        #         "side": "buy",
        #         "size": 0.0007,
        #         "status": "closed",
        #         "type": "market"
        #     }
        #
        # createOrder(conditional, "stop", "trailingStop", or "takeProfit")
        #
        #     {
        #         "createdAt": "2019-03-05T09:56:55.728933+00:00",
        #         "future": "XRP-PERP",
        #         "id": 9596912,
        #         "market": "XRP-PERP",
        #         "triggerPrice": 0.306525,
        #         "orderId": null,
        #         "side": "sell",
        #         "size": 31431,
        #         "status": "open",
        #         "type": "stop",
        #         "orderPrice": null,
        #         "error": null,
        #         "triggeredAt": null,
        #         "reduceOnly": False
        #     }
        #
        # editOrder(conditional, stop, trailing stop, take profit)
        #
        #     {
        #         "createdAt": "2019-03-05T09:56:55.728933+00:00",
        #         "future": "XRP-PERP",
        #         "id": 9596912,
        #         "market": "XRP-PERP",
        #         "triggerPrice": 0.306225,
        #         "orderId": null,
        #         "side": "sell",
        #         "size": 31431,
        #         "status": "open",
        #         "type": "stop",
        #         "orderPrice": null,
        #         "error": null,
        #         "triggeredAt": null,
        #         "reduceOnly": False,
        #         "orderType": "market",
        #         "filledSize": 0,
        #         "avgFillPrice": null,
        #         "retryUntilFilled": False
        #     }
        #
        # canceled order with a closed status
        #
        #     {
        #         "avgFillPrice":null,
        #         "clientId":null,
        #         "createdAt":"2020-09-01T13:45:57.119695+00:00",
        #         "filledSize":0.0,
        #         "future":null,
        #         "id":8553541288,
        #         "ioc":false,
        #         "liquidation":false,
        #         "market":"XRP/USDT",
        #         "postOnly":false,
        #         "price":0.5,
        #         "reduceOnly":false,
        #         "remainingSize":0.0,
        #         "side":"sell",
        #         "size":46.0,
        #         "status":"closed",
        #         "type":"limit"
        #     }
        #
        id = self.safe_string(order, 'id')
        timestamp = self.parse8601(self.safe_string(order, 'createdAt'))
        status = self.parse_order_status(self.safe_string(order, 'status'))
        amount = self.safe_string(order, 'size')
        filled = self.safe_string(order, 'filledSize')
        remaining = self.safe_string(order, 'remainingSize')
        if Precise.string_equals(remaining, '0'):
            remaining = Precise.string_sub(amount, filled)
            if Precise.string_gt(remaining, '0'):
                status = 'canceled'
        symbol = None
        marketId = self.safe_string(order, 'market')
        if marketId is not None:
            if marketId in self.markets_by_id:
                market = self.markets_by_id[marketId]
                symbol = market['symbol']
            else:
                # support for delisted market ids
                # https://github.com/ccxt/ccxt/issues/7113
                symbol = marketId
        if (symbol is None) and (market is not None):
            symbol = market['symbol']
        side = self.safe_string(order, 'side')
        type = self.safe_string(order, 'type')
        average = self.safe_string(order, 'avgFillPrice')
        price = self.safe_string_2(order, 'price', 'triggerPrice', average)
        lastTradeTimestamp = self.parse8601(self.safe_string(order, 'triggeredAt'))
        clientOrderId = self.safe_string(order, 'clientId')
        stopPrice = self.safe_number(order, 'triggerPrice')
        postOnly = self.safe_value(order, 'postOnly')
        return self.safe_order({
            'info': order,
            'id': id,
            'clientOrderId': clientOrderId,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'lastTradeTimestamp': lastTradeTimestamp,
            'symbol': symbol,
            'type': type,
            'timeInForce': None,
            'postOnly': postOnly,
            'side': side,
            'price': price,
            'stopPrice': stopPrice,
            'amount': amount,
            'cost': None,
            'average': average,
            'filled': filled,
            'remaining': remaining,
            'status': status,
            'fee': None,
            'trades': None,
        }, market)

    async def create_order(self, symbol, type, side, amount, price=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'market': market['id'],
            'side': side,  # "buy" or "sell"
            # 'price': 0.306525,  # send null for market orders
            'type': type,  # "limit", "market", "stop", "trailingStop", or "takeProfit"
            'size': float(self.amount_to_precision(symbol, amount)),
            # 'reduceOnly': False,  # optional, default is False
            # 'ioc': False,  # optional, default is False, limit or market orders only
            # 'postOnly': False,  # optional, default is False, limit or market orders only
            # 'clientId': 'abcdef0123456789',  # string, optional, client order id, limit or market orders only
        }
        clientOrderId = self.safe_string_2(params, 'clientId', 'clientOrderId')
        if clientOrderId is not None:
            request['clientId'] = clientOrderId
            params = self.omit(params, ['clientId', 'clientOrderId'])
        method = None
        if type == 'limit':
            method = 'privatePostOrders'
            request['price'] = float(self.price_to_precision(symbol, price))
        elif type == 'market':
            method = 'privatePostOrders'
            request['price'] = None
        elif (type == 'stop') or (type == 'takeProfit'):
            method = 'privatePostConditionalOrders'
            stopPrice = self.safe_number_2(params, 'stopPrice', 'triggerPrice')
            if stopPrice is None:
                raise ArgumentsRequired(self.id + ' createOrder() requires a stopPrice parameter or a triggerPrice parameter for ' + type + ' orders')
            else:
                params = self.omit(params, ['stopPrice', 'triggerPrice'])
                request['triggerPrice'] = float(self.price_to_precision(symbol, stopPrice))
            if price is not None:
                request['orderPrice'] = float(self.price_to_precision(symbol, price))  # optional, order type is limit if self is specified, otherwise market
        elif type == 'trailingStop':
            trailValue = self.safe_number(params, 'trailValue', price)
            if trailValue is None:
                raise ArgumentsRequired(self.id + ' createOrder() requires a trailValue parameter or a price argument(negative or positive) for a ' + type + ' order')
            method = 'privatePostConditionalOrders'
            request['trailValue'] = float(self.price_to_precision(symbol, trailValue))  # negative for "sell", positive for "buy"
        else:
            raise InvalidOrder(self.id + ' createOrder() does not support order type ' + type + ', only limit, market, stop, trailingStop, or takeProfit orders are supported')
        response = await getattr(self, method)(self.extend(request, params))
        #
        # orders
        #
        #     {
        #         "success": True,
        #         "result": [
        #             {
        #                 "createdAt": "2019-03-05T09:56:55.728933+00:00",
        #                 "filledSize": 0,
        #                 "future": "XRP-PERP",
        #                 "id": 9596912,
        #                 "market": "XRP-PERP",
        #                 "price": 0.306525,
        #                 "remainingSize": 31431,
        #                 "side": "sell",
        #                 "size": 31431,
        #                 "status": "open",
        #                 "type": "limit",
        #                 "reduceOnly": False,
        #                 "ioc": False,
        #                 "postOnly": False,
        #                 "clientId": null,
        #             }
        #         ]
        #     }
        #
        # conditional orders
        #
        #     {
        #         "success": True,
        #         "result": [
        #             {
        #                 "createdAt": "2019-03-05T09:56:55.728933+00:00",
        #                 "future": "XRP-PERP",
        #                 "id": 9596912,
        #                 "market": "XRP-PERP",
        #                 "triggerPrice": 0.306525,
        #                 "orderId": null,
        #                 "side": "sell",
        #                 "size": 31431,
        #                 "status": "open",
        #                 "type": "stop",
        #                 "orderPrice": null,
        #                 "error": null,
        #                 "triggeredAt": null,
        #                 "reduceOnly": False
        #             }
        #         ]
        #     }
        #
        #
        result = self.safe_value(response, 'result', [])
        return self.parse_order(result, market)

    async def edit_order(self, id, symbol, type, side, amount, price=None, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {}
        method = None
        clientOrderId = self.safe_string_2(params, 'client_order_id', 'clientOrderId')
        triggerPrice = self.safe_number(params, 'triggerPrice')
        orderPrice = self.safe_number(params, 'orderPrice')
        trailValue = self.safe_number(params, 'trailValue')
        params = self.omit(params, ['client_order_id', 'clientOrderId', 'triggerPrice', 'orderPrice', 'trailValue'])
        triggerPriceIsDefined = (triggerPrice is not None)
        orderPriceIsDefined = (orderPrice is not None)
        trailValueIsDefined = (trailValue is not None)
        if triggerPriceIsDefined or orderPriceIsDefined or trailValueIsDefined:
            method = 'privatePostConditionalOrdersOrderIdModify'
            request['order_id'] = id
            if triggerPriceIsDefined:
                request['triggerPrice'] = float(self.price_to_precision(symbol, triggerPrice))
            if orderPriceIsDefined:
                # only for stop limit or take profit limit orders
                request['orderPrice'] = float(self.price_to_precision(symbol, orderPrice))
            if trailValueIsDefined:
                # negative for sell orders, positive for buy orders
                request['trailValue'] = float(self.price_to_precision(symbol, trailValue))
        else:
            if clientOrderId is None:
                method = 'privatePostOrdersOrderIdModify'
                request['order_id'] = id
            else:
                method = 'privatePostOrdersByClientIdClientOrderIdModify'
                request['client_order_id'] = clientOrderId
                # request['clientId'] = clientOrderId
            if price is not None:
                request['price'] = float(self.price_to_precision(symbol, price))
        if amount is not None:
            request['size'] = float(self.amount_to_precision(symbol, amount))
        response = await getattr(self, method)(self.extend(request, params))
        #
        # regular order
        #
        #     {
        #         "success": True,
        #         "result": {
        #             "createdAt": "2019-03-05T11:56:55.728933+00:00",
        #             "filledSize": 0,
        #             "future": "XRP-PERP",
        #             "id": 9596932,
        #             "market": "XRP-PERP",
        #             "price": 0.326525,
        #             "remainingSize": 31431,
        #             "side": "sell",
        #             "size": 31431,
        #             "status": "open",
        #             "type": "limit",
        #             "reduceOnly": False,
        #             "ioc": False,
        #             "postOnly": False,
        #             "clientId": null,
        #         }
        #     }
        #
        # conditional trigger order
        #
        #     {
        #         "success": True,
        #         "result": {
        #             "createdAt": "2019-03-05T09:56:55.728933+00:00",
        #             "future": "XRP-PERP",
        #             "id": 9596912,
        #             "market": "XRP-PERP",
        #             "triggerPrice": 0.306225,
        #             "orderId": null,
        #             "side": "sell",
        #             "size": 31431,
        #             "status": "open",
        #             "type": "stop",
        #             "orderPrice": null,
        #             "error": null,
        #             "triggeredAt": null,
        #             "reduceOnly": False,
        #             "orderType": "market",
        #             "filledSize": 0,
        #             "avgFillPrice": null,
        #             "retryUntilFilled": False
        #         }
        #     }
        #
        result = self.safe_value(response, 'result', {})
        return self.parse_order(result, market)

    async def cancel_order(self, id, symbol=None, params={}):
        await self.load_markets()
        request = {}
        # support for canceling conditional orders
        # https://github.com/ccxt/ccxt/issues/6669
        options = self.safe_value(self.options, 'cancelOrder', {})
        defaultMethod = self.safe_string(options, 'method', 'privateDeleteOrdersOrderId')
        method = self.safe_string(params, 'method', defaultMethod)
        type = self.safe_value(params, 'type')
        clientOrderId = self.safe_value_2(params, 'client_order_id', 'clientOrderId')
        if clientOrderId is None:
            request['order_id'] = int(id)
            if (type == 'stop') or (type == 'trailingStop') or (type == 'takeProfit'):
                method = 'privateDeleteConditionalOrdersOrderId'
        else:
            request['client_order_id'] = clientOrderId
            method = 'privateDeleteOrdersByClientIdClientOrderId'
        query = self.omit(params, ['method', 'type', 'client_order_id', 'clientOrderId'])
        response = await getattr(self, method)(self.extend(request, query))
        #
        #     {
        #         "success": True,
        #         "result": "Order queued for cancelation"
        #     }
        #
        result = self.safe_value(response, 'result', {})
        return result

    async def cancel_all_orders(self, symbol=None, params={}):
        await self.load_markets()
        request = {
            # 'market': market['id'],  # optional
            # 'conditionalOrdersOnly': False,  # cancel conditional orders only
            # 'limitOrdersOnly': False,  # cancel existing limit orders(non-conditional orders) only
        }
        marketId = self.get_market_id(symbol, 'market', params)
        if marketId is not None:
            request['market'] = marketId
        response = await self.privateDeleteOrders(self.extend(request, params))
        result = self.safe_value(response, 'result', {})
        #
        #     {
        #         "success": True,
        #         "result": "Orders queued for cancelation"
        #     }
        #
        return result

    async def fetch_order(self, id, symbol=None, params={}):
        await self.load_markets()
        request = {}
        clientOrderId = self.safe_value_2(params, 'client_order_id', 'clientOrderId')
        method = 'privateGetOrdersOrderId'
        if clientOrderId is None:
            request['order_id'] = id
        else:
            request['client_order_id'] = clientOrderId
            params = self.omit(params, ['client_order_id', 'clientOrderId'])
            method = 'privateGetOrdersByClientIdClientOrderId'
        response = await getattr(self, method)(self.extend(request, params))
        #
        #     {
        #         "success": True,
        #         "result": {
        #             "createdAt": "2019-03-05T09:56:55.728933+00:00",
        #             "filledSize": 10,
        #             "future": "XRP-PERP",
        #             "id": 9596912,
        #             "market": "XRP-PERP",
        #             "price": 0.306525,
        #             "avgFillPrice": 0.306526,
        #             "remainingSize": 31421,
        #             "side": "sell",
        #             "size": 31431,
        #             "status": "open",
        #             "type": "limit",
        #             "reduceOnly": False,
        #             "ioc": False,
        #             "postOnly": False,
        #             "clientId": null
        #         }
        #     }
        #
        result = self.safe_value(response, 'result', {})
        return self.parse_order(result)

    async def fetch_open_orders(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        request = {}
        market, marketId = self.get_market_params(symbol, 'market', params)
        if marketId is not None:
            request['market'] = marketId
        # support for canceling conditional orders
        # https://github.com/ccxt/ccxt/issues/6669
        options = self.safe_value(self.options, 'fetchOpenOrders', {})
        defaultMethod = self.safe_string(options, 'method', 'privateGetOrders')
        method = self.safe_string(params, 'method', defaultMethod)
        type = self.safe_value(params, 'type')
        if (type == 'stop') or (type == 'trailingStop') or (type == 'takeProfit'):
            method = 'privateGetConditionalOrders'
        query = self.omit(params, ['method', 'type'])
        response = await getattr(self, method)(self.extend(request, query))
        #
        #     {
        #         "success": True,
        #         "result": [
        #             {
        #                 "createdAt": "2019-03-05T09:56:55.728933+00:00",
        #                 "filledSize": 10,
        #                 "future": "XRP-PERP",
        #                 "id": 9596912,
        #                 "market": "XRP-PERP",
        #                 "price": 0.306525,
        #                 "avgFillPrice": 0.306526,
        #                 "remainingSize": 31421,
        #                 "side": "sell",
        #                 "size": 31431,
        #                 "status": "open",
        #                 "type": "limit",
        #                 "reduceOnly": False,
        #                 "ioc": False,
        #                 "postOnly": False,
        #                 "clientId": null
        #             }
        #         ]
        #     }
        #
        result = self.safe_value(response, 'result', [])
        return self.parse_orders(result, market, since, limit)

    async def fetch_orders(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        request = {}
        market, marketId = self.get_market_params(symbol, 'market', params)
        if marketId is not None:
            request['market'] = marketId
        if limit is not None:
            request['limit'] = limit  # default 100, max 100
        if since is not None:
            request['start_time'] = int(since / 1000)
        # support for canceling conditional orders
        # https://github.com/ccxt/ccxt/issues/6669
        options = self.safe_value(self.options, 'fetchOrders', {})
        defaultMethod = self.safe_string(options, 'method', 'privateGetOrdersHistory')
        method = self.safe_string(params, 'method', defaultMethod)
        type = self.safe_value(params, 'type')
        if (type == 'stop') or (type == 'trailingStop') or (type == 'takeProfit'):
            method = 'privateGetConditionalOrdersHistory'
        query = self.omit(params, ['method', 'type'])
        response = await getattr(self, method)(self.extend(request, query))
        #
        #     {
        #         "success": True,
        #         "result": [
        #             {
        #                 "createdAt": "2019-03-05T09:56:55.728933+00:00",
        #                 "filledSize": 10,
        #                 "future": "XRP-PERP",
        #                 "id": 9596912,
        #                 "market": "XRP-PERP",
        #                 "price": 0.306525,
        #                 "avgFillPrice": 0.306526,
        #                 "remainingSize": 31421,
        #                 "side": "sell",
        #                 "size": 31431,
        #                 "status": "open",
        #                 "type": "limit",
        #                 "reduceOnly": False,
        #                 "ioc": False,
        #                 "postOnly": False,
        #                 "clientId": null
        #             }
        #         ]
        #     }
        #
        result = self.safe_value(response, 'result', [])
        return self.parse_orders(result, market, since, limit)

    async def fetch_order_trades(self, id, symbol=None, since=None, limit=None, params={}):
        request = {
            'orderId': id,
        }
        return await self.fetch_my_trades(symbol, since, limit, self.extend(request, params))

    async def fetch_my_trades(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        market, marketId = self.get_market_params(symbol, 'market', params)
        request = {}
        if marketId is not None:
            request['market'] = marketId
        if since is not None:
            request['start_time'] = int(since / 1000)
            request['end_time'] = self.seconds()
        response = await self.privateGetFills(self.extend(request, params))
        #
        #     {
        #         "success": True,
        #         "result": [
        #             {
        #                 "fee": 20.1374935,
        #                 "feeRate": 0.0005,
        #                 "future": "EOS-0329",
        #                 "id": 11215,
        #                 "liquidity": "taker",
        #                 "market": "EOS-0329",
        #                 "baseCurrency": null,
        #                 "quoteCurrency": null,
        #                 "orderId": 8436981,
        #                 "price": 4.201,
        #                 "side": "buy",
        #                 "size": 9587,
        #                 "time": "2019-03-27T19:15:10.204619+00:00",
        #                 "type": "order"
        #             }
        #         ]
        #     }
        #
        trades = self.safe_value(response, 'result', [])
        return self.parse_trades(trades, market, since, limit)

    async def withdraw(self, code, amount, address, tag=None, params={}):
        tag, params = self.handle_withdraw_tag_and_params(tag, params)
        await self.load_markets()
        self.check_address(address)
        currency = self.currency(code)
        request = {
            'coin': currency['id'],
            'size': amount,
            'address': address,
            # 'password': 'string',  # optional withdrawal password if it is required for your account
            # 'code': '192837',  # optional 2fa code if it is required for your account
        }
        if self.password is not None:
            request['password'] = self.password
        if tag is not None:
            request['tag'] = tag
        networks = self.safe_value(self.options, 'networks', {})
        network = self.safe_string_upper(params, 'network')  # self line allows the user to specify either ERC20 or ETH
        network = self.safe_string_lower(networks, network, network)  # handle ERC20>ETH alias
        if network is not None:
            request['method'] = network
            params = self.omit(params, 'network')
        response = await self.privatePostWalletWithdrawals(self.extend(request, params))
        #
        #     {
        #         "success": True,
        #         "result": {
        #             "coin": "USDTBEAR",
        #             "address": "0x83a127952d266A6eA306c40Ac62A4a70668FE3BE",
        #             "tag": "null",
        #             "fee": 0,
        #             "id": 1,
        #             "size": "20.2",
        #             "status": "requested",
        #             "time": "2019-03-05T09:56:55.728933+00:00",
        #             "txid": "null"
        #         }
        #     }
        #
        result = self.safe_value(response, 'result', {})
        return self.parse_transaction(result, currency)

    async def fetch_positions(self, symbols=None, params={}):
        await self.load_markets()
        request = {
            'showAvgPrice': True,
        }
        response = await self.privateGetPositions(self.extend(request, params))
        #
        #     {
        #         "success": True,
        #         "result": [
        #             {
        #                 "cost": -31.7906,
        #                 "entryPrice": 138.22,
        #                 "estimatedLiquidationPrice": 152.1,
        #                 "future": "ETH-PERP",
        #                 "initialMarginRequirement": 0.1,
        #                 "longOrderSize": 1744.55,
        #                 "maintenanceMarginRequirement": 0.04,
        #                 "netSize": -0.23,
        #                 "openSize": 1744.32,
        #                 "realizedPnl": 3.39441714,
        #                 "shortOrderSize": 1732.09,
        #                 "recentAverageOpenPrice": 278.98,
        #                 "recentPnl": 2.44,
        #                 "recentBreakEvenPrice": 278.98,
        #                 "side": "sell",
        #                 "size": 0.23,
        #                 "unrealizedPnl": 0,
        #                 "collateralUsed": 3.17906
        #             }
        #         ]
        #     }
        #
        result = self.safe_value(response, 'result', [])
        results = []
        for i in range(0, len(result)):
            results.append(self.parse_position(result[i]))
        return self.filter_by_array(results, 'symbol', symbols, False)

    def parse_position(self, position, market=None):
        #
        #   {
        #     "future": "XMR-PERP",
        #     "size": "0.0",
        #     "side": "buy",
        #     "netSize": "0.0",
        #     "longOrderSize": "0.0",
        #     "shortOrderSize": "0.0",
        #     "cost": "0.0",
        #     "entryPrice": null,
        #     "unrealizedPnl": "0.0",
        #     "realizedPnl": "0.0",
        #     "initialMarginRequirement": "0.02",
        #     "maintenanceMarginRequirement": "0.006",
        #     "openSize": "0.0",
        #     "collateralUsed": "0.0",
        #     "estimatedLiquidationPrice": null
        #   }
        #
        contractsString = self.safe_string(position, 'size')
        rawSide = self.safe_string(position, 'side')
        side = 'long' if (rawSide == 'buy') else 'short'
        marketId = self.safe_string(position, 'future')
        symbol = self.safe_symbol(marketId, market)
        liquidationPriceString = self.safe_string(position, 'estimatedLiquidationPrice')
        initialMarginPercentage = self.safe_string(position, 'initialMarginRequirement')
        leverage = int(Precise.string_div('1', initialMarginPercentage, 0))
        # on ftx the entryPrice is actually the mark price
        markPriceString = self.safe_string(position, 'entryPrice')
        notionalString = Precise.string_mul(contractsString, markPriceString)
        initialMargin = Precise.string_mul(notionalString, initialMarginPercentage)
        maintenanceMarginPercentageString = self.safe_string(position, 'maintenanceMarginRequirement')
        maintenanceMarginString = Precise.string_mul(notionalString, maintenanceMarginPercentageString)
        unrealizedPnlString = self.safe_string(position, 'recentPnl')
        percentage = self.parse_number(Precise.string_mul(Precise.string_div(unrealizedPnlString, initialMargin, 4), '100'))
        entryPriceString = self.safe_string(position, 'recentAverageOpenPrice')
        difference = None
        collateral = None
        marginRatio = None
        if (entryPriceString is not None) and (Precise.string_gt(liquidationPriceString, '0')):
            # collateral = maintenanceMargin ±((markPrice - liquidationPrice) * size)
            if side == 'long':
                difference = Precise.string_sub(markPriceString, liquidationPriceString)
            else:
                difference = Precise.string_sub(liquidationPriceString, markPriceString)
            loss = Precise.string_mul(difference, contractsString)
            collateral = Precise.string_add(loss, maintenanceMarginString)
            marginRatio = self.parse_number(Precise.string_div(maintenanceMarginString, collateral, 4))
        # ftx has a weird definition of realizedPnl
        # it keeps the historical record of the realizedPnl per contract forever
        # so we cannot use self data
        return {
            'info': position,
            'symbol': symbol,
            'timestamp': None,
            'datetime': None,
            'initialMargin': self.parse_number(initialMargin),
            'initialMarginPercentage': self.parse_number(initialMarginPercentage),
            'maintenanceMargin': self.parse_number(maintenanceMarginString),
            'maintenanceMarginPercentage': self.parse_number(maintenanceMarginPercentageString),
            'entryPrice': self.parse_number(entryPriceString),
            'notional': self.parse_number(notionalString),
            'leverage': leverage,
            'unrealizedPnl': self.parse_number(unrealizedPnlString),
            'contracts': self.parse_number(contractsString),
            'contractSize': self.safe_value(market, 'contractSize'),
            'marginRatio': marginRatio,
            'liquidationPrice': self.parse_number(liquidationPriceString),
            'markPrice': self.parse_number(markPriceString),
            'collateral': self.parse_number(collateral),
            'marginType': 'cross',
            'side': side,
            'percentage': percentage,
        }

    async def fetch_deposit_address(self, code, params={}):
        await self.load_markets()
        currency = self.currency(code)
        request = {
            'coin': currency['id'],
        }
        networks = self.safe_value(self.options, 'networks', {})
        network = self.safe_string_upper(params, 'network')  # self line allows the user to specify either ERC20 or ETH
        network = self.safe_string_lower(networks, network, network)  # handle ERC20>ETH alias
        if network is not None:
            request['method'] = network
            params = self.omit(params, 'network')
        response = await self.privateGetWalletDepositAddressCoin(self.extend(request, params))
        #
        #     {
        #         "success": True,
        #         "result": {
        #             "address": "0x83a127952d266A6eA306c40Ac62A4a70668FE3BE",
        #             "tag": null,
        #             "method": "erc20",
        #             "coin": null
        #         }
        #     }
        #
        result = self.safe_value(response, 'result', {})
        networkId = self.safe_string(result, 'method')
        address = self.safe_string(result, 'address')
        self.check_address(address)
        return {
            'currency': code,
            'address': address,
            'tag': self.safe_string(result, 'tag'),
            'network': self.safe_network(networkId),
            'info': response,
        }

    def safe_network(self, networkId):
        networksById = {
            'trx': 'TRC20',
            'erc20': 'ERC20',
            'sol': 'SOL',
            'bsc': 'BEP20',
            'bep2': 'BEP2',
        }
        return self.safe_string(networksById, networkId, networkId)

    def parse_transaction_status(self, status):
        statuses = {
            # what are other statuses here?
            'confirmed': 'ok',  # deposits
            'complete': 'ok',  # withdrawals
            'cancelled': 'canceled',  # deposits
        }
        return self.safe_string(statuses, status, status)

    def parse_transaction(self, transaction, currency=None):
        #
        # fetchDeposits
        #
        #     airdrop
        #
        #     {
        #         "id": 9147072,
        #         "coin": "SRM_LOCKED",
        #         "size": 3.12,
        #         "time": "2021-04-27T23:59:03.565983+00:00",
        #         "notes": "SRM Airdrop for FTT holdings",
        #         "status": "complete"
        #     }
        #
        #     regular deposits
        #
        #     {
        #         "coin": "TUSD",
        #         "confirmations": 64,
        #         "confirmedTime": "2019-03-05T09:56:55.728933+00:00",
        #         "fee": 0,
        #         "id": 1,
        #         "sentTime": "2019-03-05T09:56:55.735929+00:00",
        #         "size": "99.0",
        #         "status": "confirmed",
        #         "time": "2019-03-05T09:56:55.728933+00:00",
        #         "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1"
        #     }
        #
        # fetchWithdrawals
        #
        #     {
        #         "coin": "TUSD",
        #         "address": "0x83a127952d266A6eA306c40Ac62A4a70668FE3BE",
        #         "tag": "null",
        #         "fee": 0,
        #         "id": 1,
        #         "size": "99.0",
        #         "status": "complete",
        #         "time": "2019-03-05T09:56:55.728933+00:00",
        #         "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1"
        #     }
        #
        #     {
        #         "coin": 'BTC',
        #         "id": 1969806,
        #         "notes": 'Transfer to Dd6gi7m2Eg4zzBbPAxuwfEaHs6tYvyUX5hbPpsTcNPXo',
        #         "size": 0.003,
        #         "status": 'complete',
        #         "time": '2021-02-03T20:28:54.918146+00:00'
        #     }
        #
        code = self.safe_currency_code(self.safe_string(transaction, 'coin'))
        id = self.safe_string(transaction, 'id')
        amount = self.safe_number(transaction, 'size')
        status = self.parse_transaction_status(self.safe_string(transaction, 'status'))
        timestamp = self.parse8601(self.safe_string(transaction, 'time'))
        txid = self.safe_string(transaction, 'txid')
        tag = None
        address = self.safe_value(transaction, 'address')
        if not isinstance(address, basestring):
            tag = self.safe_string(address, 'tag')
            address = self.safe_string(address, 'address')
        if address is None:
            # parse address from internal transfer
            notes = self.safe_string(transaction, 'notes')
            if (notes is not None) and (notes.find('Transfer to') >= 0):
                address = notes[12:]
        fee = self.safe_number(transaction, 'fee')
        return {
            'info': transaction,
            'id': id,
            'txid': txid,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'network': None,
            'addressFrom': None,
            'address': address,
            'addressTo': address,
            'tagFrom': None,
            'tag': tag,
            'tagTo': tag,
            'type': None,
            'amount': amount,
            'currency': code,
            'status': status,
            'updated': None,
            'fee': {
                'currency': code,
                'cost': fee,
                'rate': None,
            },
        }

    async def fetch_deposits(self, code=None, since=None, limit=None, params={}):
        await self.load_markets()
        response = await self.privateGetWalletDeposits(params)
        #
        #     {
        #         "success": True,
        #         "result": {
        #             "coin": "TUSD",
        #             "confirmations": 64,
        #             "confirmedTime": "2019-03-05T09:56:55.728933+00:00",
        #             "fee": 0,
        #             "id": 1,
        #             "sentTime": "2019-03-05T09:56:55.735929+00:00",
        #             "size": "99.0",
        #             "status": "confirmed",
        #             "time": "2019-03-05T09:56:55.728933+00:00",
        #             "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1"
        #         }
        #     }
        #
        result = self.safe_value(response, 'result', [])
        currency = None
        if code is not None:
            currency = self.currency(code)
        return self.parse_transactions(result, currency, since, limit, {'type': 'deposit'})

    async def fetch_withdrawals(self, code=None, since=None, limit=None, params={}):
        await self.load_markets()
        response = await self.privateGetWalletWithdrawals(params)
        #
        #     {
        #         "success": True,
        #         "result": {
        #             "coin": "TUSD",
        #             "address": "0x83a127952d266A6eA306c40Ac62A4a70668FE3BE",
        #             "tag": "null",
        #             "fee": 0,
        #             "id": 1,
        #             "size": "99.0",
        #             "status": "complete",
        #             "time": "2019-03-05T09:56:55.728933+00:00",
        #             "txid": "0x8078356ae4b06a036d64747546c274af19581f1c78c510b60505798a7ffcaf1"
        #         }
        #     }
        #
        result = self.safe_value(response, 'result', [])
        currency = None
        if code is not None:
            currency = self.currency(code)
        return self.parse_transactions(result, currency, since, limit, {'type': 'withdrawal'})

    def sign(self, path, api='public', method='GET', params={}, headers=None, body=None):
        request = '/api/' + self.implode_params(path, params)
        signOptions = self.safe_value(self.options, 'sign', {})
        headerPrefix = self.safe_string(signOptions, self.hostname, 'FTX')
        subaccountField = headerPrefix + '-SUBACCOUNT'
        chosenSubaccount = self.safe_string_2(params, subaccountField, 'subaccount')
        if chosenSubaccount is not None:
            params = self.omit(params, [subaccountField, 'subaccount'])
        query = self.omit(params, self.extract_params(path))
        baseUrl = self.implode_hostname(self.urls['api'][api])
        url = baseUrl + request
        if method != 'POST':
            if query:
                suffix = '?' + self.urlencode(query)
                url += suffix
                request += suffix
        if api == 'private':
            self.check_required_credentials()
            timestamp = str(self.milliseconds())
            auth = timestamp + method + request
            headers = {}
            if (method == 'POST') or (method == 'DELETE'):
                body = self.json(query)
                auth += body
                headers['Content-Type'] = 'application/json'
            signature = self.hmac(self.encode(auth), self.encode(self.secret), hashlib.sha256)
            headers[headerPrefix + '-KEY'] = self.apiKey
            headers[headerPrefix + '-TS'] = timestamp
            headers[headerPrefix + '-SIGN'] = signature
            if chosenSubaccount is not None:
                headers[subaccountField] = chosenSubaccount
        return {'url': url, 'method': method, 'body': body, 'headers': headers}

    def handle_errors(self, code, reason, url, method, headers, body, response, requestHeaders, requestBody):
        if response is None:
            return  # fallback to the default error handler
        #
        #     {"error":"Invalid parameter start_time","success":false}
        #     {"error":"Not enough balances","success":false}
        #
        success = self.safe_value(response, 'success')
        if not success:
            feedback = self.id + ' ' + body
            error = self.safe_string(response, 'error')
            self.throw_exactly_matched_exception(self.exceptions['exact'], error, feedback)
            self.throw_broadly_matched_exception(self.exceptions['broad'], error, feedback)
            raise ExchangeError(feedback)  # unknown message

    async def set_leverage(self, leverage, symbol=None, params={}):
        # WARNING: THIS WILL INCREASE LIQUIDATION PRICE FOR OPEN ISOLATED LONG POSITIONS
        # AND DECREASE LIQUIDATION PRICE FOR OPEN ISOLATED SHORT POSITIONS
        if (leverage < 1) or (leverage > 20):
            raise BadRequest(self.id + ' leverage should be between 1 and 20')
        request = {
            'leverage': leverage,
        }
        return await self.privatePostAccountLeverage(self.extend(request, params))

    def parse_income(self, income, market=None):
        #
        #   {
        #       "future": "ETH-PERP",
        #        "id": 33830,
        #        "payment": 0.0441342,
        #        "time": "2019-05-15T18:00:00+00:00",
        #        "rate": 0.0001
        #   }
        #
        marketId = self.safe_string(income, 'future')
        symbol = self.safe_symbol(marketId, market)
        amount = self.safe_number(income, 'payment')
        code = self.safe_currency_code('USD')
        id = self.safe_string(income, 'id')
        time = self.safe_string(income, 'time')
        timestamp = self.parse8601(time)
        rate = self.safe_number(income, 'rate')
        return {
            'info': income,
            'symbol': symbol,
            'code': code,
            'timestamp': timestamp,
            'datetime': self.iso8601(timestamp),
            'id': id,
            'amount': amount,
            'rate': rate,
        }

    def parse_incomes(self, incomes, market=None, since=None, limit=None):
        result = []
        for i in range(0, len(incomes)):
            entry = incomes[i]
            parsed = self.parse_income(entry, market)
            result.append(parsed)
        sorted = self.sort_by(result, 'timestamp')
        return self.filter_by_since_limit(sorted, since, limit, 'timestamp')

    async def fetch_funding_history(self, symbol=None, since=None, limit=None, params={}):
        await self.load_markets()
        request = {}
        market = None
        if symbol is not None:
            market = self.market(symbol)
            request['future'] = market['id']
        if since is not None:
            request['startTime'] = since
        response = await self.privateGetFundingPayments(self.extend(request, params))
        result = self.safe_value(response, 'result', [])
        return self.parse_incomes(result, market, since, limit)

    def parse_funding_rate(self, fundingRate, market=None):
        #
        # perp
        #     {
        #       "volume": "71294.7636",
        #       "nextFundingRate": "0.000033",
        #       "nextFundingTime": "2021-10-14T20:00:00+00:00",
        #       "openInterest": "47142.994"
        #     }
        #
        # delivery
        #     {
        #       "volume": "4998.727",
        #       "predictedExpirationPrice": "3798.820141757",
        #       "openInterest": "48307.96"
        #     }
        #
        nextFundingRate = self.safe_number(fundingRate, 'nextFundingRate')
        nextFundingRateDatetimeRaw = self.safe_string(fundingRate, 'nextFundingTime')
        nextFundingRateTimestamp = self.parse8601(nextFundingRateDatetimeRaw)
        estimatedSettlePrice = self.safe_number(fundingRate, 'predictedExpirationPrice')
        return {
            'info': fundingRate,
            'symbol': market['symbol'],
            'markPrice': None,
            'indexPrice': None,
            'interestRate': self.parse_number('0'),
            'estimatedSettlePrice': estimatedSettlePrice,
            'timestamp': None,
            'datetime': None,
            'previousFundingRate': None,
            'nextFundingRate': nextFundingRate,
            'previousFundingTimestamp': None,
            'nextFundingTimestamp': nextFundingRateTimestamp,
            'previousFundingDatetime': None,
            'nextFundingDatetime': self.iso8601(nextFundingRateTimestamp),
        }

    async def fetch_funding_rate(self, symbol, params={}):
        await self.load_markets()
        market = self.market(symbol)
        request = {
            'future_name': market['id'],
        }
        response = await self.publicGetFuturesFutureNameStats(self.extend(request, params))
        #
        #     {
        #       "success": True,
        #       "result": {
        #         "volume": "71294.7636",
        #         "nextFundingRate": "0.000033",
        #         "nextFundingTime": "2021-10-14T20:00:00+00:00",
        #         "openInterest": "47142.994"
        #       }
        #     }
        #
        result = self.safe_value(response, 'result', {})
        return self.parse_funding_rate(result, market)

    async def fetch_borrow_rates(self, params={}):
        await self.load_markets()
        response = await self.privateGetSpotMarginBorrowRates()
        #
        # {
        #     "success":true,
        #     "result":[
        #         {
        #             "coin": "1INCH",
        #             "previous": 0.0000462375,
        #             "estimate": 0.0000462375
        #         }
        #         ...
        #     ]
        # }
        #
        timestamp = self.milliseconds()
        result = self.safe_value(response, 'result')
        rates = {}
        for i in range(0, len(result)):
            rate = result[i]
            code = self.safe_currency_code(self.safe_string(rate, 'coin'))
            rates[code] = {
                'currency': code,
                'rate': self.safe_number(rate, 'previous'),
                'period': 3600000,
                'timestamp': timestamp,
                'datetime': self.iso8601(timestamp),
                'info': rate,
            }
        return rates
