From 8743dc2a65d2a9c0944a9be23377059ee6e78df4 Mon Sep 17 00:00:00 2001 From: Scott Date: Mon, 23 Feb 2026 13:42:22 +1100 Subject: [PATCH 1/2] exchanges: Implement or update UpdateOrderExecutionLimits (#2168) * update execution limits * implement notyetimplemented wrapper points * limit load err, gateio ensure clientid * update standards * lint * wtf i said fix lint maaaate * Update types/time_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update exchanges/huobi/huobi_wrapper.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update types/time_test.go Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * func rename, comment and enddates * rmLine * MergeThenCheck * HEY WHO SAID YOU COULD DISAPPEAR * Fix tests * booo --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- exchanges/binance/binance_test.go | 50 +++--- exchanges/bitfinex/bitfinex_test.go | 15 +- exchanges/bitfinex/bitfinex_wrapper.go | 21 ++- exchanges/bithumb/bithumb_test.go | 13 +- exchanges/bitmex/bitmex_test.go | 24 +++ exchanges/bitmex/bitmex_wrapper.go | 177 ++++++++++++++-------- exchanges/bitstamp/bitstamp_test.go | 23 +-- exchanges/bitstamp/bitstamp_wrapper.go | 7 +- exchanges/btcmarkets/btcmarkets_test.go | 13 +- exchanges/btse/btse_test.go | 21 ++- exchanges/btse/btse_wrapper.go | 4 + exchanges/bybit/bybit_test.go | 1 - exchanges/deribit/deribit_test.go | 15 +- exchanges/gateio/gateio_test.go | 43 +++--- exchanges/gateio/gateio_websocket_test.go | 3 +- exchanges/gateio/gateio_wrapper.go | 85 ++++++----- exchanges/gemini/gemini_test.go | 13 +- exchanges/hitbtc/hitbtc_test.go | 16 +- exchanges/hitbtc/hitbtc_wrapper.go | 27 +++- exchanges/huobi/futures_types.go | 2 +- exchanges/huobi/huobi_test.go | 33 +++- exchanges/huobi/huobi_types.go | 2 +- exchanges/huobi/huobi_wrapper.go | 133 +++++++++++++--- exchanges/kraken/kraken_test.go | 19 ++- exchanges/kraken/kraken_wrapper.go | 69 +++++---- exchanges/kucoin/kucoin_test.go | 14 +- exchanges/kucoin/kucoin_wrapper_test.go | 5 + exchanges/okx/okx_test.go | 14 +- types/time.go | 17 ++- types/time_test.go | 8 +- 30 files changed, 619 insertions(+), 268 deletions(-) diff --git a/exchanges/binance/binance_test.go b/exchanges/binance/binance_test.go index a78869ea2db..107c3bd0f56 100644 --- a/exchanges/binance/binance_test.go +++ b/exchanges/binance/binance_test.go @@ -2721,31 +2721,37 @@ func TestUpdateOrderExecutionLimits(t *testing.T) { require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") pairs, err := e.CurrencyPairs.GetPairs(a, false) 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.MinPrice, "MinPrice should be positive") - assert.Positive(t, l.MaxPrice, "MaxPrice should be positive") - assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") - assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") - assert.Positive(t, l.MaximumBaseAmount, "MaximumBaseAmount should be positive") - assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") - assert.Positive(t, l.MarketMaxQty, "MarketMaxQty should be positive") - assert.Positive(t, l.MaxTotalOrders, "MaxTotalOrders should be positive") - switch a { - case asset.Spot, asset.Margin: - assert.Positive(t, l.MaxIcebergParts, "MaxIcebergParts should be positive") - case asset.USDTMarginedFutures: - assert.Positive(t, l.MinNotional, "MinNotional should be positive") - fallthrough - case asset.CoinMarginedFutures: - assert.Positive(t, l.MultiplierUp, "MultiplierUp should be positive") - assert.Positive(t, l.MultiplierDown, "MultiplierDown should be positive") - assert.Positive(t, l.MarketMinQty, "MarketMinQty should be positive") - assert.Positive(t, l.MarketStepIncrementSize, "MarketStepIncrementSize should be positive") - assert.Positive(t, l.MaxAlgoOrders, "MaxAlgoOrders should be positive") + for _, p := range pairs { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.MinPrice, "MinPrice should be positive") + assert.Positive(t, l.MaxPrice, "MaxPrice should be positive") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + assert.Positive(t, l.MaximumBaseAmount, "MaximumBaseAmount should be positive") + assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") + assert.Positive(t, l.MarketMaxQty, "MarketMaxQty should be positive") + assert.Positive(t, l.MaxTotalOrders, "MaxTotalOrders should be positive") + switch a { + case asset.Spot, asset.Margin: + assert.Positive(t, l.MaxIcebergParts, "MaxIcebergParts should be positive") + case asset.USDTMarginedFutures: + assert.Positive(t, l.MinNotional, "MinNotional should be positive") + fallthrough + case asset.CoinMarginedFutures: + assert.Positive(t, l.MultiplierUp, "MultiplierUp should be positive") + assert.Positive(t, l.MultiplierDown, "MultiplierDown should be positive") + assert.Positive(t, l.MarketMinQty, "MarketMinQty should be positive") + assert.Positive(t, l.MarketStepIncrementSize, "MarketStepIncrementSize should be positive") + assert.Positive(t, l.MaxAlgoOrders, "MaxAlgoOrders should be positive") + } } }) } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } func TestGetHistoricalFundingRates(t *testing.T) { diff --git a/exchanges/bitfinex/bitfinex_test.go b/exchanges/bitfinex/bitfinex_test.go index 898c956c11e..325ad3fcfe6 100644 --- a/exchanges/bitfinex/bitfinex_test.go +++ b/exchanges/bitfinex/bitfinex_test.go @@ -141,19 +141,24 @@ func TestUpdateTradablePairs(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() switch a { - case asset.Spot: + case asset.Spot, asset.Margin: require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") pairs, err := e.CurrencyPairs.GetPairs(a, false) 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 { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + } + case asset.MarginFunding: + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), a), asset.ErrNotSupported) default: - require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), a), common.ErrNotYetImplemented) + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), a), asset.ErrNotSupported) } }) } diff --git a/exchanges/bitfinex/bitfinex_wrapper.go b/exchanges/bitfinex/bitfinex_wrapper.go index 280611ea69a..548e999c0c0 100644 --- a/exchanges/bitfinex/bitfinex_wrapper.go +++ b/exchanges/bitfinex/bitfinex_wrapper.go @@ -269,17 +269,26 @@ func (e *Exchange) UpdateTradablePairs(ctx context.Context) error { // UpdateOrderExecutionLimits sets exchange execution order limits for an asset type func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { - if a != asset.Spot { - return common.ErrNotYetImplemented + var queryAsset asset.Item + switch a { + case asset.Spot, asset.Margin: + // Bitfinex margin and spot trade the same markets and share limits. + queryAsset = asset.Spot + case asset.Futures: + queryAsset = asset.Futures + default: + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } - l, err := e.GetSiteInfoConfigData(ctx, a) + l, err := e.GetSiteInfoConfigData(ctx, queryAsset) if err != nil { return err } - if err := limits.Load(l); err != nil { - return fmt.Errorf("%s Error loading exchange limits: %v", e.Name, err) + if a != queryAsset { + for i := range l { + l[i].Key.Asset = a + } } - return nil + return limits.Load(l) } // UpdateTickers updates the ticker for all currency pairs of a given asset type diff --git a/exchanges/bithumb/bithumb_test.go b/exchanges/bithumb/bithumb_test.go index 09e2b7b7108..0e96da2d70f 100644 --- a/exchanges/bithumb/bithumb_test.go +++ b/exchanges/bithumb/bithumb_test.go @@ -545,17 +545,24 @@ func TestGetHistoricTrades(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, false) 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 { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + } }) } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } func TestGetAmountMinimum(t *testing.T) { diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index 263ef49e0c1..91c519fbb5c 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -1157,6 +1157,30 @@ func TestGetCurrencyTradeURL(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() + err := e.UpdateOrderExecutionLimits(t.Context(), a) + require.NoError(t, err, "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(a, false) + require.NoError(t, err, "GetPairs must not error") + require.NotEmpty(t, pairs, "GetPairs must return pairs") + for _, p := range pairs { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + } + }) + } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) +} + func TestGenerateSubscriptions(t *testing.T) { t.Parallel() diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 8c8af2e63d8..dea9705e30a 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -16,6 +16,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchange/accounts" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" "github.com/thrasher-corp/gocryptotrader/exchange/websocket" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -192,80 +193,83 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, a asset.Item) (curren } pairs := make([]currency.Pair, 0, len(marketInfo)) - for x := range marketInfo { - if marketInfo[x].State != "Open" && a != asset.Index { + for i := range marketInfo { + if marketInfo[i].State != "Open" && a != asset.Index { continue } var pair currency.Pair switch a { case asset.Spot: - if marketInfo[x].Typ == spotID { - pair, err = currency.NewPairFromString(marketInfo[x].Symbol) - if err != nil { - return nil, err - } - pairs = append(pairs, pair) + if marketInfo[i].Typ != spotID { + continue } + pair, err = currency.NewPairFromString(marketInfo[i].Symbol) + if err != nil { + return nil, err + } + pairs = append(pairs, pair) case asset.PerpetualContract: - if marketInfo[x].Typ == perpetualContractID { - var settleTrail string - if strings.Contains(marketInfo[x].Symbol, currency.UnderscoreDelimiter) { - // Example: ETHUSD_ETH quoted in USD, paid out in ETH. - settlement := strings.Split(marketInfo[x].Symbol, currency.UnderscoreDelimiter) - if len(settlement) != 2 { - log.Warnf(log.ExchangeSys, "%s currency %s %s cannot be added to tradable pairs", - e.Name, - marketInfo[x].Symbol, - a) - break - } - settleTrail = currency.UnderscoreDelimiter + settlement[1] - } - pair, err = currency.NewPairFromStrings(marketInfo[x].Underlying, - marketInfo[x].QuoteCurrency+settleTrail) - if err != nil { - return nil, err - } - pairs = append(pairs, pair) + if marketInfo[i].Typ != perpetualContractID { + continue } - case asset.Futures: - if marketInfo[x].Typ == futuresID { - isolate := strings.Split(marketInfo[x].Symbol, currency.UnderscoreDelimiter) - if len(isolate[0]) < 3 { - log.Warnf(log.ExchangeSys, "%s currency %s %s be cannot added to tradable pairs", + var settleTrail string + if strings.Contains(marketInfo[i].Symbol, currency.UnderscoreDelimiter) { + // Example: ETHUSD_ETH quoted in USD, paid out in ETH. + settlement := strings.Split(marketInfo[i].Symbol, currency.UnderscoreDelimiter) + if len(settlement) != 2 { + log.Warnf(log.ExchangeSys, "%s currency %s %s cannot be added to tradable pairs", e.Name, - marketInfo[x].Symbol, + marketInfo[i].Symbol, a) break } - var settleTrail string - if len(isolate) == 2 { - // Example: ETHUSDU22_ETH quoted in USD, paid out in ETH. - settleTrail = currency.UnderscoreDelimiter + isolate[1] - } + settleTrail = currency.UnderscoreDelimiter + settlement[1] + } + pair, err = currency.NewPairFromStrings(marketInfo[i].Underlying, + marketInfo[i].QuoteCurrency+settleTrail) + if err != nil { + return nil, err + } + pairs = append(pairs, pair) + case asset.Futures: + if marketInfo[i].Typ != futuresID { + continue + } + isolate := strings.Split(marketInfo[i].Symbol, currency.UnderscoreDelimiter) + if len(isolate[0]) < 3 { + log.Warnf(log.ExchangeSys, "%s currency %s %s be cannot added to tradable pairs", + e.Name, + marketInfo[i].Symbol, + a) + break + } + var settleTrail string + if len(isolate) == 2 { + // Example: ETHUSDU22_ETH quoted in USD, paid out in ETH. + settleTrail = currency.UnderscoreDelimiter + isolate[1] + } - root := isolate[0][:len(isolate[0])-3] - contract := isolate[0][len(isolate[0])-3:] + root := isolate[0][:len(isolate[0])-3] + contract := isolate[0][len(isolate[0])-3:] - pair, err = currency.NewPairFromStrings(root, contract+settleTrail) - if err != nil { - return nil, err - } - pairs = append(pairs, pair) + pair, err = currency.NewPairFromStrings(root, contract+settleTrail) + if err != nil { + return nil, err } + pairs = append(pairs, pair) case asset.Index: // TODO: This can be expanded into individual assets later. - if marketInfo[x].Typ == bitMEXBasketIndexID || - marketInfo[x].Typ == bitMEXPriceIndexID || - marketInfo[x].Typ == bitMEXLendingPremiumIndexID || - marketInfo[x].Typ == bitMEXVolatilityIndexID { - pair, err = currency.NewPairFromString(marketInfo[x].Symbol) - if err != nil { - return nil, err - } - pairs = append(pairs, pair) + switch marketInfo[i].Typ { + case bitMEXBasketIndexID, bitMEXPriceIndexID, bitMEXLendingPremiumIndexID, bitMEXVolatilityIndexID: + default: + continue } + pair, err = currency.NewPairFromString(marketInfo[i].Symbol) + if err != nil { + return nil, err + } + pairs = append(pairs, pair) default: return nil, errors.New("unhandled asset type") } @@ -312,10 +316,7 @@ instruments: pair, enabled, err = e.MatchSymbolCheckEnabled(tick[j].Symbol, a, false) case asset.Index: switch tick[j].Typ { - case bitMEXBasketIndexID, - bitMEXPriceIndexID, - bitMEXLendingPremiumIndexID, - bitMEXVolatilityIndexID: + case bitMEXBasketIndexID, bitMEXPriceIndexID, bitMEXLendingPremiumIndexID, bitMEXVolatilityIndexID: default: continue instruments } @@ -1187,8 +1188,62 @@ func (e *Exchange) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (boo } // UpdateOrderExecutionLimits updates order execution limits -func (e *Exchange) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { - return common.ErrNotYetImplemented +func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { + if !e.SupportsAsset(a) { + return fmt.Errorf("%w for [%v]", asset.ErrNotSupported, a) + } + + instruments, err := e.GetActiveAndIndexInstruments(ctx) + if err != nil { + return err + } + + l := make([]limits.MinMaxLevel, 0, len(instruments)) + for i := range instruments { + if instruments[i].State != "Open" && a != asset.Index { + continue + } + var hasDelim bool + switch a { + case asset.Futures: + if instruments[i].Typ != futuresID { + continue + } + case asset.PerpetualContract: + if instruments[i].Typ != perpetualContractID { + continue + } + + case asset.Spot: + if instruments[i].Typ != spotID { + continue + } + hasDelim = true + case asset.Index: + switch instruments[i].Typ { + case bitMEXBasketIndexID, bitMEXPriceIndexID, bitMEXLendingPremiumIndexID, bitMEXVolatilityIndexID: + default: + continue + } + hasDelim = true + default: + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) + } + pair, err := e.MatchSymbolWithAvailablePairs(instruments[i].Symbol, a, hasDelim) + if err != nil { + return err + } + + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, pair), + MinimumBaseAmount: instruments[i].LotSize, + MaximumBaseAmount: instruments[i].MaxOrderQty, + MaxPrice: instruments[i].MaxPrice, + PriceStepIncrementSize: instruments[i].TickSize, + AmountStepIncrementSize: instruments[i].LotSize, + }) + } + return limits.Load(l) } // GetOpenInterest returns the open interest rate for a given asset pair diff --git a/exchanges/bitstamp/bitstamp_test.go b/exchanges/bitstamp/bitstamp_test.go index 141941bdf9d..bba8d10ef70 100644 --- a/exchanges/bitstamp/bitstamp_test.go +++ b/exchanges/bitstamp/bitstamp_test.go @@ -241,25 +241,28 @@ func TestUpdateTradablePairs(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, false) 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.PriceStepIncrementSize, "PriceStepIncrementSize should not be zero") - assert.NotEmpty(t, l.Key.Pair(), "Pair should not be empty") - assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") - assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") - assert.Positive(t, l.MinimumQuoteAmount, "MinimumQuoteAmount should be positive") - if mockTests { - assert.Equal(t, 0.01, l.PriceStepIncrementSize, "PriceStepIncrementSize should be 0.01") - assert.Equal(t, 20., l.MinimumQuoteAmount, "MinimumQuoteAmount should be 20") + for _, p := range pairs { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should not be zero") + assert.NotEmpty(t, l.Key.Pair(), "Pair should not be empty") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") + assert.Positive(t, l.MinimumQuoteAmount, "MinimumQuoteAmount should be positive") } }) } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } func TestGetTransactions(t *testing.T) { diff --git a/exchanges/bitstamp/bitstamp_wrapper.go b/exchanges/bitstamp/bitstamp_wrapper.go index 9763e6f1f31..ecedcb93aa7 100644 --- a/exchanges/bitstamp/bitstamp_wrapper.go +++ b/exchanges/bitstamp/bitstamp_wrapper.go @@ -210,7 +210,7 @@ func (e *Exchange) UpdateTradablePairs(ctx context.Context) error { // UpdateOrderExecutionLimits sets exchange execution order limits for an asset type func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { if a != asset.Spot { - return common.ErrNotYetImplemented + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } symbols, err := e.GetTradingPairs(ctx) if err != nil { @@ -232,10 +232,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) MinimumQuoteAmount: info.MinimumOrder, }) } - if err := limits.Load(l); err != nil { - return fmt.Errorf("%s Error loading exchange limits: %v", e.Name, err) - } - return nil + return limits.Load(l) } // UpdateTickers updates the ticker for all currency pairs of a given asset type diff --git a/exchanges/btcmarkets/btcmarkets_test.go b/exchanges/btcmarkets/btcmarkets_test.go index 3ecff7da73c..ac04c533aab 100644 --- a/exchanges/btcmarkets/btcmarkets_test.go +++ b/exchanges/btcmarkets/btcmarkets_test.go @@ -918,17 +918,24 @@ func TestWrapperModifyOrder(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, false) 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 { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + } }) } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } func TestGetWithdrawalsHistory(t *testing.T) { diff --git a/exchanges/btse/btse_test.go b/exchanges/btse/btse_test.go index 680fda06718..bbf5423af70 100644 --- a/exchanges/btse/btse_test.go +++ b/exchanges/btse/btse_test.go @@ -572,21 +572,28 @@ func TestMatchType(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") - assert.Positive(t, l.MaximumBaseAmount, "MaximumBaseAmount should be positive") - assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") - assert.Positive(t, l.MinPrice, "MinPrice should be positive") - assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + for _, p := range pairs { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + assert.Positive(t, l.MaximumBaseAmount, "MaximumBaseAmount should be positive") + assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") + assert.Positive(t, l.MinPrice, "MinPrice should be positive") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + } }) } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } func TestGetRecentTrades(t *testing.T) { diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 6ed315047f7..f290b5d31e3 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -1171,6 +1171,10 @@ func (e *Exchange) IsPerpetualFutureCurrency(a asset.Item, p currency.Pair) (boo // UpdateOrderExecutionLimits updates order execution limits func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { + if !e.SupportsAsset(a) { + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) + } + summary, err := e.GetMarketSummary(ctx, "", a == asset.Spot) if err != nil { return err diff --git a/exchanges/bybit/bybit_test.go b/exchanges/bybit/bybit_test.go index c1dbedadf52..b9b4c8ecbb7 100644 --- a/exchanges/bybit/bybit_test.go +++ b/exchanges/bybit/bybit_test.go @@ -770,7 +770,6 @@ 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) { diff --git a/exchanges/deribit/deribit_test.go b/exchanges/deribit/deribit_test.go index 5458fd5fa89..a7753f5168c 100644 --- a/exchanges/deribit/deribit_test.go +++ b/exchanges/deribit/deribit_test.go @@ -3804,18 +3804,25 @@ func TestGetLatestFundingRates(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") - assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + for _, p := range pairs { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + } }) } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } func TestGetLockedStatus(t *testing.T) { diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index e0c775d62ac..73d7b4a1b8a 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -2458,37 +2458,42 @@ func TestUpdateOrderExecutionLimits(t *testing.T) { t.Run(a.String(), func(t *testing.T) { t.Parallel() switch a { - case asset.Options: - return // Options not supported case asset.CrossMargin, asset.Margin: require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), a), asset.ErrNotSupported) default: require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") - avail, err := e.GetAvailablePairs(a) + pairs, 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) - + for _, p := range pairs { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoErrorf(t, err, "GetOrderExecutionLimits must not error for %s", p) + require.NotNilf(t, l, "GetOrderExecutionLimits %s result cannot be nil", p) + assert.Equalf(t, a, l.Key.Asset, "asset should equal for %s", p) + assert.Truef(t, p.Equal(l.Key.Pair()), "pair should equal for %s", p) switch a { + case asset.Options: + assert.Positivef(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive for %s", p) + assert.Positivef(t, l.MaximumBaseAmount, "MaximumBaseAmount should be positive for %s", p) + assert.Positivef(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive for %s", p) + assert.Positivef(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive for %s", p) 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) + assert.Positivef(t, l.MultiplierDecimal, "MultiplierDecimal should be positive for %s", p) + assert.NotZerof(t, l.Listed, "Listed should be populated for %s", p) fallthrough case asset.CoinMarginedFutures: if !l.Delisted.IsZero() { - assert.Truef(t, l.Delisted.After(l.Delisting), "Delisted should be after Delisting for %s", pair) + assert.Truef(t, l.Delisted.After(l.Delisting), "Delisted should be after Delisting for %s", p) } + assert.Positivef(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive for %s", p) 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) + assert.Positivef(t, l.MinimumQuoteAmount, "MinimumQuoteAmount should be positive for %s", p) + assert.Positivef(t, l.QuoteStepIncrementSize, "QuoteStepIncrementSize should be positive for %s", p) + assert.Positivef(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive for %s", p) + assert.Positivef(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive for %s", p) case asset.DeliveryFutures: - assert.NotZerof(t, l.Expiry, "Expiry should be populated for %s", pair) + assert.NotZerof(t, l.Expiry, "Expiry should be populated for %s", p) + assert.Positivef(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive for %s", p) + assert.Positivef(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive for %s", p) } } } @@ -3581,7 +3586,7 @@ func TestWebsocketSubmitOrders(t *testing.T) { sub.AssetType = asset.USDTMarginedFutures cpy.AssetType = asset.USDTMarginedFutures _, err = e.WebsocketSubmitOrders(t.Context(), []*order.Submit{sub, &cpy}) - require.ErrorIs(t, err, common.ErrNotYetImplemented) + require.ErrorIs(t, err, errInvalidOrderSize) sharedtestvalues.SkipTestIfCredentialsUnset(t, e, canManipulateRealOrders) diff --git a/exchanges/gateio/gateio_websocket_test.go b/exchanges/gateio/gateio_websocket_test.go index cc9d5c938d9..d1eefc64d63 100644 --- a/exchanges/gateio/gateio_websocket_test.go +++ b/exchanges/gateio/gateio_websocket_test.go @@ -21,6 +21,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange" + "github.com/thrasher-corp/gocryptotrader/types" ) func TestGetWSPingHandler(t *testing.T) { @@ -277,7 +278,7 @@ func TestProcessOrderbookUpdateWithSnapshot(t *testing.T) { payload []byte err error }{ - {payload: []byte(`{"t":"bingbong"}`), err: strconv.ErrSyntax}, + {payload: []byte(`{"t":"bingbong"}`), err: types.ErrInvalidTimestampFormat}, {payload: []byte(`{"s":"ob.50"}`), err: common.ErrMalformedData}, {payload: []byte(`{"s":"ob..50"}`), err: currency.ErrCreatingPair}, {payload: []byte(`{"s":"ob.BTC_USDT.50","full":true}`), err: orderbook.ErrLastUpdatedNotSet}, diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index e74bb4be795..cd4cc362e2b 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -871,14 +871,9 @@ func (e *Exchange) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Sub if err != nil { return nil, err } - - s.ClientOrderID = formatClientOrderID(s.ClientOrderID) - - s.Pair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType) - if err != nil { + if err := e.formatOrderClientIDAndPair(s); err != nil { return nil, err } - s.Pair = s.Pair.Upper() switch s.AssetType { case asset.Spot, asset.Margin, asset.CrossMargin: @@ -1868,20 +1863,17 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if err != nil { return err } - l = make([]limits.MinMaxLevel, 0, len(pairsData)) for i := range pairsData { if pairsData[i].TradeStatus == "untradable" { continue } - // Minimum base amounts are not always provided this will default to // precision for base deployment. This can't be done for quote. minBaseAmount := pairsData[i].MinBaseAmount.Float64() if minBaseAmount == 0 { minBaseAmount = math.Pow10(-int(pairsData[i].AmountPrecision)) } - l = append(l, limits.MinMaxLevel{ Key: key.NewExchangeAssetPair(e.Name, a, currency.NewPair(pairsData[i].Base, pairsData[i].Quote)), QuoteStepIncrementSize: math.Pow10(-int(pairsData[i].PricePrecision)), @@ -1955,7 +1947,6 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if err != nil { return err } - l = make([]limits.MinMaxLevel, 0, len(contracts)) for c := range contracts { cp, err := currency.NewPairFromString(strings.ReplaceAll(contracts[c].Name, currency.DashDelimiter, currency.UnderscoreDelimiter)) if err != nil { @@ -1971,6 +1962,7 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) }) } } + return limits.Load(l) default: return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } @@ -2259,13 +2251,6 @@ func getClientOrderIDFromText(text string) string { return "" } -func formatClientOrderID(clientOrderID string) string { - if clientOrderID == "" || strings.HasPrefix(clientOrderID, "t-") { - return clientOrderID - } - return "t-" + clientOrderID -} - // getTypeFromTimeInForce returns the order type and if the order is post only func getTypeFromTimeInForce(tif string, price float64) (orderType order.Type) { switch tif { @@ -2333,13 +2318,9 @@ func (e *Exchange) WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (* return nil, err } - s.ClientOrderID = formatClientOrderID(s.ClientOrderID) - - s.Pair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType) - if err != nil { + if err := e.formatOrderClientIDAndPair(s); err != nil { return nil, err } - s.Pair = s.Pair.Upper() switch s.AssetType { case asset.Spot: @@ -2364,16 +2345,33 @@ func (e *Exchange) WebsocketSubmitOrder(ctx context.Context, s *order.Submit) (* } return e.deriveFuturesWebsocketOrderResponse(resp) default: - return nil, common.ErrNotYetImplemented + return nil, fmt.Errorf("%w: %s", asset.ErrNotSupported, s.AssetType) } } +func formatClientOrderID(clientOrderID string) string { + if clientOrderID == "" || strings.HasPrefix(clientOrderID, "t-") { + return clientOrderID + } + return "t-" + clientOrderID +} + +func (e *Exchange) formatOrderClientIDAndPair(s *order.Submit) error { + s.ClientOrderID = formatClientOrderID(s.ClientOrderID) + var err error + s.Pair, err = e.FormatExchangeCurrency(s.Pair, s.AssetType) + if err != nil { + return err + } + s.Pair = s.Pair.Upper() + return nil +} + func getFuturesOrderRequest(s *order.Submit) (*ContractOrderCreateParams, error) { amountWithDirection, err := getFutureOrderSize(s) if err != nil { return nil, err } - tif, err := toExchangeTIF(s.TimeInForce, s.Price) if err != nil { return nil, err @@ -2610,18 +2608,16 @@ func getSettlementCurrency(p currency.Pair, a asset.Item) (currency.Code, error) // WebsocketSubmitOrders submits orders to the exchange through the websocket func (e *Exchange) WebsocketSubmitOrders(ctx context.Context, orders []*order.Submit) ([]*order.SubmitResponse, error) { var a asset.Item - for x := range orders { - if err := orders[x].Validate(e.GetTradingRequirements()); err != nil { + for _, s := range orders { + if err := s.Validate(e.GetTradingRequirements()); err != nil { return nil, err } - if a == asset.Empty { - a = orders[x].AssetType + a = s.AssetType continue } - - if a != orders[x].AssetType { - return nil, fmt.Errorf("%w; Passed %q and %q", errSingleAssetRequired, a, orders[x].AssetType) + if a != s.AssetType { + return nil, fmt.Errorf("%w; Passed %q and %q", errSingleAssetRequired, a, s.AssetType) } } @@ -2632,9 +2628,13 @@ func (e *Exchange) WebsocketSubmitOrders(ctx context.Context, orders []*order.Su switch a { case asset.Spot: reqs := make([]*CreateOrderRequest, len(orders)) - for x := range orders { + for i, s := range orders { + if err := e.formatOrderClientIDAndPair(s); err != nil { + return nil, err + } var err error - if reqs[x], err = e.getSpotOrderRequest(orders[x]); err != nil { + reqs[i], err = e.getSpotOrderRequest(s) + if err != nil { return nil, err } } @@ -2643,8 +2643,25 @@ func (e *Exchange) WebsocketSubmitOrders(ctx context.Context, orders []*order.Su return nil, err } return e.deriveSpotWebsocketOrderResponses(resp) + case asset.CoinMarginedFutures, asset.USDTMarginedFutures: + reqs := make([]*ContractOrderCreateParams, len(orders)) + for i, s := range orders { + if err := e.formatOrderClientIDAndPair(s); err != nil { + return nil, err + } + var err error + reqs[i], err = getFuturesOrderRequest(s) + if err != nil { + return nil, err + } + } + resp, err := e.WebsocketFuturesSubmitOrders(ctx, a, reqs...) + if err != nil { + return nil, err + } + return e.deriveFuturesWebsocketOrderResponses(resp) default: - return nil, fmt.Errorf("%w for %s", common.ErrNotYetImplemented, a) + return nil, fmt.Errorf("%w: %s", asset.ErrNotSupported, a) } } diff --git a/exchanges/gemini/gemini_test.go b/exchanges/gemini/gemini_test.go index dc8b0241e53..40e84b0eb05 100644 --- a/exchanges/gemini/gemini_test.go +++ b/exchanges/gemini/gemini_test.go @@ -1258,17 +1258,24 @@ func TestGetSymbolDetails(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, false) 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 { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + } }) } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } func TestGetCurrencyTradeURL(t *testing.T) { diff --git a/exchanges/hitbtc/hitbtc_test.go b/exchanges/hitbtc/hitbtc_test.go index ca9c9131e81..8d5e886fe40 100644 --- a/exchanges/hitbtc/hitbtc_test.go +++ b/exchanges/hitbtc/hitbtc_test.go @@ -1014,12 +1014,26 @@ func TestGetCurrencyTradeURL(t *testing.T) { } } +func TestUpdateOrderExecutionLimits(t *testing.T) { + t.Parallel() + testexch.UpdatePairsOnce(t, e) + require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Spot), "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(asset.Spot, false) + require.NoError(t, err, "GetPairs must not error") + require.NotEmpty(t, pairs, "GetPairs must return pairs") + l, err := e.GetOrderExecutionLimits(asset.Spot, pairs[0]) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") + + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported, "UpdateOrderExecutionLimits must error") +} + func TestGenerateSubscriptions(t *testing.T) { t.Parallel() e := new(Exchange) require.NoError(t, testexch.Setup(e), "Test instance Setup must not error") - e.Websocket.SetCanUseAuthenticatedEndpoints(true) require.True(t, e.Websocket.CanUseAuthenticatedEndpoints(), "CanUseAuthenticatedEndpoints must return true") subs, err := e.generateSubscriptions() diff --git a/exchanges/hitbtc/hitbtc_wrapper.go b/exchanges/hitbtc/hitbtc_wrapper.go index 1c1565e6d1c..6b64d961748 100644 --- a/exchanges/hitbtc/hitbtc_wrapper.go +++ b/exchanges/hitbtc/hitbtc_wrapper.go @@ -10,9 +10,11 @@ import ( "time" "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/common/key" "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchange/accounts" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" "github.com/thrasher-corp/gocryptotrader/exchange/websocket" "github.com/thrasher-corp/gocryptotrader/exchange/websocket/buffer" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -840,8 +842,29 @@ func (e *Exchange) GetLatestFundingRates(context.Context, *fundingrate.LatestRat } // UpdateOrderExecutionLimits updates order execution limits -func (e *Exchange) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { - return common.ErrNotYetImplemented +func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { + if a != asset.Spot { + return fmt.Errorf("%w: %q", asset.ErrNotSupported, a) + } + + symbols, err := e.GetSymbolsDetailed(ctx) + if err != nil { + return err + } + l := make([]limits.MinMaxLevel, 0, len(symbols)) + for i := range symbols { + // s.QuoteCurrency is actually settlement currency, so trim the base currency to get the real quote currency + p, err := currency.NewPairFromStrings(symbols[i].BaseCurrency, strings.TrimPrefix(symbols[i].ID, symbols[i].BaseCurrency)) + if err != nil { + return err + } + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, p), + AmountStepIncrementSize: symbols[i].QuantityIncrement, + PriceStepIncrementSize: symbols[i].TickSize, + }) + } + return limits.Load(l) } // GetCurrencyTradeURL returns the URL to the exchange's trade page for the given asset and currency pair diff --git a/exchanges/huobi/futures_types.go b/exchanges/huobi/futures_types.go index b9c82042ac7..f09385a6ff1 100644 --- a/exchanges/huobi/futures_types.go +++ b/exchanges/huobi/futures_types.go @@ -15,7 +15,7 @@ type FContractInfoData struct { PriceTick float64 `json:"price_tick"` DeliveryDate string `json:"delivery_date"` DeliveryTime types.Time `json:"delivery_time"` - CreateDate string `json:"create_date"` + CreateDate types.Time `json:"create_date"` ContractStatus int64 `json:"contract_status"` SettlementTime types.Time `json:"settlement_time"` } diff --git a/exchanges/huobi/huobi_test.go b/exchanges/huobi/huobi_test.go index 4b075e8c6f6..0056b4eaf0a 100644 --- a/exchanges/huobi/huobi_test.go +++ b/exchanges/huobi/huobi_test.go @@ -1911,15 +1911,36 @@ func TestGetCurrencyTradeURL(t *testing.T) { require.NoErrorf(t, err, "cannot get pairs for %s", a) require.NotEmptyf(t, pairs, "no pairs for %s", a) resp, err := e.GetCurrencyTradeURL(t.Context(), a, pairs[0]) - if (a == asset.Futures || a == asset.CoinMarginedFutures) && !pairs[0].Quote.Equal(currency.USD) && !pairs[0].Quote.Equal(currency.USDT) { - require.ErrorIs(t, err, common.ErrNotYetImplemented) - } else { - require.NoError(t, err) - assert.NotEmpty(t, resp) - } + require.NoError(t, err) + assert.NotEmpty(t, resp) } } +func TestUpdateOrderExecutionLimits(t *testing.T) { + t.Parallel() + 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, false) + require.NoError(t, err, "GetPairs must not error") + require.NotEmpty(t, pairs, "GetPairs must return pairs") + for _, p := range pairs { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + assert.Positive(t, l.AmountStepIncrementSize, "AmountStepIncrementSize should be positive") + } + }) + } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) +} + func TestGenerateSubscriptions(t *testing.T) { t.Parallel() diff --git a/exchanges/huobi/huobi_types.go b/exchanges/huobi/huobi_types.go index a5199160461..27e06247e2e 100644 --- a/exchanges/huobi/huobi_types.go +++ b/exchanges/huobi/huobi_types.go @@ -480,7 +480,7 @@ type SwapMarketsData struct { ContractSize float64 `json:"contract_size"` PriceTick float64 `json:"price_tick"` SettlementDate types.Time `json:"settlement_date"` - CreateDate string `json:"create_date"` + CreateDate types.Time `json:"create_date"` DeliveryTime types.Time `json:"delivery_time"` ContractStatus int64 `json:"contract_status"` } diff --git a/exchanges/huobi/huobi_wrapper.go b/exchanges/huobi/huobi_wrapper.go index 96faeb9cc96..6df864852b6 100644 --- a/exchanges/huobi/huobi_wrapper.go +++ b/exchanges/huobi/huobi_wrapper.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "slices" "sort" "strconv" @@ -16,6 +17,7 @@ import ( "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/currency" "github.com/thrasher-corp/gocryptotrader/exchange/accounts" + "github.com/thrasher-corp/gocryptotrader/exchange/order/limits" "github.com/thrasher-corp/gocryptotrader/exchange/websocket" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" @@ -1978,18 +1980,13 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Ite if err != nil { return nil, err } - var s time.Time - s, err = time.Parse("20060102", result[x].CreateDate) - if err != nil { - return nil, err - } resp = append(resp, futures.Contract{ Exchange: e.Name, Name: cp, Underlying: underlying, Asset: item, - StartDate: s, + StartDate: result[x].CreateDate.Time(), SettlementType: futures.Inverse, IsActive: result[x].ContractStatus == 1, Type: futures.Perpetual, @@ -2015,17 +2012,11 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Ite if err != nil { return nil, err } - var startTime, endTime time.Time - startTime, err = time.Parse("20060102", result.Data[x].CreateDate) - if err != nil { - return nil, err - } - if result.Data[x].DeliveryTime.Time().IsZero() { - endTime = result.Data[x].DeliveryTime.Time() - } else { + endTime := result.Data[x].DeliveryTime.Time() + if endTime.IsZero() { endTime = result.Data[x].SettlementTime.Time() } - contractLength := endTime.Sub(startTime) + contractLength := endTime.Sub(result.Data[x].CreateDate.Time()) var ct futures.ContractType switch { case contractLength <= kline.OneWeek.Duration()+kline.ThreeDay.Duration(): @@ -2045,7 +2036,7 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Ite Name: cp, Underlying: underlying, Asset: item, - StartDate: startTime, + StartDate: result.Data[x].CreateDate.Time(), EndDate: endTime, SettlementType: futures.Linear, IsActive: result.Data[x].ContractStatus == 1, @@ -2146,8 +2137,98 @@ func (e *Exchange) IsPerpetualFutureCurrency(a asset.Item, _ currency.Pair) (boo } // UpdateOrderExecutionLimits updates order execution limits -func (e *Exchange) UpdateOrderExecutionLimits(_ context.Context, _ asset.Item) error { - return common.ErrNotYetImplemented +func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { + if !e.SupportsAsset(a) { + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) + } + var l []limits.MinMaxLevel + switch a { + case asset.Spot: + symbols, err := e.GetSymbols(ctx) + if err != nil { + return err + } + l = make([]limits.MinMaxLevel, 0, len(symbols)) + for i := range symbols { + if symbols[i].State != "online" { + continue + } + p, err := currency.NewPairFromStrings(symbols[i].BaseCurrency, symbols[i].QuoteCurrency) + if err != nil { + return err + } + minBaseAmt := symbols[i].LimitOrderMinOrderAmt + if minBaseAmt == 0 { + minBaseAmt = symbols[i].MinOrderAmt + } + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, p), + MinimumBaseAmount: minBaseAmt, + MaximumBaseAmount: symbols[i].LimitOrderMaxOrderAmt, + MinimumQuoteAmount: symbols[i].MinOrderValue, + AmountStepIncrementSize: math.Pow10(-int(symbols[i].AmountPrecision)), + PriceStepIncrementSize: math.Pow10(-int(symbols[i].PricePrecision)), + QuoteStepIncrementSize: math.Pow10(-int(symbols[i].ValuePrecision)), + }) + } + case asset.Futures: + contracts, err := e.FGetContractInfo(ctx, "", "", currency.EMPTYPAIR) + if err != nil { + return err + } + l = make([]limits.MinMaxLevel, 0, len(contracts.Data)) + for i := range contracts.Data { + if contracts.Data[i].ContractStatus != 1 { + continue + } + p, err := e.MatchSymbolWithAvailablePairs(contracts.Data[i].ContractCode, a, true) + if err != nil { + return err + } + endTime := contracts.Data[i].DeliveryTime.Time() + if endTime.IsZero() { + endTime = contracts.Data[i].SettlementTime.Time() + } + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, p), + MinimumBaseAmount: 1, + AmountStepIncrementSize: 1, // orders are in number of contracts + PriceStepIncrementSize: contracts.Data[i].PriceTick, + MultiplierDecimal: contracts.Data[i].ContractSize, + Listed: contracts.Data[i].CreateDate.Time(), + Expiry: endTime, + }) + } + case asset.CoinMarginedFutures: + contracts, err := e.GetSwapMarkets(ctx, currency.EMPTYPAIR) + if err != nil { + return err + } + l = make([]limits.MinMaxLevel, 0, len(contracts)) + for i := range contracts { + if contracts[i].ContractStatus != 1 { + continue + } + p, err := e.MatchSymbolWithAvailablePairs(contracts[i].ContractCode, a, true) + if err != nil { + if errors.Is(err, currency.ErrPairNotFound) { + continue + } + return err + } + + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, p), + MinimumBaseAmount: 1, + AmountStepIncrementSize: 1, // orders are in number of contracts + PriceStepIncrementSize: contracts[i].PriceTick, + MultiplierDecimal: contracts[i].ContractSize, + Listed: contracts[i].CreateDate.Time(), + Delisted: contracts[i].DeliveryTime.Time(), + }) + } + } + return limits.Load(l) } // GetOpenInterest returns the open interest rate for a given asset pair @@ -2296,16 +2377,20 @@ func (e *Exchange) GetCurrencyTradeURL(_ context.Context, a asset.Item, cp curre cp.Delimiter = currency.UnderscoreDelimiter return tradeBaseURL + tradeSpot + cp.Lower().String(), nil case asset.Futures: - if !cp.Quote.Equal(currency.USD) && !cp.Quote.Equal(currency.USDT) { - // todo: support long dated currencies - return "", fmt.Errorf("%w %v requires translating currency into static contracts eg 'weekly'", common.ErrNotYetImplemented, a) + if slices.Contains(validContractExpiryCodes, strings.ToUpper(cp.Quote.String())) { + cp, err = e.pairFromContractExpiryCode(cp) + if err != nil { + return "", err + } } cp.Delimiter = currency.DashDelimiter return tradeBaseURL + tradeFutures + cp.Upper().String(), nil case asset.CoinMarginedFutures: - if !cp.Quote.Equal(currency.USD) && !cp.Quote.Equal(currency.USDT) { - // todo: support long dated currencies - return "", fmt.Errorf("%w %v requires translating currency into static contracts eg 'weekly'", common.ErrNotYetImplemented, a) + if slices.Contains(validContractExpiryCodes, strings.ToUpper(cp.Quote.String())) { + cp, err = e.pairFromContractExpiryCode(cp) + if err != nil { + return "", err + } } return tradeBaseURL + tradeCoinMargined + cp.Base.Upper().String(), nil default: diff --git a/exchanges/kraken/kraken_test.go b/exchanges/kraken/kraken_test.go index 1da8bcde936..be3610bfe73 100644 --- a/exchanges/kraken/kraken_test.go +++ b/exchanges/kraken/kraken_test.go @@ -79,23 +79,26 @@ func TestWrapperGetServerTime(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() - switch a { - case asset.Futures: - require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), a), common.ErrNotYetImplemented) - default: - require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") - pairs, err := e.CurrencyPairs.GetPairs(a, false) - require.NoError(t, err, "GetPairs must not error") - l, err := e.GetOrderExecutionLimits(a, pairs[0]) + require.NoError(t, e.UpdateOrderExecutionLimits(t.Context(), a), "UpdateOrderExecutionLimits must not error") + pairs, err := e.CurrencyPairs.GetPairs(a, false) + require.NoError(t, err, "GetPairs must not error") + for _, p := range pairs { + l, err := e.GetOrderExecutionLimits(a, p) require.NoError(t, err, "GetOrderExecutionLimits must not error") assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") } }) } + + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } func TestFetchTradablePairs(t *testing.T) { diff --git a/exchanges/kraken/kraken_wrapper.go b/exchanges/kraken/kraken_wrapper.go index 64645d6c5f3..59154ea0ada 100644 --- a/exchanges/kraken/kraken_wrapper.go +++ b/exchanges/kraken/kraken_wrapper.go @@ -244,36 +244,53 @@ func (e *Exchange) Bootstrap(ctx context.Context) (continueBootstrap bool, err e // UpdateOrderExecutionLimits sets exchange execution order limits for an asset type func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) error { - if a != asset.Spot { - return common.ErrNotYetImplemented - } - - if !assetTranslator.Seeded() { - if err := e.SeedAssets(ctx); err != nil { - return err + switch a { + case asset.Spot: + if !assetTranslator.Seeded() { + if err := e.SeedAssets(ctx); err != nil { + return err + } } - } - - pairInfo, err := e.fetchSpotPairInfo(ctx) - if err != nil { - return fmt.Errorf("%s failed to load %s pair execution limits. Err: %s", e.Name, a, err) - } - l := make([]limits.MinMaxLevel, 0, len(pairInfo)) - - for pair, info := range pairInfo { - l = append(l, limits.MinMaxLevel{ - Key: key.NewExchangeAssetPair(e.Name, a, pair), - PriceStepIncrementSize: info.TickSize, - MinimumBaseAmount: info.OrderMinimum, - }) - } + pairInfo, err := e.fetchSpotPairInfo(ctx) + if err != nil { + return fmt.Errorf("%s failed to load %s pair execution limits. Err: %s", e.Name, a, err) + } - if err := limits.Load(l); err != nil { - return fmt.Errorf("%s Error loading %s exchange limits: %w", e.Name, a, err) + l := make([]limits.MinMaxLevel, 0, len(pairInfo)) + for pair, info := range pairInfo { + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, pair), + PriceStepIncrementSize: info.TickSize, + MinimumBaseAmount: info.OrderMinimum, + }) + } + return limits.Load(l) + case asset.Futures: + instruments, err := e.GetInstruments(ctx) + if err != nil { + return fmt.Errorf("%s failed to load %s pair execution limits. Err: %s", e.Name, a, err) + } + l := make([]limits.MinMaxLevel, 0, len(instruments.Instruments)) + for i := range instruments.Instruments { + if !instruments.Instruments[i].Tradable { + continue + } + pair, err := currency.NewPairFromString(instruments.Instruments[i].Symbol) + if err != nil { + return err + } + l = append(l, limits.MinMaxLevel{ + Key: key.NewExchangeAssetPair(e.Name, a, pair), + PriceStepIncrementSize: instruments.Instruments[i].TickSize, + MinimumBaseAmount: instruments.Instruments[i].ContractSize, + AmountStepIncrementSize: instruments.Instruments[i].ContractSize, + }) + } + return limits.Load(l) + default: + return fmt.Errorf("%w %q", asset.ErrNotSupported, a) } - - return nil } func (e *Exchange) fetchSpotPairInfo(ctx context.Context) (map[currency.Pair]*AssetPairs, error) { diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index 4c172e87a5a..202d02fbe82 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -3116,13 +3116,21 @@ func TestSubscribeTickerAll(t *testing.T) { t.Parallel() ku := testInstance(t) + ku.Features.Subscriptions = subscription.List{} + testexch.SetupWs(t, ku) + done := make(chan struct{}) go func() { // drain websocket messages when subscribed to all tickers for { - <-ku.Websocket.DataHandler.C + select { + case <-done: + return + case <-ku.Websocket.DataHandler.C: + } } }() - ku.Features.Subscriptions = subscription.List{} - testexch.SetupWs(t, ku) + t.Cleanup(func() { + close(done) + }) avail, err := ku.GetAvailablePairs(asset.Spot) require.NoError(t, err, "GetAvailablePairs must not error") diff --git a/exchanges/kucoin/kucoin_wrapper_test.go b/exchanges/kucoin/kucoin_wrapper_test.go index f4de7531aa2..444125f2c0c 100644 --- a/exchanges/kucoin/kucoin_wrapper_test.go +++ b/exchanges/kucoin/kucoin_wrapper_test.go @@ -5,6 +5,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange" ) @@ -24,4 +25,8 @@ func TestUpdateOrderExecutionLimits(t *testing.T) { } }) } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index f847864b25e..db21e77264c 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -3324,12 +3324,18 @@ func TestUpdateOrderExecutionLimits(t *testing.T) { 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.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") - assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + for _, p := range pairs { + l, err := e.GetOrderExecutionLimits(a, p) + require.NoError(t, err, "GetOrderExecutionLimits must not error") + assert.Positive(t, l.PriceStepIncrementSize, "PriceStepIncrementSize should be positive") + assert.Positive(t, l.MinimumBaseAmount, "MinimumBaseAmount should be positive") + } }) } + t.Run("unsupported asset", func(t *testing.T) { + t.Parallel() + require.ErrorIs(t, e.UpdateOrderExecutionLimits(t.Context(), asset.Binary), asset.ErrNotSupported) + }) } func TestUpdateTicker(t *testing.T) { diff --git a/types/time.go b/types/time.go index ecf94493eff..dac255f0fdd 100644 --- a/types/time.go +++ b/types/time.go @@ -16,7 +16,8 @@ import ( // format requirements. type Time time.Time -var errInvalidTimestampFormat = errors.New("invalid timestamp format") +// ErrInvalidTimestampFormat indicates that a timestamp cannot be parsed into a supported format. +var ErrInvalidTimestampFormat = errors.New("invalid timestamp format") // UnmarshalJSON deserializes json, and timestamp information. func (t *Time) UnmarshalJSON(data []byte) error { @@ -38,9 +39,15 @@ func (t *Time) UnmarshalJSON(data []byte) error { } } - // Expects a string of length 10 (seconds), 13 (milliseconds), 16 (microseconds), or 19 (nanoseconds) representing a Unix timestamp switch len(s) { - case 12, 15, 18: + case 8: + parsed, err := time.Parse("20060102", s) + if err != nil { + return fmt.Errorf("%w error parsing %q into date: %w", ErrInvalidTimestampFormat, s, err) + } + *t = Time(parsed) + return nil + case 12, 15, 18: // Expects a string of length 10 (seconds), 13 (milliseconds), 16 (microseconds), or 19 (nanoseconds) representing a Unix timestamp s += "0" case 11, 14, 17: s += "00" @@ -61,7 +68,7 @@ func (t *Time) UnmarshalJSON(data []byte) error { case 19: *t = Time(time.Unix(0, unixTS)) default: - return fmt.Errorf("%w: %q", errInvalidTimestampFormat, data) + return fmt.Errorf("%w: %q", ErrInvalidTimestampFormat, data) } return nil } @@ -74,7 +81,7 @@ func (t Time) String() string { return t.Time().String() } -// MarshalJSON serializes the time to json. +// MarshalJSON serializes the time to json using RFC 3339 format. func (t Time) MarshalJSON() ([]byte, error) { return t.Time().MarshalJSON() } diff --git a/types/time_test.go b/types/time_test.go index ea018b6e166..01a454b987a 100644 --- a/types/time_test.go +++ b/types/time_test.go @@ -25,7 +25,8 @@ func TestUnmarshalJSON(t *testing.T) { {`"0.0"`, time.Time{}, nil}, {`"0.00000"`, time.Time{}, nil}, {`"0.0.0.0"`, time.Time{}, strconv.ErrSyntax}, - {`"0.1"`, time.Time{}, errInvalidTimestampFormat}, + {`"0.1"`, time.Time{}, ErrInvalidTimestampFormat}, + {`"20200325"`, time.Date(2020, 3, 25, 0, 0, 0, 0, time.UTC), nil}, {`"1628736847"`, time.Unix(1628736847, 0), nil}, {`"1726104395.5"`, time.UnixMilli(1726104395500), nil}, {`"1726104395.56"`, time.UnixMilli(1726104395560), nil}, @@ -37,8 +38,9 @@ func TestUnmarshalJSON(t *testing.T) { {`"1560516023.070651"`, time.Unix(0, 1560516023070651000), nil}, {`"1606292218213457800"`, time.Unix(0, 1606292218213457800), nil}, {`"blurp"`, time.Time{}, strconv.ErrSyntax}, - {`"123456"`, time.Time{}, errInvalidTimestampFormat}, - {`"2025-03-28T08:00:00Z"`, time.Time{}, strconv.ErrSyntax}, // RFC3339 format + {`"123456"`, time.Time{}, ErrInvalidTimestampFormat}, + {`"12345678"`, time.Time{}, ErrInvalidTimestampFormat}, + {`"2025-03-28T08:00:00Z"`, time.Time{}, strconv.ErrSyntax}, // RFC3339 format (currently unsupported) {`"1606292218213.45.8"`, time.Time{}, strconv.ErrSyntax}, // parse int failure } { t.Run(tc.input, func(t *testing.T) { From 23eb14484f47b9ffb7cc1aa334fab34754f84e3c Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 23 Feb 2026 16:54:06 +1100 Subject: [PATCH 2/2] build(deps): Bump github.com/grpc-ecosystem/grpc-gateway/v2 (#2188) Bumps [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) from 2.27.8 to 2.28.0. - [Release notes](https://github.com/grpc-ecosystem/grpc-gateway/releases) - [Commits](https://github.com/grpc-ecosystem/grpc-gateway/compare/v2.27.8...v2.28.0) --- updated-dependencies: - dependency-name: github.com/grpc-ecosystem/grpc-gateway/v2 dependency-version: 2.28.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 161630239d2..dc6c12f5799 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/gofrs/uuid v4.4.0+incompatible github.com/gorilla/websocket v1.5.3 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 github.com/lib/pq v1.11.2 github.com/mattn/go-sqlite3 v1.14.34 diff --git a/go.sum b/go.sum index f4fff43aa2f..12f1fd08432 100644 --- a/go.sum +++ b/go.sum @@ -116,8 +116,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDa github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8 h1:NpbJl/eVbvrGE0MJ6X16X9SAifesl6Fwxg/YmCvubRI= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.8/go.mod h1:mi7YA+gCzVem12exXy46ZespvGtX/lZmD/RLnQhVW7U= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 h1:HWRh5R2+9EifMyIHV7ZV+MIZqgz+PMpZ14Jynv3O2Zs= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0/go.mod h1:JfhWUomR1baixubs02l85lZYYOm7LV6om4ceouMv45c= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=