bybit: Enhance order execution limits (#2069)

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

* bybit: enhance order execution limits (cherry-pick)

* relax test to not break all the others and add Delivery field lost in cherry-pick wonderland

* boss king nits

* Update exchanges/bybit/bybit_wrapper.go

Co-authored-by: Scott <gloriousCode@users.noreply.github.com>

* glorious: nits

* Update exchanges/bybit/bybit_test.go

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

* Update exchanges/bybit/bybit_wrapper.go

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

* gk:nits

* Update exchanges/bybit/bybit_wrapper.go

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

* gk: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: Scott <gloriousCode@users.noreply.github.com>
Co-authored-by: shazbert <shazbert@DESKTOP-3QKKR6J.localdomain>
This commit is contained in:
Ryan O'Hara-Reid
2025-11-05 16:38:08 +11:00
committed by GitHub
parent c8e449dbb2
commit 9441f33f42
3 changed files with 112 additions and 32 deletions

View File

@@ -771,15 +771,42 @@ func TestGetDeliveryPrice(t *testing.T) {
func TestUpdateOrderExecutionLimits(t *testing.T) {
t.Parallel()
testexch.UpdatePairsOnce(t, e)
for _, a := range e.GetAssetTypes(false) {
t.Run(a.String(), func(t *testing.T) {
t.Parallel()
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")
for _, p := range pairs {
t.Run(p.String(), func(t *testing.T) {
t.Parallel()
l, err := e.GetOrderExecutionLimits(a, p)
require.NoError(t, err, "GetOrderExecutionLimits must not error")
assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive")
if !l.Delisted.IsZero() {
assert.NotZero(t, l.Delisting, "Delisting should be set for Delisted coins")
}
pair := l.Key.Pair()
require.True(t, pair.Equal(p), "Pair must be equal to input")
require.Greater(t, len(pair.String()), 3, "pair string length must be > 3 to check for 1xxx rule")
require.Equal(t, e.Name, l.Key.Exchange, "Exchange must be equal to input")
require.Equal(t, a, l.Key.Asset, "Asset must be equal to input")
assert.Positive(t, l.PriceDivisor, "PriceDivisor should be positive")
if pair.String()[:2] == "10" {
assert.Greater(t, l.PriceDivisor, 1.0, "PriceDivisor for 1xxx pairs should be > 1.0")
}
if a == asset.USDTMarginedFutures && !pair.Quote.Equal(currency.USDT) {
assert.NotZero(t, l.Expiry, "Expiry should be set for USDT margined non-USDT pairs")
}
})
}
})
}
}

View File

@@ -55,9 +55,9 @@ type AccountFee struct {
// InstrumentsInfo represents a category, page indicator, and list of instrument information.
type InstrumentsInfo struct {
Category string `json:"category"`
List []InstrumentInfo `json:"list"`
NextPageCursor string `json:"nextPageCursor"`
Category string `json:"category"`
List []*InstrumentInfo `json:"list"`
NextPageCursor string `json:"nextPageCursor"`
}
// InstrumentInfo holds all instrument info across
@@ -86,15 +86,16 @@ type InstrumentInfo struct {
TickSize types.Number `json:"tickSize"`
} `json:"priceFilter"`
LotSizeFilter struct {
MaxOrderQty types.Number `json:"maxOrderQty"`
MinOrderQty types.Number `json:"minOrderQty"`
QtyStep types.Number `json:"qtyStep"`
PostOnlyMaxOrderQty types.Number `json:"postOnlyMaxOrderQty"`
BasePrecision types.Number `json:"basePrecision"`
QuotePrecision types.Number `json:"quotePrecision"`
MinOrderAmt types.Number `json:"minOrderAmt"`
MaxOrderAmt types.Number `json:"maxOrderAmt"`
MinNotionalValue types.Number `json:"minNotionalValue"`
MaxOrderQuantity types.Number `json:"maxOrderQty"`
MinOrderQuantity types.Number `json:"minOrderQty"`
QuantityStep types.Number `json:"qtyStep"`
PostOnlyMaxOrderQuantity types.Number `json:"postOnlyMaxOrderQty"`
BasePrecision types.Number `json:"basePrecision"`
QuotePrecision types.Number `json:"quotePrecision"`
MinOrderAmount types.Number `json:"minOrderAmt"`
MaxOrderAmount types.Number `json:"maxOrderAmt"`
MinNotionalValue types.Number `json:"minNotionalValue"`
MaxMarketOrderQuantity types.Number `json:"maxMktOrderQty"`
} `json:"lotSizeFilter"`
UnifiedMarginTrade bool `json:"unifiedMarginTrade"`
FundingInterval int64 `json:"fundingInterval"`

View File

@@ -412,7 +412,7 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren
}
var (
pairs currency.Pairs
allPairs []InstrumentInfo
allPairs []*InstrumentInfo
response *InstrumentsInfo
)
var nextPageCursor string
@@ -1630,27 +1630,79 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item)
return fmt.Errorf("%s %w", a, asset.ErrNotSupported)
}
l := make([]limits.MinMaxLevel, 0, len(allInstrumentsInfo.List))
for x := range allInstrumentsInfo.List {
if allInstrumentsInfo.List[x].Status != "Trading" {
continue
}
symbol := allInstrumentsInfo.List[x].transformSymbol(a)
for _, inst := range allInstrumentsInfo.List {
symbol := inst.transformSymbol(a)
pair, err := e.MatchSymbolWithAvailablePairs(symbol, a, true)
if err != nil {
log.Warnf(log.ExchangeSys, "%s unable to load limits for %s %v, pair data missing", e.Name, a, symbol)
continue
}
priceDivisor := 1.0
if symbol[:2] == "10" { // handle 1000SHIBUSDT, 1000PEPEUSDT etc; screen 1INCHUSDT
for _, r := range symbol[1:] {
if r != '0' {
break
}
priceDivisor *= 10
}
}
var delistingAt time.Time
var delistedAt time.Time
var delivery time.Time
if !inst.DeliveryTime.Time().IsZero() {
switch a {
case asset.Options:
delivery = inst.DeliveryTime.Time()
case asset.USDTMarginedFutures, asset.CoinMarginedFutures, asset.USDCMarginedFutures:
switch inst.ContractType {
case "LinearFutures", "InverseFutures":
delivery = inst.DeliveryTime.Time()
default:
delistedAt = inst.DeliveryTime.Time()
// Not entirely accurate but from docs the system will use the average index price in the last
// 30 minutes before the delisting time. See: https://www.bybit.com/en/help-center/article/Bybit-Derivatives-Delisting-Mechanism-DDM
delistingAt = delistedAt.Add(-30 * time.Minute)
}
case asset.Spot:
// asset.Spot does not return a delivery time and there is no API field for delisting time
log.Warnf(log.ExchangeSys, "%s %s: delivery time returned for spot asset", e.Name, pair)
}
}
baseStepAmount := inst.LotSizeFilter.QuantityStep.Float64()
if a == asset.Spot {
baseStepAmount = inst.LotSizeFilter.BasePrecision.Float64()
}
maxBaseAmount := inst.LotSizeFilter.MaxOrderQuantity.Float64()
if a != asset.Spot && a != asset.Options {
maxBaseAmount = inst.LotSizeFilter.MaxMarketOrderQuantity.Float64()
}
minQuoteAmount := inst.LotSizeFilter.MinOrderAmount.Float64()
if a != asset.Spot {
minQuoteAmount = inst.LotSizeFilter.MinNotionalValue.Float64()
}
l = append(l, limits.MinMaxLevel{
Key: key.NewExchangeAssetPair(e.Name, a, pair),
MinimumBaseAmount: allInstrumentsInfo.List[x].LotSizeFilter.MinOrderQty.Float64(),
MaximumBaseAmount: allInstrumentsInfo.List[x].LotSizeFilter.MaxOrderQty.Float64(),
MinPrice: allInstrumentsInfo.List[x].PriceFilter.MinPrice.Float64(),
MaxPrice: allInstrumentsInfo.List[x].PriceFilter.MaxPrice.Float64(),
PriceStepIncrementSize: allInstrumentsInfo.List[x].PriceFilter.TickSize.Float64(),
AmountStepIncrementSize: allInstrumentsInfo.List[x].LotSizeFilter.BasePrecision.Float64(),
QuoteStepIncrementSize: allInstrumentsInfo.List[x].LotSizeFilter.QuotePrecision.Float64(),
MinimumQuoteAmount: allInstrumentsInfo.List[x].LotSizeFilter.MinOrderQty.Float64() * allInstrumentsInfo.List[x].PriceFilter.MinPrice.Float64(),
MaximumQuoteAmount: allInstrumentsInfo.List[x].LotSizeFilter.MaxOrderQty.Float64() * allInstrumentsInfo.List[x].PriceFilter.MaxPrice.Float64(),
MinimumBaseAmount: inst.LotSizeFilter.MinOrderQuantity.Float64(),
MaximumBaseAmount: maxBaseAmount,
MinPrice: inst.PriceFilter.MinPrice.Float64(),
MaxPrice: inst.PriceFilter.MaxPrice.Float64(),
PriceStepIncrementSize: inst.PriceFilter.TickSize.Float64(),
AmountStepIncrementSize: baseStepAmount,
QuoteStepIncrementSize: inst.LotSizeFilter.QuotePrecision.Float64(),
MinimumQuoteAmount: minQuoteAmount,
MaximumQuoteAmount: inst.LotSizeFilter.MaxOrderAmount.Float64(),
Delisting: delistingAt,
Delisted: delistedAt,
Expiry: delivery,
PriceDivisor: priceDivisor,
Listed: inst.LaunchTime.Time(),
MultiplierDecimal: 1, // All assets on Bybit are 1x
})
}
return limits.Load(l)
@@ -1780,7 +1832,7 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Ite
}
resp := make([]futures.Contract, 0, len(inverseContracts.List)+len(linearContracts.List))
var instruments []InstrumentInfo
var instruments []*InstrumentInfo
for i := range linearContracts.List {
if linearContracts.List[i].SettleCoin != "USDC" {
continue
@@ -1859,7 +1911,7 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Ite
}
resp := make([]futures.Contract, 0, len(inverseContracts.List)+len(linearContracts.List))
var instruments []InstrumentInfo
var instruments []*InstrumentInfo
for i := range linearContracts.List {
if linearContracts.List[i].SettleCoin != "USDT" {
continue