GateIO: Add various risk API endpoints (#2106)

* gateio: risk update and tests (cherry-pick)

* Update exchanges/gateio/risk.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* Update exchanges/gateio/risk.go

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>

* ai: nits

* Update exchanges/gateio/gateio_types.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: nits

* Update exchanges/gateio/risk.go

Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>

* gk: paging mr pedantic

* Update exchanges/gateio/risk.go

Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>

* linter: fix

* crank: nits

---------

Co-authored-by: Ryan O'Hara-Reid <ryan.oharareid@thrasher.io>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Gareth Kirwan <gbjkirwan@gmail.com>
Co-authored-by: Adrian Gallagher <adrian.gallagher@thrasher.io>
This commit is contained in:
Ryan O'Hara-Reid
2025-11-19 13:21:36 +11:00
committed by GitHub
parent a1e464c275
commit 7f1bbfc48c
8 changed files with 437 additions and 100 deletions

View File

@@ -118,6 +118,7 @@ const (
deliveryPath = "delivery/"
ordersPath = "/orders"
positionsPath = "/positions/"
hedgeModePath = "/dual_comp/positions/"
subAccountsPath = "sub_accounts/"
priceOrdersPaths = "/price_orders"
@@ -151,7 +152,6 @@ var (
errMissingPreviewID = errors.New("missing required parameter: preview_id")
errChangeHasToBePositive = errors.New("change has to be positive")
errInvalidLeverage = errors.New("invalid leverage value")
errInvalidRiskLimit = errors.New("new position risk limit")
errInvalidAutoSize = errors.New("invalid autoSize")
errTooManyOrderRequest = errors.New("too many order creation request")
errInvalidTimeout = errors.New("invalid timeout, should be in seconds At least 5 seconds, 0 means cancel the countdown")
@@ -2164,20 +2164,6 @@ func (e *Exchange) UpdateFuturesPositionLeverage(ctx context.Context, settle cur
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualUpdateLeverageEPL, http.MethodPost, futuresPath+settle.Item.Lower+positionsPath+contract.String()+"/leverage", params, nil, &response)
}
// UpdateFuturesPositionRiskLimit updates the position risk limit
func (e *Exchange) UpdateFuturesPositionRiskLimit(ctx context.Context, settle currency.Code, contract currency.Pair, riskLimit uint64) (*Position, error) {
if settle.IsEmpty() {
return nil, errEmptyOrInvalidSettlementCurrency
}
if contract.IsInvalid() {
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
params := url.Values{}
params.Set("risk_limit", strconv.FormatUint(riskLimit, 10))
var response *Position
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualUpdateRiskEPL, http.MethodPost, futuresPath+settle.Item.Lower+positionsPath+contract.String()+"/risk_limit", params, nil, &response)
}
// EnableOrDisableDualMode enable or disable dual mode
// Before setting dual mode, make sure all positions are closed and no orders are open
func (e *Exchange) EnableOrDisableDualMode(ctx context.Context, settle currency.Code, dualMode bool) (*DualModeResponse, error) {
@@ -2199,7 +2185,7 @@ func (e *Exchange) RetrivePositionDetailInDualMode(ctx context.Context, settle c
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
var response []Position
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualPositionsDualModeEPL, http.MethodGet, futuresPath+settle.Item.Lower+"/dual_comp/positions/"+contract.String(), nil, nil, &response)
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualPositionsDualModeEPL, http.MethodGet, futuresPath+settle.Item.Lower+hedgeModePath+contract.String(), nil, nil, &response)
}
// UpdatePositionMarginInDualMode update position margin in dual mode
@@ -2217,7 +2203,7 @@ func (e *Exchange) UpdatePositionMarginInDualMode(ctx context.Context, settle cu
}
params.Set("dual_side", dualSide)
var response []Position
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualUpdateMarginDualModeEPL, http.MethodPost, futuresPath+settle.Item.Lower+"/dual_comp/positions/"+contract.String()+"/margin", params, nil, &response)
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualUpdateMarginDualModeEPL, http.MethodPost, futuresPath+settle.Item.Lower+hedgeModePath+contract.String()+"/margin", params, nil, &response)
}
// UpdatePositionLeverageInDualMode update position leverage in dual mode
@@ -2237,24 +2223,7 @@ func (e *Exchange) UpdatePositionLeverageInDualMode(ctx context.Context, settle
params.Set("cross_leverage_limit", strconv.FormatFloat(crossLeverageLimit, 'f', -1, 64))
}
var response *Position
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualUpdateLeverageDualModeEPL, http.MethodPost, futuresPath+settle.Item.Lower+"/dual_comp/positions/"+contract.String()+"/leverage", params, nil, &response)
}
// UpdatePositionRiskLimitInDualMode update position risk limit in dual mode
func (e *Exchange) UpdatePositionRiskLimitInDualMode(ctx context.Context, settle currency.Code, contract currency.Pair, riskLimit float64) ([]Position, error) {
if settle.IsEmpty() {
return nil, errEmptyOrInvalidSettlementCurrency
}
if contract.IsInvalid() {
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
if riskLimit < 0 {
return nil, errInvalidRiskLimit
}
params := url.Values{}
params.Set("risk_limit", strconv.FormatFloat(riskLimit, 'f', -1, 64))
var response []Position
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualUpdateRiskDualModeEPL, http.MethodPost, futuresPath+settle.Item.Lower+"/dual_comp/positions/"+contract.String()+"/risk_limit", params, nil, &response)
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualUpdateLeverageDualModeEPL, http.MethodPost, futuresPath+settle.Item.Lower+hedgeModePath+contract.String()+"/leverage", params, nil, &response)
}
// PlaceFuturesOrder creates futures order
@@ -2782,20 +2751,6 @@ func (e *Exchange) UpdateDeliveryPositionLeverage(ctx context.Context, settle cu
exchange.RestSpot, deliveryUpdateLeverageEPL, http.MethodPost, deliveryPath+settle.Item.Lower+positionsPath+contract.String()+"/leverage", params, nil, &response)
}
// UpdateDeliveryPositionRiskLimit update position risk limit
func (e *Exchange) UpdateDeliveryPositionRiskLimit(ctx context.Context, settle currency.Code, contract currency.Pair, riskLimit uint64) (*Position, error) {
if settle.IsEmpty() {
return nil, errEmptyOrInvalidSettlementCurrency
}
if contract.IsInvalid() {
return nil, fmt.Errorf("%w, currency pair for contract must not be empty", errInvalidOrMissingContractParam)
}
params := url.Values{}
params.Set("risk_limit", strconv.FormatUint(riskLimit, 10))
var response *Position
return response, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, deliveryUpdateRiskLimitEPL, http.MethodPost, deliveryPath+settle.Item.Lower+positionsPath+contract.String()+"/risk_limit", params, nil, &response)
}
// PlaceDeliveryOrder create a futures order
// Zero-filled order cannot be retrieved 10 minutes after order cancellation
func (e *Exchange) PlaceDeliveryOrder(ctx context.Context, arg *ContractOrderCreateParams) (*Order, error) {

View File

@@ -1026,15 +1026,6 @@ func TestUpdateFuturesPositionLeverage(t *testing.T) {
assert.NoError(t, err, "UpdateFuturesPositionLeverage should not error for USDTMarginedFutures")
}
func TestUpdateFuturesPositionRiskLimit(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
_, err := e.UpdateFuturesPositionRiskLimit(t.Context(), currency.BTC, getPair(t, asset.CoinMarginedFutures), 10)
assert.NoError(t, err, "UpdateFuturesPositionRiskLimit should not error for CoinMarginedFutures")
_, err = e.UpdateFuturesPositionRiskLimit(t.Context(), currency.USDT, getPair(t, asset.USDTMarginedFutures), 10)
assert.NoError(t, err, "UpdateFuturesPositionRiskLimit should not error for USDTMarginedFutures")
}
func TestPlaceDeliveryOrder(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
@@ -1188,15 +1179,6 @@ func TestUpdatePositionLeverageInDualMode(t *testing.T) {
assert.NoError(t, err, "UpdatePositionLeverageInDualMode should not error for USDTMarginedFutures")
}
func TestUpdatePositionRiskLimitInDualMode(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
_, err := e.UpdatePositionRiskLimitInDualMode(t.Context(), currency.BTC, getPair(t, asset.CoinMarginedFutures), 10)
assert.NoError(t, err, "UpdatePositionRiskLimitInDualMode should not error for CoinMarginedFutures")
_, err = e.UpdatePositionRiskLimitInDualMode(t.Context(), currency.USDT, getPair(t, asset.USDTMarginedFutures), 10)
assert.NoError(t, err, "UpdatePositionRiskLimitInDualMode should not error for USDTMarginedFutures")
}
func TestPlaceFuturesOrder(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
@@ -1455,15 +1437,6 @@ func TestUpdateDeliveryPositionLeverage(t *testing.T) {
assert.NoError(t, err, "UpdateDeliveryPositionLeverage should not error")
}
func TestUpdateDeliveryPositionRiskLimit(t *testing.T) {
t.Parallel()
_, err := e.UpdateDeliveryPositionRiskLimit(t.Context(), currency.EMPTYCODE, currency.Pair{}, 0)
assert.ErrorIs(t, err, errEmptyOrInvalidSettlementCurrency)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
_, err = e.UpdateDeliveryPositionRiskLimit(t.Context(), currency.USDT, getPair(t, asset.DeliveryFutures), 30)
assert.NoError(t, err, "UpdateDeliveryPositionRiskLimit should not error")
}
func TestGetAllOptionsUnderlyings(t *testing.T) {
t.Parallel()
if _, err := e.GetAllOptionsUnderlyings(t.Context()); err != nil {

View File

@@ -982,11 +982,7 @@ type UsersPositionForUnderlying struct {
MarkPrice types.Number `json:"mark_price"`
UnrealisedPnl types.Number `json:"unrealised_pnl"`
PendingOrders int64 `json:"pending_orders"`
CloseOrder struct {
ID int64 `json:"id"`
Price types.Number `json:"price"`
IsLiq bool `json:"is_liq"`
} `json:"close_order"`
CloseOrder CloseOrder `json:"close_order"`
}
// ContractClosePosition represents user's liquidation history
@@ -1779,19 +1775,27 @@ type Position struct {
LastClosePNL types.Number `json:"last_close_pnl"`
RealisedPNLPoint types.Number `json:"realised_point"`
RealisedPNLHistoryPoint types.Number `json:"history_point"`
ADLRanking int64 `json:"adl_ranking"` // Ranking of auto deleveraging, a total of 1-5 grades, 1 is the highest, 5 is the lowest, and 6 is the special case when there is no position held or in liquidation
PendingOrders int64 `json:"pending_orders"`
CloseOrder struct {
ID int64 `json:"id"`
Price types.Number `json:"price"`
IsLiq bool `json:"is_liq"`
} `json:"close_order"`
Mode string `json:"mode"`
CrossLeverageLimit types.Number `json:"cross_leverage_limit"`
UpdateTime types.Time `json:"update_time"`
OpenTime types.Time `json:"open_time"`
UpdateID int64 `json:"update_id"`
TradeMaxSize types.Number `json:"trade_max_size"`
ADLRanking uint8 `json:"adl_ranking"` // Ranking of auto deleveraging, a total of 1-5 grades, 1 is the highest, 5 is the lowest, and 6 is the special case when there is no position held or in liquidation
PendingOrders uint64 `json:"pending_orders"`
CloseOrder CloseOrder `json:"close_order"`
Mode string `json:"mode"`
CrossLeverageLimit types.Number `json:"cross_leverage_limit"`
UpdateTime types.Time `json:"update_time"`
OpenTime types.Time `json:"open_time"`
UpdateID int64 `json:"update_id"`
TradeMaxSize types.Number `json:"trade_max_size"`
RiskLimitTable string `json:"risk_limit_table"`
AverageMaintenanceRate types.Number `json:"average_maintenance_rate"`
VoucherSize types.Number `json:"voucher_size"`
VoucherMargin types.Number `json:"voucher_margin"`
VoucherID int64 `json:"voucher_id"`
}
// CloseOrder represents close order information
type CloseOrder struct {
ID int64 `json:"id"`
Price types.Number `json:"price"`
IsLiquidation bool `json:"is_liq"`
}
// DualModeResponse represents dual mode enable or disable

View File

@@ -138,12 +138,10 @@ const (
perpetualPositionEPL
perpetualUpdateMarginEPL
perpetualUpdateLeverageEPL
perpetualUpdateRiskEPL
perpetualToggleDualModeEPL
perpetualPositionsDualModeEPL
perpetualUpdateMarginDualModeEPL
perpetualUpdateLeverageDualModeEPL
perpetualUpdateRiskDualModeEPL
perpetualSubmitOrderEPL
perpetualGetOrdersEPL
perpetualSubmitBatchOrdersEPL
@@ -165,7 +163,6 @@ const (
deliveryPositionsEPL
deliveryUpdateMarginEPL
deliveryUpdateLeverageEPL
deliveryUpdateRiskLimitEPL
deliverySubmitOrderEPL
deliveryGetOrdersEPL
deliveryCancelOrdersEPL
@@ -194,6 +191,15 @@ const (
optionsTradingHistoryEPL
websocketRateLimitNotNeededEPL
// Risk EPLs
publicFuturesRiskTableEPL
publicFuturesRiskLimitTiersEPL
publicDeliveryRiskLimitTiersEPL
unifiedUserRiskUnitDetailsEPL
deliveryUpdateRiskLimitEPL
perpetualUpdateRiskDualModeEPL
perpetualUpdateRiskEPL
)
// package level rate limits for REST API
@@ -326,12 +332,10 @@ var packageRateLimits = request.RateLimitDefinitions{
perpetualPositionEPL: standardRateLimit(),
perpetualUpdateMarginEPL: standardRateLimit(),
perpetualUpdateLeverageEPL: standardRateLimit(),
perpetualUpdateRiskEPL: standardRateLimit(),
perpetualToggleDualModeEPL: standardRateLimit(),
perpetualPositionsDualModeEPL: standardRateLimit(),
perpetualUpdateMarginDualModeEPL: standardRateLimit(),
perpetualUpdateLeverageDualModeEPL: standardRateLimit(),
perpetualUpdateRiskDualModeEPL: standardRateLimit(),
perpetualSubmitOrderEPL: perpetualOrderplacementRateLimit(),
perpetualGetOrdersEPL: standardRateLimit(),
perpetualSubmitBatchOrdersEPL: perpetualOrderplacementRateLimit(),
@@ -353,7 +357,6 @@ var packageRateLimits = request.RateLimitDefinitions{
deliveryPositionsEPL: standardRateLimit(),
deliveryUpdateMarginEPL: standardRateLimit(),
deliveryUpdateLeverageEPL: standardRateLimit(),
deliveryUpdateRiskLimitEPL: standardRateLimit(),
deliverySubmitOrderEPL: deliverySubmitCancelAmendRateLimit(),
deliveryGetOrdersEPL: standardRateLimit(),
deliveryCancelOrdersEPL: deliverySubmitCancelAmendRateLimit(),
@@ -384,6 +387,15 @@ var packageRateLimits = request.RateLimitDefinitions{
privateUnifiedSpotEPL: standardRateLimit(),
websocketRateLimitNotNeededEPL: nil, // no rate limit for certain websocket functions
// Risk limits
publicFuturesRiskTableEPL: standardRateLimit(),
publicFuturesRiskLimitTiersEPL: standardRateLimit(),
publicDeliveryRiskLimitTiersEPL: standardRateLimit(),
unifiedUserRiskUnitDetailsEPL: standardRateLimit(),
deliveryUpdateRiskLimitEPL: standardRateLimit(),
perpetualUpdateRiskDualModeEPL: standardRateLimit(),
perpetualUpdateRiskEPL: standardRateLimit(),
}
func standardRateLimit() *request.RateLimiterWithWeight {

124
exchanges/gateio/risk.go Normal file
View File

@@ -0,0 +1,124 @@
package gateio
import (
"context"
"errors"
"net/http"
"net/url"
"strconv"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/currency"
exchange "github.com/thrasher-corp/gocryptotrader/exchanges"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
var (
errTableIDEmpty = errors.New("tableID cannot be empty")
errInvalidRiskLimit = errors.New("invalid risk limit")
errPagingNotAllowed = errors.New("limit/offset pagination params not allowed when contract supplied")
)
// GetUnifiedUserRiskUnitDetails retrieves the user's risk unit details
func (e *Exchange) GetUnifiedUserRiskUnitDetails(ctx context.Context) (*UserRiskUnitDetails, error) {
var result *UserRiskUnitDetails
return result, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, unifiedUserRiskUnitDetailsEPL, http.MethodGet, "unified/risk_units", nil, nil, &result)
}
// GetFuturesRiskTable retrieves the futures risk table for a given settlement currency and table ID
func (e *Exchange) GetFuturesRiskTable(ctx context.Context, settleCurrency currency.Code, tableID string) ([]RiskTable, error) {
if settleCurrency.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
if tableID == "" {
return nil, errTableIDEmpty
}
var result []RiskTable
path := futuresPath + settleCurrency.Lower().String() + "/risk_limit_table?table_id=" + tableID
return result, e.SendHTTPRequest(ctx, exchange.RestSpot, publicFuturesRiskTableEPL, path, &result)
}
// GetFuturesRiskLimitTiers retrieves the futures risk limit tiers
// NOTE: 'limit' and 'offset' correspond to pagination queries at the market level, not to the length of the returned
// array. This only takes effect when the contract parameter is empty.
func (e *Exchange) GetFuturesRiskLimitTiers(ctx context.Context, settleCurrency currency.Code, contract currency.Pair, limit, offset uint64) ([]RiskTable, error) {
return e.getRiskLimitTiers(ctx, futuresPath, publicFuturesRiskLimitTiersEPL, settleCurrency, contract, limit, offset)
}
// GetDeliveryRiskLimitTiers retrieves the delivery risk limit tiers
// NOTE: 'limit' and 'offset' correspond to pagination queries at the market level, not to the length of the returned
// array. This only takes effect when the contract parameter is empty.
func (e *Exchange) GetDeliveryRiskLimitTiers(ctx context.Context, settleCurrency currency.Code, contract currency.Pair, limit, offset uint64) ([]RiskTable, error) {
return e.getRiskLimitTiers(ctx, deliveryPath, publicDeliveryRiskLimitTiersEPL, settleCurrency, contract, limit, offset)
}
func (e *Exchange) getRiskLimitTiers(ctx context.Context, assetPath string, epl request.EndpointLimit, settleCurrency currency.Code, contract currency.Pair, limit, offset uint64) ([]RiskTable, error) {
if settleCurrency.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
params := url.Values{}
if !contract.IsEmpty() {
if limit > 0 || offset > 0 {
return nil, errPagingNotAllowed
}
params.Set("contract", contract.Upper().String())
} else {
if limit > 0 {
params.Set("limit", strconv.FormatUint(limit, 10))
}
if offset > 0 {
params.Set("offset", strconv.FormatUint(offset, 10))
}
}
path := common.EncodeURLValues(assetPath+settleCurrency.Lower().String()+"/risk_limit_tiers", params)
var result []RiskTable
return result, e.SendHTTPRequest(ctx, exchange.RestSpot, epl, path, &result)
}
// DeliveryUpdatePositionRiskLimit updates the position risk limit for a delivery contract
func (e *Exchange) DeliveryUpdatePositionRiskLimit(ctx context.Context, settleCurrency currency.Code, contract currency.Pair, riskLimit float64) (*Position, error) {
return e.updatePositionRiskLimit(ctx, deliveryPath, positionsPath, deliveryUpdateRiskLimitEPL, settleCurrency, contract, riskLimit)
}
// FuturesUpdatePositionRiskLimit updates the position risk limit for a futures contract
func (e *Exchange) FuturesUpdatePositionRiskLimit(ctx context.Context, settleCurrency currency.Code, contract currency.Pair, riskLimit float64) (*Position, error) {
return e.updatePositionRiskLimit(ctx, futuresPath, positionsPath, perpetualUpdateRiskEPL, settleCurrency, contract, riskLimit)
}
// FuturesUpdatePositionRiskLimitDualMode updates the position risk limit for a futures contract in dual/hedge mode
func (e *Exchange) FuturesUpdatePositionRiskLimitDualMode(ctx context.Context, settleCurrency currency.Code, contract currency.Pair, riskLimit float64) (*Position, error) {
return e.updatePositionRiskLimit(ctx, futuresPath, hedgeModePath, perpetualUpdateRiskDualModeEPL, settleCurrency, contract, riskLimit)
}
func (e *Exchange) updatePositionRiskLimit(ctx context.Context, assetPath, positionsTypePath string, epl request.EndpointLimit, settleCurrency currency.Code, contract currency.Pair, riskLimit float64) (*Position, error) {
if settleCurrency.IsEmpty() {
return nil, currency.ErrCurrencyCodeEmpty
}
if contract.IsEmpty() {
return nil, currency.ErrCurrencyPairEmpty
}
if riskLimit <= 0 {
return nil, errInvalidRiskLimit
}
path := assetPath + settleCurrency.Lower().String() + positionsTypePath + contract.Upper().String() + "/risk_limit"
param := url.Values{}
param.Set("risk_limit", strconv.FormatFloat(riskLimit, 'f', -1, 64))
if positionsTypePath == hedgeModePath {
var result []Position
if err := e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, epl, http.MethodPost, path, param, nil, &result); err != nil {
return nil, err
}
// Endpoint returns an array but only one position is expected
if len(result) != 1 {
return nil, common.ErrNoResults
}
return &result[0], nil
}
var result Position
return &result, e.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, epl, http.MethodPost, path, param, nil, &result)
}

View File

@@ -0,0 +1,178 @@
package gateio
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange"
)
func TestGetUnifiedUserRiskUnitDetails(t *testing.T) {
t.Parallel()
sharedtestvalues.SkipTestIfCredentialsUnset(t, e)
got, err := e.GetUnifiedUserRiskUnitDetails(t.Context())
require.NoError(t, err)
assert.NotEmpty(t, got)
}
func TestGetFuturesRiskTable(t *testing.T) {
t.Parallel()
_, err := e.GetFuturesRiskTable(t.Context(), currency.EMPTYCODE, "")
require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
_, err = e.GetFuturesRiskTable(t.Context(), currency.USDT, "")
require.ErrorIs(t, err, errTableIDEmpty)
// mock HTTP response due to dynamically generated table IDs, which can only be retrieved via authenticated endpoint
e := new(Exchange)
require.NoError(t, testexch.Setup(e))
require.NoError(t, testexch.MockHTTPInstance(e, "/"))
got, err := e.GetFuturesRiskTable(t.Context(), currency.USDT, "BTC_USDT_202507040223")
require.NoError(t, err)
assert.NotEmpty(t, got)
}
func TestGetFuturesRiskLimitTiers(t *testing.T) {
t.Parallel()
_, err := e.GetFuturesRiskLimitTiers(t.Context(), currency.EMPTYCODE, currency.EMPTYPAIR, 0, 0)
require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
_, err = e.GetFuturesRiskLimitTiers(t.Context(), currency.USDT, currency.NewBTCUSDT(), 10, 10)
require.ErrorIs(t, err, errPagingNotAllowed)
_, err = e.GetFuturesRiskLimitTiers(t.Context(), currency.USDT, currency.NewBTCUSDT(), 0, 10)
require.ErrorIs(t, err, errPagingNotAllowed)
got, err := e.GetFuturesRiskLimitTiers(t.Context(), currency.USDT, currency.EMPTYPAIR, 10, 10)
require.NoError(t, err)
require.NotEmpty(t, got)
testexch.UpdatePairsOnce(t, e)
avail, err := e.GetAvailablePairs(asset.USDTMarginedFutures)
require.NoError(t, err)
require.NotEmpty(t, avail)
got, err = e.GetFuturesRiskLimitTiers(t.Context(), currency.USDT, avail[0], 0, 0)
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestGetDeliveryRiskLimitTiers(t *testing.T) {
t.Parallel()
_, err := e.GetDeliveryRiskLimitTiers(t.Context(), currency.EMPTYCODE, currency.EMPTYPAIR, 0, 0)
require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
_, err = e.GetDeliveryRiskLimitTiers(t.Context(), currency.USDT, currency.NewBTCUSDT(), 10, 10)
require.ErrorIs(t, err, errPagingNotAllowed)
_, err = e.GetDeliveryRiskLimitTiers(t.Context(), currency.USDT, currency.NewBTCUSDT(), 0, 10)
require.ErrorIs(t, err, errPagingNotAllowed)
got, err := e.GetDeliveryRiskLimitTiers(t.Context(), currency.USDT, currency.EMPTYPAIR, 10, 10)
require.NoError(t, err)
require.NotEmpty(t, got)
testexch.UpdatePairsOnce(t, e)
avail, err := e.GetAvailablePairs(asset.DeliveryFutures)
require.NoError(t, err)
require.NotEmpty(t, avail)
got, err = e.GetDeliveryRiskLimitTiers(t.Context(), currency.USDT, avail[0], 0, 0)
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestDeliveryUpdatePositionRiskLimit(t *testing.T) {
t.Parallel()
_, err := e.DeliveryUpdatePositionRiskLimit(t.Context(), currency.EMPTYCODE, currency.EMPTYPAIR, 0)
require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
_, err = e.DeliveryUpdatePositionRiskLimit(t.Context(), currency.USDT, currency.EMPTYPAIR, 0)
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
_, err = e.DeliveryUpdatePositionRiskLimit(t.Context(), currency.USDT, currency.NewBTCUSD(), 0)
require.ErrorIs(t, err, errInvalidRiskLimit)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
testexch.UpdatePairsOnce(t, e)
avail, err := e.GetAvailablePairs(asset.DeliveryFutures)
require.NoError(t, err)
require.NotEmpty(t, avail)
tiers, err := e.GetDeliveryRiskLimitTiers(t.Context(), currency.USDT, avail[0], 0, 0)
require.NoError(t, err)
require.NotEmpty(t, tiers)
lowestTierRiskLimit := float64(tiers[0].RiskLimit)
got, err := e.DeliveryUpdatePositionRiskLimit(request.WithVerbose(t.Context()), currency.USDT, avail[0], lowestTierRiskLimit)
require.NoError(t, err)
require.NotEmpty(t, got)
}
func TestFuturesUpdatePositionRiskLimit(t *testing.T) {
t.Parallel()
_, err := e.FuturesUpdatePositionRiskLimit(t.Context(), currency.EMPTYCODE, currency.EMPTYPAIR, 0)
require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
_, err = e.FuturesUpdatePositionRiskLimit(t.Context(), currency.USDT, currency.EMPTYPAIR, 0)
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
_, err = e.FuturesUpdatePositionRiskLimit(t.Context(), currency.USDT, currency.NewBTCUSD(), 0)
require.ErrorIs(t, err, errInvalidRiskLimit)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
testexch.UpdatePairsOnce(t, e)
avail, err := e.GetAvailablePairs(asset.USDTMarginedFutures)
require.NoError(t, err)
require.NotEmpty(t, avail)
tiers, err := e.GetFuturesRiskLimitTiers(t.Context(), currency.USDT, avail[0], 0, 0)
require.NoError(t, err)
require.NotEmpty(t, tiers)
lowestTierRiskLimit := float64(tiers[0].RiskLimit)
got, err := e.FuturesUpdatePositionRiskLimit(request.WithVerbose(t.Context()), currency.USDT, avail[0], lowestTierRiskLimit)
require.NoError(t, err)
require.NotEmpty(t, got)
assert.Equal(t, lowestTierRiskLimit, got.RiskLimit.Float64())
}
func TestFuturesUpdatePositionRiskLimitDualMode(t *testing.T) {
t.Parallel()
_, err := e.FuturesUpdatePositionRiskLimitDualMode(t.Context(), currency.EMPTYCODE, currency.EMPTYPAIR, 0)
require.ErrorIs(t, err, currency.ErrCurrencyCodeEmpty)
_, err = e.FuturesUpdatePositionRiskLimitDualMode(t.Context(), currency.USDT, currency.EMPTYPAIR, 0)
require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty)
_, err = e.FuturesUpdatePositionRiskLimitDualMode(t.Context(), currency.USDT, currency.NewBTCUSD(), 0)
require.ErrorIs(t, err, errInvalidRiskLimit)
sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders)
testexch.UpdatePairsOnce(t, e)
avail, err := e.GetAvailablePairs(asset.USDTMarginedFutures)
require.NoError(t, err)
require.NotEmpty(t, avail)
tiers, err := e.GetFuturesRiskLimitTiers(t.Context(), currency.USDT, avail[0], 0, 0)
require.NoError(t, err)
require.NotEmpty(t, tiers)
lowestTierRiskLimit := float64(tiers[0].RiskLimit)
got, err := e.FuturesUpdatePositionRiskLimitDualMode(t.Context(), currency.USDT, avail[0], lowestTierRiskLimit)
require.NoError(t, err)
require.NotEmpty(t, got)
assert.Equal(t, lowestTierRiskLimit, got.RiskLimit.Float64())
}

View File

@@ -0,0 +1,36 @@
package gateio
import (
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/types"
)
// UserRiskUnitDetails represents the risk unit details for a user
type UserRiskUnitDetails struct {
UserID int64 `json:"user_id"`
SpotHedge bool `json:"spot_hedge"`
RiskUnits []RiskUnits `json:"risk_units"`
}
// RiskUnits represents risk unit details for a specific currency
type RiskUnits struct {
Symbol currency.Code `json:"symbol"`
SpotInUse types.Number `json:"spot_in_use"`
MaintainMargin types.Number `json:"maintain_margin"`
InitialMargin types.Number `json:"initial_margin"`
Delta types.Number `json:"delta"`
Gamma types.Number `json:"gamma"`
Theta types.Number `json:"theta"`
Vega types.Number `json:"vega"`
}
// RiskTable represents the risk table information
type RiskTable struct {
Tier uint8 `json:"tier"`
RiskLimit types.Number `json:"risk_limit"`
InitialRate types.Number `json:"initial_rate"`
MaintenanceRate types.Number `json:"maintenance_rate"`
LeverageMax types.Number `json:"leverage_max"`
Deduction types.Number `json:"deduction"`
Contract currency.Pair `json:"contract"` // Only available when fetching all risk limit tiers
}

55
exchanges/gateio/testdata/http.json vendored Normal file
View File

@@ -0,0 +1,55 @@
{
"routes": {
"/futures/usdt/risk_limit_table": {
"GET": [
{
"data": [
{
"deduction": "0",
"initial_rate": "0.008",
"leverage_max": "125",
"maintenance_rate": "0.004",
"risk_limit": "1000000",
"tier": 1
},
{
"deduction": "500",
"initial_rate": "0.009009",
"leverage_max": "111",
"maintenance_rate": "0.0045",
"risk_limit": "1500000",
"tier": 2
},
{
"deduction": "1250",
"initial_rate": "0.01",
"leverage_max": "100",
"maintenance_rate": "0.005",
"risk_limit": "2000000",
"tier": 3
},
{
"deduction": "5250",
"initial_rate": "0.013333",
"leverage_max": "75",
"maintenance_rate": "0.007",
"risk_limit": "3000000",
"tier": 4
},
{
"deduction": "6750",
"initial_rate": "0.01515",
"leverage_max": "66",
"maintenance_rate": "0.0075",
"risk_limit": "5000000",
"tier": 5
}
],
"queryString": "table_id=BTC_USDT_202507040223",
"bodyParams": "",
"headers": {}
}
]
}
}
}