GateIO: Enhance order execution limits and currency pair details (#2018)

* refactor(gateio): enhance order execution limits and currency pair details

* Update exchanges/gateio/gateio_wrapper.go

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

* REEEEEHHHHHH

* linter: fix

* fix GetOpenInterest when a contract is delisted

* add handling for delisting end time correctly

* Update exchange/order/limits/limits_types.go

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

* Update exchange/order/limits/limits_types.go

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

* Update exchanges/gateio/gateio_types.go

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

* Update exchanges/gateio/gateio_types.go

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

* gk: nits

* gci: fix

* linter: fix

* gateio: Add launch and update tests (cherry-pick)

* gk: nits + removed spot setting delisting as delisted because it is not a start time value

* glorious: apply diff

---------

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>
This commit is contained in:
Ryan O'Hara-Reid
2025-10-02 14:55:43 +10:00
committed by GitHub
parent ac91fabcd5
commit e11765bc36
4 changed files with 157 additions and 149 deletions

View File

@@ -2489,11 +2489,33 @@ func TestUpdateOrderExecutionLimits(t *testing.T) {
require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), a), asset.ErrNotSupported)
default:
require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error")
pairs, err := e.CurrencyPairs.GetPairs(a, true)
require.NoError(t, err, "GetPairs must not error")
l, err := e.GetOrderExecutionLimits(a, pairs[0])
require.NoError(t, err, "GetOrderExecutionLimits must not error")
assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive")
avail, err := e.GetAvailablePairs(a)
require.NoError(t, err, "GetAvailablePairs must not error")
for _, pair := range avail {
l, err := e.GetOrderExecutionLimits(a, pair)
require.NoErrorf(t, err, "GetOrderExecutionLimits must not error for %s", pair)
require.NotNilf(t, l, "GetOrderExecutionLimits %s result cannot be nil", pair)
assert.Equalf(t, a, l.Key.Asset, "asset should equal for %s", pair)
assert.Truef(t, pair.Equal(l.Key.Pair()), "pair should equal for %s", pair)
assert.Positivef(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive for %s", pair)
assert.Positivef(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive for %s", pair)
switch a {
case asset.USDTMarginedFutures:
assert.Positivef(t, l.MultiplierDecimal, "MultiplierDecimal should be positive for %s", pair)
assert.NotZerof(t, l.Listed, "Listed should be populated for %s", pair)
fallthrough
case asset.CoinMarginedFutures:
if !l.Delisted.IsZero() {
assert.Truef(t, l.Delisted.After(l.Delisting), "Delisted should be after Delisting for %s", pair)
}
case asset.Spot:
assert.Positivef(t, l.MinimumQuoteAmount, "MinimumQuoteAmount should be positive for %s", pair)
assert.Positivef(t, l.QuoteStepIncrementSize, "QuoteStepIncrementSize should be positive for %s", pair)
case asset.DeliveryFutures:
assert.NotZerof(t, l.Expiry, "Expiry should be populated for %s", pair)
}
}
}
})
}

View File

@@ -486,17 +486,25 @@ type CurrencyInfo struct {
// CurrencyPairDetail represents a single currency pair detail.
type CurrencyPairDetail struct {
ID string `json:"id"`
Base string `json:"base"`
Quote string `json:"quote"`
TradingFee types.Number `json:"fee"`
MinBaseAmount types.Number `json:"min_base_amount"`
MinQuoteAmount types.Number `json:"min_quote_amount"`
AmountPrecision float64 `json:"amount_precision"` // Amount scale
Precision float64 `json:"precision"` // Price scale
TradeStatus string `json:"trade_status"`
SellStart float64 `json:"sell_start"`
BuyStart float64 `json:"buy_start"`
ID currency.Pair `json:"id"`
Base currency.Code `json:"base"`
BaseName string `json:"base_name"`
Quote currency.Code `json:"quote"`
QuoteName string `json:"quote_name"`
Fee types.Number `json:"fee"`
MinBaseAmount types.Number `json:"min_base_amount"`
MinQuoteAmount types.Number `json:"min_quote_amount"`
MaxBaseAmount types.Number `json:"max_base_amount"`
MaxQuoteAmount types.Number `json:"max_quote_amount"`
AmountPrecision float64 `json:"amount_precision"`
PricePrecision float64 `json:"precision"`
TradeStatus string `json:"trade_status"` // e.g. "untradable", "buyable", "sellable", "tradable"
SellStart types.Time `json:"sell_start"`
BuyStart types.Time `json:"buy_start"`
DelistingTime types.Time `json:"delisting_time"`
Type string `json:"type"` // e.g. "normal", "pre-market"
TradeURL string `json:"trade_url"`
STTag bool `json:"st_tag"`
}
// Ticker holds detail ticker information for a currency pair
@@ -641,49 +649,52 @@ type OrderbookOfLendingLoan struct {
// FuturesContract represents futures contract detailed data.
type FuturesContract struct {
Name string `json:"name"`
Type string `json:"type"`
QuantoMultiplier types.Number `json:"quanto_multiplier"`
RefDiscountRate types.Number `json:"ref_discount_rate"`
OrderPriceDeviate string `json:"order_price_deviate"`
MaintenanceRate types.Number `json:"maintenance_rate"`
MarkType string `json:"mark_type"`
LastPrice types.Number `json:"last_price"`
MarkPrice types.Number `json:"mark_price"`
IndexPrice types.Number `json:"index_price"`
FundingRateIndicative types.Number `json:"funding_rate_indicative"`
MarkPriceRound types.Number `json:"mark_price_round"`
FundingOffset int64 `json:"funding_offset"`
InDelisting bool `json:"in_delisting"`
RiskLimitBase string `json:"risk_limit_base"`
InterestRate string `json:"interest_rate"`
OrderPriceRound types.Number `json:"order_price_round"`
OrderSizeMin int64 `json:"order_size_min"`
RefRebateRate string `json:"ref_rebate_rate"`
FundingInterval int64 `json:"funding_interval"`
RiskLimitStep string `json:"risk_limit_step"`
LeverageMin types.Number `json:"leverage_min"`
LeverageMax types.Number `json:"leverage_max"`
RiskLimitMax string `json:"risk_limit_max"`
MakerFeeRate types.Number `json:"maker_fee_rate"`
TakerFeeRate types.Number `json:"taker_fee_rate"`
FundingRate types.Number `json:"funding_rate"`
OrderSizeMax int64 `json:"order_size_max"`
FundingNextApply types.Time `json:"funding_next_apply"`
ConfigChangeTime types.Time `json:"config_change_time"`
ShortUsers int64 `json:"short_users"`
TradeSize int64 `json:"trade_size"`
PositionSize int64 `json:"position_size"`
LongUsers int64 `json:"long_users"`
FundingImpactValue string `json:"funding_impact_value"`
OrdersLimit int64 `json:"orders_limit"`
TradeID int64 `json:"trade_id"`
OrderbookID int64 `json:"orderbook_id"`
EnableBonus bool `json:"enable_bonus"`
EnableCredit bool `json:"enable_credit"`
CreateTime types.Time `json:"create_time"`
FundingCapRatio types.Number `json:"funding_cap_ratio"`
VoucherLeverage types.Number `json:"voucher_leverage"`
Name currency.Pair `json:"name"`
Type string `json:"type"`
QuantoMultiplier types.Number `json:"quanto_multiplier"`
RefDiscountRate types.Number `json:"ref_discount_rate"`
OrderPriceDeviate types.Number `json:"order_price_deviate"`
MaintenanceRate types.Number `json:"maintenance_rate"`
MarkType string `json:"mark_type"`
LastPrice types.Number `json:"last_price"`
MarkPrice types.Number `json:"mark_price"`
IndexPrice types.Number `json:"index_price"`
FundingRateIndicative types.Number `json:"funding_rate_indicative"`
MarkPriceRound types.Number `json:"mark_price_round"`
FundingOffset types.Number `json:"funding_offset"`
Delisting bool `json:"in_delisting"`
RiskLimitBase types.Number `json:"risk_limit_base"`
InterestRate types.Number `json:"interest_rate"`
OrderPriceRound types.Number `json:"order_price_round"`
OrderSizeMin types.Number `json:"order_size_min"`
RefRebateRate types.Number `json:"ref_rebate_rate"`
FundingInterval int64 `json:"funding_interval"`
RiskLimitStep types.Number `json:"risk_limit_step"`
LeverageMin types.Number `json:"leverage_min"`
LeverageMax types.Number `json:"leverage_max"`
RiskLimitMax types.Number `json:"risk_limit_max"`
MakerFeeRate types.Number `json:"maker_fee_rate"`
TakerFeeRate types.Number `json:"taker_fee_rate"`
FundingRate types.Number `json:"funding_rate"`
OrderSizeMax types.Number `json:"order_size_max"`
FundingNextApply types.Time `json:"funding_next_apply"`
ShortUsers types.Number `json:"short_users"`
ConfigChangeTime types.Time `json:"config_change_time"`
TradeSize types.Number `json:"trade_size"`
PositionSize types.Number `json:"position_size"`
LongUsers types.Number `json:"long_users"`
FundingImpactValue types.Number `json:"funding_impact_value"`
OrdersLimit types.Number `json:"orders_limit"`
TradeID int64 `json:"trade_id"`
OrderbookID int64 `json:"orderbook_id"`
EnableBonus bool `json:"enable_bonus"`
EnableCredit bool `json:"enable_credit"`
CreateTime types.Time `json:"create_time"`
FundingCapRatio types.Number `json:"funding_cap_ratio"`
Status string `json:"status"`
LaunchTime types.Time `json:"launch_time"`
DelistingTime types.Time `json:"delisting_time"`
DelistedTime types.Time `json:"delisted_time"`
}
// TradingHistoryItem represents futures trading history item.

View File

@@ -422,12 +422,7 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren
if tradables[x].TradeStatus == "untradable" {
continue
}
p := strings.ToUpper(tradables[x].ID)
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
}
pairs = append(pairs, cp)
pairs = append(pairs, currency.NewPair(tradables[x].Base, tradables[x].Quote))
}
return pairs, nil
case asset.Margin, asset.CrossMargin:
@@ -459,15 +454,10 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren
}
pairs := make([]currency.Pair, 0, len(contracts))
for i := range contracts {
if contracts[i].InDelisting {
if !contracts[i].DelistedTime.Time().IsZero() && contracts[i].DelistedTime.Time().Before(time.Now()) {
continue
}
p := strings.ToUpper(contracts[i].Name)
cp, err := currency.NewPairFromString(p)
if err != nil {
return nil, err
}
pairs = append(pairs, cp)
pairs = append(pairs, contracts[i].Name)
}
return slices.Clip(pairs), nil
case asset.DeliveryFutures:
@@ -1800,23 +1790,19 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, a asset.Item)
}
resp := make([]futures.Contract, len(contracts))
for i := range contracts {
name, err := currency.NewPairFromString(contracts[i].Name)
if err != nil {
return nil, err
}
contractSettlementType := futures.Linear
switch {
case name.Base.Equal(currency.BTC) && settle.Equal(currency.BTC):
case contracts[i].Name.Base.Equal(currency.BTC) && settle.Equal(currency.BTC):
contractSettlementType = futures.Inverse
case !name.Base.Equal(settle) && !settle.Equal(currency.USDT):
case !contracts[i].Name.Base.Equal(settle) && !settle.Equal(currency.USDT):
contractSettlementType = futures.Quanto
}
c := futures.Contract{
Exchange: e.Name,
Name: name,
Underlying: name,
Name: contracts[i].Name,
Underlying: contracts[i].Name,
Asset: a,
IsActive: !contracts[i].InDelisting,
IsActive: contracts[i].DelistedTime.Time().IsZero() || contracts[i].DelistedTime.Time().After(time.Now()),
Type: futures.Perpetual,
SettlementType: contractSettlementType,
SettlementCurrencies: currency.Currencies{settle},
@@ -1904,10 +1890,6 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item)
if pairsData[i].TradeStatus == "untradable" {
continue
}
pair, err := e.MatchSymbolWithAvailablePairs(pairsData[i].ID, a, true)
if err != nil {
return err
}
// Minimum base amounts are not always provided this will default to
// precision for base deployment. This can't be done for quote.
@@ -1917,77 +1899,67 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item)
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, pair),
QuoteStepIncrementSize: math.Pow10(-int(pairsData[i].Precision)),
Key: key.NewExchangeAssetPair(e.Name, a, currency.NewPair(pairsData[i].Base, pairsData[i].Quote)),
QuoteStepIncrementSize: math.Pow10(-int(pairsData[i].PricePrecision)),
AmountStepIncrementSize: math.Pow10(-int(pairsData[i].AmountPrecision)),
MinimumBaseAmount: minBaseAmount,
MinimumQuoteAmount: pairsData[i].MinQuoteAmount.Float64(),
Delisted: pairsData[i].DelistingTime.Time(),
})
}
case asset.CoinMarginedFutures:
btcContracts, err := e.GetAllFutureContracts(ctx, currency.BTC)
case asset.USDTMarginedFutures, asset.CoinMarginedFutures:
settlement := currency.USDT
if a == asset.CoinMarginedFutures {
settlement = currency.BTC
}
contractInfo, err := e.GetAllFutureContracts(ctx, settlement)
if err != nil {
return err
}
l = make([]limits.MinMaxLevel, 0, len(btcContracts))
for x := range btcContracts {
p := strings.ToUpper(btcContracts[x].Name)
cp, err := currency.NewPairFromString(p)
if err != nil {
return err
// MBABYDOGE price is 1e6 x spot price
divCurrency := currency.NewCode("MBABYDOGE")
l = make([]limits.MinMaxLevel, 0, len(contractInfo))
for i := range contractInfo {
priceDiv := 1.0
if contractInfo[i].Name.Base.Equal(divCurrency) {
priceDiv = 1e6
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(btcContracts[x].OrderSizeMin),
MaximumBaseAmount: float64(btcContracts[x].OrderSizeMax),
PriceStepIncrementSize: btcContracts[x].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
})
}
case asset.USDTMarginedFutures:
usdtContracts, err := e.GetAllFutureContracts(ctx, currency.USDT)
if err != nil {
return err
}
l = make([]limits.MinMaxLevel, 0, len(usdtContracts))
for x := range usdtContracts {
p := strings.ToUpper(usdtContracts[x].Name)
cp, err := currency.NewPairFromString(p)
if err != nil {
return err
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(usdtContracts[x].OrderSizeMin),
MaximumBaseAmount: float64(usdtContracts[x].OrderSizeMax),
PriceStepIncrementSize: usdtContracts[x].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
Key: key.NewExchangeAssetPair(e.Name, a, contractInfo[i].Name),
MinimumBaseAmount: contractInfo[i].OrderSizeMin.Float64(),
MaximumBaseAmount: contractInfo[i].OrderSizeMax.Float64(),
PriceStepIncrementSize: contractInfo[i].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1, // 1 Contract
MultiplierDecimal: contractInfo[i].QuantoMultiplier.Float64(),
PriceDivisor: priceDiv,
Delisting: contractInfo[i].DelistingTime.Time(),
Delisted: contractInfo[i].DelistedTime.Time(),
Listed: contractInfo[i].LaunchTime.Time(),
})
}
case asset.DeliveryFutures:
btcContracts, err := e.GetAllDeliveryContracts(ctx, currency.BTC)
if err != nil {
return err
}
usdtContracts, err := e.GetAllDeliveryContracts(ctx, currency.USDT)
if err != nil {
return err
}
btcContracts = append(btcContracts, usdtContracts...)
l = make([]limits.MinMaxLevel, 0, len(btcContracts))
for x := range btcContracts {
p := strings.ToUpper(btcContracts[x].Name)
cp, err := currency.NewPairFromString(p)
for _, settlement := range []currency.Code{currency.BTC, currency.USDT} {
contractInfo, err := e.GetAllDeliveryContracts(ctx, settlement)
if err != nil {
return err
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(btcContracts[x].OrderSizeMin),
MaximumBaseAmount: float64(btcContracts[x].OrderSizeMax),
PriceStepIncrementSize: btcContracts[x].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
})
l = slices.Grow(l, len(contractInfo))
for x := range contractInfo {
p := strings.ToUpper(contractInfo[x].Name)
cp, err := currency.NewPairFromString(p)
if err != nil {
return err
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, cp),
MinimumBaseAmount: float64(contractInfo[x].OrderSizeMin),
MaximumBaseAmount: float64(contractInfo[x].OrderSizeMax),
PriceStepIncrementSize: contractInfo[x].OrderPriceRound.Float64(),
AmountStepIncrementSize: 1,
Expiry: contractInfo[x].ExpireTime.Time(),
})
}
}
case asset.Options:
underlyings, err := e.GetAllOptionsUnderlyings(ctx)
@@ -2142,14 +2114,10 @@ func (e *Exchange) GetLatestFundingRates(ctx context.Context, r *fundingrate.Lat
}
resp := make([]fundingrate.LatestRateResponse, 0, len(contracts))
for i := range contracts {
cp, err := currency.NewPairFromString(contracts[i].Name)
if err != nil {
return nil, err
}
if !pairs.Contains(cp, false) {
if !pairs.Contains(contracts[i].Name, true) {
continue
}
resp = append(resp, contractToFundingRate(e.Name, r.Asset, cp, &contracts[i], r.IncludePredictedRate))
resp = append(resp, contractToFundingRate(e.Name, r.Asset, contracts[i].Name, &contracts[i], r.IncludePredictedRate))
}
return slices.Clip(resp), nil
@@ -2212,8 +2180,10 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, keys ...key.PairAsset) (
for _, c := range contracts {
if p.IsEmpty() { // If not exactly one key provided
p, err = e.MatchSymbolWithAvailablePairs(c.contractName(), a, true)
if err != nil && !errors.Is(err, currency.ErrPairNotFound) {
errs = common.AppendError(errs, fmt.Errorf("%w from %s contract %s", err, a, c.contractName()))
if err != nil {
if err := common.ExcludeError(err, currency.ErrPairNotFound); err != nil {
errs = common.AppendError(errs, fmt.Errorf("%w from %s contract %s", err, a, c.contractName()))
}
continue
}
if len(keys) == 0 { // No keys: All enabled pairs
@@ -2257,7 +2227,7 @@ func (c *FuturesContract) openInterest() float64 {
}
func (c *FuturesContract) contractName() string {
return c.Name
return c.Name.String()
}
func (c *DeliveryContract) openInterest() float64 {