package deribit

import (
	"context"
	"errors"
	"fmt"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/shopspring/decimal"
	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/common/key"
	"github.com/thrasher-corp/gocryptotrader/config"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/exchange/accounts"
	"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
	"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
	"github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer"
	exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
	"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
	"github.com/thrasher-corp/gocryptotrader/exchanges/deposit"
	"github.com/thrasher-corp/gocryptotrader/exchanges/fundingrate"
	"github.com/thrasher-corp/gocryptotrader/exchanges/futures"
	"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
	"github.com/thrasher-corp/gocryptotrader/exchanges/protocol"
	"github.com/thrasher-corp/gocryptotrader/exchanges/request"
	"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
	"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
	"github.com/thrasher-corp/gocryptotrader/log"
	"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)

// SetDefaults sets the basic defaults for Deribit
func (e *Exchange) SetDefaults() {
	e.Name = "Deribit"
	e.Enabled = true
	e.Verbose = true
	e.API.CredentialsValidator.RequiresKey = true
	e.API.CredentialsValidator.RequiresSecret = true

	dashFormat := &currency.PairFormat{Uppercase: true, Delimiter: currency.DashDelimiter}
	underscoreFormat := &currency.PairFormat{Uppercase: true, Delimiter: currency.UnderscoreDelimiter}
	if err := e.SetAssetPairStore(asset.Spot, currency.PairStore{AssetEnabled: true, RequestFormat: underscoreFormat, ConfigFormat: underscoreFormat}); err != nil {
		log.Errorf(log.ExchangeSys, "%s error storing %q default asset formats: %s", e.Name, asset.Spot, err)
	}
	for _, a := range []asset.Item{asset.Futures, asset.Options, asset.OptionCombo, asset.FutureCombo} {
		if err := e.SetAssetPairStore(a, currency.PairStore{AssetEnabled: true, RequestFormat: dashFormat, ConfigFormat: dashFormat}); err != nil {
			log.Errorf(log.ExchangeSys, "%s error storing %q default asset formats: %s", e.Name, a, err)
		}
	}

	// Fill out the capabilities/features that the exchange supports
	e.Features = exchange.Features{
		Supports: exchange.FeaturesSupported{
			REST:      true,
			Websocket: true,
			RESTCapabilities: protocol.Features{
				TickerFetching:        true,
				KlineFetching:         true,
				TradeFetching:         true,
				OrderbookFetching:     true,
				AutoPairUpdates:       true,
				AccountInfo:           true,
				GetOrder:              true,
				GetOrders:             true,
				CancelOrders:          true,
				CancelOrder:           true,
				SubmitOrder:           true,
				UserTradeHistory:      true,
				CryptoDeposit:         true,
				CryptoWithdrawal:      true,
				TradeFee:              true,
				CryptoWithdrawalFee:   true,
				MultiChainDeposits:    true,
				MultiChainWithdrawals: true,
			},
			WebsocketCapabilities: protocol.Features{
				TickerFetching:    true,
				OrderbookFetching: true,
			},
			WithdrawPermissions: exchange.AutoWithdrawCrypto |
				exchange.AutoWithdrawFiat,
			Kline: kline.ExchangeCapabilitiesSupported{
				Intervals: true,
			},
			FuturesCapabilities: exchange.FuturesCapabilities{
				Positions:    true,
				Leverage:     true,
				FundingRates: true,
				SupportedFundingRateFrequencies: map[kline.Interval]bool{
					kline.OneHour:   true,
					kline.EightHour: true,
				},
				OpenInterest: exchange.OpenInterestSupport{
					Supported: true,
				},
			},
		},
		Enabled: exchange.FeaturesEnabled{
			AutoPairUpdates: true,
			Kline: kline.ExchangeCapabilitiesEnabled{
				Intervals: kline.DeployExchangeIntervals(
					kline.IntervalCapacity{Interval: kline.HundredMilliseconds},
					kline.IntervalCapacity{Interval: kline.OneMin},
					kline.IntervalCapacity{Interval: kline.ThreeMin},
					kline.IntervalCapacity{Interval: kline.FiveMin},
					kline.IntervalCapacity{Interval: kline.TenMin},
					kline.IntervalCapacity{Interval: kline.FifteenMin},
					kline.IntervalCapacity{Interval: kline.ThirtyMin},
					kline.IntervalCapacity{Interval: kline.OneHour},
					kline.IntervalCapacity{Interval: kline.TwoHour},
					// NOTE: The supported time intervals below are returned
					// offset to +8 hours. This may lead to
					// issues with candle quality and conversion as the
					// intervals may be broken up. The below intervals
					// are therefore constructed from the intervals above.
					// kline.IntervalCapacity{Interval: kline.ThreeHour},
					// kline.IntervalCapacity{Interval: kline.SixHour},
					// kline.IntervalCapacity{Interval: kline.TwelveHour},
					// kline.IntervalCapacity{Interval: kline.OneDay},
				),
				GlobalResultLimit: 500,
			},
		},
		Subscriptions: defaultSubscriptions.Clone(),
	}

	var err error
	e.Requester, err = request.New(e.Name,
		common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout),
		request.WithLimiter(GetRateLimits()),
	)
	if err != nil {
		log.Errorln(log.ExchangeSys, err)
	}
	for _, assetType := range []asset.Item{asset.Options, asset.OptionCombo, asset.FutureCombo} {
		if err = e.DisableAssetWebsocketSupport(assetType); err != nil {
			log.Errorln(log.ExchangeSys, err)
		}
	}
	e.API.Endpoints = e.NewEndpoints()
	err = e.API.Endpoints.SetDefaultEndpoints(map[exchange.URL]string{
		exchange.RestFutures:           "https://www.deribit.com",
		exchange.RestSpot:              "https://www.deribit.com",
		exchange.RestSpotSupplementary: "https://test.deribit.com",
	})
	if err != nil {
		log.Errorln(log.ExchangeSys, err)
	}
	e.Websocket = websocket.NewManager()
	e.WebsocketResponseMaxLimit = exchange.DefaultWebsocketResponseMaxLimit
	e.WebsocketResponseCheckTimeout = exchange.DefaultWebsocketResponseCheckTimeout
	e.WebsocketOrderbookBufferLimit = exchange.DefaultWebsocketOrderbookBufferLimit
}

// Setup takes in the supplied exchange configuration details and sets params
func (e *Exchange) Setup(exch *config.Exchange) error {
	err := exch.Validate()
	if err != nil {
		return err
	}
	if !exch.Enabled {
		e.SetEnabled(false)
		return nil
	}
	err = e.SetupDefaults(exch)
	if err != nil {
		return err
	}
	err = e.Websocket.Setup(&websocket.ManagerSetup{
		ExchangeConfig:        exch,
		DefaultURL:            deribitWebsocketAddress,
		RunningURL:            deribitWebsocketAddress,
		Connector:             e.WsConnect,
		Subscriber:            e.Subscribe,
		Unsubscriber:          e.Unsubscribe,
		GenerateSubscriptions: e.generateSubscriptions,
		Features:              &e.Features.Supports.WebsocketCapabilities,
		OrderbookBufferConfig: buffer.Config{
			SortBuffer:            true,
			SortBufferByUpdateIDs: true,
		},
	})
	if err != nil {
		return err
	}

	return e.Websocket.SetupNewConnection(&websocket.ConnectionSetup{
		URL:                  e.Websocket.GetWebsocketURL(),
		ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout,
		ResponseMaxLimit:     exch.WebsocketResponseMaxLimit,
	})
}

// FetchTradablePairs returns a list of the exchanges tradable pairs
func (e *Exchange) FetchTradablePairs(ctx context.Context, assetType asset.Item) (currency.Pairs, error) {
	if !e.SupportsAsset(assetType) {
		return nil, fmt.Errorf("%s: %w - %v", e.Name, asset.ErrNotSupported, assetType)
	}

	instruments, err := e.GetInstruments(ctx, currency.EMPTYCODE, e.GetAssetKind(assetType), false)
	if err != nil {
		return nil, err
	}

	resp := make(currency.Pairs, 0, len(instruments))
	for _, inst := range instruments {
		if !inst.IsActive {
			continue
		}
		cp, err := currency.NewPairFromString(inst.InstrumentName)
		if err != nil {
			return nil, err
		}
		resp = resp.Add(cp)
	}
	return resp, nil
}

// UpdateTradablePairs updates the exchanges available pairs and stores
// them in the exchanges config
func (e *Exchange) UpdateTradablePairs(ctx context.Context) error {
	assets := e.GetAssetTypes(false)
	errs := common.CollectErrors(len(assets))
	for x := range assets {
		go func(x int) {
			defer errs.Wg.Done()
			pairs, err := e.FetchTradablePairs(ctx, assets[x])
			if err != nil {
				errs.C <- err
				return
			}
			errs.C <- e.UpdatePairs(pairs, assets[x], false)
		}(x)
	}
	return errs.Collect()
}

// UpdateTickers updates the ticker for all currency pairs of a given asset type
func (e *Exchange) UpdateTickers(_ context.Context, _ asset.Item) error {
	return common.ErrFunctionNotSupported
}

// UpdateTicker updates and returns the ticker for a currency pair
func (e *Exchange) UpdateTicker(ctx context.Context, p currency.Pair, assetType asset.Item) (*ticker.Price, error) {
	if !e.SupportsAsset(assetType) {
		return nil, fmt.Errorf("%s: %w - %s", e.Name, asset.ErrNotSupported, assetType)
	}
	p, err := e.FormatExchangeCurrency(p, assetType)
	if err != nil {
		return nil, err
	}
	instrumentID := formatPairString(assetType, p)
	var tickerData *TickerData
	if e.Websocket.IsConnected() {
		tickerData, err = e.WSRetrievePublicTicker(ctx, instrumentID)
	} else {
		tickerData, err = e.GetPublicTicker(ctx, instrumentID)
	}
	if err != nil {
		return nil, err
	}
	resp := ticker.Price{
		ExchangeName: e.Name,
		Pair:         p,
		AssetType:    assetType,
		Ask:          tickerData.BestAskPrice,
		AskSize:      tickerData.BestAskAmount,
		Bid:          tickerData.BestBidPrice,
		BidSize:      tickerData.BestBidAmount,
		High:         tickerData.Stats.High,
		Low:          tickerData.Stats.Low,
		Last:         tickerData.LastPrice,
		Volume:       tickerData.Stats.Volume,
		Close:        tickerData.LastPrice,
		IndexPrice:   tickerData.IndexPrice,
		MarkPrice:    tickerData.MarkPrice,
		QuoteVolume:  tickerData.Stats.VolumeUSD,
	}
	err = ticker.ProcessTicker(&resp)
	if err != nil {
		return nil, err
	}
	return ticker.GetTicker(e.Name, p, assetType)
}

// UpdateOrderbook updates and returns the orderbook for a currency pair
func (e *Exchange) UpdateOrderbook(ctx context.Context, p currency.Pair, assetType asset.Item) (*orderbook.Book, error) {
	p, err := e.FormatExchangeCurrency(p, assetType)
	if err != nil {
		return nil, err
	}
	instrumentID := formatPairString(assetType, p)
	var obData *Orderbook
	if e.Websocket.IsConnected() {
		obData, err = e.WSRetrieveOrderbookData(ctx, instrumentID, 50)
	} else {
		obData, err = e.GetOrderbook(ctx, instrumentID, 50)
	}
	if err != nil {
		return nil, err
	}
	book := &orderbook.Book{
		Exchange:          e.Name,
		Pair:              p,
		Asset:             assetType,
		ValidateOrderbook: e.ValidateOrderbook,
	}
	book.Asks = make(orderbook.Levels, 0, len(obData.Asks))
	for x := range obData.Asks {
		if obData.Asks[x][0] == 0 || obData.Asks[x][1] == 0 {
			continue
		}
		book.Asks = append(book.Asks, orderbook.Level{
			Price:  obData.Asks[x][0],
			Amount: obData.Asks[x][1],
		})
	}
	book.Bids = make(orderbook.Levels, 0, len(obData.Bids))
	for x := range obData.Bids {
		if obData.Bids[x][0] == 0 || obData.Bids[x][1] == 0 {
			continue
		}
		book.Bids = append(book.Bids, orderbook.Level{
			Price:  obData.Bids[x][0],
			Amount: obData.Bids[x][1],
		})
	}
	err = book.Process()
	if err != nil {
		return book, err
	}
	return orderbook.Get(e.Name, p, assetType)
}

// UpdateAccountBalances retrieves currency balances
func (e *Exchange) UpdateAccountBalances(ctx context.Context, _ asset.Item) (accounts.SubAccounts, error) {
	currencies, err := e.GetCurrencies(ctx)
	if err != nil {
		return nil, err
	}
	subAccts := accounts.SubAccounts{accounts.NewSubAccount(asset.All, "")}
	for i := range currencies {
		var resp *AccountSummaryData
		if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
			resp, err = e.WSRetrieveAccountSummary(ctx, currencies[i].Currency, false)
		} else {
			resp, err = e.GetAccountSummary(ctx, currencies[i].Currency, false)
		}
		if err != nil {
			return nil, err
		}
		subAccts[0].Balances.Set(currencies[i].Currency, accounts.Balance{
			Total: resp.Balance,
			Hold:  resp.Balance - resp.AvailableFunds,
		})
	}
	return subAccts, e.Accounts.Save(ctx, subAccts, true)
}

// GetAccountFundingHistory returns funding history, deposits and withdrawals
func (e *Exchange) GetAccountFundingHistory(ctx context.Context) ([]exchange.FundingHistory, error) {
	var currencies []CurrencyData
	var err error
	if e.Websocket.IsConnected() {
		currencies, err = e.WSRetrieveCurrencies(ctx)
	} else {
		currencies, err = e.GetCurrencies(ctx)
	}
	if err != nil {
		return nil, err
	}
	var resp []exchange.FundingHistory
	for x := range currencies {
		var deposits *DepositsData
		if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
			deposits, err = e.WSRetrieveDeposits(ctx, currencies[x].Currency, 100, 0)
		} else {
			deposits, err = e.GetDeposits(ctx, currencies[x].Currency, 100, 0)
		}
		if err != nil {
			return nil, err
		}
		for y := range deposits.Data {
			resp = append(resp, exchange.FundingHistory{
				ExchangeName:    e.Name,
				Status:          deposits.Data[y].State,
				TransferID:      deposits.Data[y].TransactionID,
				Timestamp:       deposits.Data[y].UpdatedTimestamp.Time(),
				Currency:        currencies[x].Currency.String(),
				Amount:          deposits.Data[y].Amount,
				CryptoToAddress: deposits.Data[y].Address,
				TransferType:    "deposit",
			})
		}
		var withdrawalData *WithdrawalsData
		if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
			withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currencies[x].Currency, 100, 0)
		} else {
			withdrawalData, err = e.GetWithdrawals(ctx, currencies[x].Currency, 100, 0)
		}
		if err != nil {
			return nil, err
		}
		for z := range withdrawalData.Data {
			resp = append(resp, exchange.FundingHistory{
				ExchangeName:    e.Name,
				Status:          withdrawalData.Data[z].State,
				TransferID:      withdrawalData.Data[z].TransactionID,
				Timestamp:       withdrawalData.Data[z].UpdatedTimestamp.Time(),
				Currency:        currencies[x].Currency.String(),
				Amount:          withdrawalData.Data[z].Amount,
				CryptoToAddress: withdrawalData.Data[z].Address,
				TransferType:    "withdrawal",
			})
		}
	}
	return resp, nil
}

// GetWithdrawalsHistory returns previous withdrawals data
func (e *Exchange) GetWithdrawalsHistory(ctx context.Context, c currency.Code, _ asset.Item) ([]exchange.WithdrawalHistory, error) {
	var currencies []CurrencyData
	var err error
	if e.Websocket.IsConnected() {
		currencies, err = e.WSRetrieveCurrencies(ctx)
	} else {
		currencies, err = e.GetCurrencies(ctx)
	}
	if err != nil {
		return nil, err
	}
	resp := []exchange.WithdrawalHistory{}
	for x := range currencies {
		if !currencies[x].Currency.Equal(c) {
			continue
		}
		var withdrawalData *WithdrawalsData
		if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
			withdrawalData, err = e.WSRetrieveWithdrawals(ctx, currencies[x].Currency, 100, 0)
		} else {
			withdrawalData, err = e.GetWithdrawals(ctx, currencies[x].Currency, 100, 0)
		}
		if err != nil {
			return nil, err
		}
		for y := range withdrawalData.Data {
			resp = append(resp, exchange.WithdrawalHistory{
				Status:          withdrawalData.Data[y].State,
				TransferID:      withdrawalData.Data[y].TransactionID,
				Timestamp:       withdrawalData.Data[y].UpdatedTimestamp.Time(),
				Currency:        currencies[x].Currency.String(),
				Amount:          withdrawalData.Data[y].Amount,
				CryptoToAddress: withdrawalData.Data[y].Address,
				TransferType:    "deposit",
			})
		}
	}
	return resp, nil
}

// GetRecentTrades returns the most recent trades for a currency and asset
func (e *Exchange) GetRecentTrades(ctx context.Context, p currency.Pair, assetType asset.Item) ([]trade.Data, error) {
	if !e.SupportsAsset(assetType) {
		return nil, fmt.Errorf("%s: %w - %s", e.Name, asset.ErrNotSupported, assetType)
	}
	p, err := e.FormatExchangeCurrency(p, assetType)
	if err != nil {
		return nil, err
	}
	instrumentID := formatPairString(assetType, p)
	resp := []trade.Data{}
	var trades *PublicTradesData
	if e.Websocket.IsConnected() {
		trades, err = e.WSRetrieveLastTradesByInstrument(ctx, instrumentID, "", "", "", 0, false)
	} else {
		trades, err = e.GetLastTradesByInstrument(ctx, instrumentID, "", "", "", 0, false)
	}
	if err != nil {
		return nil, err
	}
	for a := range trades.Trades {
		sideData := order.Sell
		if trades.Trades[a].Direction == sideBUY {
			sideData = order.Buy
		}
		resp = append(resp, trade.Data{
			TID:          trades.Trades[a].TradeID,
			Exchange:     e.Name,
			Price:        trades.Trades[a].Price,
			Amount:       trades.Trades[a].Amount,
			Timestamp:    trades.Trades[a].Timestamp.Time(),
			AssetType:    assetType,
			Side:         sideData,
			CurrencyPair: p,
		})
	}
	return resp, nil
}

// GetHistoricTrades returns historic trade data within the timeframe provided
func (e *Exchange) GetHistoricTrades(ctx context.Context, p currency.Pair, assetType asset.Item, timestampStart, timestampEnd time.Time) ([]trade.Data, error) {
	if common.StartEndTimeCheck(timestampStart, timestampEnd) != nil {
		return nil, fmt.Errorf("invalid time range supplied. Start: %v End %v",
			timestampStart,
			timestampEnd)
	}
	p, err := e.FormatExchangeCurrency(p, assetType)
	if err != nil {
		return nil, err
	}
	var instrumentID string
	switch assetType {
	case asset.Futures, asset.Options, asset.Spot:
		instrumentID = formatPairString(assetType, p)
	default:
		return nil, fmt.Errorf("%w asset type %v", asset.ErrNotSupported, assetType)
	}
	var resp []trade.Data
	var tradesData *PublicTradesData
	hasMore := true
	for hasMore {
		if e.Websocket.IsConnected() {
			tradesData, err = e.WSRetrieveLastTradesByInstrumentAndTime(ctx, instrumentID, "asc", 100, true, timestampStart, timestampEnd)
		} else {
			tradesData, err = e.GetLastTradesByInstrumentAndTime(ctx, instrumentID, "asc", 100, timestampStart, timestampEnd)
		}
		if err != nil {
			return nil, err
		}
		if len(tradesData.Trades) != 100 {
			hasMore = false
		}
		for t := range tradesData.Trades {
			if t == 99 {
				if timestampStart.Equal(tradesData.Trades[t].Timestamp.Time()) {
					hasMore = false
				}
				timestampStart = tradesData.Trades[t].Timestamp.Time()
			}
			sideData := order.Sell
			if tradesData.Trades[t].Direction == sideBUY {
				sideData = order.Buy
			}
			resp = append(resp, trade.Data{
				TID:          tradesData.Trades[t].TradeID,
				Exchange:     e.Name,
				Price:        tradesData.Trades[t].Price,
				Amount:       tradesData.Trades[t].Amount,
				Timestamp:    tradesData.Trades[t].Timestamp.Time(),
				AssetType:    assetType,
				Side:         sideData,
				CurrencyPair: p,
			})
		}
	}
	return resp, nil
}

// SubmitOrder submits a new order
func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.SubmitResponse, error) {
	err := s.Validate(e.GetTradingRequirements())
	if err != nil {
		return nil, err
	}
	if !e.SupportsAsset(s.AssetType) {
		return nil, fmt.Errorf("%s: orderType %v is not valid", e.Name, s.AssetType)
	}
	var orderID string
	var fmtPair currency.Pair
	status := order.New
	fmtPair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType)
	if err != nil {
		return nil, err
	}
	timeInForce := ""
	if s.TimeInForce.Is(order.ImmediateOrCancel) {
		timeInForce = "immediate_or_cancel"
	}
	var data *PrivateTradeData
	reqParams := &OrderBuyAndSellParams{
		Instrument:   fmtPair.String(),
		OrderType:    strings.ToLower(s.Type.String()),
		Label:        s.ClientOrderID,
		TimeInForce:  timeInForce,
		Amount:       s.Amount,
		Price:        s.Price,
		TriggerPrice: s.TriggerPrice,
		PostOnly:     s.TimeInForce.Is(order.PostOnly),
		ReduceOnly:   s.ReduceOnly,
	}
	switch {
	case s.Side.IsLong():
		if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
			data, err = e.WSSubmitBuy(ctx, reqParams)
		} else {
			data, err = e.SubmitBuy(ctx, reqParams)
		}
		if err != nil {
			return nil, err
		}
		if data == nil {
			return nil, common.ErrNoResponse
		}
		orderID = data.Order.OrderID
	case s.Side.IsShort():
		if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
			data, err = e.WSSubmitSell(ctx, reqParams)
		} else {
			data, err = e.SubmitSell(ctx, reqParams)
		}
		if err != nil {
			return nil, err
		}
		if data == nil {
			return nil, common.ErrNoResponse
		}
		orderID = data.Order.OrderID
	}
	resp, err := s.DeriveSubmitResponse(orderID)
	if err != nil {
		return nil, err
	}
	resp.Status = status
	return resp, nil
}

// ModifyOrder modifies an existing order
func (e *Exchange) ModifyOrder(ctx context.Context, action *order.Modify) (*order.ModifyResponse, error) {
	if err := action.Validate(); err != nil {
		return nil, err
	}
	if !e.SupportsAsset(action.AssetType) {
		return nil, fmt.Errorf("%s: %w - %v", e.Name, asset.ErrNotSupported, action.AssetType)
	}
	var modify *PrivateTradeData
	var err error
	reqParam := &OrderBuyAndSellParams{
		TriggerPrice: action.TriggerPrice,
		PostOnly:     action.TimeInForce.Is(order.PostOnly),
		Amount:       action.Amount,
		OrderID:      action.OrderID,
		Price:        action.Price,
	}
	if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
		modify, err = e.WSSubmitEdit(ctx, reqParam)
	} else {
		modify, err = e.SubmitEdit(ctx, reqParam)
	}
	if err != nil {
		return nil, err
	}
	resp, err := action.DeriveModifyResponse()
	if err != nil {
		return nil, err
	}
	resp.OrderID = modify.Order.OrderID
	return resp, nil
}

// CancelOrder cancels an order by its corresponding ID number
func (e *Exchange) CancelOrder(ctx context.Context, ord *order.Cancel) error {
	if !e.SupportsAsset(ord.AssetType) {
		return fmt.Errorf("%s: %w - %s", e.Name, asset.ErrNotSupported, ord.AssetType)
	}
	err := ord.Validate(ord.StandardCancel())
	if err != nil {
		return err
	}
	if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
		_, err = e.WSSubmitCancel(ctx, ord.OrderID)
	} else {
		_, err = e.SubmitCancel(ctx, ord.OrderID)
	}
	if err != nil {
		return err
	}
	return nil
}

// CancelBatchOrders cancels orders by their corresponding ID numbers
func (e *Exchange) CancelBatchOrders(_ context.Context, _ []order.Cancel) (*order.CancelBatchResponse, error) {
	return nil, common.ErrFunctionNotSupported
}

// CancelAllOrders cancels all orders associated with a currency pair
func (e *Exchange) CancelAllOrders(ctx context.Context, cancel *order.Cancel) (order.CancelAllResponse, error) {
	var resp order.CancelAllResponse
	if err := cancel.Validate(); err != nil {
		return resp, err
	}

	fPair, err := e.FormatExchangeCurrency(cancel.Pair, cancel.AssetType)
	if err != nil {
		return resp, err
	}
	var orderTypeStr string
	switch cancel.Type {
	case order.Limit:
		orderTypeStr = order.Limit.String()
	case order.Market:
		orderTypeStr = order.Market.String()
	case order.AnyType, order.UnknownType:
		orderTypeStr = "all"
	default:
		return resp, fmt.Errorf("%s %w: %v", e.Name, order.ErrTypeIsInvalid, cancel.Type)
	}

	var cancelData *MultipleCancelResponse
	if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
		cancelData, err = e.WSSubmitCancelAllByInstrument(ctx, fPair.String(), orderTypeStr, true, true)
	} else {
		cancelData, err = e.SubmitCancelAllByInstrument(ctx, fPair.String(), orderTypeStr, true, true)
	}
	if err != nil {
		return resp, err
	}
	for a := range cancelData.CancelDetails {
		for b := range cancelData.CancelDetails[a].Result {
			resp.Add(cancelData.CancelDetails[a].Result[b].OrderID, cancelData.CancelDetails[a].Result[b].OrderState)
		}
	}
	return resp, nil
}

// GetOrderInfo returns order information based on order ID
func (e *Exchange) GetOrderInfo(ctx context.Context, orderID string, _ currency.Pair, assetType asset.Item) (*order.Detail, error) {
	if !e.SupportsAsset(assetType) {
		return nil, fmt.Errorf("%w assetType %v", asset.ErrNotSupported, assetType)
	}
	var orderInfo *OrderData
	var err error
	if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
		orderInfo, err = e.WSRetrievesOrderState(ctx, orderID)
	} else {
		orderInfo, err = e.GetOrderState(ctx, orderID)
	}
	if err != nil {
		return nil, err
	}
	orderSide := order.Sell
	if orderInfo.Direction == sideBUY {
		orderSide = order.Buy
	}
	orderType, err := order.StringToOrderType(orderInfo.OrderType)
	if err != nil {
		return nil, err
	}
	var pair currency.Pair
	pair, err = currency.NewPairFromString(orderInfo.InstrumentName)
	if err != nil {
		return nil, err
	}
	var orderStatus order.Status
	if orderInfo.OrderState == "untriggered" {
		orderStatus = order.UnknownStatus
	} else {
		orderStatus, err = order.StringToOrderStatus(orderInfo.OrderState)
		if err != nil {
			return nil, fmt.Errorf("%v: orderStatus %s not supported", e.Name, orderInfo.OrderState)
		}
	}
	var tif order.TimeInForce
	tif, err = timeInForceFromString(orderInfo.TimeInForce, orderInfo.PostOnly)
	if err != nil {
		return nil, err
	}
	return &order.Detail{
		AssetType:       assetType,
		Exchange:        e.Name,
		TimeInForce:     tif,
		Price:           orderInfo.Price,
		Amount:          orderInfo.Amount,
		ExecutedAmount:  orderInfo.FilledAmount,
		Fee:             orderInfo.Commission,
		RemainingAmount: orderInfo.Amount - orderInfo.FilledAmount,
		OrderID:         orderInfo.OrderID,
		Pair:            pair,
		LastUpdated:     orderInfo.LastUpdateTimestamp.Time(),
		Side:            orderSide,
		Type:            orderType,
		Status:          orderStatus,
	}, nil
}

// GetDepositAddress returns a deposit address for a specified currency
func (e *Exchange) GetDepositAddress(ctx context.Context, cryptocurrency currency.Code, _, _ string) (*deposit.Address, error) {
	var addressData *DepositAddressData
	var err error
	if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
		addressData, err = e.WSRetrieveCurrentDepositAddress(ctx, cryptocurrency)
	} else {
		addressData, err = e.GetCurrentDepositAddress(ctx, cryptocurrency)
	}
	if err != nil {
		return nil, err
	}
	return &deposit.Address{
		Address: addressData.Address,
		Chain:   addressData.Currency,
	}, nil
}

// WithdrawCryptocurrencyFunds returns a withdrawal ID when a withdrawal is
// submitted
func (e *Exchange) WithdrawCryptocurrencyFunds(ctx context.Context, withdrawRequest *withdraw.Request) (*withdraw.ExchangeResponse, error) {
	err := withdrawRequest.Validate()
	if err != nil {
		return nil, err
	}
	var withdrawData *WithdrawData
	if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
		withdrawData, err = e.WSSubmitWithdraw(ctx, withdrawRequest.Currency, withdrawRequest.Crypto.Address, "", withdrawRequest.Amount)
	} else {
		withdrawData, err = e.SubmitWithdraw(ctx, withdrawRequest.Currency, withdrawRequest.Crypto.Address, "", withdrawRequest.Amount)
	}
	if err != nil {
		return nil, err
	}
	return &withdraw.ExchangeResponse{
		ID:     strconv.FormatInt(withdrawData.ID, 10),
		Status: withdrawData.State,
	}, err
}

// WithdrawFiatFunds returns a withdrawal ID when a withdrawal is submitted
func (e *Exchange) WithdrawFiatFunds(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
	return nil, common.ErrFunctionNotSupported
}

// WithdrawFiatFundsToInternationalBank returns a withdrawal ID when a withdrawal is submitted
func (e *Exchange) WithdrawFiatFundsToInternationalBank(_ context.Context, _ *withdraw.Request) (*withdraw.ExchangeResponse, error) {
	return nil, common.ErrFunctionNotSupported
}

// GetActiveOrders retrieves any orders that are active/open
func (e *Exchange) GetActiveOrders(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) {
	if err := getOrdersRequest.Validate(); err != nil {
		return nil, err
	}
	if !e.SupportsAsset(getOrdersRequest.AssetType) {
		return nil, fmt.Errorf("%s: %w - %v", e.Name, asset.ErrNotSupported, getOrdersRequest.AssetType)
	}
	if len(getOrdersRequest.Pairs) == 0 {
		return nil, currency.ErrCurrencyPairsEmpty
	}
	resp := []order.Detail{}
	for x := range getOrdersRequest.Pairs {
		fmtPair, err := e.FormatExchangeCurrency(getOrdersRequest.Pairs[x], getOrdersRequest.AssetType)
		if err != nil {
			return nil, err
		}
		var oTypeString string
		switch getOrdersRequest.Type {
		case order.AnyType, order.UnknownType:
			oTypeString = "all"
		default:
			oTypeString = getOrdersRequest.Type.Lower()
		}
		var ordersData []OrderData
		if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
			ordersData, err = e.WSRetrieveOpenOrdersByInstrument(ctx, fmtPair.String(), oTypeString)
		} else {
			ordersData, err = e.GetOpenOrdersByInstrument(ctx, fmtPair.String(), oTypeString)
		}
		if err != nil {
			return nil, err
		}
		for y := range ordersData {
			orderSide := order.Sell
			if ordersData[y].Direction == sideBUY {
				orderSide = order.Buy
			}
			if getOrdersRequest.Side != orderSide && getOrdersRequest.Side != order.AnySide {
				continue
			}
			orderType, err := order.StringToOrderType(ordersData[y].OrderType)
			if err != nil {
				return nil, err
			}
			if getOrdersRequest.Type != orderType && getOrdersRequest.Type != order.AnyType {
				continue
			}
			var orderStatus order.Status
			ordersData[y].OrderState = strings.ToLower(ordersData[y].OrderState)
			if ordersData[y].OrderState != "open" {
				continue
			}

			var tif order.TimeInForce
			tif, err = timeInForceFromString(ordersData[y].TimeInForce, ordersData[y].PostOnly)
			if err != nil {
				return nil, err
			}
			resp = append(resp, order.Detail{
				AssetType:       getOrdersRequest.AssetType,
				Exchange:        e.Name,
				Price:           ordersData[y].Price,
				Amount:          ordersData[y].Amount,
				ExecutedAmount:  ordersData[y].FilledAmount,
				Fee:             ordersData[y].Commission,
				RemainingAmount: ordersData[y].Amount - ordersData[y].FilledAmount,
				OrderID:         ordersData[y].OrderID,
				Pair:            getOrdersRequest.Pairs[x],
				LastUpdated:     ordersData[y].LastUpdateTimestamp.Time(),
				Side:            orderSide,
				Type:            orderType,
				Status:          orderStatus,
				TimeInForce:     tif,
			})
		}
	}
	return resp, nil
}

// GetOrderHistory retrieves account order information
// Can Limit response to specific order status
func (e *Exchange) GetOrderHistory(ctx context.Context, getOrdersRequest *order.MultiOrderRequest) (order.FilteredOrders, error) {
	if err := getOrdersRequest.Validate(); err != nil {
		return nil, err
	}
	if len(getOrdersRequest.Pairs) == 0 {
		return nil, currency.ErrCurrencyPairsEmpty
	}
	var resp []order.Detail
	for x := range getOrdersRequest.Pairs {
		fmtPair, err := e.FormatExchangeCurrency(getOrdersRequest.Pairs[x], getOrdersRequest.AssetType)
		if err != nil {
			return nil, err
		}
		var ordersData []OrderData
		if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
			ordersData, err = e.WSRetrieveOrderHistoryByInstrument(ctx, fmtPair.String(), 100, 0, true, true)
		} else {
			ordersData, err = e.GetOrderHistoryByInstrument(ctx, fmtPair.String(), 100, 0, true, true)
		}
		if err != nil {
			return nil, err
		}
		for y := range ordersData {
			orderSide := order.Sell
			if ordersData[y].Direction == sideBUY {
				orderSide = order.Buy
			}
			if getOrdersRequest.Side != orderSide && getOrdersRequest.Side != order.AnySide {
				continue
			}
			orderType, err := order.StringToOrderType(ordersData[y].OrderType)
			if err != nil {
				return nil, err
			}
			if getOrdersRequest.Type != orderType && getOrdersRequest.Type != order.AnyType {
				continue
			}
			var orderStatus order.Status
			if ordersData[y].OrderState == "untriggered" {
				orderStatus = order.UnknownStatus
			} else {
				orderStatus, err = order.StringToOrderStatus(ordersData[y].OrderState)
				if err != nil {
					return resp, fmt.Errorf("%v: orderStatus %s not supported", e.Name, ordersData[y].OrderState)
				}
			}

			var tif order.TimeInForce
			tif, err = timeInForceFromString(ordersData[y].TimeInForce, ordersData[y].PostOnly)
			if err != nil {
				return nil, err
			}
			resp = append(resp, order.Detail{
				AssetType:       getOrdersRequest.AssetType,
				Exchange:        e.Name,
				Price:           ordersData[y].Price,
				Amount:          ordersData[y].Amount,
				ExecutedAmount:  ordersData[y].FilledAmount,
				Fee:             ordersData[y].Commission,
				RemainingAmount: ordersData[y].Amount - ordersData[y].FilledAmount,
				OrderID:         ordersData[y].OrderID,
				Pair:            getOrdersRequest.Pairs[x],
				LastUpdated:     ordersData[y].LastUpdateTimestamp.Time(),
				Side:            orderSide,
				Type:            orderType,
				Status:          orderStatus,
				TimeInForce:     tif,
			})
		}
	}
	return resp, nil
}

// GetFeeByType returns an estimate of fee based on the type of transaction
func (e *Exchange) GetFeeByType(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
	if feeBuilder == nil {
		return 0, fmt.Errorf("%T %w", feeBuilder, common.ErrNilPointer)
	}
	if !e.AreCredentialsValid(ctx) && // Todo check connection status
		feeBuilder.FeeType == exchange.CryptocurrencyTradeFee {
		feeBuilder.FeeType = exchange.OfflineTradeFee
	}
	var fee float64
	var err error
	switch feeBuilder.FeeType {
	case exchange.CryptocurrencyTradeFee:
		fee, err = calculateTradingFee(feeBuilder)
		if err != nil {
			return 0, err
		}
	case exchange.CryptocurrencyDepositFee:
	case exchange.CryptocurrencyWithdrawalFee:
		// Withdrawals are processed instantly if the balance in our hot wallet permits so. We keep only a small percentage of coins in hot storage,
		// therefore there is a chance that your withdrawal cannot be processed immediately. If needed, once a day we will replenish the balance of the hot wallet from the cold storage.
	case exchange.OfflineTradeFee:
		fee = getOfflineTradeFee(feeBuilder.PurchasePrice, feeBuilder.Amount)
	}
	if fee < 0 {
		fee = 0
	}
	return fee, nil
}

// ValidateAPICredentials validates current credentials used for wrapper functionality
func (e *Exchange) ValidateAPICredentials(ctx context.Context, assetType asset.Item) error {
	_, err := e.UpdateAccountBalances(ctx, assetType)
	return e.CheckTransientError(err)
}

// GetHistoricCandles returns candles between a time period for a set time interval
func (e *Exchange) GetHistoricCandles(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
	req, err := e.GetKlineRequest(pair, a, interval, start, end, false)
	if err != nil {
		return nil, err
	}
	intervalString, err := e.GetResolutionFromInterval(req.ExchangeInterval)
	if err != nil {
		return nil, err
	}
	switch a {
	case asset.Futures, asset.Spot:
		var tradingViewData *TVChartData
		if e.Websocket.IsConnected() {
			tradingViewData, err = e.WSRetrievesTradingViewChartData(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
		} else {
			tradingViewData, err = e.GetTradingViewChart(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, start, end)
		}
		if err != nil {
			return nil, err
		}
		listCandles, err := appendCandles(tradingViewData, start)
		if err != nil {
			return nil, err
		}
		return req.ProcessResponse(listCandles)
	}
	return nil, fmt.Errorf("%w candlestick data for asset type %v", asset.ErrNotSupported, a)
}

func appendCandles(tradingViewData *TVChartData, start time.Time) ([]kline.Candle, error) {
	if tradingViewData == nil || len(tradingViewData.Ticks) == 0 {
		return nil, kline.ErrNoTimeSeriesDataToConvert
	}
	checkLen := len(tradingViewData.Ticks)
	if len(tradingViewData.Open) != checkLen ||
		len(tradingViewData.High) != checkLen ||
		len(tradingViewData.Low) != checkLen ||
		len(tradingViewData.Close) != checkLen ||
		len(tradingViewData.Volume) != checkLen {
		return nil, fmt.Errorf("%w: ohlcv len must be equal", kline.ErrInsufficientCandleData)
	}
	listCandles := make([]kline.Candle, 0, len(tradingViewData.Ticks))
	for x := range tradingViewData.Ticks {
		timeInfo := time.UnixMilli(tradingViewData.Ticks[x]).UTC()
		if timeInfo.Before(start) {
			continue
		}
		listCandles = append(listCandles, kline.Candle{
			Open:   tradingViewData.Open[x],
			High:   tradingViewData.High[x],
			Low:    tradingViewData.Low[x],
			Close:  tradingViewData.Close[x],
			Volume: tradingViewData.Volume[x],
			Time:   timeInfo,
		})
	}
	return listCandles, nil
}

// GetHistoricCandlesExtended returns candles between a time period for a set time interval
func (e *Exchange) GetHistoricCandlesExtended(ctx context.Context, pair currency.Pair, a asset.Item, interval kline.Interval, start, end time.Time) (*kline.Item, error) {
	req, err := e.GetKlineExtendedRequest(pair, a, interval, start, end)
	if err != nil {
		return nil, err
	}
	var tradingViewData *TVChartData
	timeSeries := make([]kline.Candle, 0, req.Size())
	switch a {
	case asset.Futures, asset.Spot:
		for x := range req.RangeHolder.Ranges {
			intervalString, err := e.GetResolutionFromInterval(req.ExchangeInterval)
			if err != nil {
				return nil, err
			}
			if e.Websocket.IsConnected() {
				tradingViewData, err = e.WSRetrievesTradingViewChartData(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
			} else {
				tradingViewData, err = e.GetTradingViewChart(ctx, formatFuturesTradablePair(req.RequestFormatted), intervalString, req.RangeHolder.Ranges[x].Start.Time, req.RangeHolder.Ranges[x].End.Time)
			}
			if err != nil {
				return nil, err
			}
			timeSeries, err = appendCandles(tradingViewData, start)
			if err != nil {
				return nil, err
			}
		}
		return req.ProcessResponse(timeSeries)
	}
	return nil, fmt.Errorf("%w candlestick data for asset type %v", asset.ErrNotSupported, a)
}

// GetServerTime returns the current exchange server time.
func (e *Exchange) GetServerTime(ctx context.Context, _ asset.Item) (time.Time, error) {
	return e.GetTime(ctx)
}

// AuthenticateWebsocket sends an authentication message to the websocket
func (e *Exchange) AuthenticateWebsocket(ctx context.Context) error {
	return e.wsLogin(ctx)
}

// GetFuturesContractDetails returns all contracts from the exchange by asset type
func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Item) ([]futures.Contract, error) {
	if !item.IsFutures() {
		return nil, futures.ErrNotFuturesAsset
	}
	if item != asset.Futures {
		return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, item)
	}
	resp := []futures.Contract{}
	for _, ccy := range baseCurrencies {
		var marketSummary []*InstrumentData
		var err error
		if e.Websocket.IsConnected() {
			marketSummary, err = e.WSRetrieveInstrumentsData(ctx, currency.NewCode(ccy), e.GetAssetKind(item), false)
		} else {
			marketSummary, err = e.GetInstruments(ctx, currency.NewCode(ccy), e.GetAssetKind(item), false)
		}
		if err != nil {
			return nil, err
		}
		for _, inst := range marketSummary {
			if inst.Kind != "future" && inst.Kind != "future_combo" {
				continue
			}
			cp, err := currency.NewPairFromString(inst.InstrumentName)
			if err != nil {
				return nil, err
			}
			var ct futures.ContractType
			switch inst.SettlementPeriod {
			case "day":
				ct = futures.Daily
			case "week":
				ct = futures.Weekly
			case "month":
				ct = futures.Monthly
			case "perpetual":
				ct = futures.Perpetual
			}
			var contractSettlementType futures.ContractSettlementType
			if inst.InstrumentType == "reversed" {
				contractSettlementType = futures.Inverse
			} else {
				contractSettlementType = futures.Linear
			}
			resp = append(resp, futures.Contract{
				Exchange:           e.Name,
				Name:               cp,
				Underlying:         currency.NewPair(inst.BaseCurrency, inst.QuoteCurrency),
				Asset:              item,
				SettlementCurrency: inst.SettlementCurrency,
				StartDate:          inst.CreationTimestamp.Time(),
				EndDate:            inst.ExpirationTimestamp.Time(),
				Type:               ct,
				SettlementType:     contractSettlementType,
				IsActive:           inst.IsActive,
				MaxLeverage:        inst.MaxLeverage,
				Multiplier:         inst.ContractSize,
			})
		}
	}
	return resp, nil
}

// UpdateOrderExecutionLimits sets exchange execution order limits for an asset type
func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error {
	if !e.SupportsAsset(a) {
		return fmt.Errorf("%s: %w - %v", e.Name, asset.ErrNotSupported, a)
	}
	for _, bc := range baseCurrencies {
		var instrumentsData []*InstrumentData
		var err error
		if e.Websocket.IsConnected() {
			instrumentsData, err = e.WSRetrieveInstrumentsData(ctx, currency.NewCode(bc), e.GetAssetKind(a), false)
		} else {
			instrumentsData, err = e.GetInstruments(ctx, currency.NewCode(bc), e.GetAssetKind(a), false)
		}
		if err != nil {
			return err
		} else if len(instrumentsData) == 0 {
			continue
		}

		l := make([]limits.MinMaxLevel, len(instrumentsData))
		for i, inst := range instrumentsData {
			var pair currency.Pair
			pair, err = currency.NewPairFromString(inst.InstrumentName)
			if err != nil {
				return err
			}
			l[i] = limits.MinMaxLevel{
				Key:                    key.NewExchangeAssetPair(e.Name, a, pair),
				PriceStepIncrementSize: inst.TickSize,
				MinimumBaseAmount:      inst.MinimumTradeAmount,
			}
		}
		err = limits.Load(l)
		if err != nil {
			return err
		}
	}
	return nil
}

// GetFuturesPositionSummary returns position summary details for an active position
func (e *Exchange) GetFuturesPositionSummary(ctx context.Context, r *futures.PositionSummaryRequest) (*futures.PositionSummary, error) {
	if r == nil {
		return nil, fmt.Errorf("%w HistoricalRatesRequest", common.ErrNilPointer)
	}
	if r.Asset != asset.Futures {
		return nil, fmt.Errorf("%w %v", futures.ErrNotPerpetualFuture, r.Asset)
	}
	if r.Pair.IsEmpty() {
		return nil, currency.ErrCurrencyPairEmpty
	}
	fPair, err := e.FormatExchangeCurrency(r.Pair, r.Asset)
	if err != nil {
		return nil, err
	}
	var pos []PositionData
	if e.Websocket.IsConnected() && e.Websocket.CanUseAuthenticatedWebsocketForWrapper() {
		pos, err = e.WSRetrievePositions(ctx, fPair.Base, e.GetAssetKind(r.Asset))
	} else {
		pos, err = e.GetPositions(ctx, fPair.Base, e.GetAssetKind(r.Asset))
	}
	if err != nil {
		return nil, err
	}
	index := -1
	for a := range pos {
		if pos[a].InstrumentName == fPair.String() {
			index = a
			break
		}
	}
	if index == -1 {
		return nil, errors.New("position information for the instrument not found")
	}
	contracts, err := e.GetFuturesContractDetails(ctx, r.Asset)
	if err != nil {
		return nil, err
	}
	var multiplier, contractSize float64
	var settlementType futures.ContractSettlementType
	for i := range contracts {
		if !contracts[i].Name.Equal(fPair) {
			continue
		}
		multiplier = contracts[i].Multiplier
		settlementType = contracts[i].SettlementType
		break
	}

	var baseSize float64
	switch r.Asset {
	case asset.Futures:
		baseSize = pos[index].SizeCurrency
	case asset.Options:
		baseSize = pos[index].Size
	}
	contractSize = multiplier * baseSize

	return &futures.PositionSummary{
		Pair:                      r.Pair,
		Asset:                     r.Asset,
		Currency:                  fPair.Base,
		NotionalSize:              decimal.NewFromFloat(pos[index].MarkPrice),
		Leverage:                  decimal.NewFromFloat(pos[index].Leverage),
		InitialMarginRequirement:  decimal.NewFromFloat(pos[index].InitialMargin),
		EstimatedLiquidationPrice: decimal.NewFromFloat(pos[index].EstimatedLiquidationPrice),
		MarkPrice:                 decimal.NewFromFloat(pos[index].MarkPrice),
		CurrentSize:               decimal.NewFromFloat(baseSize),
		ContractSize:              decimal.NewFromFloat(contractSize),
		ContractMultiplier:        decimal.NewFromFloat(multiplier),
		ContractSettlementType:    settlementType,
		AverageOpenPrice:          decimal.NewFromFloat(pos[index].AveragePrice),
		UnrealisedPNL:             decimal.NewFromFloat(pos[index].TotalProfitLoss - pos[index].RealizedProfitLoss),
		RealisedPNL:               decimal.NewFromFloat(pos[index].RealizedProfitLoss),
		MaintenanceMarginFraction: decimal.NewFromFloat(pos[index].MaintenanceMargin),
	}, nil
}

// GetOpenInterest returns the open interest rate for a given asset pair
func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]futures.OpenInterest, error) {
	if len(k) == 0 {
		return nil, fmt.Errorf("%w requires pair", common.ErrFunctionNotSupported)
	}
	for i := range k {
		if k[i].Asset == asset.Spot ||
			!e.SupportsAsset(k[i].Asset) {
			return nil, fmt.Errorf("%w %v %v", asset.ErrNotSupported, k[i].Asset, k[i].Pair())
		}
	}
	result := make([]futures.OpenInterest, 0, len(k))
	for i := range k {
		pFmt, err := e.CurrencyPairs.GetFormat(k[i].Asset, true)
		if err != nil {
			return nil, err
		}
		cp := k[i].Pair().Format(pFmt)
		p := formatPairString(k[i].Asset, cp)
		var oi []BookSummaryData
		if e.Websocket.IsConnected() {
			oi, err = e.WSRetrieveBookSummaryByInstrument(ctx, p)
		} else {
			oi, err = e.GetBookSummaryByInstrument(ctx, p)
		}
		if err != nil {
			return nil, err
		}
		for a := range oi {
			result = append(result, futures.OpenInterest{
				Key:          key.NewExchangeAssetPair(e.Name, k[i].Asset, k[i].Pair()),
				OpenInterest: oi[a].OpenInterest,
			})
			break
		}
	}
	if len(result) == 0 {
		return nil, fmt.Errorf("%w, no data found for %v", currency.ErrCurrencyNotFound, k)
	}
	return result, nil
}

// GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair
func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp currency.Pair) (string, error) {
	if cp.IsEmpty() {
		return "", currency.ErrCurrencyPairEmpty
	}
	switch a {
	case asset.Futures:
		isPerp, err := e.IsPerpetualFutureCurrency(a, cp)
		if err != nil {
			return "", err
		}
		if isPerp {
			return tradeBaseURL + tradeFutures + cp.Base.Upper().String() + currency.UnderscoreDelimiter + cp.Quote.Upper().String(), nil
		}
		return tradeBaseURL + tradeFutures + cp.Upper().String(), nil
	case asset.Spot:
		cp.Delimiter = currency.UnderscoreDelimiter
		return tradeBaseURL + tradeSpot + cp.Upper().String(), nil
	case asset.Options:
		baseString := cp.Base.Upper().String()
		quoteString := cp.Quote.Upper().String()
		quoteSplit := strings.Split(quoteString, currency.DashDelimiter)
		if len(quoteSplit) > 1 &&
			(quoteSplit[len(quoteSplit)-1] == "C" || quoteSplit[len(quoteSplit)-1] == "P") {
			return tradeBaseURL + tradeOptions + baseString + "/" + baseString + currency.DashDelimiter + quoteSplit[0], nil
		}
		return tradeBaseURL + tradeOptions + baseString, nil
	case asset.FutureCombo:
		return tradeBaseURL + tradeFuturesCombo + cp.Upper().String(), nil
	case asset.OptionCombo:
		return tradeBaseURL + tradeOptionsCombo + cp.Base.Upper().String(), nil
	default:
		return "", fmt.Errorf("%w %q", asset.ErrNotSupported, a)
	}
}

// IsPerpetualFutureCurrency ensures a given asset and currency is a perpetual future
// differs by exchange
func (e *Exchange) IsPerpetualFutureCurrency(assetType asset.Item, pair currency.Pair) (bool, error) {
	if pair.IsEmpty() {
		return false, currency.ErrCurrencyPairEmpty
	}
	if assetType != asset.Futures {
		// deribit considers future combo, even if ending in "PERP" to not be a perpetual
		return false, nil
	}
	pqs := strings.Split(pair.Quote.Upper().String(), currency.DashDelimiter)
	return pqs[len(pqs)-1] == perpString, nil
}

// GetLatestFundingRates returns the latest funding rates data
func (e *Exchange) GetLatestFundingRates(ctx context.Context, r *fundingrate.LatestRateRequest) ([]fundingrate.LatestRateResponse, error) {
	if r == nil {
		return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer)
	}
	if !e.SupportsAsset(r.Asset) {
		return nil, fmt.Errorf("%s %w", r.Asset, asset.ErrNotSupported)
	}
	isPerpetual, err := e.IsPerpetualFutureCurrency(r.Asset, r.Pair)
	if err != nil {
		return nil, err
	}
	if !isPerpetual {
		return nil, fmt.Errorf("%w %q", futures.ErrNotPerpetualFuture, r.Pair)
	}
	pFmt, err := e.CurrencyPairs.GetFormat(r.Asset, true)
	if err != nil {
		return nil, err
	}
	cp := r.Pair.Format(pFmt)
	p := formatPairString(r.Asset, cp)
	var fri []FundingRateHistory
	fri, err = e.GetFundingRateHistory(ctx, p, time.Now().Add(-time.Hour*16), time.Now())
	if err != nil {
		return nil, err
	}

	resp := make([]fundingrate.LatestRateResponse, 1)
	latestTime := fri[0].Timestamp.Time()
	for i := range fri {
		if fri[i].Timestamp.Time().Before(latestTime) {
			continue
		}
		resp[0] = fundingrate.LatestRateResponse{
			TimeChecked: time.Now(),
			Exchange:    e.Name,
			Asset:       r.Asset,
			Pair:        r.Pair,
			LatestRate: fundingrate.Rate{
				Time: fri[i].Timestamp.Time(),
				Rate: decimal.NewFromFloat(fri[i].Interest8H),
			},
		}
		latestTime = fri[i].Timestamp.Time()
	}
	if len(resp) == 0 {
		return nil, fmt.Errorf("%w %v %v", futures.ErrNotPerpetualFuture, r.Asset, r.Pair)
	}
	return resp, nil
}

// GetHistoricalFundingRates returns historical funding rates for a future
func (e *Exchange) GetHistoricalFundingRates(ctx context.Context, r *fundingrate.HistoricalRatesRequest) (*fundingrate.HistoricalRates, error) {
	if r == nil {
		return nil, fmt.Errorf("%w LatestRateRequest", common.ErrNilPointer)
	}
	if r.Asset != asset.Futures {
		return nil, fmt.Errorf("%w %v", asset.ErrNotSupported, r.Asset)
	}
	if r.Pair.IsEmpty() {
		return nil, currency.ErrCurrencyPairEmpty
	}

	if !r.StartDate.IsZero() && !r.EndDate.IsZero() {
		err := common.StartEndTimeCheck(r.StartDate, r.EndDate)
		if err != nil {
			return nil, err
		}
	}
	if r.IncludePayments {
		return nil, fmt.Errorf("include payments %w", common.ErrNotYetImplemented)
	}
	pFmt, err := e.CurrencyPairs.GetFormat(r.Asset, true)
	if err != nil {
		return nil, err
	}
	cp := r.Pair.Format(pFmt)
	p := formatPairString(r.Asset, cp)
	ed := r.EndDate

	var fundingRates []fundingrate.Rate
	mfr := make(map[int64]struct{})
	for ed.After(r.StartDate) {
		var records []FundingRateHistory
		if e.Websocket.IsConnected() {
			records, err = e.WSRetrieveFundingRateHistory(ctx, p, r.StartDate, ed)
		} else {
			records, err = e.GetFundingRateHistory(ctx, p, r.StartDate, ed)
		}
		if err != nil {
			return nil, err
		}
		if len(records) == 0 || ed.Equal(records[0].Timestamp.Time()) {
			break
		}
		for i := range records {
			rt := records[i].Timestamp.Time()
			if rt.Before(r.StartDate) || rt.After(r.EndDate) {
				continue
			}
			if _, ok := mfr[rt.UnixMilli()]; ok {
				continue
			}
			fundingRates = append(fundingRates, fundingrate.Rate{
				Rate: decimal.NewFromFloat(records[i].Interest1H),
				Time: rt,
			})
			mfr[rt.UnixMilli()] = struct{}{}
		}
		ed = records[0].Timestamp.Time()
	}
	if len(fundingRates) == 0 {
		return nil, fundingrate.ErrNoFundingRatesFound
	}
	sort.Slice(fundingRates, func(i, j int) bool {
		return fundingRates[i].Time.Before(fundingRates[j].Time)
	})
	return &fundingrate.HistoricalRates{
		Exchange:        e.Name,
		Asset:           r.Asset,
		Pair:            r.Pair,
		FundingRates:    fundingRates,
		StartDate:       fundingRates[0].Time,
		EndDate:         r.EndDate,
		LatestRate:      fundingRates[len(fundingRates)-1],
		PaymentCurrency: r.PaymentCurrency,
	}, nil
}

func formatPairString(assetType asset.Item, pair currency.Pair) string {
	switch assetType {
	case asset.Futures:
		return formatFuturesTradablePair(pair)
	case asset.Options:
		return optionPairToString(pair)
	case asset.OptionCombo:
		return optionComboPairToString(pair)
	}
	return pair.String()
}

func timeInForceFromString(timeInForceString string, postOnly bool) (order.TimeInForce, error) {
	tif, err := order.StringToTimeInForce(timeInForceString)
	if err != nil {
		return order.UnknownTIF, err
	}
	if postOnly {
		tif |= order.PostOnly
	}
	return tif, nil
}
