Files
gocryptotrader/exchanges/bybit/bybit.go
keeghcet 560b22e2ba exchanges/refactor: Use strings.Builder for string construction (#2046)
* refactor: use strings.builder

Signed-off-by: keeghcet <keeghcet@outlook.com>

* Apply suggestion from @shazbert

Co-authored-by: Ryan O'Hara-Reid <oharareid.ryan@gmail.com>

---------

Signed-off-by: keeghcet <keeghcet@outlook.com>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
Co-authored-by: Ryan O'Hara-Reid <oharareid.ryan@gmail.com>
2025-09-10 21:03:03 +10:00

2717 lines
104 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package bybit
import (
"bytes"
"context"
"encoding/hex"
"errors"
"fmt"
"net/http"
"net/url"
"reflect"
"slices"
"strconv"
"strings"
"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/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"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 Bybit
type Exchange struct {
exchange.Base
messageIDSeq common.Counter
account accountTypeHolder
}
const (
bybitAPIURL = "https://api.bybit.com"
bybitAPIVersion = "/v5/"
tradeBaseURL = "https://www.bybit.com/"
defaultRecvWindow = "5000" // 5000 milli second
sideBuy = "Buy"
sideSell = "Sell"
cSpot, cLinear, cOption, cInverse = "spot", "linear", "option", "inverse"
accountTypeNormal AccountType = 1
accountTypeUnified AccountType = 2
longDatedFormat = "02Jan06"
)
var (
errCategoryNotSet = errors.New("category not set")
errBaseNotSet = errors.New("base coin not set when category is option")
errInvalidTriggerDirection = errors.New("invalid trigger direction")
errInvalidTriggerPriceType = errors.New("invalid trigger price type")
errNilArgument = errors.New("nil argument")
errMissingUserID = errors.New("sub user id missing")
errMissingUsername = errors.New("username is missing")
errInvalidMemberType = errors.New("invalid member type")
errMissingTransferID = errors.New("transfer ID is required")
errMemberIDRequired = errors.New("member ID is required")
errNonePointerArgument = errors.New("argument must be pointer")
errEitherOrderIDOROrderLinkIDRequired = errors.New("either orderId or orderLinkId required")
errNoOrderPassed = errors.New("no order passed")
errSymbolOrSettleCoinRequired = errors.New("provide symbol or settleCoin at least one")
errInvalidTradeModeValue = errors.New("invalid trade mode value")
errTakeProfitOrStopLossModeMissing = errors.New("TP/SL mode missing")
errMissingAccountType = errors.New("account type not specified")
errMembersIDsNotSet = errors.New("members IDs not set")
errMissingChainType = errors.New("missing chain type is empty")
errMissingChainInformation = errors.New("missing transfer chain")
errMissingAddressInfo = errors.New("address is required")
errMissingWithdrawalID = errors.New("missing withdrawal id")
errTimeWindowRequired = errors.New("time window is required")
errFrozenPeriodRequired = errors.New("frozen period required")
errQuantityLimitRequired = errors.New("quantity limit required")
errInvalidLeverage = errors.New("leverage can't be zero or less then it")
errInvalidPositionMode = errors.New("position mode is invalid")
errInvalidMode = errors.New("mode can't be empty or missing")
errInvalidOrderFilter = errors.New("invalid order filter")
errInvalidCategory = errors.New("invalid category")
errEitherSymbolOrCoinRequired = errors.New("either symbol or coin required")
errOrderLinkIDMissing = errors.New("order link id missing")
errSymbolMissing = errors.New("symbol missing")
errInvalidAutoAddMarginValue = errors.New("invalid add auto margin value")
errDisconnectTimeWindowNotSet = errors.New("disconnect time window not set")
errAPIKeyIsNotUnified = errors.New("api key is not unified")
errInvalidContractLength = errors.New("contract length cannot be less than or equal to zero")
)
var (
intervalMap = map[kline.Interval]string{kline.OneMin: "1", kline.ThreeMin: "3", kline.FiveMin: "5", kline.FifteenMin: "15", kline.ThirtyMin: "30", kline.OneHour: "60", kline.TwoHour: "120", kline.FourHour: "240", kline.SixHour: "360", kline.SevenHour: "720", kline.OneDay: "D", kline.OneWeek: "W", kline.OneMonth: "M"}
stringToIntervalMap = map[string]kline.Interval{"1": kline.OneMin, "3": kline.ThreeMin, "5": kline.FiveMin, "15": kline.FifteenMin, "30": kline.ThirtyMin, "60": kline.OneHour, "120": kline.TwoHour, "240": kline.FourHour, "360": kline.SixHour, "720": kline.SevenHour, "D": kline.OneDay, "W": kline.OneWeek, "M": kline.OneMonth}
)
func intervalToString(interval kline.Interval) (string, error) {
inter, okay := intervalMap[interval]
if okay {
return inter, nil
}
return "", kline.ErrUnsupportedInterval
}
// stringToInterval returns a kline.Interval instance from string.
func stringToInterval(s string) (kline.Interval, error) {
interval, okay := stringToIntervalMap[s]
if okay {
return interval, nil
}
return 0, kline.ErrInvalidInterval
}
// GetBybitServerTime retrieves bybit server time
func (e *Exchange) GetBybitServerTime(ctx context.Context) (*ServerTime, error) {
var resp *ServerTime
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, "market/time", defaultEPL, &resp)
}
// GetKlines query for historical klines (also known as candles/candlesticks). Charts are returned in groups based on the requested interval.
func (e *Exchange) GetKlines(ctx context.Context, category, symbol string, interval kline.Interval, startTime, endTime time.Time, limit uint64) ([]KlineItem, error) {
switch category {
case "":
return nil, errCategoryNotSet
case cSpot, cLinear, cInverse:
default:
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
if symbol == "" {
return nil, errSymbolMissing
}
params := url.Values{}
params.Set("category", category)
params.Set("symbol", symbol)
intervalString, err := intervalToString(interval)
if err != nil {
return nil, err
}
params.Set("interval", intervalString)
if !startTime.IsZero() {
params.Set("start", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("end", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatUint(limit, 10))
}
var resp KlineResponse
err = e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/kline", params), defaultEPL, &resp)
if err != nil {
return nil, err
}
return resp.List, nil
}
// GetInstrumentInfo retrieves the list of instrument details given the category and symbol.
func (e *Exchange) GetInstrumentInfo(ctx context.Context, category, symbol, status, baseCoin, cursor string, limit int64) (*InstrumentsInfo, error) {
params, err := fillCategoryAndSymbol(category, symbol, true)
if err != nil {
return nil, err
}
if status != "" {
params.Set("status", status)
}
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *InstrumentsInfo
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/instruments-info", params), defaultEPL, &resp)
}
// GetMarkPriceKline query for historical mark price klines. Charts are returned in groups based on the requested interval.
func (e *Exchange) GetMarkPriceKline(ctx context.Context, category, symbol string, interval kline.Interval, startTime, endTime time.Time, limit int64) ([]KlineItem, error) {
params, err := fillCategoryAndSymbol(category, symbol)
if err != nil {
return nil, err
}
intervalString, err := intervalToString(interval)
if err != nil {
return nil, err
}
params.Set("interval", intervalString)
if !startTime.IsZero() {
params.Set("start", strconv.FormatInt(startTime.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 MarkPriceKlineResponse
err = e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/mark-price-kline", params), defaultEPL, &resp)
if err != nil {
return nil, err
}
return resp.List, nil
}
// GetIndexPriceKline query for historical index price klines. Charts are returned in groups based on the requested interval.
func (e *Exchange) GetIndexPriceKline(ctx context.Context, category, symbol string, interval kline.Interval, startTime, endTime time.Time, limit int64) ([]KlineItem, error) {
params, err := fillCategoryAndSymbol(category, symbol)
if err != nil {
return nil, err
}
intervalString, err := intervalToString(interval)
if err != nil {
return nil, err
}
params.Set("interval", intervalString)
if !startTime.IsZero() {
params.Set("start", strconv.FormatInt(startTime.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 KlineResponse
err = e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/index-price-kline", params), defaultEPL, &resp)
if err != nil {
return nil, err
}
return resp.List, nil
}
// GetOrderBook retrieves for orderbook depth data.
func (e *Exchange) GetOrderBook(ctx context.Context, category, symbol string, limit int64) (*Orderbook, error) {
params, err := fillCategoryAndSymbol(category, symbol)
if err != nil {
return nil, err
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp orderbookResponse
err = e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/orderbook", params), defaultEPL, &resp)
if err != nil {
return nil, err
}
return constructOrderbook(&resp)
}
func fillCategoryAndSymbol(category, symbol string, optionalSymbol ...bool) (url.Values, error) {
if category == "" {
return nil, errCategoryNotSet
} else if category != cSpot && category != cLinear && category != cInverse && category != cOption {
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
params := url.Values{}
if symbol == "" && (len(optionalSymbol) == 0 || !optionalSymbol[0]) {
return nil, errSymbolMissing
} else if symbol != "" {
params.Set("symbol", symbol)
}
params.Set("category", category)
return params, nil
}
// GetTickers returns the latest price snapshot, best bid/ask price, and trading volume in the last 24 hours.
func (e *Exchange) GetTickers(ctx context.Context, category, symbol, baseCoin string, expiryDate time.Time) (*TickerData, error) {
params, err := fillCategoryAndSymbol(category, symbol, true)
if err != nil {
return nil, err
}
if category == cOption && symbol == "" && baseCoin == "" {
return nil, errBaseNotSet
}
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if !expiryDate.IsZero() {
params.Set("expData", expiryDate.Format(longDatedFormat))
}
var resp *TickerData
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/tickers", params), defaultEPL, &resp)
}
// GetFundingRateHistory retrieves historical funding rates. Each symbol has a different funding interval.
// For example, if the interval is 8 hours and the current time is UTC 12, then it returns the last funding rate, which settled at UTC 8.
func (e *Exchange) GetFundingRateHistory(ctx context.Context, category, symbol string, startTime, endTime time.Time, limit int64) (*FundingRateHistory, error) {
if category == "" {
return nil, errCategoryNotSet
} else if category != cLinear && category != cInverse {
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
params := url.Values{}
if symbol == "" {
return nil, errSymbolMissing
}
params.Set("symbol", symbol)
params.Set("category", category)
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *FundingRateHistory
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/funding/history", params), defaultEPL, &resp)
}
// GetPublicTradingHistory retrieves recent public trading data.
// Option type. 'Call' or 'Put'. For option only
func (e *Exchange) GetPublicTradingHistory(ctx context.Context, category, symbol, baseCoin, optionType string, limit int64) (*TradingHistory, error) {
params, err := fillCategoryAndSymbol(category, symbol)
if err != nil {
return nil, err
}
if category == cOption && symbol == "" && baseCoin == "" {
return nil, errBaseNotSet
}
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if optionType != "" {
params.Set("optionType", optionType)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *TradingHistory
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/recent-trade", params), defaultEPL, &resp)
}
// GetOpenInterestData retrieves open interest of each symbol.
func (e *Exchange) GetOpenInterestData(ctx context.Context, category, symbol, intervalTime string, startTime, endTime time.Time, limit int64, cursor string) (*OpenInterest, error) {
if category == "" {
return nil, errCategoryNotSet
} else if category != cLinear && category != cInverse {
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
params := url.Values{}
if symbol == "" {
return nil, errSymbolMissing
}
params.Set("symbol", symbol)
params.Set("category", category)
if intervalTime != "" {
params.Set("intervalTime", intervalTime)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if cursor != "" {
params.Set("cursor", cursor)
}
var resp *OpenInterest
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/open-interest", params), defaultEPL, &resp)
}
// GetHistoricalVolatility retrieves option historical volatility.
// The data is hourly.
// If both 'startTime' and 'endTime' are not specified, it will return the most recent 1 hours worth of data.
// 'startTime' and 'endTime' are a pair of params. Either both are passed or they are not passed at all.
// This endpoint can query the last 2 years worth of data, but make sure [endTime - startTime] <= 30 days.
func (e *Exchange) GetHistoricalVolatility(ctx context.Context, category, baseCoin string, period int64, startTime, endTime time.Time) ([]HistoricVolatility, error) {
if category == "" {
return nil, errCategoryNotSet
} else if category != cOption {
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
params := url.Values{}
params.Set("category", category)
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if period > 0 {
params.Set("period", strconv.FormatInt(period, 10))
}
var err error
if !startTime.IsZero() || !endTime.IsZero() {
err = common.StartEndTimeCheck(startTime, endTime)
if err != nil {
return nil, err
}
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
var resp []HistoricVolatility
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/historical-volatility", params), defaultEPL, &resp)
}
// GetInsurance retrieves insurance pool data (BTC/USDT/USDC etc). The data is updated every 24 hours.
func (e *Exchange) GetInsurance(ctx context.Context, coin string) (*InsuranceHistory, error) {
params := url.Values{}
if coin != "" {
params.Set("coin", coin)
}
var resp *InsuranceHistory
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/insurance", params), defaultEPL, &resp)
}
// GetRiskLimit retrieves risk limit history
func (e *Exchange) GetRiskLimit(ctx context.Context, category, symbol string) (*RiskLimitHistory, error) {
if category == "" {
return nil, errCategoryNotSet
} else if category != cLinear && category != cInverse {
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
params := url.Values{}
params.Set("category", category)
if symbol != "" {
params.Set("symbol", symbol)
}
var resp *RiskLimitHistory
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/risk-limit", params), defaultEPL, &resp)
}
// GetDeliveryPrice retrieves delivery price.
func (e *Exchange) GetDeliveryPrice(ctx context.Context, category, symbol, baseCoin, cursor string, limit int64) (*DeliveryPrice, error) {
if category == "" {
return nil, errCategoryNotSet
} else if category != cLinear && category != cInverse && category != cOption {
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
params := url.Values{}
params.Set("category", category)
if symbol != "" {
params.Set("symbol", symbol)
}
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if cursor != "" {
params.Set("cursor", cursor)
}
var resp *DeliveryPrice
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/delivery-price", params), defaultEPL, &resp)
}
func isValidCategory(category string) error {
switch category {
case cSpot, cOption, cLinear, cInverse:
return nil
case "":
return errCategoryNotSet
default:
return fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
}
// PlaceOrder creates an order for spot, spot margin, USDT perpetual, USDC perpetual, USDC futures, inverse futures and options.
func (e *Exchange) PlaceOrder(ctx context.Context, arg *PlaceOrderParams) (*OrderResponse, error) {
if arg == nil {
return nil, errNilArgument
}
err := isValidCategory(arg.Category)
if err != nil {
return nil, err
}
if arg.Symbol.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if arg.WhetherToBorrow {
arg.IsLeverage = 1
}
// specifies whether to borrow or to trade.
if arg.IsLeverage != 0 && arg.IsLeverage != 1 {
return nil, errors.New("please provide a valid isLeverage value; must be 0 for unified spot and 1 for margin trading")
}
if arg.Side == "" {
return nil, order.ErrSideIsInvalid
}
if arg.OrderType == "" { // Market and Limit order types are allowed
return nil, order.ErrTypeIsInvalid
}
if arg.OrderQuantity <= 0 {
return nil, limits.ErrAmountBelowMin
}
switch arg.TriggerDirection {
case 0, 1, 2: // 0: None, 1: triggered when market price rises to triggerPrice, 2: triggered when market price falls to triggerPrice
default:
return nil, fmt.Errorf("%w, triggerDirection: %d", errInvalidTriggerDirection, arg.TriggerDirection)
}
if arg.OrderFilter != "" && arg.Category == cSpot {
switch arg.OrderFilter {
case "Order", "tpslOrder", "StopOrder":
default:
return nil, fmt.Errorf("%w, orderFilter=%s", errInvalidOrderFilter, arg.OrderFilter)
}
}
switch arg.TriggerPriceType {
case "", "LastPrice", "IndexPrice", "MarkPrice":
default:
return nil, errInvalidTriggerPriceType
}
var resp OrderResponse
epl := createOrderEPL
if arg.Category == "spot" {
epl = createSpotOrderEPL
}
return &resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/create", nil, arg, &resp, epl)
}
// AmendOrder amends an open unfilled or partially filled orders.
func (e *Exchange) AmendOrder(ctx context.Context, arg *AmendOrderParams) (*OrderResponse, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.OrderID == "" && arg.OrderLinkID == "" {
return nil, errEitherOrderIDOROrderLinkIDRequired
}
err := isValidCategory(arg.Category)
if err != nil {
return nil, err
}
if arg.Symbol.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
var resp *OrderResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/amend", nil, arg, &resp, amendOrderEPL)
}
// CancelTradeOrder cancels an open unfilled or partially filled order.
func (e *Exchange) CancelTradeOrder(ctx context.Context, arg *CancelOrderParams) (*OrderResponse, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.OrderID == "" && arg.OrderLinkID == "" {
return nil, errEitherOrderIDOROrderLinkIDRequired
}
err := isValidCategory(arg.Category)
if err != nil {
return nil, err
}
if arg.Symbol.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
switch {
case arg.OrderFilter != "" && arg.Category == cSpot:
switch arg.OrderFilter {
case "Order", "tpslOrder", "StopOrder":
default:
return nil, fmt.Errorf("%w, orderFilter=%s", errInvalidOrderFilter, arg.OrderFilter)
}
case arg.OrderFilter != "":
return nil, fmt.Errorf("%w, orderFilter is valid for 'spot' only", errInvalidCategory)
}
var resp *OrderResponse
epl := cancelOrderEPL
if arg.Category == "spot" {
epl = cancelSpotEPL
}
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/cancel", nil, arg, &resp, epl)
}
// GetOpenOrders retrieves unfilled or partially filled orders in real-time. To query older order records, please use the order history interface.
// orderFilter: possible values are 'Order', 'StopOrder', 'tpslOrder', and 'OcoOrder'
func (e *Exchange) GetOpenOrders(ctx context.Context, category, symbol, baseCoin, settleCoin, orderID, orderLinkID, orderFilter, cursor string, openOnly, limit int64) (*TradeOrders, error) {
params, err := fillCategoryAndSymbol(category, symbol, true)
if err != nil {
return nil, err
}
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if settleCoin != "" {
params.Set("settleCoin", settleCoin)
}
if orderID != "" {
params.Set("orderId", orderID)
}
if orderLinkID != "" {
params.Set("orderLinkId", orderLinkID)
}
if openOnly != 0 {
params.Set("openOnly", strconv.FormatInt(openOnly, 10))
}
if orderFilter != "" {
params.Set("orderFilter", orderFilter)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if cursor != "" {
params.Set("cursor", cursor)
}
var resp *TradeOrders
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/order/realtime", params, nil, &resp, getOrderEPL)
}
// CancelAllTradeOrders cancel all open orders
func (e *Exchange) CancelAllTradeOrders(ctx context.Context, arg *CancelAllOrdersParam) ([]OrderResponse, error) {
if arg == nil {
return nil, errNilArgument
}
err := isValidCategory(arg.Category)
if err != nil {
return nil, err
}
if arg.OrderFilter != "" && (arg.Category != "linear" && arg.Category != "inverse") {
return nil, fmt.Errorf("%w, only used for category=linear or inverse", errInvalidOrderFilter)
}
var resp CancelAllResponse
epl := cancelAllEPL
if arg.Category == "spot" {
epl = cancelAllSpotEPL
}
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/cancel-all", nil, arg, &resp, epl)
}
// GetTradeOrderHistory retrieves order history. As order creation/cancellation is asynchronous, the data returned from this endpoint may delay.
// If you want to get real-time order information, you could query this endpoint or rely on the websocket stream (recommended).
// orderFilter: possible values are 'Order', 'StopOrder', 'tpslOrder', and 'OcoOrder'
func (e *Exchange) GetTradeOrderHistory(ctx context.Context, category, symbol, orderID, orderLinkID,
baseCoin, settleCoin, orderFilter, orderStatus, cursor string,
startTime, endTime time.Time, limit int64,
) (*TradeOrders, error) {
params, err := fillCategoryAndSymbol(category, symbol, true)
if err != nil {
return nil, err
}
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if settleCoin != "" {
params.Set("settleCoin", settleCoin)
}
if orderID != "" {
params.Set("orderId", orderID)
}
if orderLinkID != "" {
params.Set("orderLinkId", orderLinkID)
}
if orderFilter != "" {
params.Set("orderFilter", orderFilter)
}
if orderStatus != "" {
params.Set("orderStatus", orderStatus)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *TradeOrders
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/order/history", params, nil, &resp, getOrderHistoryEPL)
}
// PlaceBatchOrder place batch or trade order.
func (e *Exchange) PlaceBatchOrder(ctx context.Context, arg *PlaceBatchOrderParam) ([]BatchOrderResponse, error) {
if arg == nil {
return nil, errNilArgument
}
switch {
case arg.Category == "":
return nil, errCategoryNotSet
case arg.Category != cOption && arg.Category != cLinear:
return nil, fmt.Errorf("%w, only 'option' and 'linear' categories are allowed", errInvalidCategory)
}
if len(arg.Request) == 0 {
return nil, errNoOrderPassed
}
for a := range arg.Request {
if arg.Request[a].OrderLinkID == "" {
return nil, errOrderLinkIDMissing
}
if arg.Request[a].Symbol.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if arg.Request[a].Side == "" {
return nil, order.ErrSideIsInvalid
}
if arg.Request[a].OrderType == "" { // Market and Limit order types are allowed
return nil, order.ErrTypeIsInvalid
}
if arg.Request[a].OrderQuantity <= 0 {
return nil, limits.ErrAmountBelowMin
}
}
var resp BatchOrdersList
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/create-batch", nil, arg, &resp, createBatchOrderEPL)
}
// BatchAmendOrder represents a batch amend order.
func (e *Exchange) BatchAmendOrder(ctx context.Context, category string, args []BatchAmendOrderParamItem) (*BatchOrderResponse, error) {
if len(args) == 0 {
return nil, errNilArgument
}
switch {
case category == "":
return nil, errCategoryNotSet
case category != cOption:
return nil, fmt.Errorf("%w, only 'option' category is allowed", errInvalidCategory)
}
for a := range args {
if args[a].OrderID == "" && args[a].OrderLinkID == "" {
return nil, errEitherOrderIDOROrderLinkIDRequired
}
if args[a].Symbol.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
}
var resp *BatchOrderResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/amend-batch", nil, &BatchAmendOrderParams{
Category: category,
Request: args,
}, &resp, amendBatchOrderEPL)
}
// CancelBatchOrder cancel more than one open order in a single request.
func (e *Exchange) CancelBatchOrder(ctx context.Context, arg *CancelBatchOrder) ([]CancelBatchResponseItem, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.Category != cOption {
return nil, fmt.Errorf("%w, only 'option' category is allowed", errInvalidCategory)
}
if len(arg.Request) == 0 {
return nil, errNoOrderPassed
}
for a := range arg.Request {
if arg.Request[a].OrderID == "" && arg.Request[a].OrderLinkID == "" {
return nil, errEitherOrderIDOROrderLinkIDRequired
}
if arg.Request[a].Symbol.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
}
var resp cancelBatchResponse
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/cancel-batch", nil, arg, &resp, cancelBatchOrderEPL)
}
// GetBorrowQuota retrieves the qty and amount of borrowable coins in spot account.
func (e *Exchange) GetBorrowQuota(ctx context.Context, category, symbol, side string) (*BorrowQuota, error) {
if category == "" {
return nil, errCategoryNotSet
} else if category != cSpot {
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
params := url.Values{}
if symbol == "" {
return nil, errSymbolMissing
}
params.Set("symbol", symbol)
params.Set("category", category)
if side == "" {
return nil, order.ErrSideIsInvalid
}
params.Set("side", side)
var resp *BorrowQuota
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/order/spot-borrow-check", params, nil, &resp, defaultEPL)
}
// SetDisconnectCancelAll You can use this endpoint to get your current DCP configuration.
// Your private websocket connection must subscribe "dcp" topic in order to trigger DCP successfully
func (e *Exchange) SetDisconnectCancelAll(ctx context.Context, arg *SetDCPParams) error {
if arg == nil {
return errNilArgument
}
if arg.TimeWindow == 0 {
return errDisconnectTimeWindowNotSet
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/order/disconnected-cancel-all", nil, arg, &struct{}{}, defaultEPL)
}
// GetPositionInfo retrieves real-time position data, such as position size, cumulative realizedPNL.
func (e *Exchange) GetPositionInfo(ctx context.Context, category, symbol, baseCoin, settleCoin, cursor string, limit int64) (*PositionInfoList, error) {
if category == "" {
return nil, errCategoryNotSet
} else if category != cLinear && category != cInverse && category != cOption {
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
if symbol == "" && settleCoin == "" {
return nil, errSymbolOrSettleCoinRequired
}
params := url.Values{}
if symbol != "" {
params.Set("symbol", symbol)
}
params.Set("category", category)
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if settleCoin != "" {
params.Set("settleCoin", settleCoin)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *PositionInfoList
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/position/list", params, nil, &resp, getPositionListEPL)
}
// SetLeverageLevel sets a leverage from 0 to max leverage of corresponding risk limit
func (e *Exchange) SetLeverageLevel(ctx context.Context, arg *SetLeverageParams) error {
if arg == nil {
return errNilArgument
}
switch arg.Category {
case "":
return errCategoryNotSet
case cLinear, cInverse:
default:
return fmt.Errorf("%w, category: %s", errInvalidCategory, arg.Category)
}
if arg.Symbol == "" {
return errSymbolMissing
}
switch {
case arg.BuyLeverage <= 0:
return fmt.Errorf("%w code: 10001 msg: invalid buy leverage %f", errInvalidLeverage, arg.BuyLeverage)
case arg.BuyLeverage != arg.SellLeverage:
return fmt.Errorf("%w, buy leverage not equal sell leverage", errInvalidLeverage)
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/position/set-leverage", nil, arg, &struct{}{}, postPositionSetLeverageEPL)
}
// SwitchTradeMode sets the trade mode value either to 'cross' or 'isolated'.
// Select cross margin mode or isolated margin mode per symbol level
func (e *Exchange) SwitchTradeMode(ctx context.Context, arg *SwitchTradeModeParams) error {
if arg == nil {
return errNilArgument
}
switch arg.Category {
case "":
return errCategoryNotSet
case cLinear, cInverse:
default:
return fmt.Errorf("%w, category: %s", errInvalidCategory, arg.Category)
}
switch {
case arg.Symbol == "":
return errSymbolMissing
case arg.BuyLeverage <= 0:
return fmt.Errorf("%w code: 10001 msg: invalid buy leverage %f", errInvalidLeverage, arg.BuyLeverage)
case arg.BuyLeverage != arg.SellLeverage:
return fmt.Errorf("%w, buy leverage not equal sell leverage", errInvalidLeverage)
case arg.TradeMode != 0 && arg.TradeMode != 1:
return errInvalidTradeModeValue
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/position/switch-isolated", nil, arg, &struct{}{}, defaultEPL)
}
// SetTakeProfitStopLossMode set partial TP/SL mode, you can set the TP/SL size smaller than position size.
func (e *Exchange) SetTakeProfitStopLossMode(ctx context.Context, arg *TPSLModeParams) (*TPSLModeResponse, error) {
if arg == nil {
return nil, errNilArgument
}
switch arg.Category {
case "":
return nil, errCategoryNotSet
case cLinear, cInverse:
default:
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, arg.Category)
}
switch {
case arg.Symbol == "":
return nil, errSymbolMissing
case arg.TpslMode == "":
return nil, errTakeProfitOrStopLossModeMissing
}
var resp *TPSLModeResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/position/set-tpsl-mode", nil, arg, &resp, setPositionTPLSModeEPL)
}
// SwitchPositionMode switch the position mode for USDT perpetual and Inverse futures.
// If you are in one-way Mode, you can only open one position on Buy or Sell side.
// If you are in hedge mode, you can open both Buy and Sell side positions simultaneously.
// switches mode between MergedSingle: One-Way Mode or BothSide: Hedge Mode
func (e *Exchange) SwitchPositionMode(ctx context.Context, arg *SwitchPositionModeParams) error {
if arg == nil {
return errNilArgument
}
switch arg.Category {
case "":
return errCategoryNotSet
case cLinear, cInverse:
default:
return fmt.Errorf("%w, category: %s", errInvalidCategory, arg.Category)
}
if arg.Symbol.IsEmpty() && arg.Coin.IsEmpty() {
return errEitherSymbolOrCoinRequired
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/position/switch-mode", nil, arg, &struct{}{}, defaultEPL)
}
// SetRiskLimit risk limit will limit the maximum position value you can hold under different margin requirements.
// If you want to hold a bigger position size, you need more margin. This interface can set the risk limit of a single position.
// If the order exceeds the current risk limit when placing an order, it will be rejected.
// '0': one-way mode '1': hedge-mode Buy side '2': hedge-mode Sell side
func (e *Exchange) SetRiskLimit(ctx context.Context, arg *SetRiskLimitParam) (*RiskLimitResponse, error) {
if arg == nil {
return nil, errNilArgument
}
switch arg.Category {
case "":
return nil, errCategoryNotSet
case cLinear, cInverse:
default:
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, arg.Category)
}
if arg.PositionMode < 0 || arg.PositionMode > 2 {
return nil, errInvalidPositionMode
}
if arg.Symbol.IsEmpty() {
return nil, errSymbolMissing
}
var resp *RiskLimitResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/position/set-risk-limit", nil, arg, &resp, setPositionRiskLimitEPL)
}
// SetTradingStop set the take profit, stop loss or trailing stop for the position.
func (e *Exchange) SetTradingStop(ctx context.Context, arg *TradingStopParams) error {
if arg == nil {
return errNilArgument
}
switch arg.Category {
case cLinear, cInverse:
case "":
return errCategoryNotSet
default:
return fmt.Errorf("%w, category: %s", errInvalidCategory, arg.Category)
}
if arg.Symbol.IsEmpty() {
return errSymbolMissing
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/position/trading-stop", nil, arg, &struct{}{}, stopTradingPositionEPL)
}
// SetAutoAddMargin sets auto add margin
func (e *Exchange) SetAutoAddMargin(ctx context.Context, arg *AutoAddMarginParam) error {
if arg == nil {
return errNilArgument
}
switch arg.Category {
case "":
return errCategoryNotSet
case cLinear, cInverse:
default:
return fmt.Errorf("%w, category: %s", errInvalidCategory, arg.Category)
}
if arg.Symbol.IsEmpty() {
return errSymbolMissing
}
if arg.AutoAddmargin != 0 && arg.AutoAddmargin != 1 {
return errInvalidAutoAddMarginValue
}
if arg.PositionIndex < 0 || arg.PositionIndex > 2 {
return errInvalidPositionMode
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/position/set-auto-add-margin", nil, arg, &struct{}{}, defaultEPL)
}
// AddOrReduceMargin manually add or reduce margin for isolated margin position
func (e *Exchange) AddOrReduceMargin(ctx context.Context, arg *AddOrReduceMarginParam) (*AddOrReduceMargin, error) {
if arg == nil {
return nil, errNilArgument
}
switch arg.Category {
case "":
return nil, errCategoryNotSet
case cLinear, cInverse:
default:
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, arg.Category)
}
if arg.Symbol.IsEmpty() {
return nil, errSymbolMissing
}
if arg.Margin != 10 && arg.Margin != -10 {
return nil, errInvalidAutoAddMarginValue
}
if arg.PositionIndex < 0 || arg.PositionIndex > 2 {
return nil, errInvalidPositionMode
}
var resp *AddOrReduceMargin
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/position/add-margin", nil, arg, &resp, defaultEPL)
}
// GetExecution retrieves users' execution records, sorted by execTime in descending order. However, for Normal spot, they are sorted by execId in descending order.
// Execution Type possible values: 'Trade', 'AdlTrade' Auto-Deleveraging, 'Funding' Funding fee, 'BustTrade' Liquidation, 'Delivery' USDC futures delivery, 'BlockTrade'
// UTA Spot: 'stopOrderType', "" for normal order, "tpslOrder" for TP/SL order, "Stop" for conditional order, "OcoOrder" for OCO order
func (e *Exchange) GetExecution(ctx context.Context, category, symbol, orderID, orderLinkID, baseCoin, executionType, stopOrderType, cursor string, startTime, endTime time.Time, limit int64) (*ExecutionResponse, error) {
params, err := fillCategoryAndSymbol(category, symbol, true)
if err != nil {
return nil, err
}
if orderID != "" {
params.Set("orderId", orderID)
}
if orderLinkID != "" {
params.Set("orderLinkId", orderLinkID)
}
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if executionType != "" {
params.Set("execType", executionType)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if stopOrderType != "" {
params.Set("stopOrderType", stopOrderType)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *ExecutionResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/execution/list", params, nil, &resp, getExecutionListEPL)
}
// GetClosedPnL retrieves user's closed profit and loss records. The results are sorted by createdTime in descending order.
func (e *Exchange) GetClosedPnL(ctx context.Context, category, symbol, cursor string, startTime, endTime time.Time, limit int64) (*ClosedProfitAndLossResponse, error) {
params, err := fillOrderAndExecutionFetchParams(paramsConfig{Linear: true, Inverse: true}, category, symbol, "", "", "", "", "", cursor, startTime, endTime, limit)
if err != nil {
return nil, err
}
var resp *ClosedProfitAndLossResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/position/closed-pnl", params, nil, &resp, getPositionClosedPNLEPL)
}
func fillOrderAndExecutionFetchParams(ac paramsConfig, category, symbol, baseCoin, orderID, orderLinkID, orderFilter, orderStatus, cursor string, startTime, endTime time.Time, limit int64) (url.Values, error) {
params := url.Values{}
switch {
case !ac.OptionalCategory && category == "":
return nil, errCategoryNotSet
case ac.OptionalCategory && category == "":
case (!ac.Linear && category == cLinear) ||
(!ac.Option && category == cOption) ||
(!ac.Inverse && category == cInverse) ||
(!ac.Spot && category == cSpot):
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
default:
params.Set("category", category)
}
if ac.MandatorySymbol && symbol == "" {
return nil, errSymbolMissing
} else if symbol != "" {
params.Set("symbol", symbol)
}
if category == cOption && baseCoin == "" && !ac.OptionalBaseCoin {
return nil, fmt.Errorf("%w, baseCoin is required", errBaseNotSet)
} else if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
if orderID != "" {
params.Set("orderId", orderID)
}
if orderLinkID != "" {
params.Set("orderLinkId", orderLinkID)
}
if orderFilter != "" {
params.Set("orderFilter", orderFilter)
}
if orderStatus != "" {
params.Set("orderStatus", orderStatus)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return params, nil
}
// ConfirmNewRiskLimit t is only applicable when the user is marked as only reducing positions
// (please see the isReduceOnly field in the Get Position Info interface).
// After the user actively adjusts the risk level, this interface is called to try to calculate the adjusted risk level,
// and if it passes (retCode=0), the system will remove the position reduceOnly mark.
// You are recommended to call Get Position Info to check isReduceOnly field.
func (e *Exchange) ConfirmNewRiskLimit(ctx context.Context, category, symbol string) error {
if category == "" {
return errCategoryNotSet
}
if symbol == "" {
return errSymbolMissing
}
arg := &struct {
Category string `json:"category"`
Symbol string `json:"symbol"`
}{
Category: category,
Symbol: symbol,
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/position/confirm-pending-mmr", nil, arg, &struct{}{}, defaultEPL)
}
// GetPreUpgradeOrderHistory the account is upgraded to a Unified account, you can get the orders which occurred before the upgrade.
func (e *Exchange) GetPreUpgradeOrderHistory(ctx context.Context, category, symbol, baseCoin, orderID, orderLinkID, orderFilter, orderStatus, cursor string, startTime, endTime time.Time, limit int64) (*TradeOrders, error) {
params, err := fillOrderAndExecutionFetchParams(paramsConfig{Linear: true, Option: true, Inverse: true}, category, symbol, baseCoin, orderID, orderLinkID, orderFilter, orderStatus, cursor, startTime, endTime, limit)
if err != nil {
return nil, err
}
err = e.RequiresUnifiedAccount(ctx)
if err != nil {
return nil, err
}
var resp *TradeOrders
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/pre-upgrade/order/history", params, nil, &resp, defaultEPL)
}
// GetPreUpgradeTradeHistory retrieves users' execution records which occurred before you upgraded the account to a Unified account, sorted by execTime in descending order
func (e *Exchange) GetPreUpgradeTradeHistory(ctx context.Context, category, symbol, orderID, orderLinkID, baseCoin, executionType, cursor string, startTime, endTime time.Time, limit int64) (*ExecutionResponse, error) {
params, err := fillOrderAndExecutionFetchParams(paramsConfig{Linear: true, Option: false, Inverse: true}, category, symbol, baseCoin, orderID, orderLinkID, "", "", cursor, startTime, endTime, limit)
if err != nil {
return nil, err
}
err = e.RequiresUnifiedAccount(ctx)
if err != nil {
return nil, err
}
if executionType != "" {
params.Set("executionType", executionType)
}
var resp *ExecutionResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/pre-upgrade/execution/list", params, nil, &resp, defaultEPL)
}
// GetPreUpgradeClosedPnL retrieves user's closed profit and loss records from before you upgraded the account to a Unified account. The results are sorted by createdTime in descending order.
func (e *Exchange) GetPreUpgradeClosedPnL(ctx context.Context, category, symbol, cursor string, startTime, endTime time.Time, limit int64) (*ClosedProfitAndLossResponse, error) {
params, err := fillOrderAndExecutionFetchParams(paramsConfig{Linear: true, Inverse: true, MandatorySymbol: true}, category, symbol, "", "", "", "", "", cursor, startTime, endTime, limit)
if err != nil {
return nil, err
}
err = e.RequiresUnifiedAccount(ctx)
if err != nil {
return nil, err
}
var resp *ClosedProfitAndLossResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/pre-upgrade/position/closed-pnl", params, nil, &resp, defaultEPL)
}
// GetPreUpgradeTransactionLog retrieves transaction logs which occurred in the USDC Derivatives wallet before the account was upgraded to a Unified account.
func (e *Exchange) GetPreUpgradeTransactionLog(ctx context.Context, category, baseCoin, transactionType, cursor string, startTime, endTime time.Time, limit int64) (*TransactionLog, error) {
params, err := fillOrderAndExecutionFetchParams(paramsConfig{Linear: true, Inverse: true}, category, "", baseCoin, "", "", "", "", cursor, startTime, endTime, limit)
if err != nil {
return nil, err
}
err = e.RequiresUnifiedAccount(ctx)
if err != nil {
return nil, err
}
if transactionType != "" {
params.Set("type", transactionType)
}
var resp *TransactionLog
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/pre-upgrade/account/transaction-log", params, nil, &resp, defaultEPL)
}
// GetPreUpgradeOptionDeliveryRecord retrieves delivery records of Option before you upgraded the account to a Unified account, sorted by deliveryTime in descending order
func (e *Exchange) GetPreUpgradeOptionDeliveryRecord(ctx context.Context, category, symbol, cursor string, expiryDate time.Time, limit int64) (*PreUpdateOptionDeliveryRecord, error) {
params, err := fillOrderAndExecutionFetchParams(paramsConfig{OptionalBaseCoin: true, Option: true}, category, symbol, "", "", "", "", "", cursor, time.Time{}, time.Time{}, limit)
if err != nil {
return nil, err
}
err = e.RequiresUnifiedAccount(ctx)
if err != nil {
return nil, err
}
if !expiryDate.IsZero() {
params.Set("expData", expiryDate.Format(longDatedFormat))
}
var resp *PreUpdateOptionDeliveryRecord
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/pre-upgrade/asset/delivery-record", params, nil, &resp, defaultEPL)
}
// GetPreUpgradeUSDCSessionSettlement retrieves session settlement records of USDC perpetual before you upgrade the account to Unified account.
func (e *Exchange) GetPreUpgradeUSDCSessionSettlement(ctx context.Context, category, symbol, cursor string, limit int64) (*SettlementSession, error) {
params, err := fillOrderAndExecutionFetchParams(paramsConfig{Linear: true}, category, symbol, "", "", "", "", "", cursor, time.Time{}, time.Time{}, limit)
if err != nil {
return nil, err
}
err = e.RequiresUnifiedAccount(ctx)
if err != nil {
return nil, err
}
var resp *SettlementSession
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/pre-upgrade/asset/settlement-record", params, nil, &resp, defaultEPL)
}
// GetWalletBalance represents wallet balance, query asset information of each currency, and account risk rate information.
// By default, currency information with assets or liabilities of 0 is not returned.
// Unified account: UNIFIED (trade spot/linear/options), CONTRACT(trade inverse)
// Normal account: CONTRACT, SPOT
func (e *Exchange) GetWalletBalance(ctx context.Context, accountType, coin string) (*WalletBalance, error) {
params := url.Values{}
if accountType == "" {
return nil, errMissingAccountType
}
params.Set("accountType", accountType)
if coin != "" {
params.Set("coin", coin)
}
var resp *WalletBalance
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/account/wallet-balance", params, nil, &resp, getAccountWalletBalanceEPL)
}
// UpgradeToUnifiedAccount upgrades the account to unified account.
func (e *Exchange) UpgradeToUnifiedAccount(ctx context.Context) (*UnifiedAccountUpgradeResponse, error) {
var resp *UnifiedAccountUpgradeResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/account/upgrade-to-uta", nil, nil, &resp, defaultEPL)
}
// GetBorrowHistory retrieves interest records, sorted in reverse order of creation time.
func (e *Exchange) GetBorrowHistory(ctx context.Context, ccy, cursor string, startTime, endTime time.Time, limit int64) (*BorrowHistory, error) {
params := url.Values{}
if ccy != "" {
params.Set("currency", ccy)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if cursor != "" {
params.Set("cursor", cursor)
}
var resp *BorrowHistory
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/account/borrow-history", params, nil, &resp, defaultEPL)
}
// SetCollateralCoin decide whether the assets in the Unified account needs to be collateral coins.
func (e *Exchange) SetCollateralCoin(ctx context.Context, coin currency.Code, collateralSwitchON bool) error {
params := url.Values{}
if !coin.IsEmpty() {
params.Set("coin", coin.String())
}
if collateralSwitchON {
params.Set("collateralSwitch", "ON")
} else {
params.Set("collateralSwitch", "OFF")
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/account/set-collateral-switch", params, nil, &struct{}{}, defaultEPL)
}
// GetCollateralInfo retrieves the collateral information of the current unified margin account,
// including loan interest rate, loanable amount, collateral conversion rate,
// whether it can be mortgaged as margin, etc.
func (e *Exchange) GetCollateralInfo(ctx context.Context, ccy string) (*CollateralInfo, error) {
params := url.Values{}
if ccy != "" {
params.Set("currency", ccy)
}
var resp *CollateralInfo
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/account/collateral-info", params, nil, &resp, defaultEPL)
}
// GetCoinGreeks retrieves current account Greeks information
func (e *Exchange) GetCoinGreeks(ctx context.Context, baseCoin string) (*CoinGreeks, error) {
params := url.Values{}
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
var resp *CoinGreeks
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/coin-greeks", params, nil, &resp, defaultEPL)
}
// GetFeeRate retrieves the trading fee rate.
func (e *Exchange) GetFeeRate(ctx context.Context, category, symbol, baseCoin string) (*AccountFee, error) {
params := url.Values{}
if !slices.Contains(validCategory, category) {
return nil, fmt.Errorf("%w, valid category values are %v", errInvalidCategory, validCategory)
}
if category != "" {
params.Set("category", category)
}
if symbol != "" {
params.Set("symbol", symbol)
}
if baseCoin != "" {
params.Set("baseCoin", baseCoin)
}
var resp *AccountFee
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/account/fee-rate", params, nil, &resp, getAccountFeeEPL)
}
// GetAccountInfo retrieves the margin mode configuration of the account.
// query the margin mode and the upgraded status of account
func (e *Exchange) GetAccountInfo(ctx context.Context) (*AccountInfo, error) {
var resp *AccountInfo
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/account/info", nil, nil, &resp, defaultEPL)
}
// GetTransactionLog retrieves transaction logs in Unified account.
func (e *Exchange) GetTransactionLog(ctx context.Context, category, baseCoin, transactionType, cursor string, startTime, endTime time.Time, limit int64) (*TransactionLog, error) {
params, err := fillOrderAndExecutionFetchParams(paramsConfig{OptionalBaseCoin: true, OptionalCategory: true, Linear: true, Option: true, Spot: true}, category, "", baseCoin, "", "", "", "", cursor, startTime, endTime, limit)
if err != nil {
return nil, err
}
if transactionType != "" {
params.Set("type", transactionType)
}
var resp *TransactionLog
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/account/transaction-log", params, nil, &resp, defaultEPL)
}
// SetMarginMode set margin mode to either of ISOLATED_MARGIN, REGULAR_MARGIN(i.e. Cross margin), PORTFOLIO_MARGIN
func (e *Exchange) SetMarginMode(ctx context.Context, marginMode string) (*SetMarginModeResponse, error) {
if marginMode == "" {
return nil, fmt.Errorf("%w, margin mode should be either of ISOLATED_MARGIN, REGULAR_MARGIN, or PORTFOLIO_MARGIN", errInvalidMode)
}
arg := &struct {
SetMarginMode string `json:"setMarginMode"`
}{SetMarginMode: marginMode}
var resp *SetMarginModeResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/account/set-margin-mode", nil, arg, &resp, defaultEPL)
}
// SetSpotHedging to turn on/off Spot hedging feature in Portfolio margin for Unified account.
func (e *Exchange) SetSpotHedging(ctx context.Context, setHedgingModeOn bool) error {
resp := struct{}{}
setHedgingMode := "OFF"
if setHedgingModeOn {
setHedgingMode = "ON"
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/account/set-hedging-mode", nil, &map[string]string{"setHedgingMode": setHedgingMode}, &resp, defaultEPL)
}
// SetMMP Market Maker Protection (MMP) is an automated mechanism designed to protect market makers (MM) against liquidity risks and over-exposure in the market.
func (e *Exchange) SetMMP(ctx context.Context, arg *MMPRequestParam) error {
if arg == nil {
return errNilArgument
}
if arg.BaseCoin == "" {
return errBaseNotSet
}
if arg.TimeWindowMS <= 0 {
return errTimeWindowRequired
}
if arg.FrozenPeriod <= 0 {
return errFrozenPeriodRequired
}
if arg.TradeQuantityLimit <= 0 {
return fmt.Errorf("%w, trade quantity limit required", errQuantityLimitRequired)
}
if arg.DeltaLimit <= 0 {
return fmt.Errorf("%w, delta limit is required", errQuantityLimitRequired)
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/account/mmp-modify", nil, arg, &struct{}{}, defaultEPL)
}
// ResetMMP resets MMP.
// once the mmp triggered, you can unfreeze the account by this endpoint
func (e *Exchange) ResetMMP(ctx context.Context, baseCoin string) error {
if baseCoin == "" {
return errBaseNotSet
}
arg := &struct {
BaseCoin string `json:"baseCoin"`
}{BaseCoin: baseCoin}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/account/mmp-reset", nil, arg, &struct{}{}, defaultEPL)
}
// GetMMPState retrieve Market Maker Protection (MMP) states for different coins.
func (e *Exchange) GetMMPState(ctx context.Context, baseCoin string) (*MMPStates, error) {
if baseCoin == "" {
return nil, errBaseNotSet
}
arg := &struct {
BaseCoin string `json:"baseCoin"`
}{BaseCoin: baseCoin}
var resp *MMPStates
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/account/mmp-state", nil, arg, &resp, defaultEPL)
}
// GetCoinExchangeRecords queries the coin exchange records.
func (e *Exchange) GetCoinExchangeRecords(ctx context.Context, fromCoin, toCoin, cursor string, limit int64) (*CoinExchangeRecords, error) {
params := url.Values{}
if fromCoin != "" {
params.Set("fromCoin", fromCoin)
}
if toCoin != "" {
params.Set("toCoin", toCoin)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *CoinExchangeRecords
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/exchange/order-record", params, nil, &resp, getExchangeOrderRecordEPL)
}
// GetDeliveryRecord retrieves delivery records of USDC futures and Options, sorted by deliveryTime in descending order
func (e *Exchange) GetDeliveryRecord(ctx context.Context, category, symbol, cursor string, expiryDate time.Time, limit int64) (*DeliveryRecord, error) {
if !slices.Contains([]string{cLinear, cOption}, category) {
return nil, fmt.Errorf("%w, valid category values are %v", errInvalidCategory, []string{cLinear, cOption})
}
params := url.Values{}
params.Set("category", category)
if symbol != "" {
params.Set("symbol", symbol)
}
if !expiryDate.IsZero() {
params.Set("expData", expiryDate.Format(longDatedFormat))
}
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *DeliveryRecord
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/delivery-record", params, nil, &resp, defaultEPL)
}
// GetUSDCSessionSettlement retrieves session settlement records of USDC perpetual and futures
func (e *Exchange) GetUSDCSessionSettlement(ctx context.Context, category, symbol, cursor string, limit int64) (*SettlementSession, error) {
if category != cLinear {
return nil, fmt.Errorf("%w, valid category value is %v", errInvalidCategory, cLinear)
}
params := url.Values{}
params.Set("category", category)
if symbol != "" {
params.Set("symbol", symbol)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *SettlementSession
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/settlement-record", params, nil, &resp, defaultEPL)
}
// GetAssetInfo retrieves asset information
func (e *Exchange) GetAssetInfo(ctx context.Context, accountType, coin string) (*AccountInfos, error) {
if accountType == "" {
return nil, errMissingAccountType
}
params := url.Values{}
params.Set("accountType", accountType)
if coin != "" {
params.Set("coin", coin)
}
var resp *AccountInfos
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/transfer/query-asset-info", params, nil, &resp, getAssetTransferQueryInfoEPL)
}
func fillCoinBalanceFetchParams(accountType, memberID, coin string, withBonus int64, coinRequired bool) (url.Values, error) {
if accountType == "" {
return nil, errMissingAccountType
}
params := url.Values{}
params.Set("accountType", accountType)
if memberID != "" {
params.Set("memberId", memberID)
}
if coinRequired && coin == "" {
return nil, currency.ErrCurrencyCodeEmpty
}
if coin != "" {
params.Set("coin", coin)
}
if withBonus > 0 {
params.Set("withBonus", strconv.FormatInt(withBonus, 10))
}
return params, nil
}
// GetAllCoinBalance retrieves all coin balance of all account types under the master account, and sub account.
// It is not allowed to get master account coin balance via sub account api key.
func (e *Exchange) GetAllCoinBalance(ctx context.Context, accountType, memberID, coin string, withBonus int64) (*CoinBalances, error) {
params, err := fillCoinBalanceFetchParams(accountType, memberID, coin, withBonus, false)
if err != nil {
return nil, err
}
var resp *CoinBalances
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/transfer/query-account-coins-balance", params, nil, &resp, getAssetAccountCoinBalanceEPL)
}
// GetSingleCoinBalance retrieves the balance of a specific coin in a specific account type. Supports querying sub UID's balance.
func (e *Exchange) GetSingleCoinBalance(ctx context.Context, accountType, coin, memberID string, withBonus, withTransferSafeAmount int64) (*CoinBalance, error) {
params, err := fillCoinBalanceFetchParams(accountType, memberID, coin, withBonus, true)
if err != nil {
return nil, err
}
if withTransferSafeAmount > 0 {
params.Set("withTransferSafeAmount", strconv.FormatInt(withTransferSafeAmount, 10))
}
var resp *CoinBalance
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/transfer/query-account-coin-balance", params, nil, &resp, getAssetTransferQueryTransferCoinListEPL)
}
// GetTransferableCoin the transferable coin list between each account type
func (e *Exchange) GetTransferableCoin(ctx context.Context, fromAccountType, toAccountType string) (*TransferableCoins, error) {
if fromAccountType == "" {
return nil, fmt.Errorf("%w, from account type not specified", errMissingAccountType)
}
if toAccountType == "" {
return nil, fmt.Errorf("%w, to account type not specified", errMissingAccountType)
}
params := url.Values{}
params.Set("fromAccountType", fromAccountType)
params.Set("toAccountType", toAccountType)
var resp *TransferableCoins
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/transfer/query-transfer-coin-list", params, nil, &resp, getAssetTransferQueryTransferCoinListEPL)
}
// CreateInternalTransfer create the internal transfer between different account types under the same UID.
// Each account type has its own acceptable coins, e.g, you cannot transfer USDC from SPOT to CONTRACT.
// Please refer to transferable coin list API to find out more.
func (e *Exchange) CreateInternalTransfer(ctx context.Context, arg *TransferParams) (string, error) {
if arg == nil {
return "", errNilArgument
}
if arg.TransferID.IsNil() {
return "", errMissingTransferID
}
if arg.Coin.IsEmpty() {
return "", currency.ErrCurrencyCodeEmpty
}
if arg.Amount <= 0 {
return "", order.ErrAmountIsInvalid
}
if arg.FromAccountType == "" {
return "", fmt.Errorf("%w, from account type not specified", errMissingAccountType)
}
if arg.ToAccountType == "" {
return "", fmt.Errorf("%w, to account type not specified", errMissingAccountType)
}
resp := struct {
TransferID string `json:"transferId"`
}{}
return resp.TransferID, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/asset/transfer/inter-transfer", nil, arg, &resp, interTransferEPL)
}
// GetInternalTransferRecords retrieves the internal transfer records between different account types under the same UID.
func (e *Exchange) GetInternalTransferRecords(ctx context.Context, transferID, coin, status, cursor string, startTime, endTime time.Time, limit int64) (*TransferResponse, error) {
params := fillTransferQueryParams(transferID, coin, status, cursor, startTime, endTime, limit)
var resp *TransferResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/transfer/query-inter-transfer-list", params, nil, &resp, getAssetInterTransferListEPL)
}
// GetSubUID retrieves the sub UIDs under a main UID
func (e *Exchange) GetSubUID(ctx context.Context) (*SubUID, error) {
var resp *SubUID
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/transfer/query-sub-member-list", nil, nil, &resp, getSubMemberListEPL)
}
// EnableUniversalTransferForSubUID Transfer between sub-sub or main-sub
// Use this endpoint to enable a subaccount to take part in a universal transfer. It is a one-time switch which, once thrown, enables a subaccount permanently.
// If not set, your subaccount cannot use universal transfers.
func (e *Exchange) EnableUniversalTransferForSubUID(ctx context.Context, subMemberIDs ...string) error {
if len(subMemberIDs) == 0 {
return errMembersIDsNotSet
}
arg := map[string][]string{
"subMemberIDs": subMemberIDs,
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/asset/transfer/save-transfer-sub-member", nil, &arg, &struct{}{}, saveTransferSubMemberEPL)
}
// CreateUniversalTransfer transfer between sub-sub or main-sub. Please make sure you have enabled universal transfer on your sub UID in advance.
// To use sub acct api key, it must have "SubMemberTransferList" permission
// When use sub acct api key, it can only transfer to main account
// You can not transfer between the same UID
func (e *Exchange) CreateUniversalTransfer(ctx context.Context, arg *TransferParams) (string, error) {
if arg == nil {
return "", errNilArgument
}
if arg.TransferID.IsNil() {
return "", errMissingTransferID
}
if arg.Coin.IsEmpty() {
return "", currency.ErrCurrencyCodeEmpty
}
if arg.Amount <= 0 {
return "", order.ErrAmountIsInvalid
}
if arg.FromAccountType == "" {
return "", fmt.Errorf("%w, from account type not specified", errMissingAccountType)
}
if arg.ToAccountType == "" {
return "", fmt.Errorf("%w, to account type not specified", errMissingAccountType)
}
if arg.FromMemberID == 0 {
return "", fmt.Errorf("%w, fromMemberId is missing", errMemberIDRequired)
}
if arg.ToMemberID == 0 {
return "", fmt.Errorf("%w, toMemberId is missing", errMemberIDRequired)
}
resp := struct {
TransferID string `json:"transferId"`
}{}
return resp.TransferID, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/asset/transfer/universal-transfer", nil, arg, &resp, universalTransferEPL)
}
func fillTransferQueryParams(transferID, coin, status, cursor string, startTime, endTime time.Time, limit int64) url.Values {
params := url.Values{}
if transferID != "" {
params.Set("transferId", transferID)
}
if coin != "" {
params.Set("coin", coin)
}
if status != "" {
params.Set("status", status)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return params
}
// GetUniversalTransferRecords query universal transfer records
// Main acct api key or Sub acct api key are both supported
// Main acct api key needs "SubMemberTransfer" permission
// Sub acct api key needs "SubMemberTransferList" permission
func (e *Exchange) GetUniversalTransferRecords(ctx context.Context, transferID, coin, status, cursor string, startTime, endTime time.Time, limit int64) (*TransferResponse, error) {
params := fillTransferQueryParams(transferID, coin, status, cursor, startTime, endTime, limit)
var resp *TransferResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/transfer/query-universal-transfer-list", params, nil, &resp, getAssetUniversalTransferListEPL)
}
// GetAllowedDepositCoinInfo retrieves allowed deposit coin information. To find out paired chain of coin, please refer coin info api.
func (e *Exchange) GetAllowedDepositCoinInfo(ctx context.Context, coin, chain, cursor string, limit int64) (*AllowedDepositCoinInfo, error) {
params := url.Values{}
if coin != "" {
params.Set("coin", coin)
}
if chain != "" {
params.Set("chain", chain)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *AllowedDepositCoinInfo
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/deposit/query-allowed-list", params, nil, &resp, defaultEPL)
}
// SetDepositAccount sets auto transfer account after deposit. The same function as the setting for Deposit on web GUI
// account types: CONTRACT Derivatives Account
// 'SPOT' Spot Account 'INVESTMENT' ByFi Account (The service has been offline) 'OPTION' USDC Account 'UNIFIED' UMA or UTA 'FUND' Funding Account
func (e *Exchange) SetDepositAccount(ctx context.Context, accountType string) (*StatusResponse, error) {
if accountType == "" {
return nil, errMissingAccountType
}
arg := &struct {
AccountType string `json:"accountType"`
}{
AccountType: accountType,
}
var resp *StatusResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/asset/deposit/deposit-to-account", nil, &arg, &resp, defaultEPL)
}
func fillDepositRecordsParams(coin, cursor string, startTime, endTime time.Time, limit int64) url.Values {
params := url.Values{}
if coin != "" {
params.Set("coin", coin)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
return params
}
// GetDepositRecords query deposit records.
func (e *Exchange) GetDepositRecords(ctx context.Context, coin, cursor string, startTime, endTime time.Time, limit int64) (*DepositRecords, error) {
params := fillDepositRecordsParams(coin, cursor, startTime, endTime, limit)
var resp *DepositRecords
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/deposit/query-record", params, nil, &resp, getAssetDepositRecordsEPL)
}
// GetSubDepositRecords query subaccount's deposit records by main UID's API key. on chain
func (e *Exchange) GetSubDepositRecords(ctx context.Context, subMemberID, coin, cursor string, startTime, endTime time.Time, limit int64) (*DepositRecords, error) {
if subMemberID == "" {
return nil, errMembersIDsNotSet
}
params := fillDepositRecordsParams(coin, cursor, startTime, endTime, limit)
params.Set("subMemberId", subMemberID)
var resp *DepositRecords
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/deposit/query-sub-member-record", params, nil, &resp, getAssetDepositSubMemberRecordsEPL)
}
// GetInternalDepositRecordsOffChain retrieves deposit records within the Bybit platform. These transactions are not on the blockchain.
func (e *Exchange) GetInternalDepositRecordsOffChain(ctx context.Context, coin, cursor string, startTime, endTime time.Time, limit int64) (*InternalDepositRecords, error) {
params := fillDepositRecordsParams(coin, cursor, startTime, endTime, limit)
var resp *InternalDepositRecords
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/deposit/query-internal-record", params, nil, &resp, defaultEPL)
}
// GetMasterDepositAddress retrieves the deposit address information of MASTER account.
func (e *Exchange) GetMasterDepositAddress(ctx context.Context, coin currency.Code, chainType string) (*DepositAddresses, error) {
if coin.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
params := url.Values{}
params.Set("coin", coin.String())
if chainType != "" {
params.Set("chainType", chainType)
}
var resp *DepositAddresses
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/deposit/query-address", params, nil, &resp, getAssetDepositRecordsEPL)
}
// GetSubDepositAddress retrieves the deposit address information of SUB account.
func (e *Exchange) GetSubDepositAddress(ctx context.Context, coin currency.Code, chainType, subMemberID string) (*DepositAddresses, error) {
if coin.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
if chainType == "" {
return nil, errMissingChainType
}
if subMemberID == "" {
return nil, errMembersIDsNotSet
}
params := url.Values{}
params.Set("coin", coin.String())
params.Set("chainType", chainType)
params.Set("subMemberId", subMemberID)
var resp *DepositAddresses
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/deposit/query-sub-member-address", params, nil, &resp, getAssetDepositSubMemberAddressEPL)
}
// GetCoinInfo retrieves coin information, including chain information, withdraw and deposit status.
func (e *Exchange) GetCoinInfo(ctx context.Context, coin currency.Code) (*CoinInfo, error) {
params := url.Values{}
if coin.IsEmpty() {
params.Set("coin", coin.String())
}
var resp *CoinInfo
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/coin/query-info", params, nil, &resp, getAssetCoinInfoEPL)
}
// GetWithdrawalRecords query withdrawal records.
// endTime - startTime should be less than 30 days. Query last 30 days records by default.
// Can query by the master UID's api key only
func (e *Exchange) GetWithdrawalRecords(ctx context.Context, coin currency.Code, withdrawalID, withdrawType, cursor string, startTime, endTime time.Time, limit int64) (*WithdrawalRecords, error) {
params := url.Values{}
if withdrawalID != "" {
params.Set("withdrawID", withdrawalID)
}
if !coin.IsEmpty() {
params.Set("coin", coin.String())
}
if withdrawType != "" {
params.Set("withdrawType", withdrawType)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *WithdrawalRecords
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/withdraw/query-record", params, nil, &resp, getWithdrawRecordsEPL)
}
// GetWithdrawableAmount retrieves withdrawable amount information using currency code
func (e *Exchange) GetWithdrawableAmount(ctx context.Context, coin currency.Code) (*WithdrawableAmount, error) {
if coin.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
params := url.Values{}
params.Set("coin", coin.String())
var resp *WithdrawableAmount
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/asset/withdraw/withdrawable-amount", params, nil, &resp, defaultEPL)
}
// WithdrawCurrency Withdraw assets from your Bybit account. You can make an off-chain transfer if the target wallet address is from Bybit. This means that no blockchain fee will be charged.
func (e *Exchange) WithdrawCurrency(ctx context.Context, arg *WithdrawalParam) (string, error) {
if arg == nil {
return "", errNilArgument
}
if arg.Coin.IsEmpty() {
return "", currency.ErrCurrencyCodeEmpty
}
if arg.Chain == "" {
return "", errMissingChainInformation
}
if arg.Address == "" {
return "", errMissingAddressInfo
}
if arg.Amount <= 0 {
return "", limits.ErrAmountBelowMin
}
if arg.Timestamp == 0 {
arg.Timestamp = time.Now().UnixMilli()
}
resp := struct {
ID string `json:"id"`
}{}
return resp.ID, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/asset/withdraw/create", nil, arg, &resp, createWithdrawalEPL)
}
// CancelWithdrawal cancel the withdrawal
func (e *Exchange) CancelWithdrawal(ctx context.Context, id string) (*StatusResponse, error) {
if id == "" {
return nil, errMissingWithdrawalID
}
arg := &struct {
ID string `json:"id"`
}{
ID: id,
}
var resp *StatusResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/asset/withdraw/cancel", nil, arg, &resp, cancelWithdrawalEPL)
}
// CreateNewSubUserID created a new sub user id. Use master user's api key only.
func (e *Exchange) CreateNewSubUserID(ctx context.Context, arg *CreateSubUserParams) (*SubUserItem, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.Username == "" {
return nil, errMissingUsername
}
if arg.MemberType <= 0 {
return nil, errInvalidMemberType
}
var resp *SubUserItem
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/user/create-sub-member", nil, &arg, &resp, userCreateSubMemberEPL)
}
// CreateSubUIDAPIKey create new API key for those newly created sub UID. Use master user's api key only.
func (e *Exchange) CreateSubUIDAPIKey(ctx context.Context, arg *SubUIDAPIKeyParam) (*SubUIDAPIResponse, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.Subuid <= 0 {
return nil, fmt.Errorf("%w, subuid", errMissingUserID)
}
var resp *SubUIDAPIResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/user/create-sub-api", nil, arg, &resp, userCreateSubAPIKeyEPL)
}
// GetSubUIDList get all sub uid of master account. Use master user's api key only.
func (e *Exchange) GetSubUIDList(ctx context.Context) ([]SubUserItem, error) {
resp := struct {
SubMembers []SubUserItem `json:"subMembers"`
}{}
return resp.SubMembers, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/user/query-sub-members", nil, nil, &resp, userQuerySubMembersEPL)
}
// FreezeSubUID freeze Sub UID. Use master user's api key only.
func (e *Exchange) FreezeSubUID(ctx context.Context, subUID string, frozen bool) error {
if subUID == "" {
return fmt.Errorf("%w, subuid", errMissingUserID)
}
arg := &struct {
SubUID string `json:"subuid"`
Frozen int64 `json:"frozen"`
}{
SubUID: subUID,
}
if frozen {
arg.Frozen = 0
} else {
arg.Frozen = 1
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/user/frozen-sub-member", nil, arg, &struct{}{}, userFrozenSubMemberEPL)
}
// GetAPIKeyInformation retrieves the information of the api key.
// Use the api key pending to be checked to call the endpoint.
// Both master and sub user's api key are applicable.
func (e *Exchange) GetAPIKeyInformation(ctx context.Context) (*SubUIDAPIResponse, error) {
var resp *SubUIDAPIResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/user/query-api", nil, nil, &resp, userQueryAPIEPL)
}
// GetSubAccountAllAPIKeys retrieves all api keys information of a sub UID.
func (e *Exchange) GetSubAccountAllAPIKeys(ctx context.Context, subMemberID, cursor string, limit int64) (*SubAccountAPIKeys, error) {
if subMemberID == "" {
return nil, errMembersIDsNotSet
}
params := url.Values{}
params.Set("subMemberId", subMemberID)
if cursor != "" {
params.Set("cursor", cursor)
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
var resp *SubAccountAPIKeys
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, " /v5/user/sub-apikeys", params, nil, &resp, defaultEPL)
}
// GetUIDWalletType retrieves available wallet types for the master account or sub account
func (e *Exchange) GetUIDWalletType(ctx context.Context, memberIDs string) (*WalletType, error) {
if memberIDs == "" {
return nil, errMembersIDsNotSet
}
var resp *WalletType
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/user/get-member-type", nil, nil, &resp, defaultEPL)
}
// ModifyMasterAPIKey modify the settings of master api key.
// Use the api key pending to be modified to call the endpoint. Use master user's api key only.
func (e *Exchange) ModifyMasterAPIKey(ctx context.Context, arg *SubUIDAPIKeyUpdateParam) (*SubUIDAPIResponse, error) {
if arg == nil || reflect.DeepEqual(*arg, SubUIDAPIKeyUpdateParam{}) {
return nil, errNilArgument
}
if arg.IPs == "" && len(arg.IPAddresses) > 0 {
arg.IPs = strings.Join(arg.IPAddresses, ",")
}
var resp *SubUIDAPIResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/user/update-api", nil, arg, &resp, userUpdateAPIEPL)
}
// ModifySubAPIKey modifies the settings of sub api key. Use the api key pending to be modified to call the endpoint. Use sub user's api key only.
func (e *Exchange) ModifySubAPIKey(ctx context.Context, arg *SubUIDAPIKeyUpdateParam) (*SubUIDAPIResponse, error) {
if arg == nil || reflect.DeepEqual(*arg, SubUIDAPIKeyUpdateParam{}) {
return nil, errNilArgument
}
if arg.IPs == "" && len(arg.IPAddresses) > 0 {
arg.IPs = strings.Join(arg.IPAddresses, ",")
}
var resp *SubUIDAPIResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/user/update-sub-api", nil, &arg, &resp, userUpdateSubAPIEPL)
}
// DeleteSubUID delete a sub UID. Before deleting the UID, please make sure there is no asset.
// Use master user's api key**.
func (e *Exchange) DeleteSubUID(ctx context.Context, subMemberID string) error {
if subMemberID == "" {
return errMemberIDRequired
}
arg := &struct {
SubMemberID string `json:"subMemberId"`
}{SubMemberID: subMemberID}
var resp any
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/user/del-submember", nil, arg, &resp, defaultEPL)
}
// DeleteMasterAPIKey delete the api key of master account.
// Use the api key pending to be delete to call the endpoint. Use master user's api key only.
func (e *Exchange) DeleteMasterAPIKey(ctx context.Context) error {
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/user/delete-api", nil, nil, &struct{}{}, userDeleteAPIEPL)
}
// DeleteSubAccountAPIKey delete the api key of sub account.
// Use the api key pending to be delete to call the endpoint. Use sub user's api key only.
func (e *Exchange) DeleteSubAccountAPIKey(ctx context.Context, subAccountUID string) error {
if subAccountUID == "" {
return fmt.Errorf("%w, sub-account id missing", errMissingUserID)
}
arg := &struct {
UID string `json:"uid"`
}{
UID: subAccountUID,
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/user/delete-sub-api", nil, arg, &struct{}{}, userDeleteSubAPIEPL)
}
// GetAffiliateUserInfo the API is used for affiliate to get their users information
// The master account uid of affiliate's client
func (e *Exchange) GetAffiliateUserInfo(ctx context.Context, uid string) (*AffiliateCustomerInfo, error) {
if uid == "" {
return nil, errMissingUserID
}
params := url.Values{}
params.Set("uid", uid)
var resp *AffiliateCustomerInfo
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/user/aff-customer-info", params, nil, &resp, defaultEPL)
}
// GetLeverageTokenInfo query leverage token information
// Abbreviation of the LT, such as BTC3L
func (e *Exchange) GetLeverageTokenInfo(ctx context.Context, ltCoin currency.Code) ([]LeverageTokenInfo, error) {
params := url.Values{}
if !ltCoin.IsEmpty() {
params.Set("ltCoin", ltCoin.String())
}
resp := struct {
List []LeverageTokenInfo `json:"list"`
}{}
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/spot-lever-token/info", params, nil, &resp, defaultEPL)
}
// GetLeveragedTokenMarket retrieves leverage token market information
func (e *Exchange) GetLeveragedTokenMarket(ctx context.Context, ltCoin currency.Code) (*LeveragedTokenMarket, error) {
if ltCoin.IsEmpty() {
return nil, fmt.Errorf("%w, 'ltCoin' is required", currency.ErrCurrencyCodeEmpty)
}
params := url.Values{}
params.Set("ltCoin", ltCoin.String())
var resp *LeveragedTokenMarket
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/spot-lever-token/reference", params, nil, &resp, defaultEPL)
}
// PurchaseLeverageToken purcases a leverage token.
func (e *Exchange) PurchaseLeverageToken(ctx context.Context, ltCoin currency.Code, amount float64, serialNumber string) (*LeverageToken, error) {
if ltCoin.IsEmpty() {
return nil, fmt.Errorf("%w, 'ltCoin' is required", currency.ErrCurrencyCodeEmpty)
}
if amount <= 0 {
return nil, limits.ErrAmountBelowMin
}
arg := &struct {
LTCoin string `json:"ltCoin"`
LTAmount float64 `json:"amount,string"`
SerialNumber string `json:"serialNo,omitempty"`
}{
LTCoin: ltCoin.String(),
LTAmount: amount,
SerialNumber: serialNumber,
}
var resp *LeverageToken
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/spot-lever-token/purchase", nil, arg, &resp, spotLeverageTokenPurchaseEPL)
}
// RedeemLeverageToken redeem leverage token
func (e *Exchange) RedeemLeverageToken(ctx context.Context, ltCoin currency.Code, quantity float64, serialNumber string) (*RedeemToken, error) {
if ltCoin.IsEmpty() {
return nil, fmt.Errorf("%w, 'ltCoin' is required", currency.ErrCurrencyCodeEmpty)
}
if quantity <= 0 {
return nil, fmt.Errorf("%w, quantity=%f", limits.ErrAmountBelowMin, quantity)
}
arg := &struct {
LTCoin string `json:"ltCoin"`
Quantity float64 `json:"quantity,string"`
SerialNumber string `json:"serialNo,omitempty"`
}{
LTCoin: ltCoin.String(),
Quantity: quantity,
SerialNumber: serialNumber,
}
var resp *RedeemToken
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/spot-lever-token/redeem", nil, &arg, &resp, spotLeverTokenRedeemEPL)
}
// GetPurchaseAndRedemptionRecords retrieves purchase or redeem history.
// ltOrderType false integer LT order type. 1: purchase, 2: redemption
func (e *Exchange) GetPurchaseAndRedemptionRecords(ctx context.Context, ltCoin currency.Code, orderID, serialNo string, startTime, endTime time.Time, ltOrderType, limit int64) ([]RedeemPurchaseRecord, error) {
params := url.Values{}
if !ltCoin.IsEmpty() {
params.Set("ltCoin", ltCoin.String())
}
if orderID != "" {
params.Set("orderId", orderID)
}
if serialNo != "" {
params.Set("serialNo", serialNo)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if ltOrderType != 0 {
params.Set("ltOrderType", strconv.FormatInt(ltOrderType, 10))
}
if limit != 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
resp := struct {
List []RedeemPurchaseRecord `json:"list"`
}{}
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/spot-lever-token/order-record", params, nil, &resp, getSpotLeverageTokenOrderRecordsEPL)
}
// ToggleMarginTrade turn on / off spot margin trade
// Your account needs to activate spot margin first; i.e., you must have finished the quiz on web / app.
// spotMarginMode '1': on, '0': off
func (e *Exchange) ToggleMarginTrade(ctx context.Context, spotMarginMode bool) (*SpotMarginMode, error) {
arg := &SpotMarginMode{}
if spotMarginMode {
arg.SpotMarginMode = "1"
} else {
arg.SpotMarginMode = "0"
}
var resp *SpotMarginMode
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/spot-margin-trade/switch-mode", nil, arg, &resp, defaultEPL)
}
// SetSpotMarginTradeLeverage set the user's maximum leverage in spot cross margin
func (e *Exchange) SetSpotMarginTradeLeverage(ctx context.Context, leverage float64) error {
if leverage <= 2 {
return fmt.Errorf("%w, leverage. value range from [2 to 10]", errInvalidLeverage)
}
return e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/spot-margin-trade/set-leverage", nil, &map[string]string{"leverage": strconv.FormatFloat(leverage, 'f', -1, 64)}, &struct{}{}, defaultEPL)
}
// GetVIPMarginData retrieves public VIP Margin data
func (e *Exchange) GetVIPMarginData(ctx context.Context, vipLevel, ccy string) (*VIPMarginData, error) {
params := url.Values{}
if vipLevel != "" {
params.Set("vipLevel", vipLevel)
}
if ccy != "" {
params.Set("currency", ccy)
}
var resp *VIPMarginData
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, "spot-cross-margin-trade/data", defaultEPL, &resp)
}
// GetMarginCoinInfo retrieves margin coin information.
func (e *Exchange) GetMarginCoinInfo(ctx context.Context, coin currency.Code) ([]MarginCoinInfo, error) {
params := url.Values{}
if !coin.IsEmpty() {
params.Set("coin", coin.String())
}
resp := struct {
List []MarginCoinInfo `json:"list"`
}{}
return resp.List, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("spot-cross-margin-trade/pledge-token", params), defaultEPL, &resp)
}
// GetBorrowableCoinInfo retrieves borrowable coin info list.
func (e *Exchange) GetBorrowableCoinInfo(ctx context.Context, coin currency.Code) ([]BorrowableCoinInfo, error) {
params := url.Values{}
if !coin.IsEmpty() {
params.Set("coin", coin.String())
}
resp := struct {
List []BorrowableCoinInfo `json:"list"`
}{}
return resp.List, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("spot-cross-margin-trade/borrow-token", params), defaultEPL, &resp)
}
// GetInterestAndQuota retrieves interest and quota information.
func (e *Exchange) GetInterestAndQuota(ctx context.Context, coin currency.Code) (*InterestAndQuota, error) {
if coin.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
params := url.Values{}
params.Set("coin", coin.String())
var resp *InterestAndQuota
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/spot-cross-margin-trade/loan-info", params, nil, &resp, getSpotCrossMarginTradeLoanInfoEPL)
}
// GetLoanAccountInfo retrieves loan account information.
func (e *Exchange) GetLoanAccountInfo(ctx context.Context) (*AccountLoanInfo, error) {
var resp *AccountLoanInfo
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/spot-cross-margin-trade/account", nil, nil, &resp, getSpotCrossMarginTradeAccountEPL)
}
// Borrow borrows a coin.
func (e *Exchange) Borrow(ctx context.Context, arg *LendArgument) (*BorrowResponse, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.Coin.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
if arg.AmountToBorrow <= 0 {
return nil, limits.ErrAmountBelowMin
}
var resp *BorrowResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/spot-cross-margin-trade/loan", nil, arg, &resp, spotCrossMarginTradeLoanEPL)
}
// Repay repay a debt.
func (e *Exchange) Repay(ctx context.Context, arg *LendArgument) (*RepayResponse, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.Coin.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
if arg.AmountToBorrow <= 0 {
return nil, limits.ErrAmountBelowMin
}
var resp *RepayResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/spot-cross-margin-trade/repay", nil, arg, &resp, spotCrossMarginTradeRepayEPL)
}
// GetBorrowOrderDetail represents the borrow order detail.
// Status '0'(default)get all kinds of status '1'uncleared '2'cleared
func (e *Exchange) GetBorrowOrderDetail(ctx context.Context, startTime, endTime time.Time, coin currency.Code, status, limit int64) ([]BorrowOrderDetail, error) {
params := url.Values{}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if !coin.IsEmpty() {
params.Set("coin", coin.String())
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
if status != 0 {
params.Set("status", strconv.FormatInt(status, 10))
}
resp := struct {
List []BorrowOrderDetail `json:"list"`
}{}
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/spot-cross-margin-trade/orders", params, nil, &resp, getSpotCrossMarginTradeOrdersEPL)
}
// GetRepaymentOrderDetail retrieves repayment order detail.
func (e *Exchange) GetRepaymentOrderDetail(ctx context.Context, startTime, endTime time.Time, coin currency.Code, limit int64) ([]CoinRepaymentResponse, error) {
params := url.Values{}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if !coin.IsEmpty() {
params.Set("coin", coin.String())
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
resp := struct {
List []CoinRepaymentResponse `json:"list"`
}{}
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/spot-cross-margin-trade/repay-history", params, nil, &resp, getSpotCrossMarginTradeRepayHistoryEPL)
}
// ToggleMarginTradeNormal turn on / off spot margin trade
// Your account needs to activate spot margin first; i.e., you must have finished the quiz on web / app.
// spotMarginMode '1': on, '0': off
func (e *Exchange) ToggleMarginTradeNormal(ctx context.Context, spotMarginMode bool) (*SpotMarginMode, error) {
arg := &SpotMarginMode{}
if spotMarginMode {
arg.SpotMarginMode = "1"
} else {
arg.SpotMarginMode = "0"
}
var resp *SpotMarginMode
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/spot-cross-margin-trade/switch", nil, arg, &resp, spotCrossMarginTradeSwitchEPL)
}
// GetProductInfo represents a product info.
func (e *Exchange) GetProductInfo(ctx context.Context, productID string) (*InstitutionalProductInfo, error) {
params := url.Values{}
if productID != "" {
params.Set("productId", productID)
}
var resp *InstitutionalProductInfo
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("ins-loan/product-infos", params), defaultEPL, &resp)
}
// GetInstitutionalLengingMarginCoinInfo retrieves institutional lending margin coin information.
// ProductId. If not passed, then return all product margin coin. For spot, it returns coin that convertRation greater than 0.
func (e *Exchange) GetInstitutionalLengingMarginCoinInfo(ctx context.Context, productID string) (*InstitutionalMarginCoinInfo, error) {
params := url.Values{}
if productID != "" {
params.Set("productId", productID)
}
var resp *InstitutionalMarginCoinInfo
return resp, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("ins-loan/ensure-tokens-convert", params), defaultEPL, &resp)
}
// GetInstitutionalLoanOrders retrieves institutional loan orders.
func (e *Exchange) GetInstitutionalLoanOrders(ctx context.Context, orderID string, startTime, endTime time.Time, limit int64) ([]LoanOrderDetails, error) {
params := url.Values{}
if orderID != "" {
params.Set("orderId", orderID)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
resp := struct {
Loans []LoanOrderDetails `json:"loanInfo"`
}{}
return resp.Loans, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/ins-loan/loan-order", params, nil, &resp, defaultEPL)
}
// GetInstitutionalRepayOrders retrieves list of repaid order information.
func (e *Exchange) GetInstitutionalRepayOrders(ctx context.Context, startTime, endTime time.Time, limit int64) ([]OrderRepayInfo, error) {
params := url.Values{}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
resp := struct {
RepayInfo []OrderRepayInfo `json:"repayInfo"`
}{}
return resp.RepayInfo, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/ins-loan/repaid-history", params, nil, &resp, defaultEPL)
}
// GetLTV retrieves a loan-to-value(LTV)
func (e *Exchange) GetLTV(ctx context.Context) (*LTVInfo, error) {
var resp *LTVInfo
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/ins-loan/ltv-convert", nil, nil, &resp, defaultEPL)
}
// BindOrUnbindUID For the INS loan product, you can bind new UID to risk unit or unbind UID out from risk unit.
// possible 'operate' values: 0: bind, 1: unbind
func (e *Exchange) BindOrUnbindUID(ctx context.Context, uid, operate string) (*BindOrUnbindUIDResponse, error) {
if uid == "" {
return nil, fmt.Errorf("%w, uid is required", errMissingUserID)
}
if operate != "0" && operate != "1" {
return nil, errors.New("operation type required; 0:bind and 1:unbind")
}
arg := &struct {
UID string `json:"uid"`
Operate string `json:"operate"`
}{UID: uid, Operate: operate}
var resp *BindOrUnbindUIDResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/ins-loan/association-uid", nil, arg, &resp, defaultEPL)
}
// GetC2CLendingCoinInfo retrieves C2C basic information of lending coins
func (e *Exchange) GetC2CLendingCoinInfo(ctx context.Context, coin currency.Code) ([]C2CLendingCoinInfo, error) {
params := url.Values{}
if !coin.IsEmpty() {
params.Set("coin", coin.String())
}
resp := struct {
List []C2CLendingCoinInfo `json:"list"`
}{}
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/lending/info", params, nil, &resp, defaultEPL)
}
// C2CDepositFunds lending funds to Bybit asset pool
func (e *Exchange) C2CDepositFunds(ctx context.Context, arg *C2CLendingFundsParams) (*C2CLendingFundResponse, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.Coin.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
if arg.Quantity <= 0 {
return nil, limits.ErrAmountBelowMin
}
var resp *C2CLendingFundResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/lending/purchase", nil, &arg, &resp, defaultEPL)
}
// C2CRedeemFunds withdraw funds from the Bybit asset pool.
func (e *Exchange) C2CRedeemFunds(ctx context.Context, arg *C2CLendingFundsParams) (*C2CLendingFundResponse, error) {
if arg == nil {
return nil, errNilArgument
}
if arg.Coin.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
if arg.Quantity <= 0 {
return nil, limits.ErrAmountBelowMin
}
var resp *C2CLendingFundResponse
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodPost, "/v5/lending/redeem", nil, &arg, &resp, defaultEPL)
}
// GetC2CLendingOrderRecords retrieves lending or redeem history
func (e *Exchange) GetC2CLendingOrderRecords(ctx context.Context, coin currency.Code, orderID, orderType string, startTime, endTime time.Time, limit int64) ([]C2CLendingFundResponse, error) {
params := url.Values{}
if !coin.IsEmpty() {
params.Set("coin", coin.String())
}
if orderID != "" {
params.Set("orderId", orderID)
}
if orderType != "" {
params.Set("orderType", orderType)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
resp := struct {
List []C2CLendingFundResponse `json:"list"`
}{}
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/lending/history-order", params, nil, &resp, defaultEPL)
}
// GetC2CLendingAccountInfo retrieves C2C lending account information.
func (e *Exchange) GetC2CLendingAccountInfo(ctx context.Context, coin currency.Code) (*LendingAccountInfo, error) {
params := url.Values{}
if !coin.IsEmpty() {
params.Set("coin", coin.String())
}
var resp *LendingAccountInfo
return resp, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/lending/account", params, nil, &resp, defaultEPL)
}
// GetBrokerEarning exchange broker master account to query
// The data can support up to past 6 months until T-1
// startTime & endTime are either entered at the same time or not entered
// Business type. 'SPOT', 'DERIVATIVES', 'OPTIONS'
func (e *Exchange) GetBrokerEarning(ctx context.Context, businessType, cursor string, startTime, endTime time.Time, limit int64) ([]BrokerEarningItem, error) {
params := url.Values{}
if businessType != "" {
params.Set("bizType", businessType)
}
if cursor != "" {
params.Set("cursor", cursor)
}
if !startTime.IsZero() {
params.Set("startTime", strconv.FormatInt(startTime.UnixMilli(), 10))
}
if !endTime.IsZero() {
params.Set("endTime", strconv.FormatInt(endTime.UnixMilli(), 10))
}
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
resp := struct {
List []BrokerEarningItem `json:"list"`
}{}
return resp.List, e.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/broker/earning-record", params, nil, &resp, defaultEPL)
}
func processOB(ob [][2]types.Number) []orderbook.Level {
o := make([]orderbook.Level, len(ob))
for x := range ob {
o[x].Price = ob[x][0].Float64()
o[x].Amount = ob[x][1].Float64()
}
return o
}
// SendHTTPRequest sends an unauthenticated request
func (e *Exchange) SendHTTPRequest(ctx context.Context, ePath exchange.URL, path string, f request.EndpointLimit, result any) error {
endpointPath, err := e.API.Endpoints.GetURL(ePath)
if err != nil {
return err
}
value := reflect.ValueOf(result)
if value.Kind() != reflect.Ptr {
return fmt.Errorf("expected a pointer, got %T", value)
}
response := &RestResponse{
Result: result,
}
err = e.SendPayload(ctx, f, func() (*request.Item, error) {
return &request.Item{
Method: http.MethodGet,
Path: endpointPath + bybitAPIVersion + path,
Result: response,
Verbose: e.Verbose,
HTTPDebugging: e.HTTPDebugging,
HTTPRecording: e.HTTPRecording,
HTTPMockDataSliceLimit: e.HTTPMockDataSliceLimit,
}, nil
}, request.UnauthenticatedRequest)
if err != nil {
return err
}
if response.RetCode != 0 && response.RetMsg != "" {
return fmt.Errorf("code: %d message: %s", response.RetCode, response.RetMsg)
}
return nil
}
// SendAuthHTTPRequestV5 sends an authenticated HTTP request
func (e *Exchange) SendAuthHTTPRequestV5(ctx context.Context, ePath exchange.URL, method, path string, params url.Values, arg, result any, f request.EndpointLimit) error {
val := reflect.ValueOf(result)
if val.Kind() != reflect.Ptr {
return errNonePointerArgument
} else if val.IsNil() {
return errNilArgument
}
creds, err := e.GetCredentials(ctx)
if err != nil {
return err
}
endpointPath, err := e.API.Endpoints.GetURL(ePath)
if err != nil {
return err
}
response := &RestResponse{
Result: result,
}
err = e.SendPayload(ctx, f, func() (*request.Item, error) {
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
headers := make(map[string]string)
headers["X-BAPI-API-KEY"] = creds.Key
headers["X-BAPI-TIMESTAMP"] = timestamp
headers["X-BAPI-RECV-WINDOW"] = defaultRecvWindow
var hmacSignedStr string
var payload []byte
switch method {
case http.MethodGet:
headers["Content-Type"] = "application/x-www-form-urlencoded"
hmacSignedStr, err = getSign(timestamp+creds.Key+defaultRecvWindow+params.Encode(), creds.Secret)
case http.MethodPost:
headers["Content-Type"] = "application/json"
payload, err = json.Marshal(arg)
if err != nil {
return nil, err
}
hmacSignedStr, err = getSign(timestamp+creds.Key+defaultRecvWindow+string(payload), creds.Secret)
}
if err != nil {
return nil, err
}
headers["X-BAPI-SIGN"] = hmacSignedStr
return &request.Item{
Method: method,
Path: endpointPath + common.EncodeURLValues(path, params),
Headers: headers,
Body: bytes.NewBuffer(payload),
Result: &response,
Verbose: e.Verbose,
HTTPDebugging: e.HTTPDebugging,
HTTPRecording: e.HTTPRecording,
HTTPMockDataSliceLimit: e.HTTPMockDataSliceLimit,
}, nil
}, request.AuthenticatedRequest)
if response.RetCode != 0 && response.RetMsg != "" {
return fmt.Errorf("%w code: %d message: %s", request.ErrAuthRequestFailed, response.RetCode, response.RetMsg)
}
if len(response.RetExtInfo.List) > 0 && response.RetCode != 0 {
var errMessage strings.Builder
var failed bool
for i := range response.RetExtInfo.List {
if response.RetExtInfo.List[i].Code != 0 {
failed = true
errMessage.WriteString(fmt.Sprintf("code: %d message: %s ", response.RetExtInfo.List[i].Code, response.RetExtInfo.List[i].Message))
}
}
if failed {
return fmt.Errorf("%w %s", request.ErrAuthRequestFailed, errMessage.String())
}
}
return err
}
func getSide(side string) order.Side {
switch side {
case sideBuy:
return order.Buy
case sideSell:
return order.Sell
default:
return order.UnknownSide
}
}
// StringToOrderStatus returns order status from string
func StringToOrderStatus(status string) order.Status {
status = strings.ToUpper(status)
switch status {
case "CREATED":
return order.Open
case "NEW":
return order.New
case "REJECTED":
return order.Rejected
case "PARTIALLYFILLED", "PARTIALLY_FILLED":
return order.PartiallyFilled
case "PARTIALLYFILLEDCANCELED":
return order.PartiallyFilledCancelled
case "PENDING_CANCEL":
return order.PendingCancel
case "FILLED":
return order.Filled
case "CANCELED", "CANCELLED":
return order.Cancelled
case "UNTRIGGERED":
return order.Pending
case "TRIGGERED":
return order.Open
case "DEACTIVATED":
return order.Closed
case "ACTIVE":
return order.Active
default:
return order.UnknownStatus
}
}
func getSign(sign, secret string) (string, error) {
hmacSigned, err := crypto.GetHMAC(crypto.HashSHA256, []byte(sign), []byte(secret))
if err != nil {
return "", err
}
return hex.EncodeToString(hmacSigned), nil
}
// FetchAccountType if not set fetches the account type from the API, stores it and returns it. Else returns the stored account type.
func (e *Exchange) FetchAccountType(ctx context.Context) (AccountType, error) {
e.account.m.Lock()
defer e.account.m.Unlock()
if e.account.accountType == 0 {
accInfo, err := e.GetAPIKeyInformation(ctx)
if err != nil {
return 0, err
}
// From endpoint 0regular account; 1unified trade account
// + 1 to make it 1 and 2 so that a zero value can be used to check if the account type has been set or not.
e.account.accountType = AccountType(accInfo.IsUnifiedTradeAccount + 1)
}
return e.account.accountType, nil
}
// RequiresUnifiedAccount checks account type and returns error if not unified
func (e *Exchange) RequiresUnifiedAccount(ctx context.Context) error {
at, err := e.FetchAccountType(ctx)
if err != nil {
return nil //nolint:nilerr // if we can't get the account type, we can't check if it's unified or not, fail on call
}
if at != accountTypeUnified {
return fmt.Errorf("%w, account type: %s", errAPIKeyIsNotUnified, at)
}
return nil
}
// GetLongShortRatio retrieves long short ratio of an instrument.
func (e *Exchange) GetLongShortRatio(ctx context.Context, category, symbol string, interval kline.Interval, limit int64) ([]InstrumentInfoItem, error) {
if category == "" {
return nil, errCategoryNotSet
} else if category != cLinear && category != cInverse {
return nil, fmt.Errorf("%w, category: %s", errInvalidCategory, category)
}
if symbol == "" {
return nil, errSymbolMissing
}
intervalString, err := intervalToString(interval)
if err != nil {
return nil, err
}
params := url.Values{}
params.Set("category", category)
params.Set("symbol", symbol)
params.Set("period", intervalString)
if limit > 0 {
params.Set("limit", strconv.FormatInt(limit, 10))
}
resp := struct {
List []InstrumentInfoItem `json:"list"`
}{}
return resp.List, e.SendHTTPRequest(ctx, exchange.RestSpot, common.EncodeURLValues("market/account-ratio", params), defaultEPL, &resp)
}