package ftx

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"math"
	"net/http"
	"net/url"
	"strconv"
	"strings"
	"time"

	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/common/crypto"
	"github.com/thrasher-corp/gocryptotrader/currency"
	exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
	"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)

// FTX is the overarching type across this package
type FTX struct {
	exchange.Base
}

const (
	ftxAPIURL = "https://ftx.com/api"

	// Public endpoints
	getMarkets           = "/markets"
	getMarket            = "/markets/"
	getOrderbook         = "/markets/%s/orderbook?depth=%s"
	getTrades            = "/markets/%s/trades"
	getHistoricalData    = "/markets/%s/candles"
	getFutures           = "/futures"
	getFuture            = "/futures/"
	getFutureStats       = "/futures/%s/stats"
	getFundingRates      = "/funding_rates"
	getIndexWeights      = "/indexes/%s/weights"
	getAllWalletBalances = "/wallet/all_balances"
	getIndexCandles      = "/indexes/%s/candles"

	// Authenticated endpoints
	getAccountInfo           = "/account"
	getPositions             = "/positions"
	setLeverage              = "/account/leverage"
	getCoins                 = "/wallet/coins"
	getBalances              = "/wallet/balances"
	getDepositAddress        = "/wallet/deposit_address/"
	getDepositHistory        = "/wallet/deposits"
	getWithdrawalHistory     = "/wallet/withdrawals"
	withdrawRequest          = "/wallet/withdrawals"
	getOpenOrders            = "/orders"
	getOrderHistory          = "/orders/history"
	getOpenTriggerOrders     = "/conditional_orders"
	getTriggerOrderTriggers  = "/conditional_orders/%s/triggers"
	getTriggerOrderHistory   = "/conditional_orders/history"
	placeOrder               = "/orders"
	placeTriggerOrder        = "/conditional_orders"
	modifyOrder              = "/orders/%s/modify"
	modifyOrderByClientID    = "/orders/by_client_id/%s/modify"
	modifyTriggerOrder       = "/conditional_orders/%s/modify"
	getOrderStatus           = "/orders/"
	getOrderStatusByClientID = "/orders/by_client_id/"
	deleteOrder              = "/orders/"
	deleteOrderByClientID    = "/orders/by_client_id/"
	cancelTriggerOrder       = "/conditional_orders/"
	getFills                 = "/fills"
	getFundingPayments       = "/funding_payments"
	getLeveragedTokens       = "/lt/tokens"
	getTokenInfo             = "/lt/"
	getLTBalances            = "/lt/balances"
	getLTCreations           = "/lt/creations"
	requestLTCreation        = "/lt/%s/create"
	getLTRedemptions         = "/lt/redemptions"
	requestLTRedemption      = "/lt/%s/redeem"
	getListQuotes            = "/options/requests"
	getMyQuotesRequests      = "/options/my_requests"
	createQuoteRequest       = "/options/requests"
	deleteQuote              = "/options/requests/"
	endpointQuote            = "/options/requests/%s/quotes"
	getMyQuotes              = "/options/my_quotes"
	deleteMyQuote            = "/options/quotes/"
	acceptQuote              = "/options/quotes/%s/accept"
	getOptionsInfo           = "/options/account_info"
	getOptionsPositions      = "/options/positions"
	getPublicOptionsTrades   = "/options/trades"
	getOptionsFills          = "/options/fills"
	requestOTCQuote          = "/otc/quotes"
	getOTCQuoteStatus        = "/otc/quotes/"
	acceptOTCQuote           = "/otc/quotes/%s/accept"
	subaccounts              = "/subaccounts"
	subaccountsUpdateName    = "/subaccounts/update_name"
	subaccountsBalance       = "/subaccounts/%s/balances"
	subaccountsTransfer      = "/subaccounts/transfer"

	// Margin Endpoints
	marginBorrowRates    = "/spot_margin/borrow_rates"
	marginLendingRates   = "/spot_margin/lending_rates"
	marginLendingHistory = "/spot_margin/history"
	dailyBorrowedAmounts = "/spot_margin/borrow_summary"
	marginMarketInfo     = "/spot_margin/market_info?market=%s"
	marginBorrowHistory  = "/spot_margin/borrow_history"
	marginLendHistory    = "/spot_margin/lending_history"
	marginLendingOffers  = "/spot_margin/offers"
	marginLendingInfo    = "/spot_margin/lending_info"
	submitLendingOrder   = "/spot_margin/offers"

	// Staking endpoints
	stakes          = "/staking/stakes"
	unstakeRequests = "/staking/unstake_requests"
	stakeBalances   = "/staking/balances"
	stakingRewards  = "/staking/staking_rewards"
	serumStakes     = "/srm_stakes/stakes"

	// Other Consts
	trailingStopOrderType = "trailingStop"
	takeProfitOrderType   = "takeProfit"
	closedStatus          = "closed"
	spotString            = "spot"
	futuresString         = "future"

	ratePeriod = time.Second
	rateLimit  = 30
)

var (
	errInvalidOrderID                                    = errors.New("invalid order ID")
	errStartTimeCannotBeAfterEndTime                     = errors.New("start timestamp cannot be after end timestamp")
	errSubaccountNameMustBeSpecified                     = errors.New("a subaccount name must be specified")
	errSubaccountUpdateNameInvalid                       = errors.New("invalid subaccount old/new name")
	errCoinMustBeSpecified                               = errors.New("a coin must be specified")
	errSubaccountTransferSizeGreaterThanZero             = errors.New("transfer size must be greater than 0")
	errSubaccountTransferSourceDestinationMustNotBeEqual = errors.New("subaccount transfer source and destination must not be the same value")
	errUnrecognisedOrderStatus                           = errors.New("unrecognised order status received")
	errInvalidOrderAmounts                               = errors.New("filled amount should not exceed order amount")

	validResolutionData = []int64{15, 60, 300, 900, 3600, 14400, 86400}
)

// GetHistoricalIndex gets historical index data
func (f *FTX) GetHistoricalIndex(ctx context.Context, indexName string, resolution int64, startTime, endTime time.Time) ([]OHLCVData, error) {
	params := url.Values{}
	if indexName == "" {
		return nil, errors.New("indexName is a mandatory field")
	}
	params.Set("index_name", indexName)
	err := checkResolution(resolution)
	if err != nil {
		return nil, err
	}
	params.Set("resolution", strconv.FormatInt(resolution, 10))
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return nil, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	resp := struct {
		Data []OHLCVData `json:"result"`
	}{}
	endpoint := common.EncodeURLValues(fmt.Sprintf(getIndexCandles, indexName), params)
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, endpoint, &resp)
}

func checkResolution(res int64) error {
	for x := range validResolutionData {
		if validResolutionData[x] == res {
			return nil
		}
	}
	return errors.New("resolution data is a mandatory field and the data provided is invalid")
}

// GetMarkets gets market data
func (f *FTX) GetMarkets(ctx context.Context) ([]MarketData, error) {
	resp := struct {
		Data []MarketData `json:"result"`
	}{}
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, getMarkets, &resp)
}

// GetMarket gets market data for a provided asset type
func (f *FTX) GetMarket(ctx context.Context, marketName string) (MarketData, error) {
	resp := struct {
		Data MarketData `json:"result"`
	}{}
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, getMarket+marketName,
		&resp)
}

// GetOrderbook gets orderbook for a given market with a given depth (default depth 20)
func (f *FTX) GetOrderbook(ctx context.Context, marketName string, depth int64) (OrderbookData, error) {
	result := struct {
		Data TempOBData `json:"result"`
	}{}

	strDepth := "20" // If we send a zero value we get zero asks from the
	// endpoint
	if depth != 0 {
		strDepth = strconv.FormatInt(depth, 10)
	}

	var resp OrderbookData
	err := f.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getOrderbook, marketName, strDepth), &result)
	if err != nil {
		return resp, err
	}
	resp.MarketName = marketName
	for x := range result.Data.Asks {
		resp.Asks = append(resp.Asks, OData{
			Price: result.Data.Asks[x][0],
			Size:  result.Data.Asks[x][1],
		})
	}
	for y := range result.Data.Bids {
		resp.Bids = append(resp.Bids, OData{
			Price: result.Data.Bids[y][0],
			Size:  result.Data.Bids[y][1],
		})
	}
	return resp, nil
}

// GetTrades gets trades based on the conditions specified
func (f *FTX) GetTrades(ctx context.Context, marketName string, startTime, endTime, limit int64) ([]TradeData, error) {
	if marketName == "" {
		return nil, errors.New("a market pair must be specified")
	}

	params := url.Values{}
	if limit != 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	if startTime > 0 && endTime > 0 {
		if startTime >= (endTime) {
			return nil, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime, 10))
		params.Set("end_time", strconv.FormatInt(endTime, 10))
	}
	resp := struct {
		Data []TradeData `json:"result"`
	}{}
	endpoint := common.EncodeURLValues(fmt.Sprintf(getTrades, marketName), params)
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, endpoint, &resp)
}

// GetHistoricalData gets historical OHLCV data for a given market pair
func (f *FTX) GetHistoricalData(ctx context.Context, marketName string, timeInterval, limit int64, startTime, endTime time.Time) ([]OHLCVData, error) {
	if marketName == "" {
		return nil, errors.New("a market pair must be specified")
	}

	err := checkResolution(timeInterval)
	if err != nil {
		return nil, err
	}

	params := url.Values{}
	params.Set("resolution", strconv.FormatInt(timeInterval, 10))
	if limit != 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return nil, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	resp := struct {
		Data []OHLCVData `json:"result"`
	}{}
	endpoint := common.EncodeURLValues(fmt.Sprintf(getHistoricalData, marketName), params)
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, endpoint, &resp)
}

// GetFutures gets data on futures
func (f *FTX) GetFutures(ctx context.Context) ([]FuturesData, error) {
	resp := struct {
		Data []FuturesData `json:"result"`
	}{}
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, getFutures, &resp)
}

// GetFuture gets data on a given future
func (f *FTX) GetFuture(ctx context.Context, futureName string) (FuturesData, error) {
	resp := struct {
		Data FuturesData `json:"result"`
	}{}
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, getFuture+futureName, &resp)
}

// GetFutureStats gets data on a given future's stats
func (f *FTX) GetFutureStats(ctx context.Context, futureName string) (FutureStatsData, error) {
	resp := struct {
		Data FutureStatsData `json:"result"`
	}{}
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getFutureStats, futureName), &resp)
}

// GetFundingRates gets data on funding rates
func (f *FTX) GetFundingRates(ctx context.Context, startTime, endTime time.Time, future string) ([]FundingRatesData, error) {
	resp := struct {
		Data []FundingRatesData `json:"result"`
	}{}
	params := url.Values{}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return resp.Data, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	if future != "" {
		params.Set("future", future)
	}
	endpoint := common.EncodeURLValues(getFundingRates, params)
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, endpoint, &resp)
}

// GetIndexWeights gets index weights
func (f *FTX) GetIndexWeights(ctx context.Context, index string) (IndexWeights, error) {
	var resp IndexWeights
	return resp, f.SendHTTPRequest(ctx, exchange.RestSpot, fmt.Sprintf(getIndexWeights, index), &resp)
}

// SendHTTPRequest sends an unauthenticated HTTP request
func (f *FTX) SendHTTPRequest(ctx context.Context, ep exchange.URL, path string, result interface{}) error {
	endpoint, err := f.API.Endpoints.GetURL(ep)
	if err != nil {
		return err
	}
	item := &request.Item{
		Method:        http.MethodGet,
		Path:          endpoint + path,
		Result:        result,
		Verbose:       f.Verbose,
		HTTPDebugging: f.HTTPDebugging,
		HTTPRecording: f.HTTPRecording,
	}
	return f.SendPayload(ctx, request.Unset, func() (*request.Item, error) {
		return item, nil
	})
}

// GetMarginBorrowRates gets borrowing rates for margin trading
func (f *FTX) GetMarginBorrowRates(ctx context.Context) ([]MarginFundingData, error) {
	r := struct {
		Data []MarginFundingData `json:"result"`
	}{}
	return r.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginBorrowRates, nil, &r)
}

// GetMarginLendingRates gets lending rates for margin trading
func (f *FTX) GetMarginLendingRates(ctx context.Context) ([]MarginFundingData, error) {
	r := struct {
		Data []MarginFundingData `json:"result"`
	}{}
	return r.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginLendingRates, nil, &r)
}

// MarginDailyBorrowedAmounts gets daily borrowed amounts for margin
func (f *FTX) MarginDailyBorrowedAmounts(ctx context.Context) ([]MarginDailyBorrowStats, error) {
	r := struct {
		Data []MarginDailyBorrowStats `json:"result"`
	}{}
	return r.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, dailyBorrowedAmounts, &r)
}

// GetMarginMarketInfo gets margin market data
func (f *FTX) GetMarginMarketInfo(ctx context.Context, market string) ([]MarginMarketInfo, error) {
	r := struct {
		Data []MarginMarketInfo `json:"result"`
	}{}
	return r.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(marginMarketInfo, market), nil, &r)
}

// GetMarginBorrowHistory gets the margin borrow history data
func (f *FTX) GetMarginBorrowHistory(ctx context.Context, startTime, endTime time.Time) ([]MarginTransactionHistoryData, error) {
	r := struct {
		Data []MarginTransactionHistoryData `json:"result"`
	}{}

	params := url.Values{}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return nil, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	endpoint := common.EncodeURLValues(marginBorrowHistory, params)
	return r.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, endpoint, nil, &r)
}

// GetMarginMarketLendingHistory gets the markets margin lending rate history
func (f *FTX) GetMarginMarketLendingHistory(ctx context.Context, coin currency.Code, startTime, endTime time.Time) ([]MarginTransactionHistoryData, error) {
	r := struct {
		Data []MarginTransactionHistoryData `json:"result"`
	}{}
	params := url.Values{}
	if !coin.IsEmpty() {
		params.Set("coin", coin.Upper().String())
	}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return nil, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	endpoint := common.EncodeURLValues(marginLendingHistory, params)
	return r.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, endpoint, params, &r)
}

// GetMarginLendingHistory gets margin lending history
func (f *FTX) GetMarginLendingHistory(ctx context.Context, coin currency.Code, startTime, endTime time.Time) ([]MarginTransactionHistoryData, error) {
	r := struct {
		Data []MarginTransactionHistoryData `json:"result"`
	}{}
	params := url.Values{}
	if !coin.IsEmpty() {
		params.Set("coin", coin.Upper().String())
	}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return nil, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	endpoint := common.EncodeURLValues(marginLendHistory, params)
	return r.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginLendHistory, endpoint, &r)
}

// GetMarginLendingOffers gets margin lending offers
func (f *FTX) GetMarginLendingOffers(ctx context.Context) ([]LendingOffersData, error) {
	r := struct {
		Data []LendingOffersData `json:"result"`
	}{}
	return r.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginLendingOffers, nil, &r)
}

// GetLendingInfo gets margin lending info
func (f *FTX) GetLendingInfo(ctx context.Context) ([]LendingInfoData, error) {
	r := struct {
		Data []LendingInfoData `json:"result"`
	}{}
	return r.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, marginLendingInfo, nil, &r)
}

// SubmitLendingOffer submits an offer for margin lending
func (f *FTX) SubmitLendingOffer(ctx context.Context, coin currency.Code, size, rate float64) error {
	resp := struct {
		Result  string `json:"result"`
		Success bool   `json:"success"`
	}{}
	req := make(map[string]interface{})
	req["coin"] = coin.Upper().String()
	req["size"] = size
	req["rate"] = rate

	if err := f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, marginLendingOffers, req, &resp); err != nil {
		return err
	}

	if !resp.Success {
		return errors.New(resp.Result)
	}
	return nil
}

// GetAccountInfo gets account info
func (f *FTX) GetAccountInfo(ctx context.Context) (AccountInfoData, error) {
	resp := struct {
		Data AccountInfoData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getAccountInfo, nil, &resp)
}

// GetPositions gets the users positions
func (f *FTX) GetPositions(ctx context.Context) ([]PositionData, error) {
	resp := struct {
		Data []PositionData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getPositions, nil, &resp)
}

// ChangeAccountLeverage changes default leverage used by account
func (f *FTX) ChangeAccountLeverage(ctx context.Context, leverage float64) error {
	req := make(map[string]interface{})
	req["leverage"] = leverage
	return f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, setLeverage, req, nil)
}

// GetCoins gets coins' data in the account wallet
func (f *FTX) GetCoins(ctx context.Context) ([]WalletCoinsData, error) {
	resp := struct {
		Data []WalletCoinsData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getCoins, nil, &resp)
}

// GetBalances gets balances of the account
func (f *FTX) GetBalances(ctx context.Context) ([]WalletBalance, error) {
	resp := struct {
		Data []WalletBalance `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getBalances, nil, &resp)
}

// GetAllWalletBalances gets all wallets' balances
func (f *FTX) GetAllWalletBalances(ctx context.Context) (AllWalletBalances, error) {
	resp := struct {
		Data AllWalletBalances `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getAllWalletBalances, nil, &resp)
}

// FetchDepositAddress gets deposit address for a given coin
func (f *FTX) FetchDepositAddress(ctx context.Context, coin currency.Code, chain string) (*DepositData, error) {
	resp := struct {
		Data DepositData `json:"result"`
	}{}
	vals := url.Values{}
	if chain != "" {
		vals.Set("method", strings.ToLower(chain))
	}
	path := common.EncodeURLValues(getDepositAddress+coin.Upper().String(), vals)
	return &resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, path, nil, &resp)
}

// FetchDepositHistory gets deposit history
func (f *FTX) FetchDepositHistory(ctx context.Context) ([]DepositItem, error) {
	resp := struct {
		Data []DepositItem `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getDepositHistory, nil, &resp)
}

// FetchWithdrawalHistory gets withdrawal history
func (f *FTX) FetchWithdrawalHistory(ctx context.Context) ([]WithdrawItem, error) {
	resp := struct {
		Data []WithdrawItem `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getWithdrawalHistory, nil, &resp)
}

// Withdraw sends a withdrawal request
func (f *FTX) Withdraw(ctx context.Context, coin currency.Code, address, tag, password, chain, code string, size float64) (*WithdrawItem, error) {
	if coin.IsEmpty() || address == "" || size == 0 {
		return nil, errors.New("coin, address and size must be specified")
	}

	req := make(map[string]interface{})
	req["coin"] = coin.Upper().String()
	req["size"] = size
	req["address"] = address
	if code != "" {
		req["code"] = code
	}
	if tag != "" {
		req["tag"] = tag
	}
	if password != "" {
		req["password"] = password
	}
	if chain != "" {
		req["method"] = chain
	}
	resp := struct {
		Data WithdrawItem `json:"result"`
	}{}
	return &resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, withdrawRequest, req, &resp)
}

// GetOpenOrders gets open orders
func (f *FTX) GetOpenOrders(ctx context.Context, marketName string) ([]OrderData, error) {
	params := url.Values{}
	if marketName != "" {
		params.Set("market", marketName)
	}
	resp := struct {
		Data []OrderData `json:"result"`
	}{}
	endpoint := common.EncodeURLValues(getOpenOrders, params)
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}

// FetchOrderHistory gets order history
func (f *FTX) FetchOrderHistory(ctx context.Context, marketName string, startTime, endTime time.Time, limit string) ([]OrderData, error) {
	resp := struct {
		Data []OrderData `json:"result"`
	}{}
	params := url.Values{}
	if marketName != "" {
		params.Set("market", marketName)
	}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return resp.Data, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	if limit != "" {
		params.Set("limit", limit)
	}
	endpoint := common.EncodeURLValues(getOrderHistory, params)
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}

// GetOpenTriggerOrders gets trigger orders that are currently open
func (f *FTX) GetOpenTriggerOrders(ctx context.Context, marketName, orderType string) ([]TriggerOrderData, error) {
	params := url.Values{}
	if marketName != "" {
		params.Set("market", marketName)
	}
	if orderType != "" {
		params.Set("type", orderType)
	}
	resp := struct {
		Data []TriggerOrderData `json:"result"`
	}{}
	endpoint := common.EncodeURLValues(getOpenTriggerOrders, params)
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}

// GetTriggerOrderTriggers gets trigger orders that are currently open
func (f *FTX) GetTriggerOrderTriggers(ctx context.Context, orderID string) ([]TriggerData, error) {
	resp := struct {
		Data []TriggerData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(getTriggerOrderTriggers, orderID), nil, &resp)
}

// GetTriggerOrderHistory gets trigger orders that are currently open
func (f *FTX) GetTriggerOrderHistory(ctx context.Context, marketName string, startTime, endTime time.Time, side, orderType, limit string) ([]TriggerOrderData, error) {
	params := url.Values{}
	if marketName != "" {
		params.Set("market", marketName)
	}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return nil, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	if side != "" {
		params.Set("side", side)
	}
	if orderType != "" {
		params.Set("type", orderType)
	}
	if limit != "" {
		params.Set("limit", limit)
	}
	resp := struct {
		Data []TriggerOrderData `json:"result"`
	}{}
	endpoint := common.EncodeURLValues(getTriggerOrderHistory, params)
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}

// Order places an order
func (f *FTX) Order(
	ctx context.Context,
	marketName, side, orderType string,
	reduceOnly, ioc, postOnly bool,
	clientID string,
	price, size float64,
) (OrderData, error) {
	req := make(map[string]interface{})
	req["market"] = marketName
	req["side"] = side
	req["price"] = price
	req["type"] = orderType
	req["size"] = size
	if reduceOnly {
		req["reduceOnly"] = reduceOnly
	}
	if ioc {
		req["ioc"] = ioc
	}
	if postOnly {
		req["postOnly"] = postOnly
	}
	if clientID != "" {
		req["clientId"] = clientID
	}
	resp := struct {
		Data OrderData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, placeOrder, req, &resp)
}

// TriggerOrder places an order
func (f *FTX) TriggerOrder(ctx context.Context, marketName, side, orderType, reduceOnly, retryUntilFilled string, size, triggerPrice, orderPrice, trailValue float64) (TriggerOrderData, error) {
	req := make(map[string]interface{})
	req["market"] = marketName
	req["side"] = side
	req["type"] = orderType
	req["size"] = size
	if reduceOnly != "" {
		req["reduceOnly"] = reduceOnly
	}
	if retryUntilFilled != "" {
		req["retryUntilFilled"] = retryUntilFilled
	}
	if orderType == order.Stop.Lower() || orderType == "" {
		req["triggerPrice"] = triggerPrice
		req["orderPrice"] = orderPrice
	}
	if orderType == trailingStopOrderType {
		req["trailValue"] = trailValue
	}
	if orderType == takeProfitOrderType {
		req["triggerPrice"] = triggerPrice
		req["orderPrice"] = orderPrice
	}
	resp := struct {
		Data TriggerOrderData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, placeTriggerOrder, req, &resp)
}

// ModifyPlacedOrder modifies a placed order
func (f *FTX) ModifyPlacedOrder(ctx context.Context, orderID, clientID string, price, size float64) (OrderData, error) {
	req := make(map[string]interface{})
	req["price"] = price
	req["size"] = size
	if clientID != "" {
		req["clientID"] = clientID
	}
	resp := struct {
		Data OrderData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, fmt.Sprintf(modifyOrder, orderID), req, &resp)
}

// ModifyOrderByClientID modifies a placed order via clientOrderID
func (f *FTX) ModifyOrderByClientID(ctx context.Context, clientOrderID, clientID string, price, size float64) (OrderData, error) {
	req := make(map[string]interface{})
	req["price"] = price
	req["size"] = size
	if clientID != "" {
		req["clientID"] = clientID
	}
	resp := struct {
		Data OrderData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, fmt.Sprintf(modifyOrderByClientID, clientOrderID), req, &resp)
}

// ModifyTriggerOrder modifies an existing trigger order
// Choices for ordertype include stop, trailingStop, takeProfit
func (f *FTX) ModifyTriggerOrder(ctx context.Context, orderID, orderType string, size, triggerPrice, orderPrice, trailValue float64) (TriggerOrderData, error) {
	req := make(map[string]interface{})
	req["size"] = size
	if orderType == order.Stop.Lower() || orderType == "" {
		req["triggerPrice"] = triggerPrice
		req["orderPrice"] = orderPrice
	}
	if orderType == trailingStopOrderType {
		req["trailValue"] = trailValue
	}
	if orderType == takeProfitOrderType {
		req["triggerPrice"] = triggerPrice
		req["orderPrice"] = orderPrice
	}
	resp := struct {
		Data TriggerOrderData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, fmt.Sprintf(modifyTriggerOrder, orderID), req, &resp)
}

// GetOrderStatus gets the order status of a given orderID
func (f *FTX) GetOrderStatus(ctx context.Context, orderID string) (OrderData, error) {
	resp := struct {
		Data OrderData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOrderStatus+orderID, nil, &resp)
}

// GetOrderStatusByClientID gets the order status of a given clientOrderID
func (f *FTX) GetOrderStatusByClientID(ctx context.Context, clientOrderID string) (OrderData, error) {
	resp := struct {
		Data OrderData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOrderStatusByClientID+clientOrderID, nil, &resp)
}

func (f *FTX) deleteOrderByPath(ctx context.Context, path string) (string, error) {
	resp := struct {
		Result  string `json:"result"`
		Success bool   `json:"success"`
		Error   string `json:"error"`
	}{}
	err := f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, path, nil, &resp)
	// If there is an error reported, but the resp struct reports one of a very few
	// specific error causes, we still consider this a successful cancellation.
	if err != nil && !resp.Success && (resp.Error == "Order already closed" || resp.Error == "Order already queued for cancellation") {
		return resp.Error, nil
	}
	return resp.Result, err
}

// DeleteOrder deletes an order
func (f *FTX) DeleteOrder(ctx context.Context, orderID string) (string, error) {
	if orderID == "" {
		return "", errInvalidOrderID
	}
	return f.deleteOrderByPath(ctx, deleteOrder+orderID)
}

// DeleteOrderByClientID deletes an order
func (f *FTX) DeleteOrderByClientID(ctx context.Context, clientID string) (string, error) {
	if clientID == "" {
		return "", errInvalidOrderID
	}
	return f.deleteOrderByPath(ctx, deleteOrderByClientID+clientID)
}

// DeleteTriggerOrder deletes an order
func (f *FTX) DeleteTriggerOrder(ctx context.Context, orderID string) (string, error) {
	if orderID == "" {
		return "", errInvalidOrderID
	}
	return f.deleteOrderByPath(ctx, cancelTriggerOrder+orderID)
}

// GetFills gets fills' data
func (f *FTX) GetFills(ctx context.Context, market, limit string, startTime, endTime time.Time) ([]FillsData, error) {
	resp := struct {
		Data []FillsData `json:"result"`
	}{}
	params := url.Values{}
	if market != "" {
		params.Set("market", market)
	}
	if limit != "" {
		params.Set("limit", limit)
	}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return resp.Data, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	endpoint := common.EncodeURLValues(getFills, params)
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}

// GetFundingPayments gets funding payments
func (f *FTX) GetFundingPayments(ctx context.Context, startTime, endTime time.Time, future string) ([]FundingPaymentsData, error) {
	resp := struct {
		Data []FundingPaymentsData `json:"result"`
	}{}
	params := url.Values{}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return resp.Data, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	if future != "" {
		params.Set("future", future)
	}
	endpoint := common.EncodeURLValues(getFundingPayments, params)
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, endpoint, nil, &resp)
}

// ListLeveragedTokens lists leveraged tokens
func (f *FTX) ListLeveragedTokens(ctx context.Context) ([]LeveragedTokensData, error) {
	resp := struct {
		Data []LeveragedTokensData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getLeveragedTokens, nil, &resp)
}

// GetTokenInfo gets token info
func (f *FTX) GetTokenInfo(ctx context.Context, tokenName string) ([]LeveragedTokensData, error) {
	resp := struct {
		Data []LeveragedTokensData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getTokenInfo+tokenName, nil, &resp)
}

// ListLTBalances gets leveraged tokens' balances
func (f *FTX) ListLTBalances(ctx context.Context) ([]LTBalanceData, error) {
	resp := struct {
		Data []LTBalanceData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getLTBalances, nil, &resp)
}

// ListLTCreations lists the leveraged tokens' creation requests
func (f *FTX) ListLTCreations(ctx context.Context) ([]LTCreationData, error) {
	resp := struct {
		Data []LTCreationData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getLTCreations, nil, &resp)
}

// RequestLTCreation sends a request to create a leveraged token
func (f *FTX) RequestLTCreation(ctx context.Context, tokenName string, size float64) (RequestTokenCreationData, error) {
	req := make(map[string]interface{})
	req["size"] = size
	resp := struct {
		Data RequestTokenCreationData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, fmt.Sprintf(requestLTCreation, tokenName), req, &resp)
}

// ListLTRedemptions lists the leveraged tokens' redemption requests
func (f *FTX) ListLTRedemptions(ctx context.Context) ([]LTRedemptionData, error) {
	resp := struct {
		Data []LTRedemptionData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getLTRedemptions, nil, &resp)
}

// RequestLTRedemption sends a request to redeem a leveraged token
func (f *FTX) RequestLTRedemption(ctx context.Context, tokenName string, size float64) (LTRedemptionRequestData, error) {
	req := make(map[string]interface{})
	req["size"] = size
	resp := struct {
		Data LTRedemptionRequestData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, fmt.Sprintf(requestLTRedemption, tokenName), req, &resp)
}

// GetQuoteRequests gets a list of quote requests
func (f *FTX) GetQuoteRequests(ctx context.Context) ([]QuoteRequestData, error) {
	resp := struct {
		Data []QuoteRequestData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getListQuotes, nil, &resp)
}

// GetYourQuoteRequests gets a list of your quote requests
func (f *FTX) GetYourQuoteRequests(ctx context.Context) ([]PersonalQuotesData, error) {
	resp := struct {
		Data []PersonalQuotesData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getMyQuotesRequests, nil, &resp)
}

// CreateQuoteRequest sends a request to create a quote
func (f *FTX) CreateQuoteRequest(ctx context.Context, underlying currency.Code, optionType, side string, expiry int64, requestExpiry string, strike, size, limitPrice, counterPartyID float64, hideLimitPrice bool) (CreateQuoteRequestData, error) {
	req := make(map[string]interface{})
	req["underlying"] = underlying.Upper().String()
	req["type"] = optionType
	req["side"] = side
	req["strike"] = strike
	req["expiry"] = expiry
	req["size"] = size
	if limitPrice != 0 {
		req["limitPrice"] = limitPrice
	}
	if requestExpiry != "" {
		req["requestExpiry"] = requestExpiry
	}
	if counterPartyID != 0 {
		req["counterpartyId"] = counterPartyID
	}
	req["hideLimitPrice"] = hideLimitPrice
	resp := struct {
		Data CreateQuoteRequestData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, createQuoteRequest, req, &resp)
}

// DeleteQuote sends request to cancel a quote
func (f *FTX) DeleteQuote(ctx context.Context, requestID string) (CancelQuoteRequestData, error) {
	resp := struct {
		Data CancelQuoteRequestData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, deleteQuote+requestID, nil, &resp)
}

// GetQuotesForYourQuote gets a list of quotes for your quote
func (f *FTX) GetQuotesForYourQuote(ctx context.Context, requestID string) (QuoteForQuoteData, error) {
	var resp QuoteForQuoteData
	return resp, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(endpointQuote, requestID), nil, &resp)
}

// MakeQuote makes a quote for a quote
func (f *FTX) MakeQuote(ctx context.Context, requestID, price string) ([]QuoteForQuoteData, error) {
	params := url.Values{}
	params.Set("price", price)
	resp := struct {
		Data []QuoteForQuoteData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, fmt.Sprintf(endpointQuote, requestID), nil, &resp)
}

// MyQuotes gets a list of my quotes for quotes
func (f *FTX) MyQuotes(ctx context.Context) ([]QuoteForQuoteData, error) {
	resp := struct {
		Data []QuoteForQuoteData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getMyQuotes, nil, &resp)
}

// DeleteMyQuote deletes my quote for quotes
func (f *FTX) DeleteMyQuote(ctx context.Context, quoteID string) ([]QuoteForQuoteData, error) {
	resp := struct {
		Data []QuoteForQuoteData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, deleteMyQuote+quoteID, nil, &resp)
}

// AcceptQuote accepts the quote for quote
func (f *FTX) AcceptQuote(ctx context.Context, quoteID string) ([]QuoteForQuoteData, error) {
	resp := struct {
		Data []QuoteForQuoteData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, fmt.Sprintf(acceptQuote, quoteID), nil, &resp)
}

// GetAccountOptionsInfo gets account's options' info
func (f *FTX) GetAccountOptionsInfo(ctx context.Context) (AccountOptionsInfoData, error) {
	resp := struct {
		Data AccountOptionsInfoData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOptionsInfo, nil, &resp)
}

// GetOptionsPositions gets options' positions
func (f *FTX) GetOptionsPositions(ctx context.Context) ([]OptionsPositionsData, error) {
	resp := struct {
		Data []OptionsPositionsData `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOptionsPositions, nil, &resp)
}

// GetPublicOptionsTrades gets options' trades from public
func (f *FTX) GetPublicOptionsTrades(ctx context.Context, startTime, endTime time.Time, limit string) ([]OptionsTradesData, error) {
	params := url.Values{}
	if !startTime.IsZero() && !endTime.IsZero() {
		if startTime.After(endTime) {
			return nil, errStartTimeCannotBeAfterEndTime
		}
		params.Set("start_time", strconv.FormatInt(startTime.Unix(), 10))
		params.Set("end_time", strconv.FormatInt(endTime.Unix(), 10))
	}
	if limit != "" {
		params.Set("limit", limit)
	}
	resp := struct {
		Data []OptionsTradesData `json:"result"`
	}{}
	endpoint := common.EncodeURLValues(getPublicOptionsTrades, params)
	return resp.Data, f.SendHTTPRequest(ctx, exchange.RestSpot, endpoint, &resp)
}

// GetOptionsFills gets fills data for options
func (f *FTX) GetOptionsFills(ctx context.Context, startTime, endTime time.Time, limit string) ([]OptionFillsData, error) {
	resp := struct {
		Data []OptionFillsData `json:"result"`
	}{}
	req := make(map[string]interface{})
	if !startTime.IsZero() && !endTime.IsZero() {
		req["start_time"] = strconv.FormatInt(startTime.Unix(), 10)
		req["end_time"] = strconv.FormatInt(endTime.Unix(), 10)
		if startTime.After(endTime) {
			return resp.Data, errStartTimeCannotBeAfterEndTime
		}
	}
	if limit != "" {
		req["limit"] = limit
	}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOptionsFills, req, &resp)
}

// GetStakes returns a list of staked assets
func (f *FTX) GetStakes(ctx context.Context) ([]Stake, error) {
	resp := struct {
		Data []Stake `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, stakes, nil, &resp)
}

// GetUnstakeRequests returns a collection of unstake requests
func (f *FTX) GetUnstakeRequests(ctx context.Context) ([]UnstakeRequest, error) {
	resp := struct {
		Data []UnstakeRequest `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, unstakeRequests, nil, &resp)
}

// GetStakeBalances returns a collection of staked coin balances
func (f *FTX) GetStakeBalances(ctx context.Context) ([]StakeBalance, error) {
	resp := struct {
		Data []StakeBalance `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, stakeBalances, nil, &resp)
}

// UnstakeRequest unstakes an existing staked coin
func (f *FTX) UnstakeRequest(ctx context.Context, coin currency.Code, size float64) (*UnstakeRequest, error) {
	resp := struct {
		Data UnstakeRequest `json:"result"`
	}{}
	req := make(map[string]interface{})
	req["coin"] = coin.Upper().String()
	req["size"] = strconv.FormatFloat(size, 'f', -1, 64)
	return &resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, unstakeRequests, req, &resp)
}

// CancelUnstakeRequest cancels a pending unstake request
func (f *FTX) CancelUnstakeRequest(ctx context.Context, requestID int64) (bool, error) {
	resp := struct {
		Result string
	}{}
	path := unstakeRequests + "/" + strconv.FormatInt(requestID, 10)
	if err := f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, path, nil, &resp); err != nil {
		return false, err
	}

	if resp.Result != "Cancelled" {
		return false, errors.New("failed to cancel unstake request")
	}
	return true, nil
}

// GetStakingRewards returns a collection of staking rewards
func (f *FTX) GetStakingRewards(ctx context.Context) ([]StakeReward, error) {
	resp := struct {
		Data []StakeReward `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, stakingRewards, nil, &resp)
}

// StakeRequest submits a stake request based on the specified currency and size
func (f *FTX) StakeRequest(ctx context.Context, coin currency.Code, size float64) (*Stake, error) {
	resp := struct {
		Data Stake `json:"result"`
	}{}
	req := make(map[string]interface{})
	req["coin"] = coin.Upper().String()
	req["size"] = strconv.FormatFloat(size, 'f', -1, 64)
	return &resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, serumStakes, req, &resp)
}

// SendAuthHTTPRequest sends an authenticated request
func (f *FTX) SendAuthHTTPRequest(ctx context.Context, ep exchange.URL, method, path string, data, result interface{}) error {
	if !f.AllowAuthenticatedRequest() {
		return fmt.Errorf("%s %w", f.Name, exchange.ErrAuthenticatedRequestWithoutCredentialsSet)
	}

	endpoint, err := f.API.Endpoints.GetURL(ep)
	if err != nil {
		return err
	}

	newRequest := func() (*request.Item, error) {
		ts := strconv.FormatInt(time.Now().UnixMilli(), 10)
		var body io.Reader
		var hmac, payload []byte

		sigPayload := ts + method + "/api" + path
		if data != nil {
			payload, err = json.Marshal(data)
			if err != nil {
				return nil, err
			}
			body = bytes.NewBuffer(payload)
			sigPayload += string(payload)
		}

		hmac, err = crypto.GetHMAC(crypto.HashSHA256,
			[]byte(sigPayload),
			[]byte(f.API.Credentials.Secret))
		if err != nil {
			return nil, err
		}

		headers := make(map[string]string)
		headers["FTX-KEY"] = f.API.Credentials.Key
		headers["FTX-SIGN"] = crypto.HexEncodeToString(hmac)
		headers["FTX-TS"] = ts
		if f.API.Credentials.Subaccount != "" {
			headers["FTX-SUBACCOUNT"] = url.QueryEscape(f.API.Credentials.Subaccount)
		}
		headers["Content-Type"] = "application/json"

		return &request.Item{
			Method:        method,
			Path:          endpoint + path,
			Headers:       headers,
			Body:          body,
			Result:        result,
			AuthRequest:   true,
			Verbose:       f.Verbose,
			HTTPDebugging: f.HTTPDebugging,
			HTTPRecording: f.HTTPRecording,
		}, nil
	}
	return f.SendPayload(ctx, request.Unset, newRequest)
}

// GetFee returns an estimate of fee based on type of transaction
func (f *FTX) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
	var fee float64
	if !f.GetAuthenticatedAPISupport(exchange.RestAuthentication) {
		feeBuilder.FeeType = exchange.OfflineTradeFee
	}
	switch feeBuilder.FeeType {
	case exchange.OfflineTradeFee:
		fee = getOfflineTradeFee(feeBuilder)
	default:
		feeData, err := f.GetAccountInfo(ctx)
		if err != nil {
			return 0, err
		}
		switch feeBuilder.IsMaker {
		case true:
			fee = feeData.MakerFee * feeBuilder.Amount * feeBuilder.PurchasePrice
		case false:
			fee = feeData.TakerFee * feeBuilder.Amount * feeBuilder.PurchasePrice
		}
		if fee < 0 {
			fee = 0
		}
	}
	return fee, nil
}

// getOfflineTradeFee calculates the worst case-scenario trading fee
func getOfflineTradeFee(feeBuilder *exchange.FeeBuilder) float64 {
	if feeBuilder.IsMaker {
		return 0.0002 * feeBuilder.PurchasePrice * feeBuilder.Amount
	}
	return 0.0007 * feeBuilder.PurchasePrice * feeBuilder.Amount
}

func (f *FTX) compatibleOrderVars(ctx context.Context, orderSide, orderStatus, orderType string, amount, filledAmount, avgFillPrice float64) (OrderVars, error) {
	if filledAmount > amount {
		return OrderVars{}, fmt.Errorf("%w, amount: %f filled: %f", errInvalidOrderAmounts, amount, filledAmount)
	}
	var resp OrderVars
	switch orderSide {
	case order.Buy.Lower():
		resp.Side = order.Buy
	case order.Sell.Lower():
		resp.Side = order.Sell
	}
	switch orderStatus {
	case strings.ToLower(order.New.String()):
		resp.Status = order.New
	case strings.ToLower(order.Open.String()):
		resp.Status = order.Open
	case closedStatus:
		switch {
		case filledAmount <= 0:
			// Order is closed with a filled amount of 0, which means it's
			// cancelled.
			resp.Status = order.Cancelled
		case math.Abs(filledAmount-amount) > 1e-6:
			// Order is closed with filledAmount above 0, but not equal to the
			// full amount, which means it's partially executed and then
			// cancelled.
			resp.Status = order.PartiallyCancelled
		default:
			// Order is closed and filledAmount == amount, which means it's
			// fully executed.
			resp.Status = order.Filled
		}
	default:
		return resp, fmt.Errorf("%w %s", errUnrecognisedOrderStatus, orderStatus)
	}
	var feeBuilder exchange.FeeBuilder
	feeBuilder.PurchasePrice = avgFillPrice
	feeBuilder.Amount = amount
	resp.OrderType = order.Market
	if strings.EqualFold(orderType, order.Limit.String()) {
		resp.OrderType = order.Limit
		feeBuilder.IsMaker = true
	}
	fee, err := f.GetFee(ctx, &feeBuilder)
	if err != nil {
		return resp, err
	}
	resp.Fee = fee
	return resp, nil
}

// RequestForQuotes requests for otc quotes
func (f *FTX) RequestForQuotes(ctx context.Context, base, quote currency.Code, amount float64) (RequestQuoteData, error) {
	resp := struct {
		Data RequestQuoteData `json:"result"`
	}{}
	req := make(map[string]interface{})
	req["fromCoin"] = base.Upper().String()
	req["toCoin"] = quote.Upper().String()
	req["size"] = amount
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, requestOTCQuote, req, &resp)
}

// GetOTCQuoteStatus gets quote status of a quote
func (f *FTX) GetOTCQuoteStatus(ctx context.Context, marketName, quoteID string) (*QuoteStatusData, error) {
	resp := struct {
		Data QuoteStatusData `json:"result"`
	}{}
	params := url.Values{}
	params.Set("market", marketName)
	return &resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, getOTCQuoteStatus+quoteID, params, &resp)
}

// AcceptOTCQuote requests for otc quotes
func (f *FTX) AcceptOTCQuote(ctx context.Context, quoteID string) error {
	return f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, fmt.Sprintf(acceptOTCQuote, quoteID), nil, nil)
}

// GetSubaccounts returns the users subaccounts
func (f *FTX) GetSubaccounts(ctx context.Context) ([]Subaccount, error) {
	resp := struct {
		Data []Subaccount `json:"result"`
	}{}
	return resp.Data, f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, subaccounts, nil, &resp)
}

// CreateSubaccount creates a new subaccount
func (f *FTX) CreateSubaccount(ctx context.Context, name string) (*Subaccount, error) {
	if name == "" {
		return nil, errSubaccountNameMustBeSpecified
	}
	d := make(map[string]string)
	d["nickname"] = name

	resp := struct {
		Data Subaccount `json:"result"`
	}{}
	if err := f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, subaccounts, d, &resp); err != nil {
		return nil, err
	}
	return &resp.Data, nil
}

// UpdateSubaccountName updates an existing subaccount name
func (f *FTX) UpdateSubaccountName(ctx context.Context, oldName, newName string) (*Subaccount, error) {
	if oldName == "" || newName == "" || oldName == newName {
		return nil, errSubaccountUpdateNameInvalid
	}
	d := make(map[string]string)
	d["nickname"] = oldName
	d["newNickname"] = newName

	resp := struct {
		Data Subaccount `json:"result"`
	}{}
	if err := f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, subaccountsUpdateName, d, &resp); err != nil {
		return nil, err
	}
	return &resp.Data, nil
}

// DeleteSubaccount deletes the specified subaccount name
func (f *FTX) DeleteSubaccount(ctx context.Context, name string) error {
	if name == "" {
		return errSubaccountNameMustBeSpecified
	}
	d := make(map[string]string)
	d["nickname"] = name
	resp := struct {
		Data Subaccount `json:"result"`
	}{}
	return f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodDelete, subaccounts, d, &resp)
}

// SubaccountBalances returns the user's subaccount balances
func (f *FTX) SubaccountBalances(ctx context.Context, name string) ([]SubaccountBalance, error) {
	if name == "" {
		return nil, errSubaccountNameMustBeSpecified
	}
	resp := struct {
		Data []SubaccountBalance `json:"result"`
	}{}
	if err := f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodGet, fmt.Sprintf(subaccountsBalance, name), nil, &resp); err != nil {
		return nil, err
	}
	return resp.Data, nil
}

// SubaccountTransfer transfers a desired coin to the specified subaccount
func (f *FTX) SubaccountTransfer(ctx context.Context, coin currency.Code, source, destination string, size float64) (*SubaccountTransferStatus, error) {
	if coin.IsEmpty() {
		return nil, errCoinMustBeSpecified
	}
	if size <= 0 {
		return nil, errSubaccountTransferSizeGreaterThanZero
	}
	if source == destination {
		return nil, errSubaccountTransferSourceDestinationMustNotBeEqual
	}
	d := make(map[string]interface{})
	d["coin"] = coin.Upper().String()
	d["size"] = size
	if source == "" {
		source = "main"
	}
	d["source"] = source
	if destination == "" {
		destination = "main"
	}
	d["destination"] = destination
	resp := struct {
		Data SubaccountTransferStatus `json:"result"`
	}{}
	if err := f.SendAuthHTTPRequest(ctx, exchange.RestSpot, http.MethodPost, subaccountsTransfer, d, &resp); err != nil {
		return nil, err
	}
	return &resp.Data, nil
}

// FetchExchangeLimits fetches spot order execution limits
func (f *FTX) FetchExchangeLimits(ctx context.Context) ([]order.MinMaxLevel, error) {
	data, err := f.GetMarkets(ctx)
	if err != nil {
		return nil, err
	}

	var limits []order.MinMaxLevel
	for x := range data {
		if !data[x].Enabled {
			continue
		}
		var cp currency.Pair
		var a asset.Item
		switch data[x].MarketType {
		case "future":
			a = asset.Futures
			cp, err = currency.NewPairFromString(data[x].Name)
			if err != nil {
				return nil, err
			}
		case "spot":
			a = asset.Spot
			cp, err = currency.NewPairFromStrings(data[x].BaseCurrency, data[x].QuoteCurrency)
			if err != nil {
				return nil, err
			}
		default:
			return nil, fmt.Errorf("unhandled data type %s, cannot process exchange limit",
				data[x].MarketType)
		}

		limits = append(limits, order.MinMaxLevel{
			Pair:       cp,
			Asset:      a,
			StepPrice:  data[x].PriceIncrement,
			StepAmount: data[x].SizeIncrement,
			MinAmount:  data[x].MinProvideSize,
		})
	}
	return limits, nil
}
