From 9441f33f42dcf0a42c39e13e5073f4628858f1d7 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Wed, 5 Nov 2025 16:38:08 +1100 Subject: [PATCH] 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 * Update exchange/order/limits/limits_types.go Co-authored-by: Gareth Kirwan * Update exchanges/gateio/gateio_types.go Co-authored-by: Gareth Kirwan * Update exchanges/gateio/gateio_types.go Co-authored-by: Gareth Kirwan * 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 * glorious: nits * Update exchanges/bybit/bybit_test.go Co-authored-by: Gareth Kirwan * Update exchanges/bybit/bybit_wrapper.go Co-authored-by: Gareth Kirwan * gk:nits * Update exchanges/bybit/bybit_wrapper.go Co-authored-by: Gareth Kirwan * gk:nits --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Gareth Kirwan Co-authored-by: Scott Co-authored-by: shazbert --- exchanges/bybit/bybit_test.go | 33 ++++++++++-- exchanges/bybit/bybit_types.go | 25 +++++----- exchanges/bybit/bybit_wrapper.go | 86 +++++++++++++++++++++++++------- 3 files changed, 112 insertions(+), 32 deletions(-) diff --git a/exchanges/bybit/bybit_test.go b/exchanges/bybit/bybit_test.go index f3e429c7..e260d7f4 100644 --- a/exchanges/bybit/bybit_test.go +++ b/exchanges/bybit/bybit_test.go @@ -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") + } + }) + } }) } } diff --git a/exchanges/bybit/bybit_types.go b/exchanges/bybit/bybit_types.go index 65d6be5c..7f85aa08 100644 --- a/exchanges/bybit/bybit_types.go +++ b/exchanges/bybit/bybit_types.go @@ -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"` diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index ed2dad1b..5c6ffc4e 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -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