package okx

import (
	"bytes"
	"context"
	"encoding/base64"
	"errors"
	"fmt"
	"net"
	"net/http"
	"net/url"
	"slices"
	"strconv"
	"strings"
	"sync"
	"time"

	"github.com/thrasher-corp/gocryptotrader/common"
	"github.com/thrasher-corp/gocryptotrader/common/crypto"
	"github.com/thrasher-corp/gocryptotrader/currency"
	"github.com/thrasher-corp/gocryptotrader/encoding/json"
	"github.com/thrasher-corp/gocryptotrader/exchange/order/limits"
	exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
	"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
	"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
	"github.com/thrasher-corp/gocryptotrader/exchanges/margin"
	"github.com/thrasher-corp/gocryptotrader/exchanges/order"
	"github.com/thrasher-corp/gocryptotrader/exchanges/request"
	"github.com/thrasher-corp/gocryptotrader/types"
)

// Exchange implements exchange.IBotExchange and contains additional specific api methods for interacting with Okx
type Exchange struct {
	exchange.Base

	instrumentsInfoMapLock sync.Mutex
	instrumentsInfoMap     map[string][]Instrument
}

const (
	baseURL = "https://www.okx.com/"
	apiURL  = baseURL + apiPath

	apiPath      = "api/v5/"
	websocketURL = "wss://ws.okx.com:8443/ws/v5/"

	apiWebsocketPublicURL  = websocketURL + "public"
	apiWebsocketPrivateURL = websocketURL + "private"
)

/************************************ MarketData Endpoints *************************************************/

// PlaceOrder places an order
func (e *Exchange) PlaceOrder(ctx context.Context, arg *PlaceOrderRequestParam) (*OrderData, error) {
	if err := arg.Validate(); err != nil {
		return nil, err
	}
	var resp *OrderData
	err := e.SendHTTPRequest(ctx, exchange.RestSpot, placeOrderEPL, http.MethodPost, "trade/order", &arg, &resp, request.AuthenticatedRequest)
	if err != nil {
		if resp != nil && resp.StatusMessage != "" {
			return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
		}
		return nil, err
	}
	return resp, nil
}

// PlaceMultipleOrders  to place orders in batches. Maximum 20 orders can be placed at a time. Request parameters should be passed in the form of an array
func (e *Exchange) PlaceMultipleOrders(ctx context.Context, args []PlaceOrderRequestParam) ([]OrderData, error) {
	if len(args) == 0 {
		return nil, order.ErrSubmissionIsNil
	}
	for x := range args {
		if err := args[x].Validate(); err != nil {
			return nil, err
		}
	}
	var resp []OrderData
	err := e.SendHTTPRequest(ctx, exchange.RestSpot, placeMultipleOrdersEPL, http.MethodPost, "trade/batch-orders", &args, &resp, request.AuthenticatedRequest)
	if err != nil {
		if len(resp) == 0 {
			return nil, err
		}
		var errs error
		for x := range resp {
			errs = common.AppendError(errs, getStatusError(resp[x].StatusCode, resp[x].StatusMessage))
		}
		return nil, common.AppendError(err, errs)
	}
	return resp, nil
}

// CancelSingleOrder cancel an incomplete order
func (e *Exchange) CancelSingleOrder(ctx context.Context, arg *CancelOrderRequestParam) (*OrderData, error) {
	if *arg == (CancelOrderRequestParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if arg.OrderID == "" && arg.ClientOrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	var resp *OrderData
	err := e.SendHTTPRequest(ctx, exchange.RestSpot, cancelOrderEPL, http.MethodPost, "trade/cancel-order", &arg, &resp, request.AuthenticatedRequest)
	if err != nil {
		if resp != nil && resp.StatusMessage != "" {
			return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
		}
		return nil, err
	}
	return resp, nil
}

// CancelMultipleOrders cancel incomplete orders in batches. Maximum 20 orders can be canceled at a time.
// Request parameters should be passed in the form of an array
func (e *Exchange) CancelMultipleOrders(ctx context.Context, args []CancelOrderRequestParam) ([]*OrderData, error) {
	if len(args) == 0 {
		return nil, common.ErrEmptyParams
	}
	for x := range args {
		arg := args[x]
		if arg.InstrumentID == "" {
			return nil, errMissingInstrumentID
		}
		if arg.OrderID == "" && arg.ClientOrderID == "" {
			return nil, order.ErrOrderIDNotSet
		}
	}
	var resp []*OrderData
	err := e.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleOrdersEPL, http.MethodPost, "trade/cancel-batch-orders", args, &resp, request.AuthenticatedRequest)
	if err != nil {
		if len(resp) == 0 {
			return nil, err
		}
		var errs error
		for x := range resp {
			if resp[x].StatusCode != 0 {
				errs = common.AppendError(errs, getStatusError(resp[x].StatusCode, resp[x].StatusMessage))
			}
		}
		return nil, common.AppendError(err, errs)
	}
	return resp, nil
}

// AmendOrder an incomplete order
func (e *Exchange) AmendOrder(ctx context.Context, arg *AmendOrderRequestParams) (*OrderData, error) {
	if arg == nil {
		return nil, common.ErrNilPointer
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if arg.ClientOrderID == "" && arg.OrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	if arg.NewQuantity <= 0 && arg.NewPrice <= 0 {
		return nil, errInvalidNewSizeOrPriceInformation
	}
	var resp *OrderData
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, amendOrderEPL, http.MethodPost, "trade/amend-order", arg, &resp, request.AuthenticatedRequest)
}

// AmendMultipleOrders amend incomplete orders in batches. Maximum 20 orders can be amended at a time. Request parameters should be passed in the form of an array
func (e *Exchange) AmendMultipleOrders(ctx context.Context, args []AmendOrderRequestParams) ([]OrderData, error) {
	if len(args) == 0 {
		return nil, common.ErrEmptyParams
	}
	for x := range args {
		if args[x].InstrumentID == "" {
			return nil, errMissingInstrumentID
		}
		if args[x].ClientOrderID == "" && args[x].OrderID == "" {
			return nil, order.ErrOrderIDNotSet
		}
		if args[x].NewQuantity <= 0 && args[x].NewPrice <= 0 {
			return nil, errInvalidNewSizeOrPriceInformation
		}
	}
	var resp []OrderData
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, amendMultipleOrdersEPL, http.MethodPost, "trade/amend-batch-orders", &args, &resp, request.AuthenticatedRequest)
}

// ClosePositions close all positions of an instrument via a market order
func (e *Exchange) ClosePositions(ctx context.Context, arg *ClosePositionsRequestParams) (*ClosePositionResponse, error) {
	if *arg == (ClosePositionsRequestParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	switch arg.MarginMode {
	case "", TradeModeCross, TradeModeIsolated:
	default:
		return nil, margin.ErrMarginTypeUnsupported
	}
	var resp *ClosePositionResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, closePositionEPL, http.MethodPost, "trade/close-position", arg, &resp, request.AuthenticatedRequest)
}

// GetOrderDetail retrieves order details given instrument id and order identification
func (e *Exchange) GetOrderDetail(ctx context.Context, arg *OrderDetailRequestParam) (*OrderDetail, error) {
	if *arg == (OrderDetailRequestParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	switch {
	case arg.OrderID == "" && arg.ClientOrderID == "":
		return nil, order.ErrOrderIDNotSet
	case arg.ClientOrderID == "":
		params.Set("ordId", arg.OrderID)
	default:
		params.Set("clOrdId", arg.ClientOrderID)
	}
	params.Set("instId", arg.InstrumentID)
	var resp *OrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOrderDetEPL, http.MethodGet, common.EncodeURLValues("trade/order", params), nil, &resp, request.AuthenticatedRequest)
}

// GetOrderList retrieves all incomplete orders under the current account
func (e *Exchange) GetOrderList(ctx context.Context, arg *OrderListRequestParams) ([]OrderDetail, error) {
	if *arg == (OrderListRequestParams{}) {
		return nil, common.ErrEmptyParams
	}
	params := url.Values{}
	if arg.InstrumentType != "" {
		params.Set("instType", arg.InstrumentType)
	}
	if arg.InstrumentID != "" {
		params.Set("instId", arg.InstrumentID)
	}
	if arg.Underlying != "" {
		params.Set("uly", arg.Underlying)
	}
	if arg.OrderType != "" {
		params.Set("orderType", strings.ToLower(arg.OrderType))
	}
	if arg.State != "" {
		params.Set("state", arg.State)
	}
	if arg.Before != "" {
		params.Set("before", arg.Before)
	}
	if arg.After != "" {
		params.Set("after", arg.After)
	}
	if arg.Limit > 0 {
		params.Set("limit", strconv.FormatInt(arg.Limit, 10))
	}
	var resp []OrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOrderListEPL, http.MethodGet, common.EncodeURLValues("trade/orders-pending", params), nil, &resp, request.AuthenticatedRequest)
}

// Get7DayOrderHistory retrieves the completed order data for the last 7 days, and the incomplete orders that have been cancelled are only reserved for 2 hours
func (e *Exchange) Get7DayOrderHistory(ctx context.Context, arg *OrderHistoryRequestParams) ([]OrderDetail, error) {
	return e.getOrderHistory(ctx, arg, "trade/orders-history", getOrderHistory7DaysEPL)
}

// Get3MonthOrderHistory retrieves the completed order data for the last 7 days, and the incomplete orders that have been cancelled are only reserved for 2 hours
func (e *Exchange) Get3MonthOrderHistory(ctx context.Context, arg *OrderHistoryRequestParams) ([]OrderDetail, error) {
	return e.getOrderHistory(ctx, arg, "trade/orders-history-archive", getOrderHistory3MonthsEPL)
}

// getOrderHistory retrieves the order history of the past limited times
func (e *Exchange) getOrderHistory(ctx context.Context, arg *OrderHistoryRequestParams, route string, rateLimit request.EndpointLimit) ([]OrderDetail, error) {
	if *arg == (OrderHistoryRequestParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", strings.ToUpper(arg.InstrumentType))
	if arg.InstrumentID != "" {
		params.Set("instId", arg.InstrumentID)
	}
	if arg.Underlying != "" {
		params.Set("uly", arg.Underlying)
	}
	if arg.OrderType != "" {
		params.Set("orderType", strings.ToLower(arg.OrderType))
	}
	if arg.State != "" {
		params.Set("state", arg.State)
	}
	if arg.Before != "" {
		params.Set("before", arg.Before)
	}
	if arg.After != "" {
		params.Set("after", arg.After)
	}
	if !arg.Start.IsZero() {
		params.Set("begin", strconv.FormatInt(arg.Start.UnixMilli(), 10))
	}
	if !arg.End.IsZero() {
		params.Set("end", strconv.FormatInt(arg.End.UnixMilli(), 10))
	}
	if arg.Limit > 0 {
		params.Set("limit", strconv.FormatInt(arg.Limit, 10))
	}
	if arg.Category != "" {
		params.Set("category", strings.ToLower(arg.Category))
	}
	var resp []OrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.AuthenticatedRequest)
}

// GetTransactionDetailsLast3Days retrieves recently-filled transaction details in the last 3 day
func (e *Exchange) GetTransactionDetailsLast3Days(ctx context.Context, arg *TransactionDetailRequestParams) ([]TransactionDetail, error) {
	return e.getTransactionDetails(ctx, arg, "trade/fills", getTransactionDetail3DaysEPL)
}

// GetTransactionDetailsLast3Months retrieve recently-filled transaction details in the last 3 months
func (e *Exchange) GetTransactionDetailsLast3Months(ctx context.Context, arg *TransactionDetailRequestParams) ([]TransactionDetail, error) {
	return e.getTransactionDetails(ctx, arg, "trade/fills-history", getTransactionDetail3MonthsEPL)
}

// GetTransactionDetails retrieves recently-filled transaction details
func (e *Exchange) getTransactionDetails(ctx context.Context, arg *TransactionDetailRequestParams, route string, rateLimit request.EndpointLimit) ([]TransactionDetail, error) {
	if *arg == (TransactionDetailRequestParams{}) {
		return nil, common.ErrEmptyParams
	}
	arg.InstrumentType = strings.ToUpper(arg.InstrumentType)
	if arg.InstrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", arg.InstrumentType)
	if arg.InstrumentID != "" {
		params.Set("instId", arg.InstrumentID)
	}
	if arg.Underlying != "" {
		params.Set("uly", arg.Underlying)
	}
	if !arg.Begin.IsZero() {
		params.Set("begin", strconv.FormatInt(arg.Begin.UnixMilli(), 10))
	}
	if !arg.End.IsZero() {
		params.Set("end", strconv.FormatInt(arg.End.UnixMilli(), 10))
	}
	if arg.Limit > 0 {
		params.Set("limit", strconv.FormatInt(arg.Limit, 10))
	}
	if arg.InstrumentID != "" {
		params.Set("instId", arg.InstrumentID)
	}
	if arg.After != "" {
		params.Set("after", arg.After)
	}
	if arg.Before != "" {
		params.Set("before", arg.Before)
	}
	var resp []TransactionDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.AuthenticatedRequest)
}

// PlaceAlgoOrder order includes trigger, oco, chase, conditional, iceberg, twap and trailing orders.
// chase order only applicable to futures and swap orders
func (e *Exchange) PlaceAlgoOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) {
	if *arg == (AlgoOrderParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	arg.TradeMode = strings.ToLower(arg.TradeMode)
	switch arg.TradeMode {
	case TradeModeCross, TradeModeIsolated, TradeModeCash:
	default:
		return nil, errInvalidTradeModeValue
	}
	arg.Side = strings.ToLower(arg.Side)
	if arg.Side != order.Buy.Lower() &&
		arg.Side != order.Sell.Lower() {
		return nil, order.ErrSideIsInvalid
	}
	if arg.OrderType == "" {
		return nil, order.ErrTypeIsInvalid
	}
	if arg.Size <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	var resp *AlgoOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, placeAlgoOrderEPL, http.MethodPost, "trade/order-algo", arg, &resp, request.AuthenticatedRequest)
}

// PlaceStopOrder places a stop order.
// The order type should be "conditional" because stop orders are used for conditional take-profit or stop-loss scenarios.
func (e *Exchange) PlaceStopOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) {
	if *arg == (AlgoOrderParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.TakeProfitTriggerPrice <= 0 {
		return nil, fmt.Errorf("%w, take profit trigger price is required", limits.ErrPriceBelowMin)
	}
	if arg.TakeProfitTriggerPriceType == "" {
		return nil, order.ErrUnknownPriceType
	}
	return e.PlaceAlgoOrder(ctx, arg)
}

// PlaceTrailingStopOrder to place trailing stop order
func (e *Exchange) PlaceTrailingStopOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) {
	if *arg == (AlgoOrderParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.OrderType != "move_order_stop" {
		return nil, fmt.Errorf("%w: order type value 'move_order_stop' is only supported for move_order_stop orders", order.ErrTypeIsInvalid)
	}
	if arg.CallbackRatio == 0 && arg.CallbackSpreadVariance == 0 {
		return nil, fmt.Errorf(" %w \"callbackRatio\" or \"callbackSpread\" required", errPriceTrackingNotSet)
	}
	return e.PlaceAlgoOrder(ctx, arg)
}

// PlaceIcebergOrder to place iceberg algo order
func (e *Exchange) PlaceIcebergOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) {
	if *arg == (AlgoOrderParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.OrderType != "iceberg" {
		return nil, fmt.Errorf("%w: order type value 'iceberg' is only supported for iceberg orders", order.ErrTypeIsInvalid)
	}
	if arg.SizeLimit <= 0 {
		return nil, errMissingSizeLimit
	}
	if arg.LimitPrice <= 0 {
		return nil, errInvalidPriceLimit
	}
	return e.PlaceAlgoOrder(ctx, arg)
}

// PlaceTWAPOrder to place TWAP algo orders
func (e *Exchange) PlaceTWAPOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) {
	if *arg == (AlgoOrderParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.OrderType != "twap" {
		return nil, fmt.Errorf("%w: order type value 'twap' is only supported for twap orders", order.ErrTypeIsInvalid)
	}
	if arg.SizeLimit <= 0 {
		return nil, errMissingSizeLimit
	}
	if arg.LimitPrice <= 0 {
		return nil, errInvalidPriceLimit
	}
	if IntervalFromString(arg.TimeInterval, true) == "" {
		return nil, errMissingIntervalValue
	}
	return e.PlaceAlgoOrder(ctx, arg)
}

// PlaceTakeProfitStopLossOrder places conditional and oco orders
// When placing net TP/SL order (ordType=conditional) and both take-profit and stop-loss parameters are sent,
// only stop-loss logic will be performed and take-profit logic will be ignored
func (e *Exchange) PlaceTakeProfitStopLossOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) {
	if *arg == (AlgoOrderParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.OrderType != orderConditional {
		return nil, fmt.Errorf("%w for TPSL: %q", order.ErrTypeIsInvalid, arg.OrderType)
	}
	if arg.StopLossTriggerPrice <= 0 {
		return nil, limits.ErrPriceBelowMin
	}
	switch arg.StopLossTriggerPriceType {
	case "", "last", "index", "mark":
	default:
		return nil, fmt.Errorf("%w, only 'last', 'index', and 'mark' are supported", order.ErrUnknownPriceType)
	}
	return e.PlaceAlgoOrder(ctx, arg)
}

// PlaceChaseAlgoOrder places an order that adjusts the price of an open limit order to match the current market price
func (e *Exchange) PlaceChaseAlgoOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) {
	if *arg == (AlgoOrderParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.OrderType != orderChase {
		return nil, fmt.Errorf("%w: order type value 'chase' is only supported for chase orders", order.ErrTypeIsInvalid)
	}
	if (arg.MaxChaseType == "" || arg.MaxChaseValue == 0) &&
		(arg.MaxChaseType != "" || arg.MaxChaseValue != 0) {
		return nil, fmt.Errorf("%w, either non or both MaxChaseType and MaxChaseValue has to be provided", errPriceTrackingNotSet)
	}
	return e.PlaceAlgoOrder(ctx, arg)
}

// PlaceTriggerAlgoOrder fetches algo trigger orders for SWAP market types
func (e *Exchange) PlaceTriggerAlgoOrder(ctx context.Context, arg *AlgoOrderParams) (*AlgoOrder, error) {
	if *arg == (AlgoOrderParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.OrderType != orderTrigger {
		return nil, fmt.Errorf("%w for Trigger: %q", order.ErrTypeIsInvalid, arg.OrderType)
	}
	if arg.TriggerPrice <= 0 {
		return nil, fmt.Errorf("%w, trigger price must be greater than 0", limits.ErrPriceBelowMin)
	}
	switch arg.TriggerPriceType {
	case "", "last", "index", "mark":
	default:
		return nil, fmt.Errorf("%w, only last, index and mark trigger price types are allowed", order.ErrUnknownPriceType)
	}
	return e.PlaceAlgoOrder(ctx, arg)
}

// CancelAdvanceAlgoOrder Cancel unfilled algo orders
// A maximum of 10 orders can be canceled at a time.
// Request parameters should be passed in the form of an array
func (e *Exchange) CancelAdvanceAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams) (*AlgoOrder, error) {
	if len(args) == 0 {
		return nil, common.ErrEmptyParams
	}
	return e.cancelAlgoOrder(ctx, args, "trade/cancel-advance-algos", cancelAdvanceAlgoOrderEPL)
}

// CancelAlgoOrder to cancel unfilled algo orders (not including Iceberg order, TWAP order, Trailing Stop order).
// A maximum of 10 orders can be canceled at a time.
// Request parameters should be passed in the form of an array
func (e *Exchange) CancelAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams) (*AlgoOrder, error) {
	if len(args) == 0 {
		return nil, common.ErrEmptyParams
	}
	return e.cancelAlgoOrder(ctx, args, "trade/cancel-algos", cancelAlgoOrderEPL)
}

// cancelAlgoOrder to cancel unfilled algo orders
func (e *Exchange) cancelAlgoOrder(ctx context.Context, args []AlgoOrderCancelParams, route string, rateLimit request.EndpointLimit) (*AlgoOrder, error) {
	for x := range args {
		if args[x] == (AlgoOrderCancelParams{}) {
			return nil, common.ErrEmptyParams
		}
		if args[x].AlgoOrderID == "" {
			return nil, fmt.Errorf("%w, AlgoOrderID is required", order.ErrOrderIDNotSet)
		} else if args[x].InstrumentID == "" {
			return nil, errMissingInstrumentID
		}
	}
	var resp *AlgoOrder
	err := e.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodPost, route, &args, &resp, request.AuthenticatedRequest)
	if err != nil {
		if resp != nil && resp.StatusMessage != "" {
			return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
		}
		return nil, err
	}
	return resp, nil
}

// AmendAlgoOrder amend unfilled algo orders (Support stop order only, not including Move_order_stop order, Trigger order, Iceberg order, TWAP order, Trailing Stop order).
// Only applicable to Futures and Perpetual swap
func (e *Exchange) AmendAlgoOrder(ctx context.Context, arg *AmendAlgoOrderParam) (*AmendAlgoResponse, error) {
	if arg == nil {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if arg.AlgoID == "" && arg.ClientSuppliedAlgoOrderID == "" {
		return nil, fmt.Errorf("%w either AlgoID or ClientSuppliedAlgoOrderID is required", order.ErrOrderIDNotSet)
	}
	var resp *AmendAlgoResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, amendAlgoOrderEPL, http.MethodPost, "trade/amend-algos", arg, &resp, request.AuthenticatedRequest)
}

// GetAlgoOrderDetail retrieves algo order details
func (e *Exchange) GetAlgoOrderDetail(ctx context.Context, algoID, clientSuppliedAlgoID string) (*AlgoOrderDetail, error) {
	if algoID == "" && clientSuppliedAlgoID == "" {
		return nil, fmt.Errorf("%w either AlgoID or ClientSuppliedAlgoID is required", order.ErrOrderIDNotSet)
	}
	params := url.Values{}
	params.Set("algoId", algoID)
	params.Set("algoClOrdId", clientSuppliedAlgoID)
	var resp *AlgoOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAlgoOrderDetailEPL, http.MethodGet, common.EncodeURLValues("trade/order-algo", params), nil, &resp, request.AuthenticatedRequest)
}

// GetAlgoOrderList retrieves a list of untriggered Algo orders under the current account
func (e *Exchange) GetAlgoOrderList(ctx context.Context, orderType, algoOrderID, clientOrderID, instrumentType, instrumentID string, after, before time.Time, limit int64) ([]AlgoOrderResponse, error) {
	orderType = strings.ToLower(orderType)
	if orderType == "" {
		return nil, order.ErrTypeIsInvalid
	}
	params := url.Values{}
	params.Set("ordType", orderType)
	if algoOrderID != "" {
		params.Set("algoId", algoOrderID)
	}
	if clientOrderID != "" {
		params.Set("clOrdId", clientOrderID)
	}
	instrumentType = strings.ToUpper(instrumentType)
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if !before.IsZero() && before.Before(time.Now()) {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() && after.Before(time.Now()) {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []AlgoOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAlgoOrderListEPL, http.MethodGet, common.EncodeURLValues("trade/orders-algo-pending", params), nil, &resp, request.AuthenticatedRequest)
}

// GetAlgoOrderHistory load a list of all algo orders under the current account in the last 3 months
func (e *Exchange) GetAlgoOrderHistory(ctx context.Context, orderType, state, algoOrderID, instrumentType, instrumentID string, after, before time.Time, limit int64) ([]AlgoOrderResponse, error) {
	if orderType == "" {
		return nil, order.ErrTypeIsInvalid
	}
	if algoOrderID == "" && !slices.Contains([]string{"effective", "order_failed", "canceled"}, state) {
		return nil, errMissingEitherAlgoIDOrState
	}
	params := url.Values{}
	params.Set("ordType", strings.ToLower(orderType))
	if algoOrderID != "" {
		params.Set("algoId", algoOrderID)
	} else {
		params.Set("state", state)
	}
	instrumentType = strings.ToUpper(instrumentType)
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if !before.IsZero() && before.Before(time.Now()) {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() && after.Before(time.Now()) {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []AlgoOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAlgoOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("trade/orders-algo-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetEasyConvertCurrencyList retrieve list of small convertibles and mainstream currencies. Only applicable to the crypto balance less than $10
func (e *Exchange) GetEasyConvertCurrencyList(ctx context.Context, source string) (*EasyConvertDetail, error) {
	params := url.Values{}
	if source != "" {
		params.Set("source", source)
	}
	var resp *EasyConvertDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getEasyConvertCurrencyListEPL, http.MethodGet,
		common.EncodeURLValues("trade/easy-convert-currency-list", params), nil, &resp, request.AuthenticatedRequest)
}

// PlaceEasyConvert converts small currencies to mainstream currencies. Only applicable to the crypto balance less than $10
func (e *Exchange) PlaceEasyConvert(ctx context.Context, arg PlaceEasyConvertParam) ([]EasyConvertItem, error) {
	if len(arg.FromCurrency) == 0 {
		return nil, fmt.Errorf("%w, missing FromCurrency", currency.ErrCurrencyCodeEmpty)
	}
	if arg.ToCurrency == "" {
		return nil, fmt.Errorf("%w, missing ToCurrency", currency.ErrCurrencyCodeEmpty)
	}
	var resp []EasyConvertItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, placeEasyConvertEPL, http.MethodPost, "trade/easy-convert", &arg, &resp, request.AuthenticatedRequest)
}

// GetEasyConvertHistory retrieves the history and status of easy convert trades
func (e *Exchange) GetEasyConvertHistory(ctx context.Context, after, before time.Time, limit int64) ([]EasyConvertItem, error) {
	params := url.Values{}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.Unix(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.Unix(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []EasyConvertItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getEasyConvertHistoryEPL, http.MethodGet,
		common.EncodeURLValues("trade/easy-convert-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetOneClickRepayCurrencyList retrieves list of debt currency data and repay currencies. Debt currencies include both cross and isolated debts.
// debt level "cross", and "isolated" are allowed
func (e *Exchange) GetOneClickRepayCurrencyList(ctx context.Context, debtType string) ([]CurrencyOneClickRepay, error) {
	params := url.Values{}
	if debtType != "" {
		params.Set("debtType", debtType)
	}
	var resp []CurrencyOneClickRepay
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, oneClickRepayCurrencyListEPL, http.MethodGet,
		common.EncodeURLValues("trade/one-click-repay-currency-list", params), nil, &resp, request.AuthenticatedRequest)
}

// TradeOneClickRepay trade one-click repay to repay cross debts. Isolated debts are not applicable. The maximum repayment amount is based on the remaining available balance of funding and trading accounts
func (e *Exchange) TradeOneClickRepay(ctx context.Context, arg TradeOneClickRepayParam) ([]CurrencyOneClickRepay, error) {
	if len(arg.DebtCurrency) == 0 {
		return nil, fmt.Errorf("%w, missing 'debtCcy'", currency.ErrCurrencyCodeEmpty)
	}
	if arg.RepayCurrency == "" {
		return nil, fmt.Errorf("%w, missing 'repayCcy'", currency.ErrCurrencyCodeEmpty)
	}
	var resp []CurrencyOneClickRepay
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, tradeOneClickRepayEPL, http.MethodPost, "trade/one-click-repay", &arg, &resp, request.AuthenticatedRequest)
}

// GetOneClickRepayHistory get the history and status of one-click repay trades
func (e *Exchange) GetOneClickRepayHistory(ctx context.Context, after, before time.Time, limit int64) ([]CurrencyOneClickRepay, error) {
	params := url.Values{}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.Unix(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.Unix(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []CurrencyOneClickRepay
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOneClickRepayHistoryEPL, http.MethodGet, common.EncodeURLValues("trade/one-click-repay-history", params), nil, &resp, request.AuthenticatedRequest)
}

// CancelAllMMPOrders cancel all the MMP pending orders of an instrument family.
// Only applicable to Option in Portfolio Margin mode, and MMP privilege is required
func (e *Exchange) CancelAllMMPOrders(ctx context.Context, instrumentType, instrumentFamily string, lockInterval int64) (*CancelMMPResponse, error) {
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	if instrumentFamily == "" {
		return nil, errInstrumentFamilyRequired
	}
	if lockInterval < 0 || lockInterval > 10000 {
		return nil, fmt.Errorf("%w, LockInterval value range should be between 0 and 10000", errMissingIntervalValue)
	}
	arg := &struct {
		InstrumentType   string `json:"instType,omitempty"`
		InstrumentFamily string `json:"instFamily,omitempty"`
		LockInterval     int64  `json:"lockInterval,omitempty"`
	}{
		InstrumentType:   instrumentType,
		InstrumentFamily: instrumentFamily,
		LockInterval:     lockInterval,
	}
	var resp *CancelMMPResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, tradeOneClickRepayEPL, http.MethodPost, "trade/mass-cancel", arg, &resp, request.AuthenticatedRequest)
}

// CancelAllDelayed cancel all pending orders after the countdown timeout.
// Applicable to all trading symbols through order book (except Spread trading)
func (e *Exchange) CancelAllDelayed(ctx context.Context, timeout int64, orderTag string) (*CancelResponse, error) {
	if (timeout != 0) && (timeout < 10 || timeout > 120) {
		return nil, fmt.Errorf("%w, Range of value can be 0, [10, 120]", errCountdownTimeoutRequired)
	}
	arg := &struct {
		TimeOut  int64  `json:"timeOut,string"`
		OrderTag string `json:"tag,omitempty"`
	}{
		TimeOut:  timeout,
		OrderTag: orderTag,
	}
	var resp *CancelResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllAfterCountdownEPL, http.MethodPost, "trade/cancel-all-after", arg, &resp, request.AuthenticatedRequest)
}

// GetTradeAccountRateLimit get account rate limit related information.
// Only new order requests and amendment order requests will be counted towards this limit. For batch order requests consisting of multiple orders, each order will be counted individually
func (e *Exchange) GetTradeAccountRateLimit(ctx context.Context) (*AccountRateLimit, error) {
	var resp *AccountRateLimit
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTradeAccountRateLimitEPL, http.MethodGet, "trade/account-rate-limit", nil, &resp, request.AuthenticatedRequest)
}

// PreCheckOrder returns the account information before and after placing a potential order
// Only applicable to Multi-currency margin mode, and Portfolio margin mode
func (e *Exchange) PreCheckOrder(ctx context.Context, arg *OrderPreCheckParams) (*OrderPreCheckResponse, error) {
	if arg == nil {
		return nil, common.ErrNilPointer
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if arg.TradeMode == "" {
		return nil, errInvalidTradeModeValue
	}
	if arg.Side == "" {
		return nil, order.ErrSideIsInvalid
	}
	if arg.OrderType == "" {
		return nil, order.ErrTypeIsInvalid
	}
	if arg.Size <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	var resp *OrderPreCheckResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, orderPreCheckEPL, http.MethodPost, "trade/order-precheck", arg, &resp, request.AuthenticatedRequest)
}

/*************************************** Block trading ********************************/

// GetCounterparties retrieves the list of counterparties that the user has permissions to trade with
func (e *Exchange) GetCounterparties(ctx context.Context) ([]CounterpartiesResponse, error) {
	var resp []CounterpartiesResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getCounterpartiesEPL, http.MethodGet, "rfq/counterparties", nil, &resp, request.AuthenticatedRequest)
}

// CreateRFQ Creates a new RFQ
func (e *Exchange) CreateRFQ(ctx context.Context, arg *CreateRFQInput) (*RFQResponse, error) {
	if len(arg.CounterParties) == 0 {
		return nil, errInvalidCounterParties
	}
	if len(arg.Legs) == 0 {
		return nil, errMissingLegs
	}
	var resp *RFQResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, createRFQEPL, http.MethodPost, "rfq/create-rfq", &arg, &resp, request.AuthenticatedRequest)
}

// CancelRFQ cancels a request for quotation
func (e *Exchange) CancelRFQ(ctx context.Context, rfqID, clientRFQID string) (*CancelRFQResponse, error) {
	if rfqID == "" && clientRFQID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	var resp *CancelRFQResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelRFQEPL, http.MethodPost, "rfq/cancel-rfq", &CancelRFQRequestParam{
		RFQID:       rfqID,
		ClientRFQID: clientRFQID,
	}, &resp, request.AuthenticatedRequest)
}

// CancelMultipleRFQs cancel multiple active RFQs in a single batch. Maximum 100 RFQ orders can be canceled at a time
func (e *Exchange) CancelMultipleRFQs(ctx context.Context, arg *CancelRFQRequestsParam) ([]CancelRFQResponse, error) {
	if arg == nil {
		return nil, common.ErrNilPointer
	}
	if len(arg.RFQIDs) == 0 && len(arg.ClientRFQIDs) == 0 {
		return nil, order.ErrOrderIDNotSet
	} else if len(arg.RFQIDs)+len(arg.ClientRFQIDs) > 100 {
		return nil, errMaxRFQOrdersToCancel
	}
	var resp []CancelRFQResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleRFQEPL, http.MethodPost, "rfq/cancel-batch-rfqs", &arg, &resp, request.AuthenticatedRequest)
}

// CancelAllRFQs cancels all active RFQs
func (e *Exchange) CancelAllRFQs(ctx context.Context) (types.Time, error) {
	resp := &tsResp{}
	return resp.Timestamp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllRFQsEPL, http.MethodPost, "rfq/cancel-all-rfqs", nil, resp, request.AuthenticatedRequest)
}

// ExecuteQuote executes a Quote. It is only used by the creator of the RFQ
func (e *Exchange) ExecuteQuote(ctx context.Context, rfqID, quoteID string) (*ExecuteQuoteResponse, error) {
	if rfqID == "" || quoteID == "" {
		return nil, errMissingRFQIDOrQuoteID
	}
	var resp *ExecuteQuoteResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, executeQuoteEPL, http.MethodPost, "rfq/execute-quote", &ExecuteQuoteParams{
		RFQID:   rfqID,
		QuoteID: quoteID,
	}, &resp, request.AuthenticatedRequest)
}

// GetQuoteProducts retrieve the products which makers want to quote and receive RFQs for, and the corresponding price and size limit
func (e *Exchange) GetQuoteProducts(ctx context.Context) ([]QuoteProduct, error) {
	var resp []QuoteProduct
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getQuoteProductsEPL, http.MethodGet, "rfq/maker-instrument-settings", nil, &resp, request.AuthenticatedRequest)
}

// SetQuoteProducts customize the products which makers want to quote and receive RFQs for, and the corresponding price and size limit
func (e *Exchange) SetQuoteProducts(ctx context.Context, args []SetQuoteProductParam) (*SetQuoteProductsResult, error) {
	if len(args) == 0 {
		return nil, common.ErrEmptyParams
	}
	for x := range args {
		args[x].InstrumentType = strings.ToUpper(args[x].InstrumentType)
		if !slices.Contains([]string{instTypeSwap, instTypeSpot, instTypeFutures, instTypeOption}, args[x].InstrumentType) {
			return nil, fmt.Errorf("%w received %v", errInvalidInstrumentType, args[x].InstrumentType)
		}
		if len(args[x].Data) == 0 {
			return nil, errMissingMakerInstrumentSettings
		}
		for y := range args[x].Data {
			if slices.Contains([]string{instTypeSwap, instTypeFutures, instTypeOption}, args[x].InstrumentType) && args[x].Data[y].Underlying == "" {
				return nil, fmt.Errorf("%w, for instrument type %s and %s", errInvalidUnderlying, args[x].InstrumentType, args[x].Data[x].Underlying)
			}
			if (args[x].InstrumentType == instTypeSpot) && args[x].Data[x].InstrumentID == "" {
				return nil, fmt.Errorf("%w, for instrument type %s and %s", errMissingInstrumentID, args[x].InstrumentType, args[x].Data[x].InstrumentID)
			}
		}
	}
	var resp *SetQuoteProductsResult
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setQuoteProductsEPL, http.MethodPost, "rfq/maker-instrument-settings", &args, &resp, request.AuthenticatedRequest)
}

// ResetRFQMMPStatus reset the MMP status to be inactive
func (e *Exchange) ResetRFQMMPStatus(ctx context.Context) (types.Time, error) {
	resp := &tsResp{}
	return resp.Timestamp, e.SendHTTPRequest(ctx, exchange.RestSpot, resetRFQMMPEPL, http.MethodPost, "rfq/mmp-reset", nil, resp, request.AuthenticatedRequest)
}

// CreateQuote allows the user to Quote an RFQ that they are a counterparty to. The user MUST quote
// the entire RFQ and not part of the legs or part of the quantity. Partial quoting or partial fills are not allowed
func (e *Exchange) CreateQuote(ctx context.Context, arg *CreateQuoteParams) (*QuoteResponse, error) {
	if arg == nil {
		return nil, common.ErrNilPointer
	}
	arg.QuoteSide = strings.ToLower(arg.QuoteSide)
	switch {
	case arg.RFQID == "":
		return nil, errMissingRFQID
	case arg.QuoteSide != order.Buy.Lower() && arg.QuoteSide != order.Sell.Lower():
		return nil, order.ErrSideIsInvalid
	case len(arg.Legs) == 0:
		return nil, errMissingLegs
	}
	for x := range arg.Legs {
		switch {
		case arg.Legs[x].InstrumentID == "":
			return nil, errMissingInstrumentID
		case arg.Legs[x].SizeOfQuoteLeg <= 0:
			return nil, errMissingSizeOfQuote
		case arg.Legs[x].Price <= 0:
			return nil, errMissingLegsQuotePrice
		case arg.Legs[x].Side == order.UnknownSide:
			return nil, order.ErrSideIsInvalid
		}
	}
	var resp *QuoteResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, createQuoteEPL, http.MethodPost, "rfq/create-quote", &arg, &resp, request.AuthenticatedRequest)
}

// CancelQuote cancels an existing active quote you have created in response to an RFQ
func (e *Exchange) CancelQuote(ctx context.Context, quoteID, clientQuoteID string) (*CancelQuoteResponse, error) {
	if clientQuoteID == "" && quoteID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	var resp *CancelQuoteResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelQuoteEPL, http.MethodPost, "rfq/cancel-quote", &CancelQuoteRequestParams{
		QuoteID:       quoteID,
		ClientQuoteID: clientQuoteID,
	}, &resp, request.AuthenticatedRequest)
}

// CancelMultipleQuote cancels multiple active quotes in a single batch, with a maximum of 100 quote orders cancellable at once
func (e *Exchange) CancelMultipleQuote(ctx context.Context, arg CancelQuotesRequestParams) ([]CancelQuoteResponse, error) {
	if len(arg.QuoteIDs) == 0 && len(arg.ClientQuoteIDs) == 0 {
		return nil, order.ErrOrderIDNotSet
	}
	var resp []CancelQuoteResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelMultipleQuotesEPL, http.MethodPost, "rfq/cancel-batch-quotes", &arg, &resp, request.AuthenticatedRequest)
}

// CancelAllRFQQuotes cancels all active quote orders
func (e *Exchange) CancelAllRFQQuotes(ctx context.Context) (types.Time, error) {
	resp := &tsResp{}
	return resp.Timestamp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllQuotesEPL, http.MethodPost, "rfq/cancel-all-quotes", nil, resp, request.AuthenticatedRequest)
}

// GetRFQs retrieves details of RFQs where the user is a counterparty, either as the creator or the recipient
func (e *Exchange) GetRFQs(ctx context.Context, arg *RFQRequestParams) ([]RFQResponse, error) {
	if *arg == (RFQRequestParams{}) {
		return nil, common.ErrEmptyParams
	}
	params := url.Values{}
	if arg.RFQID != "" {
		params.Set("rfqId", arg.RFQID)
	}
	if arg.ClientRFQID != "" {
		params.Set("clRFQId", arg.ClientRFQID)
	}
	if arg.State != "" {
		params.Set("state", strings.ToLower(arg.State))
	}
	if arg.BeginningID != "" {
		params.Set("beginId", arg.BeginningID)
	}
	if arg.EndID != "" {
		params.Set("endId", arg.EndID)
	}
	if arg.Limit > 0 {
		params.Set("limit", strconv.FormatInt(arg.Limit, 10))
	}
	var resp []RFQResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getRFQsEPL, http.MethodGet, common.EncodeURLValues("rfq/rfqs", params), nil, &resp, request.AuthenticatedRequest)
}

// GetQuotes retrieves all Quotes where the user is a counterparty, either as the creator or the receiver
func (e *Exchange) GetQuotes(ctx context.Context, arg *QuoteRequestParams) ([]QuoteResponse, error) {
	if *arg == (QuoteRequestParams{}) {
		return nil, common.ErrEmptyParams
	}
	params := url.Values{}
	if arg.RFQID != "" {
		params.Set("rfqId", arg.RFQID)
	}
	if arg.ClientRFQID != "" {
		params.Set("clRFQId", arg.ClientRFQID)
	}
	if arg.QuoteID != "" {
		params.Set("quoteId", arg.QuoteID)
	}
	if arg.ClientQuoteID != "" {
		params.Set("clQuoteId", arg.ClientQuoteID)
	}
	if arg.State != "" {
		params.Set("state", strings.ToLower(arg.State))
	}
	if arg.BeginID != "" {
		params.Set("beginId", arg.BeginID)
	}
	if arg.EndID != "" {
		params.Set("endId", arg.EndID)
	}
	if arg.Limit > 0 {
		params.Set("limit", strconv.FormatInt(arg.Limit, 10))
	}
	var resp []QuoteResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getQuotesEPL, http.MethodGet, common.EncodeURLValues("rfq/quotes", params), nil, &resp, request.AuthenticatedRequest)
}

// GetRFQTrades retrieves executed trades where the user is a counterparty, either as the creator or the receiver
func (e *Exchange) GetRFQTrades(ctx context.Context, arg *RFQTradesRequestParams) ([]RFQTradeResponse, error) {
	if *arg == (RFQTradesRequestParams{}) {
		return nil, common.ErrEmptyParams
	}
	params := url.Values{}
	if arg.RFQID != "" {
		params.Set("rfqId", arg.RFQID)
	}
	if arg.ClientRFQID != "" {
		params.Set("clRFQId", arg.ClientRFQID)
	}
	if arg.QuoteID != "" {
		params.Set("quoteId", arg.QuoteID)
	}
	if arg.ClientQuoteID != "" {
		params.Set("clQuoteId", arg.ClientQuoteID)
	}
	if arg.State != "" {
		params.Set("state", strings.ToLower(arg.State))
	}
	if arg.BlockTradeID != "" {
		params.Set("blockTdId", arg.BlockTradeID)
	}
	if arg.BeginID != "" {
		params.Set("beginId", arg.BeginID)
	}
	if arg.EndID != "" {
		params.Set("endId", arg.EndID)
	}
	if arg.Limit > 0 {
		params.Set("limit", strconv.FormatInt(arg.Limit, 10))
	}
	var resp []RFQTradeResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTradesEPL, http.MethodGet, common.EncodeURLValues("rfq/trades", params), nil, &resp, request.AuthenticatedRequest)
}

// GetPublicRFQTrades retrieves recent executed block trades
func (e *Exchange) GetPublicRFQTrades(ctx context.Context, beginID, endID string, limit int64) ([]PublicTradesResponse, error) {
	params := url.Values{}
	if beginID != "" {
		params.Set("beginId", beginID)
	}
	if endID != "" {
		params.Set("endId", endID)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []PublicTradesResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getPublicTradesEPL, http.MethodGet, common.EncodeURLValues("rfq/public-trades", params), nil, &resp, request.UnauthenticatedRequest)
}

/*************************************** Funding Tradings ********************************/

// GetFundingCurrencies retrieve a list of all currencies
func (e *Exchange) GetFundingCurrencies(ctx context.Context, ccy currency.Code) ([]CurrencyResponse, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []CurrencyResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getCurrenciesEPL, http.MethodGet,
		common.EncodeURLValues("asset/currencies", params), nil, &resp, request.AuthenticatedRequest)
}

// GetBalance retrieves the funding account balances of all the assets and the amount that is available or on hold
func (e *Exchange) GetBalance(ctx context.Context, ccy currency.Code) ([]AssetBalance, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []AssetBalance
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBalanceEPL, http.MethodGet, common.EncodeURLValues("asset/balances", params), nil, &resp, request.AuthenticatedRequest)
}

// GetNonTradableAssets retrieves non tradable assets
func (e *Exchange) GetNonTradableAssets(ctx context.Context, ccy currency.Code) ([]NonTradableAsset, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []NonTradableAsset
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getNonTradableAssetsEPL, http.MethodGet, common.EncodeURLValues("asset/non-tradable-assets", params), nil, &resp, request.AuthenticatedRequest)
}

// GetAccountAssetValuation view account asset valuation
func (e *Exchange) GetAccountAssetValuation(ctx context.Context, ccy currency.Code) ([]AccountAssetValuation, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.Upper().String())
	}
	var resp []AccountAssetValuation
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAccountAssetValuationEPL, http.MethodGet, common.EncodeURLValues("asset/asset-valuation", params), nil, &resp, request.AuthenticatedRequest)
}

// FundingTransfer transfer of funds between your funding account and trading account,
// and from the master account to sub-accounts
func (e *Exchange) FundingTransfer(ctx context.Context, arg *FundingTransferRequestInput) ([]FundingTransferResponse, error) {
	if *arg == (FundingTransferRequestInput{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.Amount <= 0 {
		return nil, fmt.Errorf("%w, funding amount must be greater than 0", limits.ErrAmountBelowMin)
	}
	if arg.Currency.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if arg.RemittingAccountType != "6" && arg.RemittingAccountType != "18" {
		return nil, fmt.Errorf("%w, remitting account type 6: Funding account 18: Trading account", errAddressRequired)
	}
	if arg.BeneficiaryAccountType != "6" && arg.BeneficiaryAccountType != "18" {
		return nil, fmt.Errorf("%w, beneficiary account type 6: Funding account 18: Trading account", errAddressRequired)
	}
	var resp []FundingTransferResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, fundsTransferEPL, http.MethodPost, "asset/transfer", arg, &resp, request.AuthenticatedRequest)
}

// GetFundsTransferState get funding rate response
func (e *Exchange) GetFundsTransferState(ctx context.Context, transferID, clientID string, transferType int64) ([]TransferFundRateResponse, error) {
	if transferID == "" && clientID == "" {
		return nil, fmt.Errorf("%w, 'transfer id' or 'client id' is required", order.ErrOrderIDNotSet)
	}
	params := url.Values{}
	if transferID != "" {
		params.Set("transId", transferID)
	}
	if clientID != "" {
		params.Set("clientId", clientID)
	}
	if transferType > 0 && transferType <= 4 {
		params.Set("type", strconv.FormatInt(transferType, 10))
	}
	var resp []TransferFundRateResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFundsTransferStateEPL, http.MethodGet, common.EncodeURLValues("asset/transfer-state", params), nil, &resp, request.AuthenticatedRequest)
}

// GetAssetBillsDetails query the billing record, you can get the latest 1 month historical data
// Bills type possible values are listed here: https://www.okx.com/docs-v5/en/#funding-account-rest-api-asset-bills-details
func (e *Exchange) GetAssetBillsDetails(ctx context.Context, ccy currency.Code, clientID string, after, before time.Time, billType, limit int64) ([]AssetBillDetail, error) {
	params := url.Values{}
	if billType > 0 {
		params.Set("type", strconv.FormatInt(billType, 10))
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if clientID != "" {
		params.Set("clientId", clientID)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []AssetBillDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, assetBillsDetailsEPL, http.MethodGet, common.EncodeURLValues("asset/bills", params), nil, &resp, request.AuthenticatedRequest)
}

// GetLightningDeposits users can create up to 10 thousand different invoices within 24 hours.
// this method fetches list of lightning deposits filtered by a currency and amount
func (e *Exchange) GetLightningDeposits(ctx context.Context, ccy currency.Code, amount float64, to int64) ([]LightningDepositItem, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	params := url.Values{}
	params.Set("ccy", ccy.String())
	if amount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	params.Set("amt", strconv.FormatFloat(amount, 'f', 0, 64))
	if to == 6 || to == 18 {
		params.Set("to", strconv.FormatInt(to, 10))
	}
	var resp []LightningDepositItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, lightningDepositsEPL, http.MethodGet, common.EncodeURLValues("asset/deposit-lightning", params), nil, &resp, request.AuthenticatedRequest)
}

// GetCurrencyDepositAddress retrieve the deposit addresses of currencies, including previously-used addresses
func (e *Exchange) GetCurrencyDepositAddress(ctx context.Context, ccy currency.Code) ([]CurrencyDepositResponseItem, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	params := url.Values{}
	params.Set("ccy", ccy.String())
	var resp []CurrencyDepositResponseItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getDepositAddressEPL, http.MethodGet, common.EncodeURLValues("asset/deposit-address", params), nil, &resp, request.AuthenticatedRequest)
}

// GetCurrencyDepositHistory retrieves deposit records and withdrawal status information depending on the currency, timestamp, and chronological order.
// Possible deposit 'type' are Deposit Type '3': internal transfer '4': deposit from chain
func (e *Exchange) GetCurrencyDepositHistory(ctx context.Context, ccy currency.Code, depositID, transactionID, fromWithdrawalID, depositType string, after, before time.Time, state, limit int64) ([]DepositHistoryResponseItem, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if depositID != "" {
		params.Set("depId", depositID)
	}
	if fromWithdrawalID != "" {
		params.Set("fromWdId", fromWithdrawalID)
	}
	if transactionID != "" {
		params.Set("txId", transactionID)
	}
	if depositType != "" {
		params.Set("type", depositType)
	}
	params.Set("state", strconv.FormatInt(state, 10))
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []DepositHistoryResponseItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getDepositHistoryEPL, http.MethodGet, common.EncodeURLValues("asset/deposit-history", params), nil, &resp, request.AuthenticatedRequest)
}

// Withdrawal to perform a withdrawal action. Sub-account does not support withdrawal
func (e *Exchange) Withdrawal(ctx context.Context, arg *WithdrawalInput) (*WithdrawalResponse, error) {
	if *arg == (WithdrawalInput{}) {
		return nil, common.ErrEmptyParams
	}
	switch {
	case arg.Currency.IsEmpty():
		return nil, currency.ErrCurrencyCodeEmpty
	case arg.Amount <= 0:
		return nil, fmt.Errorf("%w, withdrawal amount required", limits.ErrAmountBelowMin)
	case arg.WithdrawalDestination == "":
		return nil, fmt.Errorf("%w, withdrawal destination required", errAddressRequired)
	case arg.ToAddress == "":
		return nil, fmt.Errorf("%w, missing verified digital currency address \"toAddr\" information", errAddressRequired)
	}
	var resp *WithdrawalResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, withdrawalEPL, http.MethodPost, "asset/withdrawal", &arg, &resp, request.AuthenticatedRequest)
}

/*
 This API function service is only open to some users. If you need this function service, please send an email to `liz.jensen@okg.com` to apply
*/

// LightningWithdrawal to withdraw a currency from an invoice
func (e *Exchange) LightningWithdrawal(ctx context.Context, arg *LightningWithdrawalRequestInput) (*LightningWithdrawalResponse, error) {
	if *arg == (LightningWithdrawalRequestInput{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.Currency.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	} else if arg.Invoice == "" {
		return nil, errInvoiceTextMissing
	}
	var resp *LightningWithdrawalResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, lightningWithdrawalsEPL, http.MethodPost, "asset/withdrawal-lightning", &arg, &resp, request.AuthenticatedRequest)
}

// CancelWithdrawal cancels a normal withdrawal request but cannot be used to cancel Lightning withdrawals
func (e *Exchange) CancelWithdrawal(ctx context.Context, withdrawalID string) (string, error) {
	if withdrawalID == "" {
		return "", errMissingValidWithdrawalID
	}
	arg := &withdrawData{
		WithdrawalID: withdrawalID,
	}
	var resp withdrawData
	return resp.WithdrawalID, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelWithdrawalEPL, http.MethodPost, "asset/cancel-withdrawal", arg, &resp, request.AuthenticatedRequest)
}

// GetWithdrawalHistory retrieves the withdrawal records according to the currency, withdrawal status, and time range in reverse chronological order.
// The 100 most recent records are returned by default
func (e *Exchange) GetWithdrawalHistory(ctx context.Context, ccy currency.Code, withdrawalID, clientID, transactionID, state string, after, before time.Time, limit int64) ([]WithdrawalHistoryResponse, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if withdrawalID != "" {
		params.Set("wdId", withdrawalID)
	}
	if clientID != "" {
		params.Set("clientId", clientID)
	}
	if transactionID != "" {
		params.Set("txId", transactionID)
	}
	if state != "" {
		params.Set("state", state)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []WithdrawalHistoryResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getWithdrawalHistoryEPL, http.MethodGet, common.EncodeURLValues("asset/withdrawal-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetDepositWithdrawalStatus retrieves the detailed status and estimated completion time for deposits and withdrawals
func (e *Exchange) GetDepositWithdrawalStatus(ctx context.Context, ccy currency.Code, withdrawalID, transactionID, addressTo, chain string) ([]DepositWithdrawStatus, error) {
	if withdrawalID == "" && transactionID == "" {
		return nil, fmt.Errorf("%w, either withdrawal id or transaction id is required", order.ErrOrderIDNotSet)
	}
	if withdrawalID == "" {
		return nil, errMissingValidWithdrawalID
	}
	params := url.Values{}
	params.Set("wdId", withdrawalID)
	if transactionID != "" {
		params.Set("txId", transactionID)
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if addressTo != "" {
		params.Set("to", addressTo)
	}
	if chain != "" {
		params.Set("chain", chain)
	}
	var resp []DepositWithdrawStatus
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getDepositWithdrawalStatusEPL, http.MethodGet, common.EncodeURLValues("asset/deposit-withdraw-status", params), nil, &resp, request.AuthenticatedRequest)
}

// SmallAssetsConvert Convert small assets in funding account to OKB. Only one convert is allowed within 24 hours
func (e *Exchange) SmallAssetsConvert(ctx context.Context, currencies []string) (*SmallAssetConvertResponse, error) {
	var resp *SmallAssetConvertResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, smallAssetsConvertEPL, http.MethodPost, "asset/convert-dust-assets", map[string][]string{"ccy": currencies}, &resp, request.AuthenticatedRequest)
}

// GetPublicExchangeList retrieves exchanges
func (e *Exchange) GetPublicExchangeList(ctx context.Context) ([]ExchangeInfo, error) {
	var resp []ExchangeInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getPublicExchangeListEPL, http.MethodGet, "asset/exchange-list", nil, &resp, request.UnauthenticatedRequest)
}

// GetSavingBalance returns saving balance, and only assets in the funding account can be used for saving
func (e *Exchange) GetSavingBalance(ctx context.Context, ccy currency.Code) ([]SavingBalanceResponse, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []SavingBalanceResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSavingBalanceEPL, http.MethodGet, common.EncodeURLValues("finance/savings/balance", params), nil, &resp, request.AuthenticatedRequest)
}

// SavingsPurchaseOrRedemption creates a purchase or redemption instance
func (e *Exchange) SavingsPurchaseOrRedemption(ctx context.Context, arg *SavingsPurchaseRedemptionInput) (*SavingsPurchaseRedemptionResponse, error) {
	if *arg == (SavingsPurchaseRedemptionInput{}) {
		return nil, common.ErrEmptyParams
	}
	arg.ActionType = strings.ToLower(arg.ActionType)
	switch {
	case arg.Currency.IsEmpty():
		return nil, currency.ErrCurrencyCodeEmpty
	case arg.Amount <= 0:
		return nil, limits.ErrAmountBelowMin
	case arg.ActionType != "purchase" && arg.ActionType != "redempt":
		return nil, fmt.Errorf("%w, side has to be either 'redempt' or 'purchase'", order.ErrSideIsInvalid)
	case arg.ActionType == "purchase" && (arg.Rate < 0.01 || arg.Rate > 3.65):
		return nil, fmt.Errorf("%w, the rate value range is between 0.01 and 3.65", errRateRequired)
	}
	var resp *SavingsPurchaseRedemptionResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, savingsPurchaseRedemptionEPL, http.MethodPost, "finance/savings/purchase-redempt", &arg, &resp, request.AuthenticatedRequest)
}

// GetLendingHistory lending history
func (e *Exchange) GetLendingHistory(ctx context.Context, ccy currency.Code, before, after time.Time, limit int64) ([]LendingHistory, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []LendingHistory
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLendingHistoryEPL, http.MethodGet, common.EncodeURLValues("finance/savings/lending-history", params), nil, &resp, request.AuthenticatedRequest)
}

// SetLendingRate sets an assets lending rate
func (e *Exchange) SetLendingRate(ctx context.Context, arg *LendingRate) (*LendingRate, error) {
	if *arg == (LendingRate{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.Currency.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	} else if arg.Rate < 0.01 || arg.Rate > 3.65 {
		return nil, fmt.Errorf("%w, rate value range is between 1 percent (0.01) and 365 percent (3.65)", errRateRequired)
	}
	var resp *LendingRate
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setLendingRateEPL, http.MethodPost, "finance/savings/set-lending-rate", &arg, &resp, request.AuthenticatedRequest)
}

// GetPublicBorrowInfo returns the public borrow info
func (e *Exchange) GetPublicBorrowInfo(ctx context.Context, ccy currency.Code) ([]PublicBorrowInfo, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []PublicBorrowInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getPublicBorrowInfoEPL, http.MethodGet, common.EncodeURLValues("finance/savings/lending-rate-summary", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetPublicBorrowHistory return list of publix borrow history
func (e *Exchange) GetPublicBorrowHistory(ctx context.Context, ccy currency.Code, before, after time.Time, limit int64) ([]PublicBorrowHistory, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []PublicBorrowHistory
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getPublicBorrowHistoryEPL, http.MethodGet, common.EncodeURLValues("finance/savings/lending-rate-history", params), nil, &resp, request.UnauthenticatedRequest)
}

/***********************************Convert Endpoints | Authenticated s*****************************************/

// ApplyForMonthlyStatement requests a monthly statement for any month within the past year
func (e *Exchange) ApplyForMonthlyStatement(ctx context.Context, month string) (types.Time, error) {
	if month == "" {
		return types.Time{}, errMonthNameRequired
	}
	resp := &tsResp{}
	return resp.Timestamp, e.SendHTTPRequest(ctx, exchange.RestSpot, applyForMonthlyStatementEPL,
		http.MethodPost, "asset/monthly-statement", &map[string]string{"month": month}, resp, request.AuthenticatedRequest)
}

// GetMonthlyStatement retrieves monthly statements for the past year.
// Month is in the form of Jan, Feb, March etc.
func (e *Exchange) GetMonthlyStatement(ctx context.Context, month string) ([]MonthlyStatement, error) {
	if month == "" {
		return nil, errMonthNameRequired
	}
	params := url.Values{}
	params.Set("month", month)
	var resp []MonthlyStatement
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getMonthlyStatementEPL, http.MethodGet,
		common.EncodeURLValues("asset/monthly-statement", params), nil, &resp, request.AuthenticatedRequest)
}

// GetConvertCurrencies retrieves the currency conversion information
func (e *Exchange) GetConvertCurrencies(ctx context.Context) ([]ConvertCurrency, error) {
	var resp []ConvertCurrency
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getConvertCurrenciesEPL, http.MethodGet, "asset/convert/currencies", nil, &resp, request.AuthenticatedRequest)
}

// GetConvertCurrencyPair retrieves the currency conversion response detail given the 'currency from' and 'currency to'
func (e *Exchange) GetConvertCurrencyPair(ctx context.Context, fromCcy, toCcy currency.Code) (*ConvertCurrencyPair, error) {
	if fromCcy.IsEmpty() {
		return nil, fmt.Errorf("%w, source currency is required", currency.ErrCurrencyCodeEmpty)
	}
	if toCcy.IsEmpty() {
		return nil, fmt.Errorf("%w, target currency is required", currency.ErrCurrencyCodeEmpty)
	}
	params := url.Values{}
	params.Set("fromCcy", fromCcy.String())
	params.Set("toCcy", toCcy.String())
	var resp *ConvertCurrencyPair
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getConvertCurrencyPairEPL, http.MethodGet, common.EncodeURLValues("asset/convert/currency-pair", params), nil, &resp, request.AuthenticatedRequest)
}

// EstimateQuote retrieves quote estimation detail result given the base and quote currency
func (e *Exchange) EstimateQuote(ctx context.Context, arg *EstimateQuoteRequestInput) (*EstimateQuoteResponse, error) {
	if *arg == (EstimateQuoteRequestInput{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.BaseCurrency.IsEmpty() {
		return nil, fmt.Errorf("%w, base currency is required", currency.ErrCurrencyCodeEmpty)
	}
	if arg.QuoteCurrency.IsEmpty() {
		return nil, fmt.Errorf("%w, quote currency is required", currency.ErrCurrencyCodeEmpty)
	}
	arg.Side = strings.ToLower(arg.Side)
	switch arg.Side {
	case order.Buy.Lower(), order.Sell.Lower():
	default:
		return nil, order.ErrSideIsInvalid
	}
	if arg.RFQAmount <= 0 {
		return nil, fmt.Errorf("%w, RFQ amount required", limits.ErrAmountBelowMin)
	}
	if arg.RFQSzCurrency == "" {
		return nil, fmt.Errorf("%w, missing RFQ currency", currency.ErrCurrencyCodeEmpty)
	}
	var resp *EstimateQuoteResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, estimateQuoteEPL, http.MethodPost, "asset/convert/estimate-quote", arg, &resp, request.AuthenticatedRequest)
}

// ConvertTrade converts a base currency to quote currency
func (e *Exchange) ConvertTrade(ctx context.Context, arg *ConvertTradeInput) (*ConvertTradeResponse, error) {
	if *arg == (ConvertTradeInput{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.BaseCurrency == "" {
		return nil, fmt.Errorf("%w, base currency required", currency.ErrCurrencyCodeEmpty)
	}
	if arg.QuoteCurrency == "" {
		return nil, fmt.Errorf("%w, quote currency required", currency.ErrCurrencyCodeEmpty)
	}
	arg.Side = strings.ToLower(arg.Side)
	switch arg.Side {
	case order.Buy.Lower(), order.Sell.Lower():
	default:
		return nil, order.ErrSideIsInvalid
	}
	if arg.Size <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	if arg.SizeCurrency.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if arg.QuoteID == "" {
		return nil, fmt.Errorf("%w, quote id required", order.ErrOrderIDNotSet)
	}
	var resp *ConvertTradeResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, convertTradeEPL, http.MethodPost, "asset/convert/trade", &arg, &resp, request.AuthenticatedRequest)
}

// GetConvertHistory gets the recent history
func (e *Exchange) GetConvertHistory(ctx context.Context, before, after time.Time, limit int64, tag string) ([]ConvertHistory, error) {
	params := url.Values{}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	if tag != "" {
		params.Set("tag", tag)
	}
	var resp []ConvertHistory
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getConvertHistoryEPL, http.MethodGet, common.EncodeURLValues("asset/convert/history", params), nil, &resp, request.AuthenticatedRequest)
}

/********************************** Account endpoints ***************************************************/

// GetAccountInstruments retrieve available instruments info of current account
func (e *Exchange) GetAccountInstruments(ctx context.Context, instrumentType asset.Item, underlying, instrumentFamily, instrumentID string) ([]AccountInstrument, error) {
	if instrumentType == asset.Empty {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	switch instrumentType {
	case asset.Margin, asset.PerpetualSwap, asset.Futures:
		if underlying == "" {
			return nil, fmt.Errorf("%w, underlying is required", errInvalidUnderlying)
		}
		params.Set("uly", underlying)
	case asset.Options:
		if underlying == "" && instrumentFamily == "" {
			return nil, errInstrumentFamilyOrUnderlyingRequired
		}
		if underlying != "" {
			params.Set("uly", underlying)
		}
		if instrumentFamily != "" {
			params.Set("instFamily", instrumentFamily)
		}
	}
	instTypeString, err := assetTypeString(instrumentType)
	if err != nil {
		return nil, err
	}
	params.Set("instType", instTypeString)
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	var resp []AccountInstrument
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAccountInstrumentsEPL, http.MethodGet, common.EncodeURLValues("account/instruments", params), nil, &resp, request.AuthenticatedRequest)
}

// AccountBalance retrieves a list of assets (with non-zero balance), remaining balance, and available amount in the trading account.
// Interest-free quota and discount rates are public data and not displayed on the account interface
func (e *Exchange) AccountBalance(ctx context.Context, ccy currency.Code) ([]Account, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []Account
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAccountBalanceEPL, http.MethodGet, common.EncodeURLValues("account/balance", params), nil, &resp, request.AuthenticatedRequest)
}

// GetPositions retrieves information on your positions. When the account is in net mode, net positions will be displayed, and when the account is in long/short mode, long or short positions will be displayed
func (e *Exchange) GetPositions(ctx context.Context, instrumentType, instrumentID, positionID string) ([]AccountPosition, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if positionID != "" {
		params.Set("posId", positionID)
	}
	var resp []AccountPosition
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getPositionsEPL, http.MethodGet, common.EncodeURLValues("account/positions", params), nil, &resp, request.AuthenticatedRequest)
}

// GetPositionsHistory retrieves the updated position data for the last 3 months
func (e *Exchange) GetPositionsHistory(ctx context.Context, instrumentType, instrumentID, marginMode, positionID string, closePositionType, limit int64, after, before time.Time) ([]AccountPositionHistory, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if marginMode != "" {
		params.Set("mgnMode", marginMode) // margin mode 'cross' and 'isolated' are allowed
	}
	// The type of closing position
	// 1：Close position partially;2：Close all;3：Liquidation;4：Partial liquidation; 5：ADL;
	// It is the latest type if there are several types for the same position.
	if closePositionType > 0 {
		params.Set("type", strconv.FormatInt(closePositionType, 10))
	}
	if positionID != "" {
		params.Set("posId", positionID)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []AccountPositionHistory
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getPositionsHistoryEPL, http.MethodGet, common.EncodeURLValues("account/positions-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetAccountAndPositionRisk  get account and position risks
func (e *Exchange) GetAccountAndPositionRisk(ctx context.Context, instrumentType string) ([]AccountAndPositionRisk, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", strings.ToUpper(instrumentType))
	}
	var resp []AccountAndPositionRisk
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAccountAndPositionRiskEPL, http.MethodGet, common.EncodeURLValues("account/account-position-risk", params), nil, &resp, request.AuthenticatedRequest)
}

// GetBillsDetailLast7Days The bill refers to all transaction records that result in changing the balance of an account. Pagination is supported, and the response is sorted with the most recent first. This endpoint can retrieve data from the last 7 days
func (e *Exchange) GetBillsDetailLast7Days(ctx context.Context, arg *BillsDetailQueryParameter) ([]BillsDetailResponse, error) {
	return e.GetBillsDetail(ctx, arg, "account/bills", getBillsDetailsEPL)
}

// GetBillsDetail3Months retrieves the account’s bills.
// The bill refers to all transaction records that result in changing the balance of an account.
// Pagination is supported, and the response is sorted with most recent first.
// This endpoint can retrieve data from the last 3 months
func (e *Exchange) GetBillsDetail3Months(ctx context.Context, arg *BillsDetailQueryParameter) ([]BillsDetailResponse, error) {
	return e.GetBillsDetail(ctx, arg, "account/bills-archive", getBillsDetailArchiveEPL)
}

// ApplyBillDetails apply for bill data since 1 February, 2021 except for the current quarter.
// Quarter, valid value is Q1, Q2, Q3, Q4
func (e *Exchange) ApplyBillDetails(ctx context.Context, year, quarter string) ([]BillsDetailResp, error) {
	if year == "" {
		return nil, errYearRequired
	}
	if quarter == "" {
		return nil, errQuarterValueRequired
	}
	var resp []BillsDetailResp
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, billHistoryArchiveEPL, http.MethodPost, "account/bills-history-archive", map[string]string{"year": year, "quarter": quarter}, &resp, request.AuthenticatedRequest)
}

// GetBillsHistoryArchive retrieves bill data archive
func (e *Exchange) GetBillsHistoryArchive(ctx context.Context, year, quarter string) ([]BillsArchiveInfo, error) {
	if year == "" {
		return nil, errYearRequired
	}
	if quarter == "" {
		return nil, errQuarterValueRequired
	}
	params := url.Values{}
	params.Set("year", year)
	params.Set("quarter", quarter)
	var resp []BillsArchiveInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBillHistoryArchiveEPL, http.MethodGet, common.EncodeURLValues("account/bills-history-archive", params), nil, &resp, request.AuthenticatedRequest)
}

// GetBillsDetail retrieves the bills of the account
func (e *Exchange) GetBillsDetail(ctx context.Context, arg *BillsDetailQueryParameter, route string, epl request.EndpointLimit) ([]BillsDetailResponse, error) {
	if *arg == (BillsDetailQueryParameter{}) {
		return nil, common.ErrEmptyParams
	}
	params := url.Values{}
	if arg.InstrumentType != "" {
		params.Set("instType", strings.ToUpper(arg.InstrumentType))
	}
	if arg.InstrumentID != "" {
		params.Set("instId", arg.InstrumentID)
	}
	if !arg.Currency.IsEmpty() {
		params.Set("ccy", arg.Currency.Upper().String())
	}
	if arg.MarginMode != "" {
		params.Set("mgnMode", arg.MarginMode)
	}
	if arg.ContractType != "" {
		params.Set("ctType", arg.ContractType)
	}
	if arg.BillType >= 1 && arg.BillType <= 13 {
		params.Set("type", strconv.Itoa(int(arg.BillType)))
	}
	if arg.BillSubType != 0 {
		params.Set("subType", strconv.FormatInt(arg.BillSubType, 10))
	}
	if arg.After != "" {
		params.Set("after", arg.After)
	}
	if arg.Before != "" {
		params.Set("before", arg.Before)
	}
	if !arg.BeginTime.IsZero() {
		params.Set("begin", strconv.FormatInt(arg.BeginTime.UnixMilli(), 10))
	}
	if !arg.EndTime.IsZero() {
		params.Set("end", strconv.FormatInt(arg.EndTime.UnixMilli(), 10))
	}
	if arg.Limit > 0 {
		params.Set("limit", strconv.FormatInt(arg.Limit, 10))
	}
	var resp []BillsDetailResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, epl, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.AuthenticatedRequest)
}

// GetAccountConfiguration retrieves current account configuration
func (e *Exchange) GetAccountConfiguration(ctx context.Context) (*AccountConfigurationResponse, error) {
	var resp *AccountConfigurationResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAccountConfigurationEPL, http.MethodGet, "account/config", nil, &resp, request.AuthenticatedRequest)
}

// SetPositionMode FUTURES and SWAP support both long/short mode and net mode. In net mode, users can only have positions in one direction; In long/short mode, users can hold positions in long and short directions.
// Position mode 'long_short_mode': long/short, only applicable to  FUTURES/SWAP'net_mode': net
func (e *Exchange) SetPositionMode(ctx context.Context, positionMode string) (*PositionMode, error) {
	if positionMode != "long_short_mode" && positionMode != "net_mode" {
		return nil, errInvalidPositionMode
	}
	var resp *PositionMode
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setPositionModeEPL, http.MethodPost, "account/set-position-mode", &PositionMode{
		PositionMode: positionMode,
	}, &resp, request.AuthenticatedRequest)
}

// SetLeverageRate sets a leverage setting for instrument id
func (e *Exchange) SetLeverageRate(ctx context.Context, arg *SetLeverageInput) (*SetLeverageResponse, error) {
	if *arg == (SetLeverageInput{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentID == "" && arg.Currency.IsEmpty() {
		return nil, errEitherInstIDOrCcyIsRequired
	}
	switch arg.AssetType {
	case asset.Futures, asset.PerpetualSwap:
		if arg.PositionSide == "" && arg.MarginMode == "isolated" {
			return nil, fmt.Errorf("%w: %q", order.ErrSideIsInvalid, arg.PositionSide)
		}
	}
	arg.PositionSide = strings.ToLower(arg.PositionSide)
	var resp *SetLeverageResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setLeverageEPL, http.MethodPost, "account/set-leverage", &arg, &resp, request.AuthenticatedRequest)
}

// GetMaximumBuySellAmountOROpenAmount retrieves the maximum buy or sell amount for a sell id
func (e *Exchange) GetMaximumBuySellAmountOROpenAmount(ctx context.Context, ccy currency.Code, instrumentID, tradeMode, leverage string, price float64, unSpotOffset bool) ([]MaximumBuyAndSell, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	switch tradeMode {
	case TradeModeCross, TradeModeIsolated, TradeModeCash:
	default:
		return nil, fmt.Errorf("%w, trade mode: %s", errInvalidTradeModeValue, tradeMode)
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	params.Set("tdMode", tradeMode)
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if price > 0 {
		params.Set("px", strconv.FormatFloat(price, 'f', -1, 64))
	}
	if leverage != "" {
		params.Set("leverage", leverage)
	}
	if unSpotOffset {
		params.Set("unSpotOffset", "true")
	}
	var resp []MaximumBuyAndSell
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getMaximumBuyOrSellAmountEPL, http.MethodGet, common.EncodeURLValues("account/max-size", params), nil, &resp, request.AuthenticatedRequest)
}

// GetMaximumAvailableTradableAmount retrieves the maximum tradable amount for specific instrument id, and/or currency
func (e *Exchange) GetMaximumAvailableTradableAmount(ctx context.Context, ccy currency.Code, instrumentID, tradeMode, quickMarginType string, reduceOnly, upSpotOffset bool, price float64) ([]MaximumTradableAmount, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if tradeMode == "" {
		// Supported tradeMode values are 'cross', 'isolated', 'cash', and 'spot_isolated'
		return nil, fmt.Errorf("%w, possible values are 'cross', 'isolated', 'cash', and 'spot_isolated'", errInvalidTradeModeValue)
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	params.Set("tdMode", tradeMode)
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if reduceOnly {
		params.Set("reduceOnly", "true")
	}
	if price != 0 {
		params.Set("px", strconv.FormatFloat(price, 'f', 0, 64))
	}
	if quickMarginType != "" {
		params.Set("quickMgnType", quickMarginType)
	}
	if upSpotOffset {
		params.Set("upSpotOffset", "true")
	}
	var resp []MaximumTradableAmount
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getMaximumAvailableTradableAmountEPL, http.MethodGet, common.EncodeURLValues("account/max-avail-size", params), nil, &resp, request.AuthenticatedRequest)
}

// IncreaseDecreaseMargin Increase or decrease the margin of the isolated position. Margin reduction may result in the change of the actual leverage
func (e *Exchange) IncreaseDecreaseMargin(ctx context.Context, arg *IncreaseDecreaseMarginInput) (*IncreaseDecreaseMargin, error) {
	if *arg == (IncreaseDecreaseMarginInput{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if !slices.Contains([]string{positionSideLong, positionSideShort, positionSideNet}, arg.PositionSide) {
		return nil, fmt.Errorf("%w, position side is required", order.ErrSideIsInvalid)
	}
	if arg.MarginBalanceType != marginBalanceAdd && arg.MarginBalanceType != marginBalanceReduce {
		return nil, fmt.Errorf("%w, missing valid 'type', 'add': add margin 'reduce': reduce margin are allowed", order.ErrTypeIsInvalid)
	}
	if arg.Amount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	var resp *IncreaseDecreaseMargin
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, increaseOrDecreaseMarginEPL, http.MethodPost, "account/position/margin-balance", &arg, &resp, request.AuthenticatedRequest)
}

// GetLeverageRate retrieves leverage data for different instrument id or margin mode
func (e *Exchange) GetLeverageRate(ctx context.Context, instrumentID, marginMode string, ccy currency.Code) ([]LeverageResponse, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	switch marginMode {
	case TradeModeCross, TradeModeIsolated, TradeModeCash:
	default:
		return nil, margin.ErrMarginTypeUnsupported
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	params.Set("mgnMode", marginMode)
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []LeverageResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeverageEPL, http.MethodGet, common.EncodeURLValues("account/leverage-info", params), nil, &resp, request.AuthenticatedRequest)
}

// GetLeverageEstimatedInfo retrieves leverage estimated information.
// Instrument type: possible values are MARGIN, SWAP, FUTURES
func (e *Exchange) GetLeverageEstimatedInfo(ctx context.Context, instrumentType, marginMode, leverage, positionSide, instrumentID string, ccy currency.Code) ([]LeverageEstimatedInfo, error) {
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	switch marginMode {
	case TradeModeCross, TradeModeIsolated:
	default:
		return nil, margin.ErrMarginTypeUnsupported
	}
	if leverage == "" {
		return nil, errInvalidLeverage
	}
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	params := url.Values{}
	params.Set("instType", instrumentType)
	params.Set("lever", leverage)
	params.Set("mgnMode", marginMode)
	params.Set("ccy", ccy.String())
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if positionSide != "" {
		params.Set("posSide", positionSide)
	}
	var resp []LeverageEstimatedInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeverateEstimatedInfoEPL, http.MethodGet, common.EncodeURLValues("account/adjust-leverage-info", params), nil, &resp, request.AuthenticatedRequest)
}

// GetMaximumLoanOfInstrument returns list of maximum loan of instruments
func (e *Exchange) GetMaximumLoanOfInstrument(ctx context.Context, instrumentID, marginMode string, mgnCurrency currency.Code) ([]MaximumLoanInstrument, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if marginMode == "" {
		return nil, margin.ErrInvalidMarginType
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	params.Set("mgnMode", marginMode)
	if !mgnCurrency.IsEmpty() {
		params.Set("mgnCcy", mgnCurrency.String())
	}
	var resp []MaximumLoanInstrument
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTheMaximumLoanOfInstrumentEPL, http.MethodGet, common.EncodeURLValues("account/max-loan", params), nil, &resp, request.AuthenticatedRequest)
}

// GetFee returns Cryptocurrency trade fee, and offline trade fee
func (e *Exchange) GetFee(ctx context.Context, feeBuilder *exchange.FeeBuilder) (float64, error) {
	// Here the Asset Type for the instrument Type is needed for getting the CryptocurrencyTrading Fee.
	var fee float64
	switch feeBuilder.FeeType {
	case exchange.CryptocurrencyTradeFee:
		uly, err := e.GetUnderlying(feeBuilder.Pair, asset.Spot)
		if err != nil {
			return 0, err
		}
		responses, err := e.GetTradeFee(ctx, instTypeSpot, uly, "", "", "")
		if err != nil {
			return 0, err
		} else if len(responses) == 0 {
			return 0, common.ErrNoResponse
		}
		if feeBuilder.IsMaker {
			if feeBuilder.Pair.Quote.Equal(currency.USDC) {
				fee = responses[0].FeeRateMakerUSDC.Float64()
			} else if fee = responses[0].FeeRateMaker.Float64(); fee == 0 {
				fee = responses[0].FeeRateMakerUSDT.Float64()
			}
		} else {
			if feeBuilder.Pair.Quote.Equal(currency.USDC) {
				fee = responses[0].FeeRateTakerUSDC.Float64()
			} else if fee = responses[0].FeeRateTaker.Float64(); fee == 0 {
				fee = responses[0].FeeRateTakerUSDT.Float64()
			}
		}
		if fee != 0 {
			fee = -fee // A negative fee rate indicates a commission charge; a positive rate indicates a rebate.
		}
		return fee * feeBuilder.Amount * feeBuilder.PurchasePrice, nil
	case exchange.OfflineTradeFee:
		return 0.0015 * feeBuilder.PurchasePrice * feeBuilder.Amount, nil
	default:
		return fee, errFeeTypeUnsupported
	}
}

// GetTradeFee queries the trade fee rates for various instrument types and their respective IDs
func (e *Exchange) GetTradeFee(ctx context.Context, instrumentType, instrumentID, underlying, instrumentFamily, ruleType string) ([]TradeFeeRate, error) {
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", instrumentType)
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if underlying != "" {
		params.Set("uly", underlying)
	}
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	if ruleType != "" {
		params.Set("ruleType", ruleType)
	}
	var resp []TradeFeeRate
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFeeRatesEPL, http.MethodGet, common.EncodeURLValues("account/trade-fee", params), nil, &resp, request.AuthenticatedRequest)
}

// GetInterestAccruedData retrieves data on accrued interest
func (e *Exchange) GetInterestAccruedData(ctx context.Context, loanType, limit int64, ccy currency.Code, instrumentID, marginMode string, after, before time.Time) ([]InterestAccruedData, error) {
	params := url.Values{}
	if loanType == 1 || loanType == 2 {
		params.Set("type", strconv.FormatInt(loanType, 10))
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if marginMode != "" {
		params.Set("mgnMode", strings.ToLower(marginMode))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []InterestAccruedData
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getInterestAccruedDataEPL, http.MethodGet, common.EncodeURLValues("account/interest-accrued", params), nil, &resp, request.AuthenticatedRequest)
}

// GetInterestRate get the user's current leveraged currency borrowing interest rate
func (e *Exchange) GetInterestRate(ctx context.Context, ccy currency.Code) ([]InterestRateResponse, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []InterestRateResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getInterestRateEPL, http.MethodGet, common.EncodeURLValues("account/interest-rate", params), nil, &resp, request.AuthenticatedRequest)
}

// SetGreeks set the display type of Greeks. PA: Greeks in coins BS: Black-Scholes Greeks in dollars
func (e *Exchange) SetGreeks(ctx context.Context, greeksType string) (*GreeksType, error) {
	greeksType = strings.ToUpper(greeksType)
	if greeksType != "PA" && greeksType != "BS" {
		return nil, errMissingValidGreeksType
	}
	input := &GreeksType{
		GreeksType: greeksType,
	}
	var resp *GreeksType
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setGreeksEPL, http.MethodPost, "account/set-greeks", input, &resp, request.AuthenticatedRequest)
}

// IsolatedMarginTradingSettings configures the currency margin and sets the isolated margin trading mode for futures or perpetual contracts
func (e *Exchange) IsolatedMarginTradingSettings(ctx context.Context, arg *IsolatedMode) (*IsolatedMode, error) {
	if *arg == (IsolatedMode{}) {
		return nil, common.ErrEmptyParams
	}
	arg.IsoMode = strings.ToLower(arg.IsoMode)
	if arg.IsoMode != "automatic" &&
		arg.IsoMode != "autonomy" {
		return nil, errMissingIsolatedMarginTradingSetting
	}
	if arg.InstrumentType != instTypeMargin &&
		arg.InstrumentType != instTypeContract {
		return nil, fmt.Errorf("%w, received '%v' only margin and contract instrument types are allowed", errInvalidInstrumentType, arg.InstrumentType)
	}
	var resp *IsolatedMode
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, isolatedMarginTradingSettingsEPL, http.MethodPost, "account/set-isolated-mode", &arg, &resp, request.AuthenticatedRequest)
}

// ManualBorrowAndRepayInQuickMarginMode initiates a new manual borrow and repayment process in Quick Margin mode
func (e *Exchange) ManualBorrowAndRepayInQuickMarginMode(ctx context.Context, arg *BorrowAndRepay) (*BorrowAndRepay, error) {
	if *arg == (BorrowAndRepay{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.Amount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	if arg.LoanCcy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if arg.Side == "" {
		return nil, fmt.Errorf("%w, possible values are 'borrow' and 'repay'", order.ErrSideIsInvalid)
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	var resp *BorrowAndRepay
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, manualBorrowAndRepayEPL, http.MethodPost, "account/quick-margin-borrow-repay", arg, &resp, request.AuthenticatedRequest)
}

// GetBorrowAndRepayHistoryInQuickMarginMode retrieves borrow and repay history in quick margin mode
func (e *Exchange) GetBorrowAndRepayHistoryInQuickMarginMode(ctx context.Context, instrumentID currency.Pair, ccy currency.Code, side, afterPaginationID, beforePaginationID string, beginTime, endTime time.Time, limit int64) ([]BorrowRepayHistoryItem, error) {
	params := url.Values{}
	if !instrumentID.IsEmpty() {
		params.Set("instId", instrumentID.String())
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if side != "" {
		params.Set("side", side)
	}
	if afterPaginationID != "" {
		params.Set("after", afterPaginationID)
	}
	if beforePaginationID != "" {
		params.Set("before", beforePaginationID)
	}
	if !beginTime.IsZero() {
		params.Set("begin", strconv.FormatInt(beginTime.UnixMilli(), 10))
	}
	if !endTime.IsZero() {
		params.Set("end", strconv.FormatInt(endTime.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []BorrowRepayHistoryItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowAndRepayHistoryEPL, http.MethodGet, common.EncodeURLValues("account/quick-margin-borrow-repay-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetMaximumWithdrawals retrieves the maximum transferable amount from a trading account to a funding account for quick margin borrowing and repayment
func (e *Exchange) GetMaximumWithdrawals(ctx context.Context, ccy currency.Code) ([]MaximumWithdrawal, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []MaximumWithdrawal
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getMaximumWithdrawalsEPL, http.MethodGet, common.EncodeURLValues("account/max-withdrawal", params), nil, &resp, request.AuthenticatedRequest)
}

// GetAccountRiskState gets the account risk status.
// only applicable to Portfolio margin account
func (e *Exchange) GetAccountRiskState(ctx context.Context) ([]AccountRiskState, error) {
	var resp []AccountRiskState
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAccountRiskStateEPL, http.MethodGet, "account/risk-state", nil, &resp, request.AuthenticatedRequest)
}

// VIPLoansBorrowAndRepay creates VIP borrow or repay for a currency
func (e *Exchange) VIPLoansBorrowAndRepay(ctx context.Context, arg *LoanBorrowAndReplayInput) (*LoanBorrowAndReplay, error) {
	if *arg == (LoanBorrowAndReplayInput{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.Currency.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if arg.Side == "" {
		return nil, order.ErrSideIsInvalid
	}
	if arg.Amount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	var resp *LoanBorrowAndReplay
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, vipLoansBorrowAnsRepayEPL, http.MethodPost, "account/borrow-repay", &arg, &resp, request.AuthenticatedRequest)
}

// GetBorrowAndRepayHistoryForVIPLoans retrieves borrow and repay history for VIP loans
func (e *Exchange) GetBorrowAndRepayHistoryForVIPLoans(ctx context.Context, ccy currency.Code, after, before time.Time, limit int64) ([]BorrowRepayHistory, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []BorrowRepayHistory
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowAnsRepayHistoryHistoryEPL, http.MethodGet, common.EncodeURLValues("account/borrow-repay-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetVIPInterestAccruedData retrieves VIP interest accrued data
func (e *Exchange) GetVIPInterestAccruedData(ctx context.Context, ccy currency.Code, orderID string, after, before time.Time, limit int64) ([]VIPInterestData, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if orderID != "" {
		params.Set("ordId", orderID)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []VIPInterestData
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getVIPInterestAccruedDataEPL, http.MethodGet, common.EncodeURLValues("account/vip-interest-accrued", params), nil, &resp, request.AuthenticatedRequest)
}

// GetVIPInterestDeductedData retrieves a VIP interest deducted data
func (e *Exchange) GetVIPInterestDeductedData(ctx context.Context, ccy currency.Code, orderID string, after, before time.Time, limit int64) ([]VIPInterestData, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if orderID != "" {
		params.Set("ordId", orderID)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []VIPInterestData
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getVIPInterestDeductedDataEPL, http.MethodGet, common.EncodeURLValues("account/vip-interest-deducted", params), nil, &resp, request.AuthenticatedRequest)
}

// GetVIPLoanOrderList retrieves VIP loan order list
// state: possible values are 1:Borrowing 2:Borrowed 3:Repaying 4:Repaid 5:Borrow failed
func (e *Exchange) GetVIPLoanOrderList(ctx context.Context, orderID, state string, ccy currency.Code, after, before time.Time, limit int64) ([]VIPLoanOrder, error) {
	params := url.Values{}
	if orderID != "" {
		params.Set("ordId", orderID)
	}
	if state != "" {
		params.Set("state", state)
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []VIPLoanOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getVIPLoanOrderListEPL, http.MethodGet, common.EncodeURLValues("account/vip-loan-order-list", params), nil, &resp, request.AuthenticatedRequest)
}

// GetVIPLoanOrderDetail retrieves list of loan order details
func (e *Exchange) GetVIPLoanOrderDetail(ctx context.Context, orderID string, ccy currency.Code, after, before time.Time, limit int64) (*VIPLoanOrderDetail, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	params := url.Values{}
	params.Set("ordId", orderID)
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp *VIPLoanOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getVIPLoanOrderDetailEPL, http.MethodGet, common.EncodeURLValues("account/vip-loan-order-detail", params), nil, &resp, request.AuthenticatedRequest)
}

// GetBorrowInterestAndLimit borrow interest and limit
func (e *Exchange) GetBorrowInterestAndLimit(ctx context.Context, loanType int64, ccy currency.Code) ([]BorrowInterestAndLimitResponse, error) {
	params := url.Values{}
	if loanType == 1 || loanType == 2 {
		params.Set("type", strconv.FormatInt(loanType, 10))
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []BorrowInterestAndLimitResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowInterestAndLimitEPL, http.MethodGet, common.EncodeURLValues("account/interest-limits", params), nil, &resp, request.AuthenticatedRequest)
}

// GetFixedLoanBorrowLimit retrieves a fixed loadn borrow limit information
func (e *Exchange) GetFixedLoanBorrowLimit(ctx context.Context) (*FixedLoanBorrowLimitInformation, error) {
	var resp *FixedLoanBorrowLimitInformation
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFixedLoanBorrowLimitEPL, http.MethodGet, "account/fixed-loan/borrowing-limit", nil, &resp, request.AuthenticatedRequest)
}

// GetFixedLoanBorrowQuote retrieves a fixed loan borrow quote information
func (e *Exchange) GetFixedLoanBorrowQuote(ctx context.Context, borrowingCurrency currency.Code, borrowType, term, orderID string, amount, maxRate float64) (*FixedLoanBorrowQuote, error) {
	if borrowType == "" {
		return nil, errBorrowTypeRequired
	}

	switch borrowType {
	case "normal":
		if borrowingCurrency.IsEmpty() {
			return nil, currency.ErrCurrencyCodeEmpty
		}
		if amount <= 0 {
			return nil, limits.ErrAmountBelowMin
		}
		if maxRate <= 0 {
			return nil, errMaxRateRequired
		}
		if term == "" {
			return nil, errLendingTermIsRequired
		}
	case "reborrow":
		if orderID == "" {
			return nil, order.ErrOrderIDNotSet
		}
	}

	params := url.Values{}
	params.Set("type", borrowType)
	if !borrowingCurrency.IsEmpty() {
		params.Set("ccy", borrowingCurrency.String())
	}
	if amount > 0 {
		params.Set("amt", strconv.FormatFloat(amount, 'f', -1, 64))
	}
	if maxRate > 0 {
		params.Set("maxRate", strconv.FormatFloat(maxRate, 'f', -1, 64))
	}
	if term != "" {
		params.Set("term", term)
	}
	if orderID != "" {
		params.Set("ordId", orderID)
	}
	var resp *FixedLoanBorrowQuote
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFixedLoanBorrowQuoteEPL, http.MethodGet, common.EncodeURLValues("account/fixed-loan/borrowing-quote", params), nil, &resp, request.AuthenticatedRequest)
}

// PlaceFixedLoanBorrowingOrder for new borrowing orders, they belong to the IOC (immediately close and cancel the remaining) type. For renewal orders, they belong to the FOK (Fill-or-kill) type
func (e *Exchange) PlaceFixedLoanBorrowingOrder(ctx context.Context, ccy currency.Code, amount, maxRate, reborrowRate float64, term string, reborrow bool) (*OrderIDResponse, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if amount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	if maxRate <= 0 {
		return nil, errMaxRateRequired
	}
	if term == "" {
		return nil, errLendingTermIsRequired
	}
	arg := &struct {
		Currency     string  `json:"ccy"`
		Amount       float64 `json:"amt,string"`
		MaxRate      float64 `jsons:"maxRate,string"`
		Term         string  `json:"term"`
		Reborrow     bool    `json:"reborrow,omitempty"`
		ReborrowRate float64 `json:"reborrowRate,string,omitempty"`
	}{
		Currency:     ccy.String(),
		Amount:       amount,
		MaxRate:      maxRate,
		Term:         term,
		Reborrow:     reborrow,
		ReborrowRate: reborrowRate,
	}
	var resp *OrderIDResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, placeFixedLoanBorrowingOrderEPL, http.MethodPost, "account/fixed-loan/borrowing-order", arg, &resp, request.AuthenticatedRequest)
}

// AmendFixedLoanBorrowingOrder amends a fixed loan borrowing order
func (e *Exchange) AmendFixedLoanBorrowingOrder(ctx context.Context, orderID string, reborrow bool, renewMaxRate float64) (*OrderIDResponse, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	arg := &struct {
		OrderID      string  `json:"ordId"`
		Reborrow     bool    `json:"reborrow,omitempty"`
		RenewMaxRate float64 `json:"renewMaxRate,omitempty,string"`
	}{
		OrderID:      orderID,
		Reborrow:     reborrow,
		RenewMaxRate: renewMaxRate,
	}
	var resp *OrderIDResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, amendFixedLaonBorrowingOrderEPL, http.MethodPost, "account/fixed-loan/amend-borrowing-order", arg, &resp, request.AuthenticatedRequest)
}

// ManualRenewFixedLoanBorrowingOrder manual renew fixed loan borrowing order
func (e *Exchange) ManualRenewFixedLoanBorrowingOrder(ctx context.Context, orderID string, maxRate float64) (*OrderIDResponse, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	if maxRate <= 0 {
		return nil, errMaxRateRequired
	}
	arg := &struct {
		OrderID string  `json:"ordId"`
		MaxRate float64 `json:"maxRate,string"`
	}{
		OrderID: orderID,
		MaxRate: maxRate,
	}
	var resp *OrderIDResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, manualRenewFixedLoanBorrowingOrderEPL, http.MethodPost, "account/fixed-loan/manual-reborrow", arg, &resp, request.AuthenticatedRequest)
}

// RepayFixedLoanBorrowingOrder repays fixed loan borrowing order
func (e *Exchange) RepayFixedLoanBorrowingOrder(ctx context.Context, orderID string) (*OrderIDResponse, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	var resp *OrderIDResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, repayFixedLoanBorrowingOrderEPL, http.MethodPost, "account/fixed-loan/repay-borrowing-order", map[string]string{"ordId": orderID}, &resp, request.AuthenticatedRequest)
}

// ConvertFixedLoanToMarketLoan converts fixed loan to market loan
func (e *Exchange) ConvertFixedLoanToMarketLoan(ctx context.Context, orderID string) (*OrderIDResponse, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	var resp *OrderIDResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, convertFixedLoanToMarketLoanEPL, http.MethodPost, "account/fixed-loan/convert-to-market-loan", nil, &resp, request.AuthenticatedRequest)
}

// ReduceLiabilitiesForFixedLoan provide the function of "setting pending repay state / canceling pending repay state" for fixed loan order
func (e *Exchange) ReduceLiabilitiesForFixedLoan(ctx context.Context, orderID string, pendingRepay bool) (*ReduceLiabilities, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	arg := &struct {
		OrderID          string `json:"ordId"`
		PendingRepayment bool   `json:"pendingRepay,string"`
	}{
		OrderID:          orderID,
		PendingRepayment: pendingRepay,
	}
	var resp *ReduceLiabilities
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, reduceLiabilitiesForFixedLoanEPL, http.MethodPost, "account/fixed-loan/reduce-liabilities", arg, &resp, request.AuthenticatedRequest)
}

// GetFixedLoanBorrowOrderList retrieves fixed loan borrow order list
// State '1': Borrowing '2': Borrowed '3': Settled (Repaid) '4': Borrow failed '5': Overdue '6': Settling '7': Reborrowing '8': Pending repay
func (e *Exchange) GetFixedLoanBorrowOrderList(ctx context.Context, ccy currency.Code, orderID, state, term string, after, before time.Time, limit int64) ([]FixedLoanBorrowOrderDetail, error) {
	params := url.Values{}
	if orderID != "" {
		params.Set("ordId", orderID)
	}
	if ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if state != "" {
		params.Set("state", state)
	}
	if term != "" {
		params.Set("term", term)
	}
	if !after.IsZero() && !before.IsZero() {
		err := common.StartEndTimeCheck(after, before)
		if err != nil {
			return nil, err
		}
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []FixedLoanBorrowOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFixedLoanBorrowOrderListEPL, http.MethodGet, common.EncodeURLValues("account/fixed-loan/borrowing-orders-list", params), nil, &resp, request.AuthenticatedRequest)
}

// ManualBorrowOrRepay borrow or repay assets. only applicable to Spot mode (enabled borrowing)
func (e *Exchange) ManualBorrowOrRepay(ctx context.Context, ccy currency.Code, side string, amount float64) (*BorrowOrRepay, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if side == "" {
		return nil, errLendingSideRequired
	}
	if amount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	arg := &struct {
		Currency string  `json:"ccy"`
		Side     string  `json:"side"`
		Amount   float64 `json:"amt"`
	}{
		Currency: ccy.String(),
		Side:     side,
		Amount:   amount,
	}
	var resp *BorrowOrRepay
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, manualBorrowOrRepayEPL, http.MethodPost, "account/spot-manual-borrow-repay", arg, &resp, request.AuthenticatedRequest)
}

// SetAutoRepay represents an auto-repay. Only applicable to Spot mode (enabled borrowing)
func (e *Exchange) SetAutoRepay(ctx context.Context, autoRepay bool) (*AutoRepay, error) {
	arg := AutoRepay{
		AutoRepay: autoRepay,
	}
	var resp *AutoRepay
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setAutoRepayEPL, http.MethodPost, "account/set-auto-repay", arg, &resp, request.AuthenticatedRequest)
}

// GetBorrowRepayHistory retrieve the borrow/repay history under Spot mode
func (e *Exchange) GetBorrowRepayHistory(ctx context.Context, ccy currency.Code, eventType string, after, before time.Time, limit int64) ([]BorrowRepayItem, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if eventType != "" {
		params.Set("type", eventType)
	}
	if !after.IsZero() && !before.IsZero() {
		err := common.StartEndTimeCheck(after, before)
		if err != nil {
			return nil, err
		}
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []BorrowRepayItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBorrowRepayHistoryEPL, http.MethodGet, common.EncodeURLValues("account/spot-borrow-repay-history", params), nil, &resp, request.AuthenticatedRequest)
}

// NewPositionBuilder calculates portfolio margin information for virtual position/assets or current position of the user.
// You can add up to 200 virtual positions and 200 virtual assets in one request
func (e *Exchange) NewPositionBuilder(ctx context.Context, arg *PositionBuilderParam) (*PositionBuilderDetail, error) {
	if arg == nil {
		return nil, common.ErrNilPointer
	}
	var resp *PositionBuilderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, newPositionBuilderEPL, http.MethodPost, "account/position-builder", arg, &resp, request.AuthenticatedRequest)
}

// SetRiskOffsetAmount set risk offset amount. This does not represent the actual spot risk offset amount. Only applicable to Portfolio Margin Mode
func (e *Exchange) SetRiskOffsetAmount(ctx context.Context, ccy currency.Code, clientSpotInUseAmount float64) (*RiskOffsetAmount, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if clientSpotInUseAmount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	arg := &struct {
		Currency              string  `json:"ccy"`
		ClientSpotInUseAmount float64 `json:"clSpotInUseAmt,string"`
	}{
		Currency:              ccy.String(),
		ClientSpotInUseAmount: clientSpotInUseAmount,
	}
	var resp *RiskOffsetAmount
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setRiskOffsetAmountEPL, http.MethodPost, "account/set-riskOffset-amt", arg, &resp, request.AuthenticatedRequest)
}

// GetGreeks retrieves a greeks list of all assets in the account
func (e *Exchange) GetGreeks(ctx context.Context, ccy currency.Code) ([]GreeksItem, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []GreeksItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getGreeksEPL, http.MethodGet, common.EncodeURLValues("account/greeks", params), nil, &resp, request.AuthenticatedRequest)
}

// GetPMPositionLimitation retrieve cross position limitation of SWAP/FUTURES/OPTION under Portfolio margin mode
func (e *Exchange) GetPMPositionLimitation(ctx context.Context, instrumentType, underlying, instrumentFamily string) ([]PMLimitationResponse, error) {
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	if underlying == "" && instrumentFamily == "" {
		return nil, errInstrumentFamilyOrUnderlyingRequired
	}
	params := url.Values{}
	params.Set("instType", strings.ToUpper(instrumentType))
	if underlying != "" {
		params.Set("uly", underlying)
	}
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	var resp []PMLimitationResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getPMLimitationEPL, http.MethodGet, common.EncodeURLValues("account/position-tiers", params), nil, &resp, request.AuthenticatedRequest)
}

// SetRiskOffsetType configure the risk offset type in portfolio margin mode.
// riskOffsetType possible values are:
// 1: Spot-derivatives (USDT) risk offset
// 2: Spot-derivatives (Crypto) risk offset
// 3:Derivatives only mode
func (e *Exchange) SetRiskOffsetType(ctx context.Context, riskOffsetType string) (*RiskOffsetType, error) {
	if riskOffsetType == "" {
		return nil, errors.New("missing risk offset type")
	}
	var resp *RiskOffsetType
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setRiskOffsetLimiterEPL, http.MethodPost, "account/set-riskOffset-type", &map[string]string{"type": riskOffsetType}, &resp, request.AuthenticatedRequest)
}

// ActivateOption activates option
func (e *Exchange) ActivateOption(ctx context.Context) (types.Time, error) {
	resp := &tsResp{}
	return resp.Timestamp, e.SendHTTPRequest(ctx, exchange.RestSpot, activateOptionEPL, http.MethodPost, "account/activate-option", nil, resp, request.AuthenticatedRequest)
}

// SetAutoLoan only applicable to Multi-currency margin and Portfolio margin
func (e *Exchange) SetAutoLoan(ctx context.Context, autoLoan bool) (*AutoLoan, error) {
	var resp *AutoLoan
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setAutoLoanEPL, http.MethodPost, "account/set-auto-loan", &AutoLoan{AutoLoan: autoLoan}, &resp, request.AuthenticatedRequest)
}

// SetAccountMode to set on the Web/App for the first set of every account mode.
// Account mode 1: Simple mode 2: Single-currency margin mode  3: Multi-currency margin code  4: Portfolio margin mode
func (e *Exchange) SetAccountMode(ctx context.Context, accountLevel string) (*AccountMode, error) {
	var resp *AccountMode
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setAccountLevelEPL, http.MethodPost, "account/set-account-level", &map[string]string{"acctLv": accountLevel}, &resp, request.AuthenticatedRequest)
}

// ResetMMPStatus reset the MMP status to be inactive.
// you can unfreeze by this endpoint once MMP is triggered.
// Only applicable to Option in Portfolio Margin mode, and MMP privilege is required
func (e *Exchange) ResetMMPStatus(ctx context.Context, instrumentType, instrumentFamily string) (*MMPStatusResponse, error) {
	if instrumentFamily == "" {
		return nil, errInstrumentFamilyRequired
	}
	arg := &struct {
		InstrumentType   string `json:"instType,omitempty"`
		InstrumentFamily string `json:"instFamily"`
	}{
		InstrumentType:   instrumentType,
		InstrumentFamily: instrumentFamily,
	}
	var resp *MMPStatusResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, resetMMPStatusEPL, http.MethodPost, "account/mmp-reset", arg, &resp, request.AuthenticatedRequest)
}

// SetMMP set MMP configure
// Only applicable to Option in Portfolio Margin mode, and MMP privilege is required
func (e *Exchange) SetMMP(ctx context.Context, arg *MMPConfig) (*MMPConfig, error) {
	if *arg == (MMPConfig{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentFamily == "" {
		return nil, errInstrumentFamilyRequired
	}
	if arg.QuantityLimit <= 0 {
		return nil, errInvalidQuantityLimit
	}
	var resp *MMPConfig
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setMMPEPL, http.MethodPost, "account/mmp-config", arg, &resp, request.AuthenticatedRequest)
}

// GetMMPConfig retrieves MMP configure information
// Only applicable to Option in Portfolio Margin mode, and MMP privilege is required
func (e *Exchange) GetMMPConfig(ctx context.Context, instrumentFamily string) ([]MMPConfigDetail, error) {
	params := url.Values{}
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	var resp []MMPConfigDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getMMPConfigEPL, http.MethodGet, common.EncodeURLValues("account/mmp-config", params), nil, &resp, request.AuthenticatedRequest)
}

/********************************** Subaccount Endpoints ***************************************************/

// ViewSubAccountList applies to master accounts only
func (e *Exchange) ViewSubAccountList(ctx context.Context, enable bool, subaccountName string, after, before time.Time, limit int64) ([]SubaccountInfo, error) {
	params := url.Values{}
	if enable {
		params.Set("enable", "true")
	}
	if subaccountName != "" {
		params.Set("subAcct", subaccountName)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []SubaccountInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, viewSubaccountListEPL, http.MethodGet, common.EncodeURLValues("users/subaccount/list", params), nil, &resp, request.AuthenticatedRequest)
}

// ResetSubAccountAPIKey applies to master accounts only and master accounts APIKey must be linked to IP addresses
func (e *Exchange) ResetSubAccountAPIKey(ctx context.Context, arg *SubAccountAPIKeyParam) (*SubAccountAPIKeyResponse, error) {
	if arg == nil {
		return nil, common.ErrNilPointer
	}
	if arg.SubAccountName == "" {
		return nil, errInvalidSubAccountName
	}
	if arg.APIKey == "" {
		return nil, errInvalidAPIKey
	}
	if arg.IP != "" && net.ParseIP(arg.IP).To4() == nil {
		return nil, errInvalidIPAddress
	}
	if arg.APIKeyPermission == "" && len(arg.Permissions) != 0 {
		for x := range arg.Permissions {
			if !slices.Contains([]string{"read", "withdraw", "trade", "read_only"}, arg.Permissions[x]) {
				return nil, errInvalidAPIKeyPermission
			}
			if x != 0 {
				arg.APIKeyPermission += ","
			}
			arg.APIKeyPermission += arg.Permissions[x]
		}
	} else if !slices.Contains([]string{"read", "withdraw", "trade", "read_only"}, arg.APIKeyPermission) {
		return nil, errInvalidAPIKeyPermission
	}
	var resp *SubAccountAPIKeyResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, resetSubAccountAPIKeyEPL, http.MethodPost, "users/subaccount/modify-apikey", &arg, &resp, request.AuthenticatedRequest)
}

// GetSubaccountTradingBalance query detailed balance info of Trading Account of a sub-account via the master account (applies to master accounts only)
func (e *Exchange) GetSubaccountTradingBalance(ctx context.Context, subaccountName string) ([]SubaccountBalanceResponse, error) {
	if subaccountName == "" {
		return nil, errInvalidSubAccountName
	}
	params := url.Values{}
	params.Set("subAcct", subaccountName)
	var resp []SubaccountBalanceResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSubaccountTradingBalanceEPL, http.MethodGet, common.EncodeURLValues("account/subaccount/balances", params), nil, &resp, request.AuthenticatedRequest)
}

// GetSubaccountFundingBalance query detailed balance info of Funding Account of a sub-account via the master account (applies to master accounts only)
func (e *Exchange) GetSubaccountFundingBalance(ctx context.Context, subaccountName string, ccy currency.Code) ([]FundingBalance, error) {
	if subaccountName == "" {
		return nil, errInvalidSubAccountName
	}
	params := url.Values{}
	params.Set("subAcct", subaccountName)
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []FundingBalance
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSubaccountFundingBalanceEPL, http.MethodGet,
		common.EncodeURLValues("asset/subaccount/balances", params), nil, &resp, request.AuthenticatedRequest)
}

// GetSubAccountMaximumWithdrawal retrieve the maximum withdrawal information of a sub-account via the master account (applies to master accounts only). If no currency is specified, the transferable amount of all owned currencies will be returned
func (e *Exchange) GetSubAccountMaximumWithdrawal(ctx context.Context, subAccountName string, ccy currency.Code) ([]SubAccountMaximumWithdrawal, error) {
	if subAccountName == "" {
		return nil, errInvalidSubAccountName
	}
	params := url.Values{}
	params.Set("subAcct", subAccountName)
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []SubAccountMaximumWithdrawal
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSubAccountMaxWithdrawalEPL, http.MethodGet,
		common.EncodeURLValues("account/subaccount/max-withdrawal", params), nil, &resp, request.AuthenticatedRequest)
}

// HistoryOfSubaccountTransfer retrieves subaccount transfer histories; applies to master accounts only.
// retrieve the transfer data for the last 3 months
func (e *Exchange) HistoryOfSubaccountTransfer(ctx context.Context, ccy currency.Code, subaccountType, subaccountName string, before, after time.Time, limit int64) ([]SubaccountBillItem, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if subaccountType != "" {
		params.Set("type", subaccountType)
	}
	if subaccountName != "" {
		params.Set("subacct", subaccountName)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []SubaccountBillItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, historyOfSubaccountTransferEPL, http.MethodGet, common.EncodeURLValues("asset/subaccount/bills", params), nil, &resp, request.AuthenticatedRequest)
}

// GetHistoryOfManagedSubAccountTransfer retrieves managed sub-account transfers.
// nly applicable to the trading team's master account to getting transfer records of managed sub accounts entrusted to oneself
func (e *Exchange) GetHistoryOfManagedSubAccountTransfer(ctx context.Context, ccy currency.Code, transferType, subAccountName, subAccountUID string, after, before time.Time, limit int64) ([]SubAccountTransfer, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if transferType != "" {
		params.Set("type", transferType)
	}
	if subAccountName != "" {
		params.Set("subAcct", subAccountName)
	}
	if subAccountUID != "" {
		params.Set("subUid", subAccountUID)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []SubAccountTransfer
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, managedSubAccountTransferEPL, http.MethodGet, common.EncodeURLValues("asset/subaccount/managed-subaccount-bills", params), nil, &resp, request.AuthenticatedRequest)
}

// MasterAccountsManageTransfersBetweenSubaccounts master accounts manage the transfers between sub-accounts applies to master accounts only
func (e *Exchange) MasterAccountsManageTransfersBetweenSubaccounts(ctx context.Context, arg *SubAccountAssetTransferParams) ([]TransferIDInfo, error) {
	if *arg == (SubAccountAssetTransferParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.Currency.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if arg.Amount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	if arg.From == 0 {
		return nil, errInvalidSubaccount
	}
	if arg.To != 6 && arg.To != 18 {
		return nil, errInvalidSubaccount
	}
	if arg.FromSubAccount == "" {
		return nil, errInvalidSubAccountName
	}
	if arg.ToSubAccount == "" {
		return nil, errInvalidSubAccountName
	}
	var resp []TransferIDInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, masterAccountsManageTransfersBetweenSubaccountEPL, http.MethodPost, "asset/subaccount/transfer", &arg, &resp, request.AuthenticatedRequest)
}

// SetPermissionOfTransferOut set permission of transfer out for sub-account(only applicable to master account). Sub-account can transfer out to master account by default
func (e *Exchange) SetPermissionOfTransferOut(ctx context.Context, arg *PermissionOfTransfer) ([]PermissionOfTransfer, error) {
	if *arg == (PermissionOfTransfer{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.SubAcct == "" {
		return nil, errInvalidSubAccountName
	}
	var resp []PermissionOfTransfer
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setPermissionOfTransferOutEPL, http.MethodPost, "users/subaccount/set-transfer-out", &arg, &resp, request.AuthenticatedRequest)
}

// GetCustodyTradingSubaccountList the trading team uses this interface to view the list of sub-accounts currently under escrow
// usersEntrustSubaccountList ="users/entrust-subaccount-list"
func (e *Exchange) GetCustodyTradingSubaccountList(ctx context.Context, subaccountName string) ([]SubaccountName, error) {
	params := url.Values{}
	if subaccountName != "" {
		params.Set("setAcct", subaccountName)
	}
	var resp []SubaccountName
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getCustodyTradingSubaccountListEPL, http.MethodGet, common.EncodeURLValues("users/entrust-subaccount-list", params), nil, &resp, request.AuthenticatedRequest)
}

// SetSubAccountVIPLoanAllocation set the VIP loan allocation of sub-accounts. Only Applicable to master account API keys with Trade access
func (e *Exchange) SetSubAccountVIPLoanAllocation(ctx context.Context, arg *SubAccountLoanAllocationParam) (bool, error) {
	if len(arg.Alloc) == 0 {
		return false, common.ErrEmptyParams
	}
	for a := range arg.Alloc {
		if arg.Alloc[a] == (subAccountVIPLoanAllocationInfo{}) {
			return false, common.ErrEmptyParams
		}
		if arg.Alloc[a].SubAcct == "" {
			return false, errInvalidSubAccountName
		}
		if arg.Alloc[a].LoanAlloc < 0 {
			return false, errInvalidLoanAllocationValue
		}
	}
	resp := &struct {
		Result bool `json:"result"`
	}{}
	return resp.Result, e.SendHTTPRequest(ctx, exchange.RestSpot, setSubAccountVIPLoanAllocationEPL, http.MethodPost, "account/subaccount/set-loan-allocation", arg, resp, request.AuthenticatedRequest)
}

// GetSubAccountBorrowInterestAndLimit retrieves sub-account borrow interest and limit
// Only applicable to master account API keys. Only return VIP loan information
func (e *Exchange) GetSubAccountBorrowInterestAndLimit(ctx context.Context, subAccount string, ccy currency.Code) ([]SubAccounBorrowInterestAndLimit, error) {
	if subAccount == "" {
		return nil, errInvalidSubAccountName
	}
	params := url.Values{}
	params.Set("subAcct", subAccount)
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []SubAccounBorrowInterestAndLimit
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSubAccountBorrowInterestAndLimitEPL, http.MethodGet, common.EncodeURLValues("account/subaccount/interest-limits", params), nil, &resp, request.AuthenticatedRequest)
}

/*************************************** Grid Trading Endpoints ***************************************************/

// PlaceGridAlgoOrder place spot grid algo order
func (e *Exchange) PlaceGridAlgoOrder(ctx context.Context, arg *GridAlgoOrder) (*GridAlgoOrderIDResponse, error) {
	if *arg == (GridAlgoOrder{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	arg.AlgoOrdType = strings.ToLower(arg.AlgoOrdType)
	if arg.AlgoOrdType != AlgoOrdTypeGrid && arg.AlgoOrdType != AlgoOrdTypeContractGrid {
		return nil, errMissingAlgoOrderType
	}
	if arg.MaxPrice <= 0 {
		return nil, limits.ErrPriceBelowMin
	}
	if arg.MinPrice <= 0 {
		return nil, limits.ErrPriceBelowMin
	}
	if arg.GridQuantity < 0 {
		return nil, errInvalidGridQuantity
	}
	isSpotGridOrder := arg.QuoteSize > 0 || arg.BaseSize > 0
	if !isSpotGridOrder {
		if arg.Size <= 0 {
			return nil, fmt.Errorf("%w: parameter Size is required", order.ErrAmountMustBeSet)
		}
		arg.Direction = strings.ToLower(arg.Direction)
		if !slices.Contains([]string{positionSideLong, positionSideShort, "neutral"}, arg.Direction) {
			return nil, errMissingRequiredArgumentDirection
		}
		if arg.Leverage == "" {
			return nil, errInvalidLeverage
		}
	}
	var resp *GridAlgoOrderIDResponse
	err := e.SendHTTPRequest(ctx, exchange.RestSpot, gridTradingEPL, http.MethodPost, "tradingBot/grid/order-algo", &arg, &resp, request.AuthenticatedRequest)
	if err != nil {
		if resp != nil && resp.StatusMessage != "" {
			return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
		}
		return nil, err
	}
	return resp, nil
}

// AmendGridAlgoOrder supported contract grid algo order amendment
func (e *Exchange) AmendGridAlgoOrder(ctx context.Context, arg *GridAlgoOrderAmend) (*GridAlgoOrderIDResponse, error) {
	if *arg == (GridAlgoOrderAmend{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.AlgoID == "" {
		return nil, errAlgoIDRequired
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	var resp *GridAlgoOrderIDResponse
	err := e.SendHTTPRequest(ctx, exchange.RestSpot, amendGridAlgoOrderEPL, http.MethodPost, "tradingBot/grid/amend-order-algo", &arg, &resp, request.AuthenticatedRequest)
	if err != nil {
		if resp != nil && resp.StatusMessage == "" {
			return nil, fmt.Errorf("%w; %w", err, getStatusError(resp.StatusCode, resp.StatusMessage))
		}
		return nil, err
	}
	return resp, nil
}

// StopGridAlgoOrder stop a batch of grid algo orders.
// A maximum of 10 orders can be canceled per request
func (e *Exchange) StopGridAlgoOrder(ctx context.Context, arg []StopGridAlgoOrderRequest) ([]GridAlgoOrderIDResponse, error) {
	if len(arg) == 0 {
		return nil, common.ErrEmptyParams
	}
	for x := range arg {
		if (arg[x]) == (StopGridAlgoOrderRequest{}) {
			return nil, common.ErrEmptyParams
		}
		if arg[x].AlgoID == "" {
			return nil, errAlgoIDRequired
		}
		if arg[x].InstrumentID == "" {
			return nil, errMissingInstrumentID
		}
		arg[x].AlgoOrderType = strings.ToLower(arg[x].AlgoOrderType)
		if arg[x].AlgoOrderType != AlgoOrdTypeGrid && arg[x].AlgoOrderType != AlgoOrdTypeContractGrid {
			return nil, errMissingAlgoOrderType
		}
		if arg[x].StopType != 1 && arg[x].StopType != 2 {
			return nil, errMissingValidStopType
		}
	}
	var resp []GridAlgoOrderIDResponse
	err := e.SendHTTPRequest(ctx, exchange.RestSpot, stopGridAlgoOrderEPL, http.MethodPost, "tradingBot/grid/stop-order-algo", arg, &resp, request.AuthenticatedRequest)
	if err != nil {
		if len(resp) == 0 {
			return nil, err
		}
		var errs error
		for x := range resp {
			if resp[x].StatusMessage != "" {
				errs = common.AppendError(errs, getStatusError(resp[x].StatusCode, resp[x].StatusMessage))
			}
		}
		return nil, common.AppendError(err, errs)
	}
	return resp, nil
}

// ClosePositionForContractID close position when the contract grid stop type is 'keep position'
func (e *Exchange) ClosePositionForContractID(ctx context.Context, arg *ClosePositionParams) (*ClosePositionContractGridResponse, error) {
	if *arg == (ClosePositionParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.AlgoID == "" {
		return nil, errAlgoIDRequired
	}
	if !arg.MarketCloseAllPositions && arg.Size <= 0 {
		return nil, fmt.Errorf("%w 'size' is required", order.ErrAmountMustBeSet)
	}
	if !arg.MarketCloseAllPositions && arg.Price <= 0 {
		return nil, limits.ErrPriceBelowMin
	}
	var resp *ClosePositionContractGridResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, closePositionForForContractGridEPL, http.MethodPost, "tradingBot/grid/close-position", arg, &resp, request.AuthenticatedRequest)
}

// CancelClosePositionOrderForContractGrid cancels close position order for contract grid
func (e *Exchange) CancelClosePositionOrderForContractGrid(ctx context.Context, arg *CancelClosePositionOrder) (*ClosePositionContractGridResponse, error) {
	if *arg == (CancelClosePositionOrder{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.AlgoID == "" {
		return nil, errAlgoIDRequired
	}
	if arg.OrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	var resp *ClosePositionContractGridResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelClosePositionOrderForContractGridEPL, http.MethodPost, "tradingBot/grid/cancel-close-order", arg, &resp, request.AuthenticatedRequest)
}

// InstantTriggerGridAlgoOrder triggers grid algo order
func (e *Exchange) InstantTriggerGridAlgoOrder(ctx context.Context, algoID string) (*TriggeredGridAlgoOrderInfo, error) {
	var resp *TriggeredGridAlgoOrderInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, instantTriggerGridAlgoOrderEPL, http.MethodPost, "tradingBot/grid/order-instant-trigger", &map[string]string{"algoId": algoID}, &resp, request.AuthenticatedRequest)
}

// GetGridAlgoOrdersList retrieves list of pending grid algo orders with the complete data
func (e *Exchange) GetGridAlgoOrdersList(ctx context.Context, algoOrderType, algoID,
	instrumentID, instrumentType,
	after, before string, limit int64,
) ([]GridAlgoOrderResponse, error) {
	return e.getGridAlgoOrders(ctx, algoOrderType, algoID,
		instrumentID, instrumentType,
		after, before, "tradingBot/grid/orders-algo-pending", limit)
}

// GetGridAlgoOrderHistory retrieves list of grid algo orders with the complete data including the stopped orders
func (e *Exchange) GetGridAlgoOrderHistory(ctx context.Context, algoOrderType, algoID,
	instrumentID, instrumentType,
	after, before string, limit int64,
) ([]GridAlgoOrderResponse, error) {
	return e.getGridAlgoOrders(ctx, algoOrderType, algoID,
		instrumentID, instrumentType,
		after, before, "tradingBot/grid/orders-algo-history", limit)
}

// getGridAlgoOrderList retrieves list of grid algo orders with the complete data
func (e *Exchange) getGridAlgoOrders(ctx context.Context, algoOrderType, algoID,
	instrumentID, instrumentType,
	after, before, route string, limit int64,
) ([]GridAlgoOrderResponse, error) {
	algoOrderType = strings.ToLower(algoOrderType)
	if algoOrderType != AlgoOrdTypeGrid && algoOrderType != AlgoOrdTypeContractGrid {
		return nil, errMissingAlgoOrderType
	}
	params := url.Values{}
	params.Set("algoOrdType", algoOrderType)
	if algoID != "" {
		params.Set("algoId", algoID)
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if instrumentType != "" {
		params.Set("instType", strings.ToUpper(instrumentType))
	}
	if after != "" {
		params.Set("after", after)
	}
	if before != "" {
		params.Set("before", before)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	epl := getGridAlgoOrderListEPL
	if route == "tradingBot/grid/orders-algo-history" {
		epl = getGridAlgoOrderHistoryEPL
	}
	var resp []GridAlgoOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, epl, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.AuthenticatedRequest)
}

// GetGridAlgoOrderDetails retrieves grid algo order details
func (e *Exchange) GetGridAlgoOrderDetails(ctx context.Context, algoOrderType, algoID string) (*GridAlgoOrderResponse, error) {
	if algoOrderType != AlgoOrdTypeGrid &&
		algoOrderType != AlgoOrdTypeContractGrid {
		return nil, errMissingAlgoOrderType
	}
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	params := url.Values{}
	params.Set("algoOrdType", algoOrderType)
	params.Set("algoId", algoID)
	var resp *GridAlgoOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getGridAlgoOrderDetailsEPL, http.MethodGet, common.EncodeURLValues("tradingBot/grid/orders-algo-details", params), nil, &resp, request.AuthenticatedRequest)
}

// GetGridAlgoSubOrders retrieves grid algo sub orders
func (e *Exchange) GetGridAlgoSubOrders(ctx context.Context, algoOrderType, algoID, subOrderType, groupID, after, before string, limit int64) ([]GridAlgoOrderResponse, error) {
	if algoOrderType != AlgoOrdTypeGrid &&
		algoOrderType != AlgoOrdTypeContractGrid {
		return nil, errMissingAlgoOrderType
	}
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	if subOrderType != "live" && subOrderType != order.Filled.String() {
		return nil, errMissingSubOrderType
	}
	params := url.Values{}
	params.Set("algoOrdType", algoOrderType)
	params.Set("algoId", algoID)
	params.Set("type", subOrderType)
	if groupID != "" {
		params.Set("groupId", groupID)
	}
	if after != "" {
		params.Set("after", after)
	}
	if before != "" {
		params.Set("before", before)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []GridAlgoOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getGridAlgoSubOrdersEPL, http.MethodGet, common.EncodeURLValues("tradingBot/grid/sub-orders", params), nil, &resp, request.AuthenticatedRequest)
}

// GetGridAlgoOrderPositions retrieves grid algo order positions
func (e *Exchange) GetGridAlgoOrderPositions(ctx context.Context, algoOrderType, algoID string) ([]AlgoOrderPosition, error) {
	if algoOrderType != AlgoOrdTypeGrid && algoOrderType != AlgoOrdTypeContractGrid {
		return nil, errInvalidAlgoOrderType
	}
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	params := url.Values{}
	params.Set("algoOrdType", algoOrderType)
	params.Set("algoId", algoID)
	var resp []AlgoOrderPosition
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getGridAlgoOrderPositionsEPL, http.MethodGet, common.EncodeURLValues("tradingBot/grid/positions", params), nil, &resp, request.AuthenticatedRequest)
}

// SpotGridWithdrawProfit returns the spot grid orders withdrawal profit given an instrument id
func (e *Exchange) SpotGridWithdrawProfit(ctx context.Context, algoID string) (*AlgoOrderWithdrawalProfit, error) {
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	input := &struct {
		AlgoID string `json:"algoId"`
	}{
		AlgoID: algoID,
	}
	var resp *AlgoOrderWithdrawalProfit
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, spotGridWithdrawIncomeEPL, http.MethodPost, "tradingBot/grid/withdraw-income", input, &resp, request.AuthenticatedRequest)
}

// ComputeMarginBalance computes margin balance with 'add' and 'reduce' balance type
func (e *Exchange) ComputeMarginBalance(ctx context.Context, arg MarginBalanceParam) (*ComputeMarginBalance, error) {
	if arg.AlgoID == "" {
		return nil, errAlgoIDRequired
	}
	if arg.AdjustMarginBalanceType != "add" && arg.AdjustMarginBalanceType != marginBalanceReduce {
		return nil, errInvalidMarginTypeAdjust
	}
	var resp *ComputeMarginBalance
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, computeMarginBalanceEPL, http.MethodPost, "tradingBot/grid/compute-margin-balance", &arg, &resp, request.AuthenticatedRequest)
}

// AdjustMarginBalance retrieves adjust margin balance with 'add' and 'reduce' balance type
func (e *Exchange) AdjustMarginBalance(ctx context.Context, arg *MarginBalanceParam) (*AdjustMarginBalanceResponse, error) {
	if *arg == (MarginBalanceParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.AlgoID == "" {
		return nil, errAlgoIDRequired
	}
	if arg.AdjustMarginBalanceType != "add" && arg.AdjustMarginBalanceType != marginBalanceReduce {
		return nil, errInvalidMarginTypeAdjust
	}
	if arg.Percentage <= 0 && arg.Amount <= 0 {
		return nil, fmt.Errorf("%w, either percentage or amount is required", order.ErrAmountIsInvalid)
	}
	var resp *AdjustMarginBalanceResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, adjustMarginBalanceEPL, http.MethodPost, "tradingBot/grid/margin-balance", &arg, &resp, request.AuthenticatedRequest)
}

// GetGridAIParameter retrieves grid AI parameter
func (e *Exchange) GetGridAIParameter(ctx context.Context, algoOrderType, instrumentID, direction, duration string) ([]GridAIParameterResponse, error) {
	if !slices.Contains([]string{"moon_grid", "contract_grid", "grid"}, algoOrderType) {
		return nil, errInvalidAlgoOrderType
	}
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if algoOrderType == "contract_grid" && !slices.Contains([]string{positionSideLong, positionSideShort, "neutral"}, direction) {
		return nil, fmt.Errorf("%w, required for 'contract_grid' algo order type", errMissingRequiredArgumentDirection)
	}
	params := url.Values{}
	params.Set("direction", direction)
	params.Set("algoOrdType", algoOrderType)
	params.Set("instId", instrumentID)
	if !slices.Contains([]string{"", "7D", "30D", "180D"}, duration) {
		return nil, errInvalidDuration
	}
	var resp []GridAIParameterResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getGridAIParameterEPL, http.MethodGet, common.EncodeURLValues("tradingBot/grid/ai-param", params), nil, &resp, request.UnauthenticatedRequest)
}

// ComputeMinInvestment computes minimum investment
func (e *Exchange) ComputeMinInvestment(ctx context.Context, arg *ComputeInvestmentDataParam) (*InvestmentResult, error) {
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	switch arg.AlgoOrderType {
	case "grid", "contract_grid":
	default:
		return nil, errInvalidAlgoOrderType
	}
	if arg.MaxPrice <= 0 {
		return nil, fmt.Errorf("%w, maxPrice = %f", limits.ErrPriceBelowMin, arg.MaxPrice)
	}
	if arg.MinPrice <= 0 {
		return nil, fmt.Errorf("%w, minPrice = %f", limits.ErrPriceBelowMin, arg.MaxPrice)
	}
	if arg.GridNumber == 0 {
		return nil, fmt.Errorf("%w, grid number is required", errInvalidGridQuantity)
	}
	if arg.RunType == "" {
		return nil, errRunTypeRequired
	}
	if arg.AlgoOrderType == "contract_grid" {
		switch arg.Direction {
		case positionSideLong, positionSideShort, "neutral":
		default:
			return nil, fmt.Errorf("%w, invalid grid direction %s", errMissingRequiredArgumentDirection, arg.Direction)
		}
		if arg.Leverage <= 0 {
			return nil, errInvalidLeverage
		}
	}
	for x := range arg.InvestmentData {
		if arg.InvestmentData[x].Amount <= 0 {
			return nil, fmt.Errorf("%w, investment amt = %f", limits.ErrAmountBelowMin, arg.InvestmentData[x].Amount)
		}
		if arg.InvestmentData[x].Currency.IsEmpty() {
			return nil, currency.ErrCurrencyCodeEmpty
		}
	}
	var resp *InvestmentResult
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, computeMinInvestmentEPL, http.MethodPost, "tradingBot/grid/min-investment", arg, &resp, request.UnauthenticatedRequest)
}

// RSIBackTesting relative strength index(RSI) backtesting
// Parameters:
//
//	TriggerCondition: possible values are "cross_up" "cross_down" "above" "below" "cross" Default is cross_down
//
// Threshold: The value should be an integer between 1 to 100
func (e *Exchange) RSIBackTesting(ctx context.Context, instrumentID, triggerCondition, duration string, threshold, timePeriod int64, timeFrame kline.Interval) (*RSIBacktestingResponse, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if threshold > 100 || threshold < 1 {
		return nil, errors.New("threshold should be an integer between 1 to 100")
	}
	timeFrameString := IntervalFromString(timeFrame, false)
	if timeFrameString == "" {
		return nil, errors.New("timeframe is required")
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	params.Set("timeframe", timeFrameString)
	params.Set("thold", strconv.FormatInt(threshold, 10))
	if timePeriod > 0 {
		params.Set("timePeriod", strconv.FormatInt(timePeriod, 10))
	}
	if triggerCondition != "" {
		params.Set("triggerCond", triggerCondition)
	}
	if duration != "" {
		params.Set("duration", duration)
	}
	var resp *RSIBacktestingResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, rsiBackTestingEPL, http.MethodGet, common.EncodeURLValues("tradingBot/public/rsi-back-testing", params), nil, &resp, request.UnauthenticatedRequest)
}

// ****************************************** Signal bot trading **************************************************

// GetSignalBotOrderDetail create and customize your own signals while gaining access to a diverse selection of signals from top providers.
// Empower your trading strategies and stay ahead of the game with our comprehensive signal trading platform
func (e *Exchange) GetSignalBotOrderDetail(ctx context.Context, algoOrderType, algoID string) (*SignalBotOrderDetail, error) {
	if algoOrderType == "" {
		return nil, errInvalidAlgoOrderType
	}
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	params := url.Values{}
	params.Set("algoId", algoID)
	params.Set("algoOrdType", algoOrderType)
	var resp *SignalBotOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, signalBotOrderDetailsEPL, http.MethodGet, common.EncodeURLValues("tradingBot/signal/orders-algo-details", params), nil, &resp, request.AuthenticatedRequest)
}

// GetSignalOrderPositions retrieves signal bot order positions
func (e *Exchange) GetSignalOrderPositions(ctx context.Context, algoOrderType, algoID string) (*SignalBotPosition, error) {
	if algoOrderType == "" {
		return nil, errInvalidAlgoOrderType
	}
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	params := url.Values{}
	params.Set("algoId", algoID)
	params.Set("algoOrdType", algoOrderType)
	var resp *SignalBotPosition
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, signalBotOrderPositionsEPL, http.MethodGet, common.EncodeURLValues("tradingBot/signal/positions", params), nil, &resp, request.AuthenticatedRequest)
}

// GetSignalBotSubOrders retrieves historical filled sub orders and designated sub orders
func (e *Exchange) GetSignalBotSubOrders(ctx context.Context, algoID, algoOrderType, subOrderType, clientOrderID, afterPaginationID, beforePaginationID string, begin, end time.Time, limit int64) ([]SubOrder, error) {
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	if algoOrderType == "" {
		return nil, errInvalidAlgoOrderType
	}
	if subOrderType == "" && clientOrderID == "" {
		return nil, fmt.Errorf("%w, either client order ID or sub-order state is required", order.ErrOrderIDNotSet)
	}
	params := url.Values{}
	params.Set("algoId", algoID)
	params.Set("algoOrdType", algoOrderType)
	if subOrderType != "" {
		params.Set("type", subOrderType)
	} else {
		params.Set("clOrdId", clientOrderID)
	}
	if afterPaginationID != "" {
		params.Set("after", afterPaginationID)
	}
	if beforePaginationID != "" {
		params.Set("before", beforePaginationID)
	}
	if !begin.IsZero() {
		params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	if !end.IsZero() {
		params.Set("end", strconv.FormatInt(end.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []SubOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, signalBotSubOrdersEPL, http.MethodGet, common.EncodeURLValues("tradingBot/signal/sub-orders", params), nil, &resp, request.AuthenticatedRequest)
}

// GetSignalBotEventHistory retrieves signal bot event history
func (e *Exchange) GetSignalBotEventHistory(ctx context.Context, algoID string, after, before time.Time, limit int64) ([]SignalBotEventHistory, error) {
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	params := url.Values{}
	params.Set("algoId", algoID)
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []SignalBotEventHistory
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, signalBotEventHistoryEPL, http.MethodGet, common.EncodeURLValues("tradingBot/signal/event-history", params), nil, &resp, request.AuthenticatedRequest)
}

// ****************************************** Recurring Buy *****************************************

// PlaceRecurringBuyOrder recurring buy is a strategy for investing a fixed amount in crypto at fixed intervals.
// An appropriate recurring approach in volatile markets allows you to buy crypto at lower costs. Learn more
// The API endpoints of Recurring buy require authentication
func (e *Exchange) PlaceRecurringBuyOrder(ctx context.Context, arg *PlaceRecurringBuyOrderParam) (*RecurringOrderResponse, error) {
	if arg == nil {
		return nil, common.ErrNilPointer
	}
	if arg.StrategyName == "" {
		return nil, errStrategyNameRequired
	}
	if len(arg.RecurringList) == 0 {
		return nil, fmt.Errorf("%w, no recurring list is provided", common.ErrEmptyParams)
	}
	for x := range arg.RecurringList {
		if arg.RecurringList[x].Currency.IsEmpty() {
			return nil, currency.ErrCurrencyCodeEmpty
		}
	}
	if arg.RecurringDay == "" {
		return nil, errRecurringDayRequired
	}
	if arg.RecurringTime > 23 || arg.RecurringTime < 0 {
		return nil, errRecurringBuyTimeRequired
	}
	if arg.TradeMode == "" {
		return nil, errInvalidTradeModeValue
	}
	var resp *RecurringOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, placeRecurringBuyOrderEPL, http.MethodPost, "tradingBot/recurring/order-algo", arg, &resp, request.AuthenticatedRequest)
}

// AmendRecurringBuyOrder amends recurring order
func (e *Exchange) AmendRecurringBuyOrder(ctx context.Context, arg *AmendRecurringOrderParam) (*RecurringOrderResponse, error) {
	if (*arg) == (AmendRecurringOrderParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.AlgoID == "" {
		return nil, errAlgoIDRequired
	}
	if arg.StrategyName == "" {
		return nil, errStrategyNameRequired
	}
	var resp *RecurringOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, amendRecurringBuyOrderEPL, http.MethodPost, "tradingBot/recurring/amend-order-algo", arg, &resp, request.AuthenticatedRequest)
}

// StopRecurringBuyOrder stops recurring buy order. A maximum of 10 orders can be stopped per request
func (e *Exchange) StopRecurringBuyOrder(ctx context.Context, arg []StopRecurringBuyOrder) ([]RecurringOrderResponse, error) {
	if len(arg) == 0 {
		return nil, common.ErrEmptyParams
	}
	for x := range arg {
		if arg[x].AlgoID == "" {
			return nil, errAlgoIDRequired
		}
	}
	var resp []RecurringOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, stopRecurringBuyOrderEPL, http.MethodPost, "tradingBot/recurring/stop-order-algo", arg, &resp, request.AuthenticatedRequest)
}

// GetRecurringBuyOrderList retrieves recurring buy order list
func (e *Exchange) GetRecurringBuyOrderList(ctx context.Context, algoID, algoOrderState string, after, before time.Time, limit int64) ([]RecurringOrderItem, error) {
	params := url.Values{}
	if algoOrderState != "" {
		params.Set("state", algoOrderState)
	}
	if algoID != "" {
		params.Set("algoId", algoID)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []RecurringOrderItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getRecurringBuyOrderListEPL, http.MethodGet, common.EncodeURLValues("tradingBot/recurring/orders-algo-pending", params), nil, &resp, request.AuthenticatedRequest)
}

// GetRecurringBuyOrderHistory retrieves recurring buy order history
func (e *Exchange) GetRecurringBuyOrderHistory(ctx context.Context, algoID string, after, before time.Time, limit int64) ([]RecurringOrderItem, error) {
	params := url.Values{}
	if algoID != "" {
		params.Set("algoId", algoID)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []RecurringOrderItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getRecurringBuyOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("tradingBot/recurring/orders-algo-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetRecurringOrderDetails retrieves a single recurring order detail
func (e *Exchange) GetRecurringOrderDetails(ctx context.Context, algoID, algoOrderState string) (*RecurringOrderDeail, error) {
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	params := url.Values{}
	params.Set("algoId", algoID)
	if algoOrderState != "" {
		params.Set("state", algoOrderState)
	}
	var resp *RecurringOrderDeail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getRecurringBuyOrderDetailEPL, http.MethodGet, common.EncodeURLValues("tradingBot/recurring/orders-algo-details", params), nil, &resp, request.AuthenticatedRequest)
}

// GetRecurringSubOrders retrieves recurring buy sub orders
func (e *Exchange) GetRecurringSubOrders(ctx context.Context, algoID, orderID string, after, before time.Time, limit int64) ([]RecurringBuySubOrder, error) {
	if algoID == "" {
		return nil, errAlgoIDRequired
	}
	params := url.Values{}
	params.Set("algoId", algoID)
	if orderID != "" {
		params.Set("ordId", orderID)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []RecurringBuySubOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getRecurringBuySubOrdersEPL, http.MethodGet, common.EncodeURLValues("tradingBot/recurring/sub-orders", params), nil, &resp, request.AuthenticatedRequest)
}

// ****************************************** Earn **************************************************

// GetExistingLeadingPositions retrieves leading positions that are not closed
func (e *Exchange) GetExistingLeadingPositions(ctx context.Context, instrumentType, instrumentID string, before, after time.Time, limit int64) ([]PositionInfo, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []PositionInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getExistingLeadingPositionsEPL, http.MethodGet, common.EncodeURLValues("copytrading/current-subpositions", params), nil, &resp, request.AuthenticatedRequest)
}

// GetLeadingPositionsHistory leading trader retrieves the completed leading position of the last 3 months.
// Returns reverse chronological order with subPosId
func (e *Exchange) GetLeadingPositionsHistory(ctx context.Context, instrumentType, instrumentID string, before, after time.Time, limit int64) ([]PositionInfo, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []PositionInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeadingPositionHistoryEPL, http.MethodGet, common.EncodeURLValues("copytrading/subpositions-history", params), nil, &resp, request.AuthenticatedRequest)
}

// PlaceLeadingStopOrder holds leading trader sets TP/SL for the current leading position that are not closed
func (e *Exchange) PlaceLeadingStopOrder(ctx context.Context, arg *TPSLOrderParam) (*PositionIDInfo, error) {
	if *arg == (TPSLOrderParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.SubPositionID == "" {
		return nil, errSubPositionIDRequired
	}
	var resp *PositionIDInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, placeLeadingStopOrderEPL, http.MethodPost, "copytrading/algo-order", arg, &resp, request.AuthenticatedRequest)
}

// CloseLeadingPosition close a leading position once a time
func (e *Exchange) CloseLeadingPosition(ctx context.Context, arg *CloseLeadingPositionParam) (*PositionIDInfo, error) {
	if *arg == (CloseLeadingPositionParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.SubPositionID == "" {
		return nil, errSubPositionIDRequired
	}
	var resp *PositionIDInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, closeLeadingPositionEPL, http.MethodPost, "copytrading/close-subposition", arg, &resp, request.AuthenticatedRequest)
}

// GetLeadingInstrument retrieves leading instruments
func (e *Exchange) GetLeadingInstrument(ctx context.Context, instrumentType string) ([]LeadingInstrumentItem, error) {
	params := url.Values{}
	if instrumentType == "" {
		params.Set("instType", instrumentType)
	}
	var resp []LeadingInstrumentItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeadingInstrumentsEPL, http.MethodGet, common.EncodeURLValues("copytrading/instruments", params), nil, &resp, request.AuthenticatedRequest)
}

// AmendLeadingInstruments amend current leading instruments, need to set initial leading instruments while applying to become a leading trader.
// All non-leading contracts can't have position or pending orders for the current request when setting non-leading contracts as leading contracts
func (e *Exchange) AmendLeadingInstruments(ctx context.Context, instrumentID, instrumentType string) ([]LeadingInstrumentItem, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	var resp []LeadingInstrumentItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeadingInstrumentsEPL, http.MethodPost, "copytrading/set-instruments", &struct {
		InstrumentType string `json:"instType,omitempty"`
		InstrumentID   string `json:"instId"`
	}{
		InstrumentID:   instrumentID,
		InstrumentType: instrumentType,
	}, &resp, request.AuthenticatedRequest)
}

// GetProfitSharingDetails gets profits shared details for the last 3 months
func (e *Exchange) GetProfitSharingDetails(ctx context.Context, instrumentType string, before, after time.Time, limit int64) ([]ProfitSharingItem, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []ProfitSharingItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getProfitSharingLimitEPL, http.MethodGet, common.EncodeURLValues("copytrading/profit-sharing-details", params), nil, &resp, request.AuthenticatedRequest)
}

// GetTotalProfitSharing gets the total amount of profit shared since joining the platform.
// Instrument type 'SPOT' 'SWAP' It returns all types by default
func (e *Exchange) GetTotalProfitSharing(ctx context.Context, instrumentType string) ([]TotalProfitSharing, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	var resp []TotalProfitSharing
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTotalProfitSharingEPL, http.MethodGet, common.EncodeURLValues("copytrading/total-profit-sharing", params), nil, &resp, request.AuthenticatedRequest)
}

// GetUnrealizedProfitSharingDetails gets leading trader gets the profit sharing details that are expected to be shared in the next settlement cycle.
// The unrealized profit sharing details will update once there copy position is closed
func (e *Exchange) GetUnrealizedProfitSharingDetails(ctx context.Context, instrumentType string) ([]ProfitSharingItem, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	var resp []ProfitSharingItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTotalProfitSharingEPL, http.MethodGet, common.EncodeURLValues("copytrading/unrealized-profit-sharing-details", params), nil, &resp, request.AuthenticatedRequest)
}

// SetFirstCopySettings set first copy settings for the certain lead trader. You need to first copy settings after stopping copying
func (e *Exchange) SetFirstCopySettings(ctx context.Context, arg *FirstCopySettings) (*ResponseResult, error) {
	err := validateFirstCopySettings(arg)
	if err != nil {
		return nil, err
	}
	var resp *ResponseResult
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setFirstCopySettingsEPL, http.MethodPost, "copytrading/first-copy-settings", arg, &resp, request.AuthenticatedRequest)
}

// AmendCopySettings amends need to use this endpoint for amending copy settings
func (e *Exchange) AmendCopySettings(ctx context.Context, arg *FirstCopySettings) (*ResponseResult, error) {
	err := validateFirstCopySettings(arg)
	if err != nil {
		return nil, err
	}
	var resp *ResponseResult
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, amendFirstCopySettingsEPL, http.MethodPost, "copytrading/amend-copy-settings", arg, &resp, request.AuthenticatedRequest)
}

func validateFirstCopySettings(arg *FirstCopySettings) error {
	if *arg == (FirstCopySettings{}) {
		return common.ErrEmptyParams
	}
	if arg.UniqueCode == "" {
		return errUniqueCodeRequired
	}
	if arg.CopyInstrumentIDType == "" {
		return errCopyInstrumentIDTypeRequired
	}
	if arg.CopyTotalAmount <= 0 {
		return fmt.Errorf("%w, copyTotalAmount value %f", limits.ErrAmountBelowMin, arg.CopyTotalAmount)
	}
	if arg.SubPosCloseType == "" {
		return errSubPositionCloseTypeRequired
	}
	return nil
}

// StopCopying need to use this endpoint for amending copy settings
func (e *Exchange) StopCopying(ctx context.Context, arg *StopCopyingParameter) (*ResponseResult, error) {
	if *arg == (StopCopyingParameter{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.UniqueCode == "" {
		return nil, errUniqueCodeRequired
	}
	if arg.SubPositionCloseType == "" {
		return nil, errSubPositionCloseTypeRequired
	}
	var resp *ResponseResult
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, stopCopyingEPL, http.MethodPost, "copytrading/stop-copy-trading", arg, &resp, request.AuthenticatedRequest)
}

// GetCopySettings retrieve the copy settings about certain lead trader
func (e *Exchange) GetCopySettings(ctx context.Context, instrumentType, uniqueCode string) (*CopySetting, error) {
	if uniqueCode == "" {
		return nil, errUniqueCodeRequired
	}
	params := url.Values{}
	params.Set("uniqueCode", uniqueCode)
	if instrumentType == "" {
		params.Set("instType", instrumentType)
	}
	var resp *CopySetting
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getCopySettingsEPL, http.MethodGet, common.EncodeURLValues("copytrading/copy-settings", params), nil, &resp, request.AuthenticatedRequest)
}

// GetMultipleLeverages retrieve leverages that belong to the lead trader and you
func (e *Exchange) GetMultipleLeverages(ctx context.Context, marginMode, uniqueCode, instrumentID string) ([]Leverages, error) {
	if marginMode == "" {
		return nil, margin.ErrInvalidMarginType
	}
	if uniqueCode == "" {
		return nil, errUniqueCodeRequired
	}
	params := url.Values{}
	params.Set("mgnMode", marginMode)
	params.Set("uniqueCode", uniqueCode)
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	var resp []Leverages
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getMultipleLeveragesEPL, http.MethodGet, common.EncodeURLValues("copytrading/batch-leverage-info", params), nil, &resp, request.AuthenticatedRequest)
}

// SetMultipleLeverages set Multiple leverages
func (e *Exchange) SetMultipleLeverages(ctx context.Context, arg *SetLeveragesParam) (*SetMultipleLeverageResponse, error) {
	if *arg == (SetLeveragesParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.MarginMode == "" {
		return nil, margin.ErrInvalidMarginType
	}
	if arg.Leverage <= 0 {
		return nil, errInvalidLeverage
	}
	if arg.InstrumentID == "" {
		return nil, errMissingInstrumentID
	}
	var resp *SetMultipleLeverageResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, setBatchLeverageEPL, http.MethodPost, "copytrading/batch-set-leverage", arg, &resp, request.AuthenticatedRequest)
}

// GetMyLeadTraders retrieve my lead traders
func (e *Exchange) GetMyLeadTraders(ctx context.Context, instrumentType string) ([]CopyTradingLeadTrader, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	var resp []CopyTradingLeadTrader
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getMyLeadTradersEPL, http.MethodGet, common.EncodeURLValues("copytrading/current-lead-traders", params), nil, &resp, request.AuthenticatedRequest)
}

// GetHistoryLeadTraders retrieve my history lead traders
func (e *Exchange) GetHistoryLeadTraders(ctx context.Context, instrumentType, after, before string, limit int64) ([]CopyTradingLeadTrader, error) {
	params := url.Values{}
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if after != "" {
		params.Set("after", after)
	}
	if before != "" {
		params.Set("before", before)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []CopyTradingLeadTrader
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getMyLeadTradersEPL, http.MethodGet, common.EncodeURLValues("copytrading/lead-traders-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetLeadTradersRanks retrieves lead trader ranks
func (e *Exchange) GetLeadTradersRanks(ctx context.Context, req *LeadTraderRanksRequest) ([]LeadTradersRank, error) {
	params := url.Values{}
	if req.InstrumentType != "" {
		params.Set("instType", req.InstrumentType)
	}
	if req.SortType != "" {
		params.Set("sortType", req.SortType)
	}
	if req.HasVacancy {
		params.Set("state", "1")
	}
	if req.MinLeadDays != 0 {
		params.Set("minLeadDays", strconv.FormatUint(req.MinLeadDays, 10))
	}
	if req.MinAssets > 0 {
		params.Set("minAssets", strconv.FormatFloat(req.MinAssets, 'f', -1, 64))
	}
	if req.MaxAssets > 0 {
		params.Set("maxAssets", strconv.FormatFloat(req.MaxAssets, 'f', -1, 64))
	}
	if req.MinAssetsUnderManagement > 0 {
		params.Set("minAum", strconv.FormatFloat(req.MinAssetsUnderManagement, 'f', -1, 64))
	}
	if req.MaxAssetsUnderManagement > 0 {
		params.Set("maxAum", strconv.FormatFloat(req.MaxAssetsUnderManagement, 'f', -1, 64))
	}
	if req.DataVersion != 0 {
		params.Set("dataVer", strconv.FormatUint(req.DataVersion, 10))
	}
	if req.Page != 0 {
		params.Set("page", strconv.FormatUint(req.Page, 10))
	}
	if req.Limit != 0 {
		params.Set("limit", strconv.FormatUint(req.Limit, 10))
	}
	var resp []LeadTradersRank
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderRanksEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-lead-traders", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetWeeklyTraderProfitAndLoss retrieve lead trader weekly pnl. Results are returned in counter chronological order
func (e *Exchange) GetWeeklyTraderProfitAndLoss(ctx context.Context, instrumentType, uniqueCode string) ([]TraderWeeklyProfitAndLoss, error) {
	if uniqueCode == "" {
		return nil, errUniqueCodeRequired
	}
	params := url.Values{}
	params.Set("uniqueCode", uniqueCode)
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	var resp []TraderWeeklyProfitAndLoss
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderWeeklyPNLEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-weekly-pnl", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetDailyLeadTraderPNL retrieve lead trader daily pnl. Results are returned in counter chronological order.
// Last days "1": last 7 days  "2": last 30 days "3": last 90 days  "4": last 365 days
func (e *Exchange) GetDailyLeadTraderPNL(ctx context.Context, instrumentType, uniqueCode, lastDays string) ([]TraderWeeklyProfitAndLoss, error) {
	if uniqueCode == "" {
		return nil, errUniqueCodeRequired
	}
	if lastDays == "" {
		return nil, errLastDaysRequired
	}
	params := url.Values{}
	params.Set("uniqueCode", uniqueCode)
	params.Set("lastDays", lastDays)
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	var resp []TraderWeeklyProfitAndLoss
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderDailyPNLEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-weekly-pnl", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetLeadTraderStats retrieves key data related to lead trader performance
func (e *Exchange) GetLeadTraderStats(ctx context.Context, instrumentType, uniqueCode, lastDays string) ([]LeadTraderStat, error) {
	if uniqueCode == "" {
		return nil, errUniqueCodeRequired
	}
	if lastDays == "" {
		return nil, errLastDaysRequired
	}
	params := url.Values{}
	params.Set("uniqueCode", uniqueCode)
	params.Set("lastDays", lastDays)
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	var resp []LeadTraderStat
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderStatsEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-stats", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetLeadTraderCurrencyPreferences retrieves the most frequently traded crypto of this lead trader. Results are sorted by ratio from large to small
func (e *Exchange) GetLeadTraderCurrencyPreferences(ctx context.Context, instrumentType, uniqueCode, lastDays string) ([]LeadTraderCurrencyPreference, error) {
	if uniqueCode == "" {
		return nil, errUniqueCodeRequired
	}
	if lastDays == "" {
		return nil, errLastDaysRequired
	}
	params := url.Values{}
	params.Set("uniqueCode", uniqueCode)
	params.Set("lastDays", lastDays)
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	var resp []LeadTraderCurrencyPreference
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderCurrencyPreferencesEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-preference-currency", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetLeadTraderCurrentLeadPositions get current leading positions of lead trader
// Instrument type "SPOT" "SWAP"
func (e *Exchange) GetLeadTraderCurrentLeadPositions(ctx context.Context, instrumentType, uniqueCode, afterSubPositionID,
	beforeSubPositionID string, limit int64,
) ([]LeadTraderCurrentLeadPosition, error) {
	if uniqueCode == "" {
		return nil, errUniqueCodeRequired
	}
	params := url.Values{}
	params.Set("uniqueCode", uniqueCode)
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if afterSubPositionID != "" {
		params.Set("after", afterSubPositionID)
	}
	if beforeSubPositionID != "" {
		params.Set("before", beforeSubPositionID)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []LeadTraderCurrentLeadPosition
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTraderCurrentLeadPositionsEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-current-subpositions", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetLeadTraderLeadPositionHistory retrieve the lead trader completed leading position of the last 3 months. Returns reverse chronological order with subPosId
func (e *Exchange) GetLeadTraderLeadPositionHistory(ctx context.Context, instrumentType, uniqueCode, afterSubPositionID, beforeSubPositionID string, limit int64) ([]LeadPosition, error) {
	if uniqueCode == "" {
		return nil, errUniqueCodeRequired
	}
	params := url.Values{}
	params.Set("uniqueCode", uniqueCode)
	if instrumentType != "" {
		params.Set("instType", instrumentType)
	}
	if afterSubPositionID != "" {
		params.Set("after", afterSubPositionID)
	}
	if beforeSubPositionID != "" {
		params.Set("before", beforeSubPositionID)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []LeadPosition
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLeadTraderLeadPositionHistoryEPL, http.MethodGet, common.EncodeURLValues("copytrading/public-subpositions-history", params), nil, &resp, request.UnauthenticatedRequest)
}

// ****************************************** Earn **************************************************

// GetOffers retrieves list of offers for different protocols
func (e *Exchange) GetOffers(ctx context.Context, productID, protocolType string, ccy currency.Code) ([]Offer, error) {
	params := url.Values{}
	if productID != "" {
		params.Set("productId", productID)
	}
	if protocolType != "" {
		params.Set("protocolType", protocolType)
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	var resp []Offer
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOfferEPL, http.MethodGet, common.EncodeURLValues("finance/staking-defi/offers", params), nil, &resp, request.AuthenticatedRequest)
}

// Purchase invest on specific product
func (e *Exchange) Purchase(ctx context.Context, arg *PurchaseRequestParam) (*OrderIDResponse, error) {
	if arg == nil {
		return nil, common.ErrNilPointer
	}
	if arg.ProductID == "" {
		return nil, fmt.Errorf("%w, missing product id", errMissingRequiredParameter)
	}
	for x := range arg.InvestData {
		if arg.InvestData[x].Currency.IsEmpty() {
			return nil, fmt.Errorf("%w, currency information for investment is required", currency.ErrCurrencyCodeEmpty)
		}
		if arg.InvestData[x].Amount <= 0 {
			return nil, limits.ErrAmountBelowMin
		}
	}
	var resp *OrderIDResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, purchaseEPL, http.MethodPost, "finance/staking-defi/purchase", &arg, &resp, request.AuthenticatedRequest)
}

// Redeem redemption of investment
func (e *Exchange) Redeem(ctx context.Context, arg *RedeemRequestParam) (*OrderIDResponse, error) {
	if *arg == (RedeemRequestParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.OrderID == "" {
		return nil, fmt.Errorf("%w, missing 'orderId'", order.ErrOrderIDNotSet)
	}
	if arg.ProtocolType != "staking" && arg.ProtocolType != "defi" {
		return nil, errInvalidProtocolType
	}
	var resp *OrderIDResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, redeemEPL, http.MethodPost, "finance/staking-defi/redeem", &arg, &resp, request.AuthenticatedRequest)
}

// CancelPurchaseOrRedemption cancels Purchases or Redemptions
// after cancelling, returning funds will go to the funding account
func (e *Exchange) CancelPurchaseOrRedemption(ctx context.Context, arg *CancelFundingParam) (*OrderIDResponse, error) {
	if *arg == (CancelFundingParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.OrderID == "" {
		return nil, fmt.Errorf("%w, missing 'orderId'", order.ErrOrderIDNotSet)
	}
	if arg.ProtocolType != "staking" && arg.ProtocolType != "defi" {
		return nil, errInvalidProtocolType
	}
	var resp *OrderIDResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelPurchaseOrRedemptionEPL, http.MethodPost, "finance/staking-defi/cancel", &arg, &resp, request.AuthenticatedRequest)
}

// GetEarnActiveOrders retrieves active orders
func (e *Exchange) GetEarnActiveOrders(ctx context.Context, productID, protocolType, state string, ccy currency.Code) ([]ActiveFundingOrder, error) {
	params := url.Values{}
	if productID != "" {
		params.Set("productId", productID)
	}

	if protocolType != "" {
		// protocol type 'staking' and 'defi' is allowed by default
		if protocolType != "staking" && protocolType != "defi" {
			return nil, errInvalidProtocolType
		}
		params.Set("protocolType", protocolType)
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if state != "" {
		params.Set("state", state)
	}
	var resp []ActiveFundingOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getEarnActiveOrdersEPL, http.MethodGet, common.EncodeURLValues("finance/staking-defi/orders-active", params), nil, &resp, request.AuthenticatedRequest)
}

// GetFundingOrderHistory retrieves funding order history
// valid protocol types are 'staking' and 'defi'
func (e *Exchange) GetFundingOrderHistory(ctx context.Context, productID, protocolType string, ccy currency.Code, after, before time.Time, limit int64) ([]ActiveFundingOrder, error) {
	params := url.Values{}
	if productID != "" {
		params.Set("productId", productID)
	}
	if protocolType != "" {
		params.Set("protocolType", protocolType)
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []ActiveFundingOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFundingOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("finance/staking-defi/orders-history", params), nil, &resp, request.AuthenticatedRequest)
}

// **************************************************************** ETH Staking ****************************************************************

// GetProductInfo retrieves ETH staking products
func (e *Exchange) GetProductInfo(ctx context.Context) (*ProductInfo, error) {
	var resp *ProductInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getProductInfoEPL, http.MethodGet,
		"finance/staking-defi/eth/product-info", nil, &resp, request.AuthenticatedRequest)
}

// PurchaseETHStaking staking ETH for BETH
// Only the assets in the funding account can be used
func (e *Exchange) PurchaseETHStaking(ctx context.Context, amount float64) error {
	if amount <= 0 {
		return limits.ErrAmountBelowMin
	}
	var resp []string
	return e.SendHTTPRequest(ctx, exchange.RestSpot, purchaseETHStakingEPL, http.MethodPost, "finance/staking-defi/eth/purchase", map[string]string{"amt": strconv.FormatFloat(amount, 'f', -1, 64)}, &resp, request.AuthenticatedRequest)
}

// RedeemETHStaking only the assets in the funding account can be used. If your BETH is in your trading account, you can make funding transfer first
func (e *Exchange) RedeemETHStaking(ctx context.Context, amount float64) error {
	if amount <= 0 {
		return limits.ErrAmountBelowMin
	}
	var resp []string
	return e.SendHTTPRequest(ctx, exchange.RestSpot, redeemETHStakingEPL, http.MethodPost, "finance/staking-defi/eth/redeem",
		map[string]string{"amt": strconv.FormatFloat(amount, 'f', -1, 64)}, &resp, request.AuthenticatedRequest)
}

// GetBETHAssetsBalance balance is a snapshot summarized all BETH assets in trading and funding accounts. Also, the snapshot updates hourly
func (e *Exchange) GetBETHAssetsBalance(ctx context.Context) (*BETHAssetsBalance, error) {
	var resp *BETHAssetsBalance
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBETHBalanceEPL, http.MethodGet,
		"finance/staking-defi/eth/balance", nil, &resp, request.AuthenticatedRequest)
}

// GetPurchaseAndRedeemHistory retrieves purchase and redeem history
// kind possible values are 'purchase' and 'redeem'
// Status 'pending' 'success' 'failed'
func (e *Exchange) GetPurchaseAndRedeemHistory(ctx context.Context, kind, status string, after, before time.Time, limit int64) ([]PurchaseRedeemHistory, error) {
	if kind == "" {
		return nil, fmt.Errorf("%w, possible values are 'purchase' and 'redeem'", errLendingTermIsRequired)
	}
	params := url.Values{}
	params.Set("type", kind)
	if status != "" {
		params.Set("status", status)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []PurchaseRedeemHistory
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getPurchaseRedeemHistoryEPL, http.MethodGet,
		common.EncodeURLValues("finance/staking-defi/eth/purchase-redeem-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetAPYHistory retrieves Annual percentage yield(APY) history
func (e *Exchange) GetAPYHistory(ctx context.Context, days int64) ([]APYItem, error) {
	if days == 0 || days > 365 {
		return nil, errors.New("field days is required; possible values from 1 to 365")
	}
	var resp []APYItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAPYHistoryEPL, http.MethodGet, fmt.Sprintf("finance/staking-defi/eth/apy-history?days=%d", days), nil, &resp, request.UnauthenticatedRequest)
}

// GetTickers retrieves the latest price snapshots best bid/ ask price, and trading volume in the last 24 hours
func (e *Exchange) GetTickers(ctx context.Context, instType, uly, instFamily string) ([]TickerResponse, error) {
	if instType == "" {
		return nil, errInvalidInstrumentType
	}
	params := url.Values{}
	params.Set("instType", instType)
	if instFamily != "" {
		params.Set("instFamily", instFamily)
	}
	if uly != "" {
		params.Set("uly", uly)
	}
	var response []TickerResponse
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getTickersEPL, http.MethodGet, common.EncodeURLValues("market/tickers", params), nil, &response, request.UnauthenticatedRequest)
}

// GetTicker retrieves the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours
func (e *Exchange) GetTicker(ctx context.Context, instrumentID string) (*TickerResponse, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	var resp *TickerResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTickerEPL, http.MethodGet, common.EncodeURLValues("market/ticker", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetPremiumHistory returns premium data in the past 6 months
func (e *Exchange) GetPremiumHistory(ctx context.Context, instrumentID string, after, before time.Time, limit int64) ([]PremiumInfo, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	if !after.IsZero() && !before.IsZero() {
		err := common.StartEndTimeCheck(after, before)
		if err != nil {
			return nil, err
		}
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []PremiumInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getPremiumHistoryEPL, http.MethodGet, common.EncodeURLValues("public/premium-history", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetIndexTickers Retrieves index tickers
func (e *Exchange) GetIndexTickers(ctx context.Context, quoteCurrency currency.Code, instID string) ([]IndexTicker, error) {
	if instID == "" && quoteCurrency.IsEmpty() {
		return nil, fmt.Errorf("%w, QuoteCurrency or InstrumentID is required", errEitherInstIDOrCcyIsRequired)
	}
	params := url.Values{}
	if !quoteCurrency.IsEmpty() {
		params.Set("quoteCcy", quoteCurrency.String())
	}
	if instID != "" {
		params.Set("instId", instID)
	}
	var resp []IndexTicker
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getIndexTickersEPL, http.MethodGet, common.EncodeURLValues("market/index-tickers", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetInstrumentTypeFromAssetItem returns a string representation of asset.Item; which is an equivalent term for InstrumentType in Okx exchange
func GetInstrumentTypeFromAssetItem(a asset.Item) string {
	switch a {
	case asset.PerpetualSwap:
		return instTypeSwap
	case asset.Options:
		return instTypeOption
	default:
		return strings.ToUpper(a.String())
	}
}

// GetUnderlying returns the instrument ID for the corresponding asset pairs and asset type( Instrument Type )
func (e *Exchange) GetUnderlying(pair currency.Pair, a asset.Item) (string, error) {
	if !pair.IsPopulated() {
		return "", currency.ErrCurrencyPairsEmpty
	}
	format, err := e.GetPairFormat(a, false)
	if err != nil {
		return "", err
	}
	return pair.Base.String() + format.Delimiter + pair.Quote.String(), nil
}

// GetPairFromInstrumentID returns a currency pair give an instrument ID and asset Item, which represents the instrument type
func (e *Exchange) GetPairFromInstrumentID(instrumentID string) (currency.Pair, error) {
	codes := strings.Split(instrumentID, currency.DashDelimiter)
	if len(codes) >= 2 {
		instrumentID = codes[0] + currency.DashDelimiter + strings.Join(codes[1:], currency.DashDelimiter)
	}
	return currency.NewPairFromString(instrumentID)
}

// GetOrderBookDepth returns the recent order asks and bids before specified timestamp
func (e *Exchange) GetOrderBookDepth(ctx context.Context, instrumentID string, depth int64) (*OrderBookResponseDetail, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	if depth > 0 {
		params.Set("sz", strconv.FormatInt(depth, 10))
	}
	var resp *OrderBookResponseDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOrderBookEPL, http.MethodGet, common.EncodeURLValues("market/books", params), nil, &resp, request.UnauthenticatedRequest)
}

// IntervalFromString returns a kline.Interval instance from string
func IntervalFromString(interval kline.Interval, appendUTC bool) string {
	switch interval {
	case kline.OneMin:
		return "1m"
	case kline.ThreeMin:
		return "3m"
	case kline.FiveMin:
		return "5m"
	case kline.FifteenMin:
		return "15m"
	case kline.ThirtyMin:
		return "30m"
	case kline.OneHour:
		return "1H"
	case kline.TwoHour:
		return "2H"
	case kline.FourHour:
		return "4H"
	}

	duration := ""
	switch interval {
	case kline.SixHour: // NOTE: Cases here and below can either be local Hong Kong time or UTC time.
		duration = "6H"
	case kline.TwelveHour:
		duration = "12H"
	case kline.OneDay:
		duration = "1D"
	case kline.TwoDay:
		duration = "2D"
	case kline.ThreeDay:
		duration = "3D"
	case kline.FiveDay:
		duration = "5D"
	case kline.OneWeek:
		duration = "1W"
	case kline.OneMonth:
		duration = "1M"
	case kline.ThreeMonth:
		duration = "3M"
	case kline.SixMonth:
		duration = "6M"
	case kline.OneYear:
		duration = "1Y"
	default:
		return duration
	}
	if appendUTC {
		duration += "utc"
	}
	return duration
}

// GetCandlesticks retrieve the candlestick charts. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar
func (e *Exchange) GetCandlesticks(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64) ([]CandleStick, error) {
	return e.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, "market/candles", getCandlesticksEPL)
}

// GetCandlesticksHistory retrieve history candlestick charts from recent years
func (e *Exchange) GetCandlesticksHistory(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64) ([]CandleStick, error) {
	return e.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, "market/history-candles", getCandlestickHistoryEPL)
}

// GetIndexCandlesticks retrieve the candlestick charts of the index. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar.
// the response is a list of Candlestick data
func (e *Exchange) GetIndexCandlesticks(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64) ([]CandleStick, error) {
	return e.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, "market/index-candles", getIndexCandlesticksEPL)
}

// GetMarkPriceCandlesticks retrieve the candlestick charts of mark price. This endpoint can retrieve the latest 1,440 data entries. Charts are returned in groups based on the requested bar
func (e *Exchange) GetMarkPriceCandlesticks(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64) ([]CandleStick, error) {
	return e.GetCandlestickData(ctx, instrumentID, interval, before, after, limit, "market/mark-price-candles", getCandlestickHistoryEPL)
}

// GetHistoricIndexCandlesticksHistory retrieve the candlestick charts of the index from recent years
func (e *Exchange) GetHistoricIndexCandlesticksHistory(ctx context.Context, instrumentID string, after, before time.Time, bar kline.Interval, limit int64) ([]CandlestickHistoryItem, error) {
	return e.getHistoricCandlesticks(ctx, instrumentID, "market/history-index-candles", after, before, bar, limit, getIndexCandlesticksHistoryEPL)
}

// GetMarkPriceCandlestickHistory retrieve the candlestick charts of the mark price from recent years
func (e *Exchange) GetMarkPriceCandlestickHistory(ctx context.Context, instrumentID string, after, before time.Time, bar kline.Interval, limit int64) ([]CandlestickHistoryItem, error) {
	return e.getHistoricCandlesticks(ctx, instrumentID, "market/history-mark-price-candles", after, before, bar, limit, getMarkPriceCandlesticksHistoryEPL)
}

func (e *Exchange) getHistoricCandlesticks(ctx context.Context, instrumentID, path string, after, before time.Time, bar kline.Interval, limit int64, epl request.EndpointLimit) ([]CandlestickHistoryItem, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	barString := IntervalFromString(bar, false)
	if barString != "" {
		params.Set("bar", barString)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []CandlestickHistoryItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, epl, http.MethodGet, common.EncodeURLValues(path, params), nil, &resp, request.UnauthenticatedRequest)
}

// GetEconomicCalendarData retrieves the macro-economic calendar data within 3 months. Historical data from 3 months ago is only available to users with trading fee tier VIP1 and above
func (e *Exchange) GetEconomicCalendarData(ctx context.Context, region, importance string, before, after time.Time, limit int64) ([]EconomicCalendar, error) {
	params := url.Values{}
	if region != "" {
		params.Set("region", region)
	}
	if importance != "" {
		params.Set("importance", importance)
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []EconomicCalendar
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getEconomicCalendarEPL, http.MethodGet, common.EncodeURLValues("public/economic-calendar", params), nil, &resp, request.AuthenticatedRequest)
}

// GetCandlestickData handles fetching the data for both the default GetCandlesticks, GetCandlesticksHistory, and GetIndexCandlesticks() methods
func (e *Exchange) GetCandlestickData(ctx context.Context, instrumentID string, interval kline.Interval, before, after time.Time, limit int64, route string, rateLimit request.EndpointLimit) ([]CandleStick, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	params.Set("limit", strconv.FormatInt(limit, 10))
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	bar := IntervalFromString(interval, true)
	if bar != "" {
		params.Set("bar", bar)
	}
	var resp []CandleStick
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, rateLimit, http.MethodGet, common.EncodeURLValues(route, params), nil, &resp, request.UnauthenticatedRequest)
}

// GetTrades retrieve the recent transactions of an instrument
func (e *Exchange) GetTrades(ctx context.Context, instrumentID string, limit int64) ([]TradeResponse, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []TradeResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTradesRequestEPL, http.MethodGet, common.EncodeURLValues("market/trades", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetTradesHistory retrieves the recent transactions of an instrument from the last 3 months with pagination
func (e *Exchange) GetTradesHistory(ctx context.Context, instrumentID, before, after string, limit int64) ([]TradeResponse, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	if before != "" {
		params.Set("before", before)
	}
	if after != "" {
		params.Set("after", after)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []TradeResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTradesHistoryEPL, http.MethodGet, common.EncodeURLValues("market/history-trades", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetOptionTradesByInstrumentFamily retrieve the recent transactions of an instrument under same instFamily. The maximum is 100
func (e *Exchange) GetOptionTradesByInstrumentFamily(ctx context.Context, instrumentFamily string) ([]InstrumentFamilyTrade, error) {
	if instrumentFamily == "" {
		return nil, errInstrumentFamilyRequired
	}
	var resp []InstrumentFamilyTrade
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, optionInstrumentTradeFamilyEPL, http.MethodGet, "market/option/instrument-family-trades?instFamily="+instrumentFamily, nil, &resp, request.UnauthenticatedRequest)
}

// GetOptionTrades retrieves option trades
// Option type, 'C': Call 'P': put
func (e *Exchange) GetOptionTrades(ctx context.Context, instrumentID, instrumentFamily, optionType string) ([]OptionTrade, error) {
	if instrumentID == "" && instrumentFamily == "" {
		return nil, errInstrumentIDorFamilyRequired
	}
	params := url.Values{}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	if optionType != "" {
		params.Set("optType", optionType)
	}
	var resp []OptionTrade
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, optionTradesEPL, http.MethodGet, common.EncodeURLValues("public/option-trades", params), nil, &resp, request.UnauthenticatedRequest)
}

// Get24HTotalVolume The 24-hour trading volume is calculated on a rolling basis, using USD as the pricing unit
func (e *Exchange) Get24HTotalVolume(ctx context.Context) (*TradingVolumeIn24HR, error) {
	var resp *TradingVolumeIn24HR
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, get24HTotalVolumeEPL, http.MethodGet, "market/platform-24-volume", nil, &resp, request.UnauthenticatedRequest)
}

// GetOracle Get the crypto price of signing using Open Oracle smart contract
func (e *Exchange) GetOracle(ctx context.Context) (*OracleSmartContractResponse, error) {
	var resp *OracleSmartContractResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOracleEPL, http.MethodGet, "market/open-oracle", nil, &resp, request.UnauthenticatedRequest)
}

// GetExchangeRate this interface provides the average exchange rate data for 2 weeks
// from USD to CNY
func (e *Exchange) GetExchangeRate(ctx context.Context) (*UsdCnyExchangeRate, error) {
	var resp *UsdCnyExchangeRate
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getExchangeRateRequestEPL, http.MethodGet, "market/exchange-rate", nil, &resp, request.UnauthenticatedRequest)
}

// GetIndexComponents returns the index component information data on the market
func (e *Exchange) GetIndexComponents(ctx context.Context, index string) (*IndexComponent, error) {
	if index == "" {
		return nil, errIndexComponentNotFound
	}
	params := url.Values{}
	params.Set("index", index)
	var resp *IndexComponent
	err := e.SendHTTPRequest(ctx, exchange.RestSpot, getIndexComponentsEPL, http.MethodGet, common.EncodeURLValues("market/index-components", params), nil, &resp, request.UnauthenticatedRequest)
	if err != nil {
		return nil, err
	}
	if resp == nil {
		return nil, errIndexComponentNotFound
	}
	return resp, nil
}

// GetBlockTickers retrieves the latest block trading volume in the last 24 hours.
// Instrument Type Is Mandatory, and Underlying is Optional
func (e *Exchange) GetBlockTickers(ctx context.Context, instrumentType, underlying string) ([]BlockTicker, error) {
	instrumentType = strings.ToUpper(instrumentType)
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", instrumentType)
	if underlying != "" {
		params.Set("uly", underlying)
	}
	var resp []BlockTicker
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBlockTickersEPL, http.MethodGet, common.EncodeURLValues("market/block-tickers", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetBlockTicker retrieves the latest block trading volume in the last 24 hours
func (e *Exchange) GetBlockTicker(ctx context.Context, instrumentID string) (*BlockTicker, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	var resp *BlockTicker
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBlockTickersEPL, http.MethodGet, common.EncodeURLValues("market/block-ticker", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetPublicBlockTrades retrieves the recent block trading transactions of an instrument. Descending order by tradeId
func (e *Exchange) GetPublicBlockTrades(ctx context.Context, instrumentID string) ([]BlockTrade, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	var resp []BlockTrade
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getBlockTradesEPL, http.MethodGet, common.EncodeURLValues("public/block-trades", params), nil, &resp, request.UnauthenticatedRequest)
}

// ********************************************* Spread Trading ***********************************************

// Spread Trading: As Introduced in the Okx exchange. URL: https://www.okx.com/docs-v5/en/#spread-trading-introduction

// PlaceSpreadOrder places new spread order
func (e *Exchange) PlaceSpreadOrder(ctx context.Context, arg *SpreadOrderParam) (*SpreadOrderResponse, error) {
	err := e.validatePlaceSpreadOrderParam(arg)
	if err != nil {
		return nil, err
	}
	var resp *SpreadOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, placeSpreadOrderEPL, http.MethodPost, "sprd/order", arg, &resp, request.AuthenticatedRequest)
}

func (e *Exchange) validatePlaceSpreadOrderParam(arg *SpreadOrderParam) error {
	if *arg == (SpreadOrderParam{}) {
		return common.ErrEmptyParams
	}
	if arg.SpreadID == "" {
		return fmt.Errorf("%w, spread ID missing", errMissingInstrumentID)
	}
	if arg.OrderType == "" {
		return fmt.Errorf("%w spread order type is required", order.ErrTypeIsInvalid)
	}
	if arg.Size <= 0 {
		return limits.ErrAmountBelowMin
	}
	if arg.Price <= 0 {
		return limits.ErrPriceBelowMin
	}
	arg.Side = strings.ToLower(arg.Side)
	switch arg.Side {
	case order.Buy.Lower(), order.Sell.Lower():
	default:
		return fmt.Errorf("%w %s", order.ErrSideIsInvalid, arg.Side)
	}
	return nil
}

// CancelSpreadOrder cancels an incomplete spread order
func (e *Exchange) CancelSpreadOrder(ctx context.Context, orderID, clientOrderID string) (*SpreadOrderResponse, error) {
	if orderID == "" && clientOrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	arg := make(map[string]string)
	if orderID != "" {
		arg["ordId"] = orderID
	}
	if clientOrderID != "" {
		arg["clOrdId"] = clientOrderID
	}
	var resp *SpreadOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelSpreadOrderEPL, http.MethodPost, "sprd/cancel-order", arg, &resp, request.AuthenticatedRequest)
}

// CancelAllSpreadOrders cancels all spread orders and return success message
// spreadID is optional
// the function returns success status and error message
func (e *Exchange) CancelAllSpreadOrders(ctx context.Context, spreadID string) (bool, error) {
	arg := make(map[string]string, 1)
	if spreadID != "" {
		arg["sprdId"] = spreadID
	}
	resp := &struct {
		Result bool `json:"result"`
	}{}
	return resp.Result, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllSpreadOrderEPL, http.MethodPost, "sprd/mass-cancel", arg, resp, request.AuthenticatedRequest)
}

// AmendSpreadOrder amends incomplete spread order
func (e *Exchange) AmendSpreadOrder(ctx context.Context, arg *AmendSpreadOrderParam) (*SpreadOrderResponse, error) {
	if *arg == (AmendSpreadOrderParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.OrderID == "" && arg.ClientOrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	if arg.NewPrice == 0 && arg.NewSize == 0 {
		return nil, errSizeOrPriceIsRequired
	}
	var resp *SpreadOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, amendSpreadOrderEPL, http.MethodPost, "sprd/amend-order", arg, &resp, request.AuthenticatedRequest)
}

// GetSpreadOrderDetails retrieves spread order details
func (e *Exchange) GetSpreadOrderDetails(ctx context.Context, orderID, clientOrderID string) (*SpreadOrder, error) {
	if orderID == "" && clientOrderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	params := url.Values{}
	if orderID != "" {
		params.Set("ordId", orderID)
	}
	if clientOrderID != "" {
		params.Set("clOrdId", clientOrderID)
	}
	var resp *SpreadOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadOrderDetailsEPL, http.MethodGet, common.EncodeURLValues("sprd/order", params), nil, &resp, request.AuthenticatedRequest)
}

// GetActiveSpreadOrders retrieves list of incomplete spread orders
func (e *Exchange) GetActiveSpreadOrders(ctx context.Context, spreadID, orderType, state, beginID, endID string, limit int64) ([]SpreadOrder, error) {
	params := url.Values{}
	if spreadID != "" {
		params.Set("sprdId", spreadID)
	}
	if orderType != "" {
		params.Set("ordType", orderType)
	}
	if state != "" {
		params.Set("state", state)
	}
	if beginID != "" {
		params.Set("beginId", beginID)
	}
	if endID != "" {
		params.Set("endId", endID)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []SpreadOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getActiveSpreadOrdersEPL, http.MethodGet, common.EncodeURLValues("sprd/orders-pending", params), nil, &resp, request.AuthenticatedRequest)
}

// GetCompletedSpreadOrdersLast7Days retrieve the completed order data for the last 7 days, and the incomplete orders (filledSz =0 & state = canceled) that have been canceled are only reserved for 2 hours. Results are returned in counter chronological order
func (e *Exchange) GetCompletedSpreadOrdersLast7Days(ctx context.Context, spreadID, orderType, state, beginID, endID string, begin, end time.Time, limit int64) ([]SpreadOrder, error) {
	params := url.Values{}
	if spreadID != "" {
		params.Set("sprdId", spreadID)
	}
	if orderType != "" {
		params.Set("ordType", orderType)
	}
	if state != "" {
		params.Set("state", state)
	}
	if beginID != "" {
		params.Set("beginId", beginID)
	}
	if endID != "" {
		params.Set("endId", endID)
	}
	if !begin.IsZero() {
		params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	if !end.IsZero() {
		params.Set("end", strconv.FormatInt(end.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []SpreadOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadOrders7DaysEPL, http.MethodGet, common.EncodeURLValues("sprd/orders-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetSpreadTradesOfLast7Days retrieve historical transaction details for the last 7 days. Results are returned in counter chronological order
func (e *Exchange) GetSpreadTradesOfLast7Days(ctx context.Context, spreadID, tradeID, orderID, beginID, endID string, begin, end time.Time, limit int64) ([]SpreadTrade, error) {
	params := url.Values{}
	if spreadID != "" {
		params.Set("sprdId", spreadID)
	}
	if tradeID != "" {
		params.Set("tradeId", tradeID)
	}
	if orderID != "" {
		params.Set("ordId", orderID)
	}
	if beginID != "" {
		params.Set("beginId", beginID)
	}
	if endID != "" {
		params.Set("endId", endID)
	}
	if !begin.IsZero() {
		params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	if !end.IsZero() {
		params.Set("end", strconv.FormatInt(end.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []SpreadTrade
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadOrderTradesEPL, http.MethodGet, common.EncodeURLValues("sprd/trades", params), nil, &resp, request.AuthenticatedRequest)
}

// GetPublicSpreads retrieve all available spreads based on the request parameters
func (e *Exchange) GetPublicSpreads(ctx context.Context, baseCurrency, instrumentID, spreadID, state string) ([]SpreadInstrument, error) {
	params := url.Values{}
	if baseCurrency != "" {
		params.Set("baseCcy", baseCurrency)
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if spreadID != "" {
		params.Set("sprdId", spreadID)
	}
	if state != "" {
		params.Set("state", state)
	}
	var resp []SpreadInstrument
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadsEPL, http.MethodGet, common.EncodeURLValues("sprd/spreads", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetPublicSpreadOrderBooks retrieve the order book of the spread
func (e *Exchange) GetPublicSpreadOrderBooks(ctx context.Context, spreadID string, orderbookSize int64) ([]SpreadOrderbook, error) {
	if spreadID == "" {
		return nil, fmt.Errorf("%w, spread ID missing", errMissingInstrumentID)
	}
	params := url.Values{}
	params.Set("sprdId", spreadID)
	if orderbookSize != 0 {
		params.Set("size", strconv.FormatInt(orderbookSize, 10))
	}
	var resp []SpreadOrderbook
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadOrderbookEPL, http.MethodGet, common.EncodeURLValues("sprd/books", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetPublicSpreadTickers retrieve the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours
func (e *Exchange) GetPublicSpreadTickers(ctx context.Context, spreadID string) ([]SpreadTicker, error) {
	if spreadID == "" {
		return nil, fmt.Errorf("%w, spread ID is required", errMissingInstrumentID)
	}
	params := url.Values{}
	params.Set("sprdId", spreadID)
	var resp []SpreadTicker
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadTickerEPL, http.MethodGet, common.EncodeURLValues("sprd/ticker", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetPublicSpreadTrades retrieve the recent transactions of an instrument (at most 500 records per request). Results are returned in counter chronological order
func (e *Exchange) GetPublicSpreadTrades(ctx context.Context, spreadID string) ([]SpreadPublicTradeItem, error) {
	params := url.Values{}
	if spreadID != "" {
		params.Set("sprdId", spreadID)
	}
	var resp []SpreadPublicTradeItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadPublicTradesEPL, http.MethodGet, common.EncodeURLValues("sprd/public-trades", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetSpreadCandlesticks retrieves candlestick charts for a given spread instrument
func (e *Exchange) GetSpreadCandlesticks(ctx context.Context, spreadID string, interval kline.Interval, before, after time.Time, limit uint64) ([]SpreadCandlestick, error) {
	if spreadID == "" {
		return nil, fmt.Errorf("%w, spread ID is required", errMissingInstrumentID)
	}
	params := url.Values{}
	params.Set("sprdId", spreadID)
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if bar := IntervalFromString(interval, true); bar != "" {
		params.Set("bar", bar)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatUint(limit, 10))
	}
	var resp []SpreadCandlestick
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadCandlesticksEPL, http.MethodGet, common.EncodeURLValues("market/sprd-candles", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetSpreadCandlesticksHistory retrieves candlestick chart history for a given spread instrument for a period of up to 3 months
func (e *Exchange) GetSpreadCandlesticksHistory(ctx context.Context, spreadID string, interval kline.Interval, before, after time.Time, limit uint64) ([]SpreadCandlestick, error) {
	if spreadID == "" {
		return nil, fmt.Errorf("%w, spread ID is required", errMissingInstrumentID)
	}
	params := url.Values{}
	params.Set("sprdId", spreadID)
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if bar := IntervalFromString(interval, true); bar != "" {
		params.Set("bar", bar)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatUint(limit, 10))
	}
	var resp []SpreadCandlestick
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSpreadCandlesticksHistoryEPL, http.MethodGet, common.EncodeURLValues("market/sprd-history-candles", params), nil, &resp, request.UnauthenticatedRequest)
}

// CancelAllSpreadOrdersAfterCountdown cancel all pending orders after the countdown timeout. Only applicable to spread trading
func (e *Exchange) CancelAllSpreadOrdersAfterCountdown(ctx context.Context, timeoutDuration int64) (*SpreadOrderCancellationResponse, error) {
	if (timeoutDuration != 0) && (timeoutDuration < 10 || timeoutDuration > 120) {
		return nil, fmt.Errorf("%w, range of value can be 0, [10, 120]", errCountdownTimeoutRequired)
	}
	arg := &struct {
		TimeOut int64 `json:"timeOut,string"`
	}{
		TimeOut: timeoutDuration,
	}
	var resp *SpreadOrderCancellationResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelAllSpreadOrdersAfterEPL, http.MethodPost, "sprd/cancel-all-after", arg, &resp, request.AuthenticatedRequest)
}

/************************************ Public Data Endpoints *************************************************/

// GetInstruments retrieve a list of instruments with open contracts
func (e *Exchange) GetInstruments(ctx context.Context, arg *InstrumentsFetchParams) ([]Instrument, error) {
	if arg.InstrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	if arg.InstrumentType == instTypeOption &&
		arg.InstrumentFamily == "" && arg.Underlying == "" {
		return nil, errInstrumentFamilyOrUnderlyingRequired
	}
	params := url.Values{}
	arg.InstrumentType = strings.ToUpper(arg.InstrumentType)
	params.Set("instType", arg.InstrumentType)
	if arg.Underlying != "" {
		params.Set("uly", arg.Underlying)
	}
	if arg.InstrumentFamily != "" {
		params.Set("instFamily", arg.InstrumentFamily)
	}
	if arg.InstrumentID != "" {
		params.Set("instId", arg.InstrumentID)
	}
	var resp []Instrument
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getInstrumentsEPL, http.MethodGet,
		common.EncodeURLValues("public/instruments", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetDeliveryHistory retrieves the estimated delivery price of the last 3 months, which will only have a return value one hour before the delivery/exercise
func (e *Exchange) GetDeliveryHistory(ctx context.Context, instrumentType, underlying, instrumentFamily string, after, before time.Time, limit int64) ([]DeliveryHistory, error) {
	if instrumentType == "" {
		return nil, errInvalidInstrumentType
	}
	switch instrumentType {
	case instTypeFutures, instTypeOption:
		if underlying == "" && instrumentFamily == "" {
			return nil, errInstrumentFamilyOrUnderlyingRequired
		}
	}
	if limit > 100 {
		return nil, errLimitValueExceedsMaxOf100
	}
	params := url.Values{}
	params.Set("instType", instrumentType)
	if underlying != "" {
		params.Set("uly", underlying)
	}
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []DeliveryHistory
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getDeliveryExerciseHistoryEPL, http.MethodGet,
		common.EncodeURLValues("public/delivery-exercise-history", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetOpenInterestData retrieves the total open interest for contracts on OKX
func (e *Exchange) GetOpenInterestData(ctx context.Context, instType, uly, instrumentFamily, instID string) ([]OpenInterest, error) {
	if instType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	if instType == instTypeOption && uly == "" && instrumentFamily == "" {
		return nil, errInstrumentFamilyOrUnderlyingRequired
	}
	params := url.Values{}
	instType = strings.ToUpper(instType)
	params.Set("instType", instType)
	if uly != "" {
		params.Set("uly", uly)
	}
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	if instID != "" {
		params.Set("instId", instID)
	}
	var resp []OpenInterest
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestEPL, http.MethodGet, common.EncodeURLValues("public/open-interest", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetSingleFundingRate returns the latest funding rate
func (e *Exchange) GetSingleFundingRate(ctx context.Context, instrumentID string) (*FundingRateResponse, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	var resp *FundingRateResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFundingEPL, http.MethodGet, common.EncodeURLValues("public/funding-rate", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetFundingRateHistory retrieves funding rate history. This endpoint can retrieve data from the last 3 months
func (e *Exchange) GetFundingRateHistory(ctx context.Context, instrumentID string, before, after time.Time, limit int64) ([]FundingRateResponse, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	if !before.IsZero() {
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if !after.IsZero() {
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []FundingRateResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFundingRateHistoryEPL, http.MethodGet, common.EncodeURLValues("public/funding-rate-history", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetLimitPrice retrieves the highest buy limit and lowest sell limit of the instrument
func (e *Exchange) GetLimitPrice(ctx context.Context, instrumentID string) (*LimitPriceResponse, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	var resp *LimitPriceResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLimitPriceEPL, http.MethodGet, common.EncodeURLValues("public/price-limit", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetOptionMarketData retrieves option market data
func (e *Exchange) GetOptionMarketData(ctx context.Context, underlying, instrumentFamily string, expTime time.Time) ([]OptionMarketDataResponse, error) {
	if underlying == "" && instrumentFamily == "" {
		return nil, errInstrumentFamilyOrUnderlyingRequired
	}
	params := url.Values{}
	if underlying != "" {
		params.Set("uly", underlying)
	}
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	if !expTime.IsZero() {
		params.Set("expTime", fmt.Sprintf("%d%d%d", expTime.Year(), expTime.Month(), expTime.Day()))
	}
	var resp []OptionMarketDataResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOptionMarketDateEPL, http.MethodGet, common.EncodeURLValues("public/opt-summary", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetEstimatedDeliveryPrice retrieves the estimated delivery price which will only have a return value one hour before the delivery/exercise
func (e *Exchange) GetEstimatedDeliveryPrice(ctx context.Context, instrumentID string) ([]DeliveryEstimatedPrice, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	var resp []DeliveryEstimatedPrice
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getEstimatedDeliveryPriceEPL, http.MethodGet, common.EncodeURLValues("public/estimated-price", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetDiscountRateAndInterestFreeQuota retrieves discount rate level and interest-free quota
func (e *Exchange) GetDiscountRateAndInterestFreeQuota(ctx context.Context, ccy currency.Code, discountLevel int8) ([]DiscountRate, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if discountLevel > 0 {
		params.Set("discountLv", strconv.FormatInt(int64(discountLevel), 10))
	}
	var response []DiscountRate
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getDiscountRateAndInterestFreeQuotaEPL, http.MethodGet, common.EncodeURLValues("public/discount-rate-interest-free-quota", params), nil, &response, request.UnauthenticatedRequest)
}

// GetSystemTime retrieve API server time
func (e *Exchange) GetSystemTime(ctx context.Context) (types.Time, error) {
	resp := &tsResp{}
	return resp.Timestamp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSystemTimeEPL, http.MethodGet, "public/time", nil, resp, request.UnauthenticatedRequest)
}

// GetLiquidationOrders retrieves information on liquidation orders in the last day
func (e *Exchange) GetLiquidationOrders(ctx context.Context, arg *LiquidationOrderRequestParams) (*LiquidationOrder, error) {
	arg.InstrumentType = strings.ToUpper(arg.InstrumentType)
	if arg.InstrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", arg.InstrumentType)
	arg.MarginMode = strings.ToLower(arg.MarginMode)
	if arg.MarginMode != "" {
		params.Set("mgnMode", arg.MarginMode)
	}
	switch {
	case arg.InstrumentType == instTypeMargin && arg.InstrumentID != "":
		params.Set("instId", arg.InstrumentID)
	case arg.InstrumentType == instTypeMargin && arg.Currency.String() != "":
		params.Set("ccy", arg.Currency.String())
	default:
		return nil, errEitherInstIDOrCcyIsRequired
	}
	if arg.InstrumentType != instTypeMargin && arg.Underlying != "" {
		params.Set("uly", arg.Underlying)
	}
	if arg.InstrumentType == instTypeFutures && arg.Alias != "" {
		params.Set("alias", arg.Alias)
	}
	if !arg.Before.IsZero() {
		params.Set("before", strconv.FormatInt(arg.Before.UnixMilli(), 10))
	}
	if !arg.After.IsZero() {
		params.Set("after", strconv.FormatInt(arg.After.UnixMilli(), 10))
	}
	if arg.Limit > 0 && arg.Limit < 100 {
		params.Set("limit", strconv.FormatInt(arg.Limit, 10))
	}
	var resp *LiquidationOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getLiquidationOrdersEPL, http.MethodGet, common.EncodeURLValues("public/liquidation-orders", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetMarkPrice  retrieve mark price
func (e *Exchange) GetMarkPrice(ctx context.Context, instrumentType, underlying, instrumentFamily, instrumentID string) ([]MarkPrice, error) {
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", strings.ToUpper(instrumentType))
	if underlying != "" {
		params.Set("uly", underlying)
	}
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	var response []MarkPrice
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getMarkPriceEPL, http.MethodGet, common.EncodeURLValues("public/mark-price", params), nil, &response, request.UnauthenticatedRequest)
}

// GetPositionTiers retrieves position tiers information，maximum leverage depends on your borrowings and margin ratio
func (e *Exchange) GetPositionTiers(ctx context.Context, instrumentType, tradeMode, underlying, instrumentFamily, instrumentID, tiers string, ccy currency.Code) ([]PositionTiers, error) {
	instrumentType = strings.ToUpper(instrumentType)
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	tradeMode = strings.ToLower(tradeMode)
	switch tradeMode {
	case TradeModeCross, TradeModeIsolated:
	default:
		return nil, errInvalidTradeMode
	}
	params := url.Values{}
	params.Set("instType", strings.ToUpper(instrumentType))
	params.Set("tdMode", tradeMode)
	if underlying != "" {
		params.Set("uly", underlying)
	}

	switch instrumentType {
	case instTypeSwap, instTypeFutures, instTypeOption:
		if instrumentFamily == "" && underlying == "" {
			return nil, errInstrumentFamilyOrUnderlyingRequired
		}
		if ccy.IsEmpty() && instrumentID == "" {
			return nil, errEitherInstIDOrCcyIsRequired
		}
	}
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	if underlying != "" {
		params.Set("uly", underlying)
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if instrumentID != "" {
		params.Set("instId", instrumentID)
	}
	if tiers != "" {
		params.Set("tiers", tiers)
	}
	var response []PositionTiers
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getPositionTiersEPL, http.MethodGet, common.EncodeURLValues("public/position-tiers", params), nil, &response, request.UnauthenticatedRequest)
}

// GetInterestRateAndLoanQuota retrieves an interest rate and loan quota information for various currencies
func (e *Exchange) GetInterestRateAndLoanQuota(ctx context.Context) ([]InterestRateLoanQuotaItem, error) {
	var resp []InterestRateLoanQuotaItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getInterestRateAndLoanQuotaEPL, http.MethodGet, "public/interest-rate-loan-quota", nil, &resp, request.UnauthenticatedRequest)
}

// GetInterestRateAndLoanQuotaForVIPLoans retrieves an interest rate and loan quota information for VIP users of various currencies
func (e *Exchange) GetInterestRateAndLoanQuotaForVIPLoans(ctx context.Context) ([]VIPInterestRateAndLoanQuotaInformation, error) {
	var response []VIPInterestRateAndLoanQuotaInformation
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getInterestRateAndLoanQuoteForVIPLoansEPL, http.MethodGet, "public/vip-interest-rate-loan-quota", nil, &response, request.UnauthenticatedRequest)
}

// GetPublicUnderlyings returns list of underlyings for various instrument types
func (e *Exchange) GetPublicUnderlyings(ctx context.Context, instrumentType string) ([]string, error) {
	instrumentType = strings.ToUpper(instrumentType)
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", strings.ToUpper(instrumentType))
	var resp []string
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getUnderlyingEPL, http.MethodGet, common.EncodeURLValues("public/underlying", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetInsuranceFundInformation returns insurance fund balance information
func (e *Exchange) GetInsuranceFundInformation(ctx context.Context, arg *InsuranceFundInformationRequestParams) (*InsuranceFundInformation, error) {
	if *arg == (InsuranceFundInformationRequestParams{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.InstrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", strings.ToUpper(arg.InstrumentType))
	arg.InsuranceType = strings.ToLower(arg.InsuranceType)
	if arg.InsuranceType != "" {
		params.Set("type", arg.InsuranceType)
	}
	switch arg.InstrumentType {
	case instTypeFutures, instTypeSwap, instTypeOption:
		if arg.Underlying == "" && arg.InstrumentFamily == "" {
			return nil, errInstrumentFamilyOrUnderlyingRequired
		}
	}
	if arg.Underlying != "" {
		params.Set("uly", arg.Underlying)
	}
	if arg.InstrumentFamily != "" {
		params.Set("instFamily", arg.InstrumentFamily)
	}
	if !arg.Currency.IsEmpty() {
		params.Set("ccy", arg.Currency.String())
	}
	if !arg.Before.IsZero() {
		params.Set("before", strconv.FormatInt(arg.Before.UnixMilli(), 10))
	}
	if !arg.After.IsZero() {
		params.Set("after", strconv.FormatInt(arg.After.UnixMilli(), 10))
	}
	if arg.Limit > 0 {
		params.Set("limit", strconv.FormatInt(arg.Limit, 10))
	}
	var resp *InsuranceFundInformation
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getInsuranceFundEPL, http.MethodGet, common.EncodeURLValues("public/insurance-fund", params), nil, &resp, request.UnauthenticatedRequest)
}

// CurrencyUnitConvert convert currency to contract, or contract to currency
func (e *Exchange) CurrencyUnitConvert(ctx context.Context, instrumentID string, quantity, orderPrice float64, convertType uint64, unitOfCcy currency.Code, operationTypeOpen bool) (*UnitConvertResponse, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	if quantity <= 0 {
		return nil, errMissingQuantity
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	params.Set("sz", strconv.FormatFloat(quantity, 'f', 0, 64))
	if orderPrice > 0 {
		params.Set("px", strconv.FormatFloat(orderPrice, 'f', 0, 64))
	}
	if convertType > 0 {
		params.Set("type", strconv.FormatUint(convertType, 10))
	}
	switch unitOfCcy {
	case currency.USDC, currency.USDT:
		params.Set("unit", "usds")
	default:
		params.Set("unit", "coin")
	}
	// Applicable to FUTURES and SWAP orders
	if operationTypeOpen {
		params.Set("opType", "open")
	} else {
		params.Set("opType", "close")
	}
	var resp *UnitConvertResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, unitConvertEPL, http.MethodGet,
		common.EncodeURLValues("public/convert-contract-coin", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetOptionsTickBands retrieves option tick bands information.
// Instrument type OPTION
func (e *Exchange) GetOptionsTickBands(ctx context.Context, instrumentType, instrumentFamily string) ([]OptionTickBand, error) {
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", instrumentType)
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	var resp []OptionTickBand
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, optionTickBandsEPL, http.MethodGet,
		common.EncodeURLValues("public/instrument-tick-bands", params), nil, &resp, request.UnauthenticatedRequest)
}

// Trading Data Endpoints

// GetSupportCoins retrieves the currencies supported by the trading data endpoints
func (e *Exchange) GetSupportCoins(ctx context.Context) (*SupportedCoinsData, error) {
	var resp *SupportedCoinsData
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getSupportCoinEPL, http.MethodGet, "rubik/stat/trading-data/support-coin", nil, &resp, request.UnauthenticatedRequest)
}

// GetTakerVolume retrieves the taker volume for both buyers and sellers
func (e *Exchange) GetTakerVolume(ctx context.Context, ccy currency.Code, instrumentType, instrumentFamily string, begin, end time.Time, period kline.Interval) ([]TakerVolume, error) {
	if instrumentType == "" {
		return nil, fmt.Errorf("%w, empty instrument type", errInvalidInstrumentType)
	}
	params := url.Values{}
	params.Set("instType", strings.ToUpper(instrumentType))
	if instrumentFamily != "" {
		params.Set("instFamily", instrumentFamily)
	}
	interval := IntervalFromString(period, false)
	if interval != "" {
		params.Set("period", interval)
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !begin.IsZero() {
		params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	if !end.IsZero() {
		params.Set("end", strconv.FormatInt(end.UnixMilli(), 10))
	}
	var response []TakerVolume
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getTakerVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/taker-volume", params), nil, &response, request.UnauthenticatedRequest)
}

// GetMarginLendingRatio retrieves the ratio of cumulative amount between currency margin quote currency and base currency
func (e *Exchange) GetMarginLendingRatio(ctx context.Context, ccy currency.Code, begin, end time.Time, period kline.Interval) ([]MarginLendRatioItem, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !begin.IsZero() {
		params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	if !end.IsZero() {
		params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	interval := IntervalFromString(period, false)
	if interval != "" {
		params.Set("period", interval)
	}
	var response []MarginLendRatioItem
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getMarginLendingRatioEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/margin/loan-ratio", params), nil, &response, request.UnauthenticatedRequest)
}

// GetLongShortRatio retrieves the ratio of users with net long vs net short positions for futures and perpetual swaps
func (e *Exchange) GetLongShortRatio(ctx context.Context, ccy currency.Code, begin, end time.Time, period kline.Interval) ([]LongShortRatio, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !begin.IsZero() {
		params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	if !end.IsZero() {
		params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	interval := IntervalFromString(period, false)
	if interval != "" {
		params.Set("period", interval)
	}
	var response []LongShortRatio
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getLongShortRatioEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/contracts/long-short-account-ratio", params), nil, &response, request.UnauthenticatedRequest)
}

// GetContractsOpenInterestAndVolume retrieves the open interest and trading volume for futures and perpetual swaps
func (e *Exchange) GetContractsOpenInterestAndVolume(ctx context.Context, ccy currency.Code, begin, end time.Time, period kline.Interval) ([]OpenInterestVolume, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !begin.IsZero() {
		params.Set("begin", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	if !end.IsZero() {
		params.Set("end", strconv.FormatInt(begin.UnixMilli(), 10))
	}
	interval := IntervalFromString(period, false)
	if interval != "" {
		params.Set("period", interval)
	}
	var response []OpenInterestVolume
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getContractsOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/contracts/open-interest-volume", params), nil, &response, request.UnauthenticatedRequest)
}

// GetOptionsOpenInterestAndVolume retrieves the open interest and trading volume for options
func (e *Exchange) GetOptionsOpenInterestAndVolume(ctx context.Context, ccy currency.Code, period kline.Interval) ([]OpenInterestVolume, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	interval := IntervalFromString(period, false)
	if interval != "" {
		params.Set("period", interval)
	}
	var response []OpenInterestVolume
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getOptionsOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/open-interest-volume", params), nil, &response, request.UnauthenticatedRequest)
}

// GetPutCallRatio retrieves the open interest ration and trading volume ratio of calls vs puts
func (e *Exchange) GetPutCallRatio(ctx context.Context, ccy currency.Code, period kline.Interval) ([]OpenInterestVolumeRatio, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	interval := IntervalFromString(period, false)
	if interval != "" {
		params.Set("period", interval)
	}
	var response []OpenInterestVolumeRatio
	return response, e.SendHTTPRequest(ctx, exchange.RestSpot, getPutCallRatioEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/open-interest-volume-ratio", params), nil, &response, request.UnauthenticatedRequest)
}

// GetOpenInterestAndVolumeExpiry retrieves the open interest and trading volume of calls and puts for each upcoming expiration
func (e *Exchange) GetOpenInterestAndVolumeExpiry(ctx context.Context, ccy currency.Code, period kline.Interval) ([]ExpiryOpenInterestAndVolume, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	interval := IntervalFromString(period, false)
	if interval != "" {
		params.Set("period", interval)
	}
	var resp []ExpiryOpenInterestAndVolume
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/open-interest-volume-expiry", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetOpenInterestAndVolumeStrike retrieves the taker volume for both buyers and sellers of calls and puts
func (e *Exchange) GetOpenInterestAndVolumeStrike(ctx context.Context, ccy currency.Code,
	expTime time.Time, period kline.Interval,
) ([]StrikeOpenInterestAndVolume, error) {
	if expTime.IsZero() {
		return nil, errMissingExpiryTimeParameter
	}
	params := url.Values{}
	params.Set("expTime", expTime.UTC().Format("20060102"))
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	interval := IntervalFromString(period, false)
	if interval != "" {
		params.Set("period", interval)
	}
	var resp []StrikeOpenInterestAndVolume
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getOpenInterestAndVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/open-interest-volume-strike", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetTakerFlow shows the relative buy/sell volume for calls and puts
// It shows whether traders are bullish or bearish on price and volatility
func (e *Exchange) GetTakerFlow(ctx context.Context, ccy currency.Code, period kline.Interval) (*CurrencyTakerFlow, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	interval := IntervalFromString(period, false)
	if interval != "" {
		params.Set("period", interval)
	}
	var resp *CurrencyTakerFlow
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getTakerFlowEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/option/taker-block-volume", params), nil, &resp, request.UnauthenticatedRequest)
}

// ********************************************************** Affiliate **********************************************************************

// The Affiliate API offers affiliate users a flexible function to query the invitee information
// Simply enter the UID of your direct invitee to access their relevant information, empowering your affiliate business growth and day-to-day business operation
// If you have additional data requirements regarding the Affiliate API, please don't hesitate to contact your BD
// We will reach out to you through your BD to provide more comprehensive API support

// GetInviteesDetail retrieves affiliate invitees details
func (e *Exchange) GetInviteesDetail(ctx context.Context, uid string) (*AffilateInviteesDetail, error) {
	if uid == "" {
		return nil, errUserIDRequired
	}
	var resp *AffilateInviteesDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAffilateInviteesDetailEPL, http.MethodGet, "affiliate/invitee/detail?uid="+uid, nil, &resp, request.AuthenticatedRequest)
}

// GetUserAffiliateRebateInformation this endpoint is used to get the user's affiliate rebate information for affiliate
func (e *Exchange) GetUserAffiliateRebateInformation(ctx context.Context, apiKey string) (*AffilateRebateInfo, error) {
	if apiKey == "" {
		return nil, errInvalidAPIKey
	}
	var resp *AffilateRebateInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getUserAffiliateRebateInformationEPL, http.MethodGet, "users/partner/if-rebate?apiKey="+apiKey, nil, &resp, request.AuthenticatedRequest)
}

// Status

// SystemStatusResponse retrieves the system status.
// state supports valid values 'scheduled', 'ongoing', 'pre_open', 'completed', and 'canceled'
func (e *Exchange) SystemStatusResponse(ctx context.Context, state string) ([]SystemStatusResponse, error) {
	params := url.Values{}
	if state != "" {
		params.Set("state", state)
	}
	var resp []SystemStatusResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getEventStatusEPL, http.MethodGet, common.EncodeURLValues("system/status", params), nil, &resp, request.UnauthenticatedRequest)
}

// -------------------------------------------------------  Lending Orders  ------------------------------------------------------

// PlaceLendingOrder places a lending order
func (e *Exchange) PlaceLendingOrder(ctx context.Context, arg *LendingOrderParam) (*LendingOrderResponse, error) {
	if *arg == (LendingOrderParam{}) {
		return nil, common.ErrEmptyParams
	}
	if arg.Currency.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if arg.Amount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	if arg.Rate <= 0 {
		return nil, errRateRequired
	}
	if arg.Term == "" {
		return nil, errLendingTermIsRequired
	}
	var resp *LendingOrderResponse
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, placeLendingOrderEPL, http.MethodPost, "finance/fixed-loan/lending-order", arg, &resp, request.AuthenticatedRequest)
}

// AmendLendingOrder amends a lending order
func (e *Exchange) AmendLendingOrder(ctx context.Context, orderID string, changeAmount, rate float64, autoRenewal bool) (string, error) {
	if orderID == "" {
		return "", order.ErrOrderIDNotSet
	}
	arg := &struct {
		OrderID      string  `json:"ordId"`
		ChangeAmount float64 `json:"changeAmt,omitempty,string"`
		Rate         float64 `json:"rate,omitempty,string"`
		AutoRenewal  bool    `json:"autoRenewal,omitempty"`
	}{
		OrderID:      orderID,
		ChangeAmount: changeAmount,
		Rate:         rate,
		AutoRenewal:  autoRenewal,
	}
	var resp OrderIDResponse
	return resp.OrderID, e.SendHTTPRequest(ctx, exchange.RestSpot, amendLendingOrderEPL, http.MethodPost, "finance/fixed-loan/amend-lending-order", arg, &resp, request.AuthenticatedRequest)
}

// Note: the documentation for Amending lending order has similar url, request method, and parameters to the placing order. Therefore, the implementation is skipped for now.

// GetLendingOrders retrieves list of lending orders.
// State: possible values are 'pending', 'earning', 'expired', 'settled'
func (e *Exchange) GetLendingOrders(ctx context.Context, orderID, state string, ccy currency.Code, startAt, endAt time.Time, limit int64) ([]LendingOrderDetail, error) {
	params := url.Values{}
	if orderID != "" {
		params.Set("ordId", orderID)
	}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if !startAt.IsZero() && !endAt.IsZero() {
		err := common.StartEndTimeCheck(startAt, endAt)
		if err != nil {
			return nil, err
		}
		params.Set("after", strconv.FormatInt(startAt.UnixMilli(), 10))
		params.Set("before", strconv.FormatInt(endAt.UnixMilli(), 10))
	}
	if state != "" {
		params.Set("state", state)
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []LendingOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, lendingOrderListEPL, http.MethodGet,
		common.EncodeURLValues("finance/fixed-loan/lending-orders-list", params), nil, &resp, request.AuthenticatedRequest)
}

// GetLendingSubOrderList retrieves a lending sub-orders list
func (e *Exchange) GetLendingSubOrderList(ctx context.Context, orderID, state string, startAt, endAt time.Time, limit int64) ([]LendingSubOrder, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	params := url.Values{}
	params.Set("ordId", orderID)
	if state != "" {
		params.Set("state", state)
	}
	if !startAt.IsZero() && !endAt.IsZero() {
		err := common.StartEndTimeCheck(startAt, endAt)
		if err != nil {
			return nil, err
		}
		params.Set("after", strconv.FormatInt(startAt.UnixMilli(), 10))
		params.Set("before", strconv.FormatInt(endAt.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []LendingSubOrder
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, lendingSubOrderListEPL, http.MethodGet, common.EncodeURLValues("finance/fixed-loan/lending-sub-orders", params), nil, &resp, request.AuthenticatedRequest)
}

// Trading Statistics endpoints

// GetFuturesContractsOpenInterestHistory retrieve the contract open interest statistics of futures and perp. This endpoint returns a maximum of 1440 records
func (e *Exchange) GetFuturesContractsOpenInterestHistory(ctx context.Context, instrumentID string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]ContractOpenInterestHistoryItem, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	if period != kline.Interval(0) {
		params.Set("period", IntervalFromString(period, true))
	}
	if !startAt.IsZero() {
		params.Set("begin", strconv.FormatInt(startAt.UnixMilli(), 10))
	}
	if !endAt.IsZero() {
		params.Set("end", strconv.FormatInt(endAt.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []ContractOpenInterestHistoryItem
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, rubikGetContractOpenInterestHistoryEPL, http.MethodGet,
		common.EncodeURLValues("rubik/stat/contracts/open-interest-history", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetFuturesContractTakerVolume retrieve the contract taker volume for both buyers and sellers. This endpoint returns a maximum of 1440 records.
// The unit of buy/sell volume, the default is 1. '0': Crypto '1': Contracts '2': U
func (e *Exchange) GetFuturesContractTakerVolume(ctx context.Context, instrumentID string, period kline.Interval, unit, limit int64, startAt, endAt time.Time) ([]ContractTakerVolume, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	if period != kline.Interval(0) {
		params.Set("period", IntervalFromString(period, true))
	}
	if !startAt.IsZero() {
		params.Set("begin", strconv.FormatInt(startAt.UnixMilli(), 10))
	}
	if !endAt.IsZero() {
		params.Set("end", strconv.FormatInt(endAt.UnixMilli(), 10))
	}
	if unit != 1 {
		params.Set("unit", strconv.FormatInt(unit, 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []ContractTakerVolume
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, rubikContractTakerVolumeEPL, http.MethodGet, common.EncodeURLValues("rubik/stat/taker-volume-contract", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetFuturesContractLongShortAccountRatio retrieve the account long/short ratio of a contract. This endpoint returns a maximum of 1440 records
func (e *Exchange) GetFuturesContractLongShortAccountRatio(ctx context.Context, instrumentID string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]TopTraderContractsLongShortRatio, error) {
	return e.getTopTradersFuturesContractLongShortRatio(ctx, instrumentID, "rubik/stat/contracts/long-short-account-ratio-contract", period, startAt, endAt, limit)
}

// GetTopTradersFuturesContractLongShortAccountRatio retrieve the account net long/short ratio of a contract for top traders.
// Top traders refer to the top 5% of traders with the largest open position value.
// This endpoint returns a maximum of 1440 records
func (e *Exchange) GetTopTradersFuturesContractLongShortAccountRatio(ctx context.Context, instrumentID string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]TopTraderContractsLongShortRatio, error) {
	return e.getTopTradersFuturesContractLongShortRatio(ctx, instrumentID, "rubik/stat/contracts/long-short-account-ratio-contract-top-trader", period, startAt, endAt, limit)
}

// GetTopTradersFuturesContractLongShortPositionRatio retrieve the position long/short ratio of a contract for top traders. Top traders refer to the top 5% of traders with the largest open position value. This endpoint returns a maximum of 1440 records
func (e *Exchange) GetTopTradersFuturesContractLongShortPositionRatio(ctx context.Context, instrumentID string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]TopTraderContractsLongShortRatio, error) {
	return e.getTopTradersFuturesContractLongShortRatio(ctx, instrumentID, "rubik/stat/contracts/long-short-position-ratio-contract-top-trader", period, startAt, endAt, limit)
}

func (e *Exchange) getTopTradersFuturesContractLongShortRatio(ctx context.Context, instrumentID, path string, period kline.Interval, startAt, endAt time.Time, limit int64) ([]TopTraderContractsLongShortRatio, error) {
	if instrumentID == "" {
		return nil, errMissingInstrumentID
	}
	params := url.Values{}
	params.Set("instId", instrumentID)
	if period != kline.Interval(0) {
		params.Set("period", IntervalFromString(period, true))
	}
	if !startAt.IsZero() {
		params.Set("begin", strconv.FormatInt(startAt.UnixMilli(), 10))
	}
	if !endAt.IsZero() {
		params.Set("end", strconv.FormatInt(endAt.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []TopTraderContractsLongShortRatio
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, rubikTopTradersContractLongShortRatioEPL, http.MethodGet, common.EncodeURLValues(path, params), nil, &resp, request.UnauthenticatedRequest)
}

// GetAnnouncements get announcements, the response is sorted by pTime with the most recent first. The sort will not be affected if the announcement is updated. Every page has 20 records
//
// There are differences between public endpoint and private endpoint.
// For public endpoint, the response is restricted based on your request IP.
// For private endpoint, the response is restricted based on your country of residence
func (e *Exchange) GetAnnouncements(ctx context.Context, announcementType string, page int64) (*AnnouncementDetail, error) {
	params := url.Values{}
	if announcementType != "" {
		params.Set("annType", announcementType)
	}
	if page > 0 {
		params.Set("page", strconv.FormatInt(page, 10))
	}
	var resp *AnnouncementDetail
	if e.AreCredentialsValid(ctx) {
		return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAnnouncementsEPL, http.MethodGet, common.EncodeURLValues("support/announcements", params), nil, &resp, request.AuthenticatedRequest)
	}
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAnnouncementsEPL, http.MethodGet, common.EncodeURLValues("support/announcements", params), nil, &resp, request.UnauthenticatedRequest)
}

// GetAnnouncementTypes represents a list of announcement types
func (e *Exchange) GetAnnouncementTypes(ctx context.Context) ([]AnnouncementTypeInfo, error) {
	var resp []AnnouncementTypeInfo
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getAnnouncementTypeEPL, http.MethodGet,
		"support/announcement-types", nil, &resp, request.UnauthenticatedRequest)
}

// Fiat endpoints

// GetDepositOrderDetail retrieves fiat deposit order detail
func (e *Exchange) GetDepositOrderDetail(ctx context.Context, orderID string) (*FiatOrderDetail, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	params := url.Values{}
	params.Set("ordID", orderID)
	var resp *FiatOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getDepositOrderDetailEPL, http.MethodGet, common.EncodeURLValues("fiat/deposit", params), nil, &resp, request.AuthenticatedRequest)
}

// GetFiatDepositOrderHistory retrieves fiat deposit order history
func (e *Exchange) GetFiatDepositOrderHistory(ctx context.Context, ccy currency.Code, paymentMethod, state string, after, before time.Time, limit int64) ([]FiatOrderDetail, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if paymentMethod != "" {
		params.Set("paymentMethod", paymentMethod)
	}
	if state != "" {
		params.Set("state", state)
	}
	if !after.IsZero() && !before.IsZero() {
		err := common.StartEndTimeCheck(after, before)
		if err != nil {
			return nil, err
		}
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []FiatOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getDepositOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("fiat/deposit-order-history", params), nil, &resp, request.AuthenticatedRequest)
}

// GetWithdrawalOrderDetail retrieves fiat withdrawal order detail
func (e *Exchange) GetWithdrawalOrderDetail(ctx context.Context, orderID string) (*FiatOrderDetail, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	params := url.Values{}
	params.Set("ordId", orderID)
	var resp *FiatOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getWithdrawalOrderDetailEPL, http.MethodGet,
		common.EncodeURLValues("fiat/withdrawal", params), &map[string]string{"ordId": orderID}, &resp, request.AuthenticatedRequest)
}

// GetFiatWithdrawalOrderHistory retrieves fiat withdrawal order history
func (e *Exchange) GetFiatWithdrawalOrderHistory(ctx context.Context, ccy currency.Code, paymentMethod, state string, after, before time.Time, limit int64) ([]FiatOrderDetail, error) {
	params := url.Values{}
	if !ccy.IsEmpty() {
		params.Set("ccy", ccy.String())
	}
	if paymentMethod != "" {
		params.Set("paymentMethod", paymentMethod)
	}
	if state != "" {
		params.Set("state", state)
	}
	if !after.IsZero() && !before.IsZero() {
		err := common.StartEndTimeCheck(after, before)
		if err != nil {
			return nil, err
		}
		params.Set("after", strconv.FormatInt(after.UnixMilli(), 10))
		params.Set("before", strconv.FormatInt(before.UnixMilli(), 10))
	}
	if limit > 0 {
		params.Set("limit", strconv.FormatInt(limit, 10))
	}
	var resp []FiatOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFiatWithdrawalOrderHistoryEPL, http.MethodGet, common.EncodeURLValues("fiat/withdrawal-order-history", params), nil, &resp, request.AuthenticatedRequest)
}

// CancelWithdrawalOrder cancel a pending fiat withdrawal order, currently only applicable to TRY
func (e *Exchange) CancelWithdrawalOrder(ctx context.Context, orderID string) (*OrderIDAndState, error) {
	if orderID == "" {
		return nil, order.ErrOrderIDNotSet
	}
	var resp *OrderIDAndState
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, cancelWithdrawalOrderEPL, http.MethodPost, "fiat/cancel-withdrawal", &map[string]string{"ordId": orderID}, &resp, request.AuthenticatedRequest)
}

// CreateWithdrawalOrder initiate a fiat withdrawal request (Authenticated endpoint, Only for API keys with "Withdrawal" access)
func (e *Exchange) CreateWithdrawalOrder(ctx context.Context, ccy currency.Code, paymentAccountID, paymentMethod, clientID string, amount float64) (*FiatOrderDetail, error) {
	if paymentAccountID == "" {
		return nil, fmt.Errorf("%w, payment account ID is required", errIDNotSet)
	}
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	if amount <= 0 {
		return nil, limits.ErrAmountBelowMin
	}
	if paymentMethod == "" {
		return nil, errPaymentMethodRequired
	}
	if clientID == "" {
		return nil, fmt.Errorf("%w, client ID is required", errIDNotSet)
	}
	arg := &struct {
		PaymentMethod string  `json:"paymentMethod"`
		PaymentAcctID string  `json:"paymentAcctId"`
		ClientID      string  `json:"clientId"`
		Amount        float64 `json:"amt,string"`
		Currency      string  `json:"ccy"`
	}{
		PaymentMethod: paymentMethod,
		PaymentAcctID: paymentAccountID,
		ClientID:      clientID,
		Amount:        amount,
		Currency:      ccy.String(),
	}
	var resp *FiatOrderDetail
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, createWithdrawalOrderEPL, http.MethodPost, "fiat/create-withdrawal", arg, &resp, request.AuthenticatedRequest)
}

// GetFiatWithdrawalPaymentMethods to display all the available fiat withdrawal payment methods
func (e *Exchange) GetFiatWithdrawalPaymentMethods(ctx context.Context, ccy currency.Code) (*FiatWithdrawalPaymentMethods, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	params := url.Values{}
	params.Set("ccy", ccy.String())
	var resp *FiatWithdrawalPaymentMethods
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getWithdrawalPaymentMethodsEPL, http.MethodGet,
		common.EncodeURLValues("fiat/withdrawal-payment-methods", params), nil, &resp, request.AuthenticatedRequest)
}

// GetFiatDepositPaymentMethods to display all the available fiat deposit payment methods
func (e *Exchange) GetFiatDepositPaymentMethods(ctx context.Context, ccy currency.Code) (*FiatDepositPaymentMethods, error) {
	if ccy.IsEmpty() {
		return nil, currency.ErrCurrencyCodeEmpty
	}
	params := url.Values{}
	params.Set("ccy", ccy.String())
	var resp *FiatDepositPaymentMethods
	return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, getFiatDepositPaymentMethodsEPL, http.MethodGet,
		common.EncodeURLValues("fiat/deposit-payment-methods", params), nil, &resp, request.AuthenticatedRequest)
}

/*
SendHTTPRequest sends an http request, optionally with a JSON payload
URL arguments must be encoded in the request path
result must be a pointer
The response will be unmarshalled first into []any{result}, which matches most APIs, and fallback to directly into result
*/
func (e *Exchange) SendHTTPRequest(ctx context.Context, ep exchange.URL, f request.EndpointLimit, httpMethod, requestPath string, data, result any, requestType request.AuthType) (err error) {
	endpoint, err := e.API.Endpoints.GetURL(ep)
	if err != nil {
		return err
	}
	var resp struct {
		Code types.Number    `json:"code"`
		Msg  string          `json:"msg"`
		Data json.RawMessage `json:"data"`
	}
	newRequest := func() (*request.Item, error) {
		var payload []byte
		if data != nil {
			payload, err = json.Marshal(data)
			if err != nil {
				return nil, err
			}
		}
		headers := make(map[string]string)
		headers["Content-Type"] = "application/json"
		if simulate, okay := ctx.Value(testNetVal).(bool); okay && simulate {
			headers["x-simulated-trading"] = "1"
		}
		if requestType == request.AuthenticatedRequest {
			creds, err := e.GetCredentials(ctx)
			if err != nil {
				return nil, err
			}
			signPath := "/" + apiPath + requestPath
			utcTime := time.Now().UTC().Format(time.RFC3339)
			hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte(utcTime+httpMethod+signPath+string(payload)), []byte(creds.Secret))
			if err != nil {
				return nil, err
			}
			headers["OK-ACCESS-KEY"] = creds.Key
			headers["OK-ACCESS-SIGN"] = base64.StdEncoding.EncodeToString(hmac)
			headers["OK-ACCESS-TIMESTAMP"] = utcTime
			headers["OK-ACCESS-PASSPHRASE"] = creds.ClientID
		}
		return &request.Item{
			Method:                 strings.ToUpper(httpMethod),
			Path:                   endpoint + requestPath,
			Headers:                headers,
			Body:                   bytes.NewBuffer(payload),
			Result:                 &resp,
			Verbose:                e.Verbose,
			HTTPDebugging:          e.HTTPDebugging,
			HTTPRecording:          e.HTTPRecording,
			HTTPMockDataSliceLimit: e.HTTPMockDataSliceLimit,
		}, nil
	}
	if err := e.SendPayload(ctx, f, newRequest, requestType); err != nil {
		return err
	}
	if resp.Code.Int64() != 0 {
		if requestType == request.AuthenticatedRequest {
			err = request.ErrAuthRequestFailed
		}
		if resp.Msg != "" {
			return common.AppendError(err, fmt.Errorf("error code: `%d`; message: %q", resp.Code.Int64(), resp.Msg))
		}
		if mErr, ok := ErrorCodes[resp.Code.String()]; ok {
			return common.AppendError(err, mErr)
		}
		return common.AppendError(err, fmt.Errorf("error code: `%d`", resp.Code.Int64()))
	}

	// First see if resp.Data can unmarshal into a slice of result, which is true for most APIs
	if sliceErr := json.Unmarshal(resp.Data, &[]any{result}); sliceErr != nil {
		// Otherwise, resp.Data should unmarshal directly into result; e.g. index-components, support-coin, and taker-block-volume
		if directErr := json.Unmarshal(resp.Data, result); directErr != nil {
			return fmt.Errorf("cannot unmarshal as a slice of result (error: %w) or as a reference to result (error: %w)", sliceErr, directErr)
		}
	}

	return nil
}

func getStatusError(statusCode int64, statusMessage string) error {
	if statusCode == 0 {
		return nil
	}
	return fmt.Errorf("status code: `%d` status message: %q", statusCode, statusMessage)
}
