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) }