From 7f412e2772f40cb1a526653ac8451b4fca9312b3 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Wed, 17 Dec 2025 12:42:34 +1100 Subject: [PATCH] Kucoin: Update order execution limits (#2124) * refactor(kucoin): enhance contract and symbol structures, update order execution limits tests * fix(number): handle null input in UnmarshalJSON and update tests * Update exchanges/kucoin/kucoin_futures_types.go Co-authored-by: Scott * Update exchanges/kucoin/kucoin_futures_types.go Co-authored-by: Scott * Update exchanges/kucoin/kucoin_wrapper.go Co-authored-by: Scott * Update exchanges/kucoin/kucoin_wrapper.go Co-authored-by: Scott * Update exchanges/kucoin/kucoin_wrapper.go Co-authored-by: Scott * Update exchanges/kucoin/kucoin_types.go Co-authored-by: Scott * Update exchanges/kucoin/kucoin_wrapper.go Co-authored-by: Scott * glorious: destroyed this code across all implementations * glorious: rename * ai overlord: nit * Update exchanges/kucoin/kucoin_futures_types.go Co-authored-by: Adrian Gallagher * thrasher: nits --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott Co-authored-by: Adrian Gallagher --- exchanges/kucoin/kucoin_futures_types.go | 135 ++++++++++------- exchanges/kucoin/kucoin_test.go | 16 --- exchanges/kucoin/kucoin_types.go | 45 +++--- exchanges/kucoin/kucoin_wrapper.go | 175 ++++++++--------------- exchanges/kucoin/kucoin_wrapper_test.go | 27 ++++ types/number.go | 6 +- types/number_test.go | 6 +- 7 files changed, 206 insertions(+), 204 deletions(-) create mode 100644 exchanges/kucoin/kucoin_wrapper_test.go diff --git a/exchanges/kucoin/kucoin_futures_types.go b/exchanges/kucoin/kucoin_futures_types.go index 9400e445..c1a59102 100644 --- a/exchanges/kucoin/kucoin_futures_types.go +++ b/exchanges/kucoin/kucoin_futures_types.go @@ -14,61 +14,86 @@ var validGranularity = []string{ // Contract store contract details type Contract struct { - Symbol string `json:"symbol"` - RootSymbol string `json:"rootSymbol"` - ContractType string `json:"type"` - FirstOpenDate types.Time `json:"firstOpenDate"` - ExpireDate types.Time `json:"expireDate"` - SettleDate types.Time `json:"settleDate"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - SettleCurrency string `json:"settleCurrency"` - MaxOrderQty float64 `json:"maxOrderQty"` - MaxPrice float64 `json:"maxPrice"` - LotSize float64 `json:"lotSize"` - TickSize float64 `json:"tickSize"` - IndexPriceTickSize float64 `json:"indexPriceTickSize"` - Multiplier float64 `json:"multiplier"` - InitialMargin float64 `json:"initialMargin"` - MaintainMargin float64 `json:"maintainMargin"` - MaxRiskLimit float64 `json:"maxRiskLimit"` - MinRiskLimit float64 `json:"minRiskLimit"` - RiskStep float64 `json:"riskStep"` - MakerFeeRate float64 `json:"makerFeeRate"` - TakerFeeRate float64 `json:"takerFeeRate"` - TakerFixFee float64 `json:"takerFixFee"` - MakerFixFee float64 `json:"makerFixFee"` - SettlementFee float64 `json:"settlementFee"` - IsDeleverage bool `json:"isDeleverage"` - IsQuanto bool `json:"isQuanto"` - IsInverse bool `json:"isInverse"` - MarkMethod string `json:"markMethod"` - FairMethod string `json:"fairMethod"` - FundingBaseSymbol string `json:"fundingBaseSymbol"` - FundingQuoteSymbol string `json:"fundingQuoteSymbol"` - FundingRateSymbol string `json:"fundingRateSymbol"` - IndexSymbol string `json:"indexSymbol"` - SettlementSymbol string `json:"settlementSymbol"` - Status string `json:"status"` - FundingFeeRate float64 `json:"fundingFeeRate"` - PredictedFundingFeeRate float64 `json:"predictedFundingFeeRate"` - OpenInterest types.Number `json:"openInterest"` - TurnoverOf24h float64 `json:"turnoverOf24h"` - VolumeOf24h float64 `json:"volumeOf24h"` - MarkPrice float64 `json:"markPrice"` - IndexPrice float64 `json:"indexPrice"` - LastTradePrice float64 `json:"lastTradePrice"` - NextFundingRateTime int64 `json:"nextFundingRateTime"` - MaxLeverage float64 `json:"maxLeverage"` - SourceExchanges []string `json:"sourceExchanges"` - PremiumsSymbol1M string `json:"premiumsSymbol1M"` - PremiumsSymbol8H string `json:"premiumsSymbol8H"` - FundingBaseSymbol1M string `json:"fundingBaseSymbol1M"` - FundingQuoteSymbol1M string `json:"fundingQuoteSymbol1M"` - LowPrice float64 `json:"lowPrice"` - HighPrice float64 `json:"highPrice"` - PriceChgPct float64 `json:"priceChgPct"` - PriceChg float64 `json:"priceChg"` + Symbol string `json:"symbol"` + RootSymbol currency.Code `json:"rootSymbol"` + ContractType string `json:"type"` + FirstOpenDate types.Time `json:"firstOpenDate"` + ExpireDate types.Time `json:"expireDate"` + SettleDate types.Time `json:"settleDate"` + BaseCurrency currency.Code `json:"baseCurrency"` + QuoteCurrency currency.Code `json:"quoteCurrency"` + SettleCurrency currency.Code `json:"settleCurrency"` + MaxOrderQty float64 `json:"maxOrderQty"` + MarketMaxOrderQty float64 `json:"marketMaxOrderQty"` + MaxPrice float64 `json:"maxPrice"` + LotSize float64 `json:"lotSize"` + TickSize float64 `json:"tickSize"` + IndexPriceTickSize float64 `json:"indexPriceTickSize"` + Multiplier float64 `json:"multiplier"` + InitialMargin float64 `json:"initialMargin"` + MaintainMargin float64 `json:"maintainMargin"` + MaxRiskLimit float64 `json:"maxRiskLimit"` + MinRiskLimit float64 `json:"minRiskLimit"` + RiskStep float64 `json:"riskStep"` + MakerFeeRate float64 `json:"makerFeeRate"` + TakerFeeRate float64 `json:"takerFeeRate"` + TakerFixFee float64 `json:"takerFixFee"` + MakerFixFee float64 `json:"makerFixFee"` + SettlementFee float64 `json:"settlementFee"` + IsDeleverage bool `json:"isDeleverage"` + IsQuanto bool `json:"isQuanto"` + IsInverse bool `json:"isInverse"` + MarkMethod string `json:"markMethod"` + FairMethod string `json:"fairMethod"` + FundingBaseSymbol string `json:"fundingBaseSymbol"` + FundingQuoteSymbol string `json:"fundingQuoteSymbol"` + FundingRateSymbol string `json:"fundingRateSymbol"` + IndexSymbol string `json:"indexSymbol"` + SettlementSymbol string `json:"settlementSymbol"` + Status string `json:"status"` + FundingFeeRate float64 `json:"fundingFeeRate"` + PredictedFundingFeeRate float64 `json:"predictedFundingFeeRate"` + DailyInterestRate float64 `json:"dailyInterestRate"` + FundingRateGranularity int64 `json:"fundingRateGranularity"` + FundingRateCap float64 `json:"fundingRateCap"` + FundingRateFloor float64 `json:"fundingRateFloor"` + Period float64 `json:"period"` + EffectiveFundingRateCycleStartTime types.Time `json:"effectiveFundingRateCycleStartTime"` + CurrentFundingRateGranularity int64 `json:"currentFundingRateGranularity"` + OpenInterest types.Number `json:"openInterest"` + TurnoverOf24h float64 `json:"turnoverOf24h"` + VolumeOf24h float64 `json:"volumeOf24h"` + MarkPrice float64 `json:"markPrice"` + IndexPrice float64 `json:"indexPrice"` + LastTradePrice float64 `json:"lastTradePrice"` + NextFundingRateTime int64 `json:"nextFundingRateTime"` // Not a timestamp + NextFundingRateDateTime types.Time `json:"nextFundingRateDateTime"` + MaxLeverage float64 `json:"maxLeverage"` + SourceExchanges []string `json:"sourceExchanges"` + PremiumsSymbol1M string `json:"premiumsSymbol1M"` + PremiumsSymbol8H string `json:"premiumsSymbol8H"` + FundingBaseSymbol1M string `json:"fundingBaseSymbol1M"` + FundingQuoteSymbol1M string `json:"fundingQuoteSymbol1M"` + LowPrice float64 `json:"lowPrice"` + HighPrice float64 `json:"highPrice"` + PriceChangePercentage float64 `json:"priceChgPct"` + PriceChange float64 `json:"priceChg"` + K float64 `json:"k"` // Max open size amplification factor + M float64 `json:"m"` // Margin-curve slope/smoothing constant (affects MMR growth with size) + F float64 `json:"f"` // IMR to MMR safety multiplier + MMRLimit float64 `json:"mmrLimit"` + MMRLeverageConstant float64 `json:"mmrLevConstant"` + SupportCross bool `json:"supportCross"` + BuyLimit float64 `json:"buyLimit"` + SellLimit float64 `json:"sellLimit"` + AdjustK types.Number `json:"adjustK"` + AdjustM types.Number `json:"adjustM"` + AdjustMMRLeverageConstant types.Number `json:"adjustMmrLevConstant"` + AdjustActiveTime types.Time `json:"adjustActiveTime"` + CrossRiskLimit float64 `json:"crossRiskLimit"` + MarketStage string `json:"marketStage"` + PreMarketToPerpDate types.Time `json:"preMarketToPerpDate"` + OrderPriceRange float64 `json:"orderPriceRange"` } // FuturesTicker stores ticker data diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index e31f86cf..5a228dc2 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -3289,22 +3289,6 @@ func TestGetFuturesPositionOrders(t *testing.T) { assert.NotNil(t, result) } -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.AmountStepIncrementSize, "AmountStepIncrementSize should not be zero") - }) - } -} - func BenchmarkIntervalToString(b *testing.B) { for b.Loop() { result, err := IntervalToString(kline.OneWeek) diff --git a/exchanges/kucoin/kucoin_types.go b/exchanges/kucoin/kucoin_types.go index af0af324..76ac4c02 100644 --- a/exchanges/kucoin/kucoin_types.go +++ b/exchanges/kucoin/kucoin_types.go @@ -89,23 +89,34 @@ func (e Error) GetError() error { // SymbolInfo stores symbol information type SymbolInfo struct { - Symbol string `json:"symbol"` - Name string `json:"name"` - BaseCurrency string `json:"baseCurrency"` - QuoteCurrency string `json:"quoteCurrency"` - FeeCurrency string `json:"feeCurrency"` - Market string `json:"market"` - BaseMinSize float64 `json:"baseMinSize,string"` - QuoteMinSize float64 `json:"quoteMinSize,string"` - BaseMaxSize float64 `json:"baseMaxSize,string"` - QuoteMaxSize float64 `json:"quoteMaxSize,string"` - BaseIncrement float64 `json:"baseIncrement,string"` - QuoteIncrement float64 `json:"quoteIncrement,string"` - PriceIncrement float64 `json:"priceIncrement,string"` - PriceLimitRate float64 `json:"priceLimitRate,string"` - MinFunds float64 `json:"minFunds,string"` - IsMarginEnabled bool `json:"isMarginEnabled"` - EnableTrading bool `json:"enableTrading"` + Symbol currency.Pair `json:"symbol"` + Name currency.Pair `json:"name"` + BaseCurrency currency.Code `json:"baseCurrency"` + QuoteCurrency currency.Code `json:"quoteCurrency"` + FeeCurrency currency.Code `json:"feeCurrency"` + Market string `json:"market"` + BaseMinSize types.Number `json:"baseMinSize"` + QuoteMinSize types.Number `json:"quoteMinSize"` + BaseMaxSize types.Number `json:"baseMaxSize"` + QuoteMaxSize types.Number `json:"quoteMaxSize"` + BaseIncrement types.Number `json:"baseIncrement"` + QuoteIncrement types.Number `json:"quoteIncrement"` + PriceIncrement types.Number `json:"priceIncrement"` + PriceLimitRate types.Number `json:"priceLimitRate"` + MinFunds types.Number `json:"minFunds"` + IsMarginEnabled bool `json:"isMarginEnabled"` + EnableTrading bool `json:"enableTrading"` + FeeCategory int64 `json:"feeCategory"` + MakerFeeCoefficient types.Number `json:"makerFeeCoefficient"` + TakerFeeCoefficient types.Number `json:"takerFeeCoefficient"` + SpecialTreatment bool `json:"st"` + CallAuctionIsEnabled bool `json:"callauctionIsEnabled"` + CallAuctionPriceFloor types.Number `json:"callauctionPriceFloor"` + CallAuctionPriceCeiling types.Number `json:"callauctionPriceCeiling"` + CallAuctionFirstStageStartTime types.Time `json:"callauctionFirstStageStartTime"` + CallAuctionSecondStageStartTime types.Time `json:"callauctionSecondStageStartTime"` + CallAuctionThirdStageStartTime types.Time `json:"callauctionThirdStageStartTime"` + TradingStartTime types.Time `json:"tradingStartTime"` } // Ticker stores ticker data diff --git a/exchanges/kucoin/kucoin_wrapper.go b/exchanges/kucoin/kucoin_wrapper.go index d593a0c0..658a9d14 100644 --- a/exchanges/kucoin/kucoin_wrapper.go +++ b/exchanges/kucoin/kucoin_wrapper.go @@ -220,7 +220,6 @@ func (e *Exchange) Setup(exch *config.Exchange) error { // FetchTradablePairs returns a list of the exchanges tradable pairs func (e *Exchange) FetchTradablePairs(ctx context.Context, assetType asset.Item) (currency.Pairs, error) { - var cp currency.Pair switch assetType { case asset.Futures: myPairs, err := e.GetFuturesOpenContracts(ctx) @@ -232,11 +231,8 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, assetType asset.Item) if strings.ToLower(myPairs[x].Status) != "open" { //nolint:gocritic // strings.ToLower is faster continue } - cp, err = currency.NewPairFromStrings(myPairs[x].BaseCurrency, myPairs[x].Symbol[len(myPairs[x].BaseCurrency):]) - if err != nil { - return nil, err - } - pairs = pairs.Add(cp) + quote := currency.NewCode(myPairs[x].Symbol[len(myPairs[x].BaseCurrency.String()):]) + pairs = pairs.Add(currency.NewPair(myPairs[x].BaseCurrency, quote)) } configFormat, err := e.GetPairFormat(asset.Futures, false) if err != nil { @@ -255,11 +251,7 @@ func (e *Exchange) FetchTradablePairs(ctx context.Context, assetType asset.Item) } // Symbol field must be used to generate pair as this is the symbol // to fetch data from the API. e.g. BSV-USDT name is BCHSV-USDT as symbol. - cp, err = currency.NewPairFromString(strings.ToUpper(myPairs[x].Symbol)) - if err != nil { - return nil, err - } - pairs = pairs.Add(cp) + pairs = pairs.Add(myPairs[x].Symbol) } return pairs, nil default: @@ -309,11 +301,8 @@ func (e *Exchange) UpdateTickers(ctx context.Context, assetType asset.Item) erro return err } for x := range ticks { - var pair currency.Pair - pair, err = currency.NewPairFromStrings(ticks[x].BaseCurrency, ticks[x].Symbol[len(ticks[x].BaseCurrency):]) - if err != nil { - return err - } + pair := currency.NewPair(ticks[x].BaseCurrency, + currency.NewCode(ticks[x].Symbol[len(ticks[x].BaseCurrency.String()):])) if !pairs.Contains(pair, true) { continue } @@ -1213,16 +1202,10 @@ func (e *Exchange) GetActiveOrders(ctx context.Context, getOrdersRequest *order. if !futuresOrders.Items[x].IsActive { continue } - var dPair currency.Pair - var enabled bool - dPair, enabled, err = e.MatchSymbolCheckEnabled(futuresOrders.Items[x].Symbol, getOrdersRequest.AssetType, false) + pair, err := e.MatchSymbolWithAvailablePairs(futuresOrders.Items[x].Symbol, getOrdersRequest.AssetType, false) if err != nil { return nil, err } - if !enabled { - continue - } - side, err := order.StringToOrderSide(futuresOrders.Items[x].Side) if err != nil { return nil, err @@ -1257,7 +1240,7 @@ func (e *Exchange) GetActiveOrders(ctx context.Context, getOrdersRequest *order. Price: futuresOrders.Items[x].Price, Side: side, Type: oType, - Pair: dPair, + Pair: pair, TimeInForce: StringToTimeInForce(futuresOrders.Items[x].TimeInForce, futuresOrders.Items[x].PostOnly), ReduceOnly: futuresOrders.Items[x].ReduceOnly, Status: status, @@ -1322,16 +1305,11 @@ func (e *Exchange) GetActiveOrders(ctx context.Context, getOrdersRequest *order. if response.Items[a].Status != "New" { continue } - var dPair currency.Pair - var enabled bool - dPair, enabled, err = e.MatchSymbolCheckEnabled(response.Items[a].Symbol, getOrdersRequest.AssetType, false) + pair, err := e.MatchSymbolWithAvailablePairs(response.Items[a].Symbol, getOrdersRequest.AssetType, false) if err != nil { return nil, err } - if !enabled { - continue - } - if len(getOrdersRequest.Pairs) > 1 && !getOrdersRequest.Pairs.Contains(dPair, true) { + if len(getOrdersRequest.Pairs) > 1 && !getOrdersRequest.Pairs.Contains(pair, true) { continue } side, err := order.StringToOrderSide(response.Items[a].Side) @@ -1353,7 +1331,7 @@ func (e *Exchange) GetActiveOrders(ctx context.Context, getOrdersRequest *order. Price: response.Items[a].Price, Side: side, Type: order.Stop, - Pair: dPair, + Pair: pair, TimeInForce: StringToTimeInForce(response.Items[a].TimeInForce, response.Items[a].PostOnly), Status: status, AssetType: getOrdersRequest.AssetType, @@ -1380,16 +1358,11 @@ func (e *Exchange) GetActiveOrders(ctx context.Context, getOrdersRequest *order. if !spotOrders.Items[x].IsActive { continue } - var dPair currency.Pair - var isEnabled bool - dPair, isEnabled, err = e.MatchSymbolCheckEnabled(spotOrders.Items[x].Symbol, getOrdersRequest.AssetType, true) + pair, err := e.MatchSymbolWithAvailablePairs(spotOrders.Items[x].Symbol, getOrdersRequest.AssetType, true) if err != nil { return nil, err } - if !isEnabled { - continue - } - if len(getOrdersRequest.Pairs) > 0 && !getOrdersRequest.Pairs.Contains(dPair, true) { + if len(getOrdersRequest.Pairs) > 0 && !getOrdersRequest.Pairs.Contains(pair, true) { continue } side, err := order.StringToOrderSide(spotOrders.Items[x].Side) @@ -1410,7 +1383,7 @@ func (e *Exchange) GetActiveOrders(ctx context.Context, getOrdersRequest *order. Price: spotOrders.Items[x].Price.Float64(), Side: side, Type: oType, - Pair: dPair, + Pair: pair, }) } } @@ -1439,10 +1412,7 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, getOrdersRequest *order. } var orders []order.Detail - var orderSide order.Side var orderStatus order.Status - var oType order.Type - var pair currency.Pair switch getOrdersRequest.AssetType { case asset.Futures: var futuresOrders *FutureOrdersResponse @@ -1471,19 +1441,15 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, getOrdersRequest *order. } orders = make(order.FilteredOrders, 0, len(futuresOrders.Items)) for i := range orders { - orderSide, err = order.StringToOrderSide(futuresOrders.Items[i].Side) + orderSide, err := order.StringToOrderSide(futuresOrders.Items[i].Side) if err != nil { return nil, err } - var isEnabled bool - pair, isEnabled, err = e.MatchSymbolCheckEnabled(futuresOrders.Items[i].Symbol, getOrdersRequest.AssetType, true) + pair, err := e.MatchSymbolWithAvailablePairs(futuresOrders.Items[i].Symbol, getOrdersRequest.AssetType, true) if err != nil { return nil, err } - if !isEnabled { - continue - } - oType, err = order.StringToOrderType(futuresOrders.Items[i].OrderType) + oType, err := order.StringToOrderType(futuresOrders.Items[i].OrderType) if err != nil { log.Errorf(log.ExchangeSys, "%s %v", e.Name, err) } @@ -1555,16 +1521,11 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, getOrdersRequest *order. return nil, err } for a := range response.Items { - var dPair currency.Pair - var enabled bool - dPair, enabled, err = e.MatchSymbolCheckEnabled(response.Items[a].Symbol, getOrdersRequest.AssetType, false) + pair, err := e.MatchSymbolWithAvailablePairs(response.Items[a].Symbol, getOrdersRequest.AssetType, false) if err != nil { return nil, err } - if !enabled { - continue - } - if len(getOrdersRequest.Pairs) > 1 && !getOrdersRequest.Pairs.Contains(dPair, true) { + if len(getOrdersRequest.Pairs) > 1 && !getOrdersRequest.Pairs.Contains(pair, true) { continue } var ( @@ -1591,7 +1552,7 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, getOrdersRequest *order. Price: response.Items[a].Price, Side: side, Type: order.Stop, - Pair: dPair, + Pair: pair, TimeInForce: StringToTimeInForce(response.Items[a].TimeInForce, response.Items[a].PostOnly), Status: status, AssetType: getOrdersRequest.AssetType, @@ -1621,17 +1582,15 @@ func (e *Exchange) GetOrderHistory(ctx context.Context, getOrdersRequest *order. } orders = make([]order.Detail, len(responseOrders.Items)) for i := range orders { - orderSide, err = order.StringToOrderSide(responseOrders.Items[i].Side) + orderSide, err := order.StringToOrderSide(responseOrders.Items[i].Side) if err != nil { return nil, err } - var orderStatus order.Status - pair, err = currency.NewPairFromString(responseOrders.Items[i].Symbol) + pair, err := currency.NewPairFromString(responseOrders.Items[i].Symbol) if err != nil { return nil, err } - var oType order.Type - oType, err = order.StringToOrderType(responseOrders.Items[i].Type) + oType, err := order.StringToOrderType(responseOrders.Items[i].Type) if err != nil { log.Errorf(log.ExchangeSys, "%s %v", e.Name, err) } @@ -1868,21 +1827,9 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Ite resp := make([]futures.Contract, len(contracts)) for i := range contracts { - var cp, underlying currency.Pair - underlying, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].QuoteCurrency) - if err != nil { - return nil, err - } - cp, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].Symbol[len(contracts[i].BaseCurrency):]) - if err != nil { - return nil, err - } - settleCurr := currency.NewCode(contracts[i].SettleCurrency) - var ct futures.ContractType + ct := futures.Quarterly if contracts[i].ContractType == "FFWCSX" { ct = futures.Perpetual - } else { - ct = futures.Quarterly } contractSettlementType := futures.Linear if contracts[i].IsInverse { @@ -1897,11 +1844,12 @@ func (e *Exchange) GetFuturesContractDetails(ctx context.Context, item asset.Ite } timeOfCurrentFundingRate := time.Now().Add((time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond) - fri).Truncate(time.Hour).UTC() resp[i] = futures.Contract{ - Exchange: e.Name, - Name: cp, - Underlying: underlying, - SettlementCurrency: settleCurr, - MarginCurrency: settleCurr, + Exchange: e.Name, + Name: currency.NewPair(contracts[i].BaseCurrency, + currency.NewCode(contracts[i].Symbol[len(contracts[i].BaseCurrency.String()):])), + Underlying: currency.NewPair(contracts[i].BaseCurrency, contracts[i].QuoteCurrency), + SettlementCurrency: contracts[i].SettleCurrency, + MarginCurrency: contracts[i].SettleCurrency, Asset: item, StartDate: contracts[i].FirstOpenDate.Time(), EndDate: contracts[i].ExpireDate.Time(), @@ -1944,11 +1892,8 @@ func (e *Exchange) GetLatestFundingRates(ctx context.Context, r *fundingrate.Lat resp := make([]fundingrate.LatestRateResponse, 0, len(contracts)) for i := range contracts { timeOfNextFundingRate := time.Now().Add(time.Duration(contracts[i].NextFundingRateTime) * time.Millisecond).Truncate(time.Hour).UTC() - var cp currency.Pair - cp, err = currency.NewPairFromStrings(contracts[i].BaseCurrency, contracts[i].Symbol[len(contracts[i].BaseCurrency):]) - if err != nil { - return nil, err - } + cp := currency.NewPair(contracts[i].BaseCurrency, + currency.NewCode(contracts[i].Symbol[len(contracts[i].BaseCurrency.String()):])) var isPerp bool isPerp, err = e.IsPerpetualFutureCurrency(r.Asset, cp) if err != nil { @@ -2323,22 +2268,16 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if a == asset.Margin && !symbols[x].IsMarginEnabled { continue } - pair, enabled, err := e.MatchSymbolCheckEnabled(symbols[x].Symbol, a, true) - if err != nil && !errors.Is(err, currency.ErrPairNotFound) { - return err - } - if !enabled { - continue - } l = append(l, limits.MinMaxLevel{ - Key: key.NewExchangeAssetPair(e.Name, a, pair), - AmountStepIncrementSize: symbols[x].BaseIncrement, - QuoteStepIncrementSize: symbols[x].QuoteIncrement, - PriceStepIncrementSize: symbols[x].PriceIncrement, - MinimumBaseAmount: symbols[x].BaseMinSize, - MaximumBaseAmount: symbols[x].BaseMaxSize, - MinimumQuoteAmount: symbols[x].QuoteMinSize, - MaximumQuoteAmount: symbols[x].QuoteMaxSize, + Key: key.NewExchangeAssetPair(e.Name, a, symbols[x].Symbol), + AmountStepIncrementSize: symbols[x].BaseIncrement.Float64(), + QuoteStepIncrementSize: symbols[x].QuoteIncrement.Float64(), + PriceStepIncrementSize: symbols[x].PriceIncrement.Float64(), + MinimumBaseAmount: symbols[x].BaseMinSize.Float64(), + MaximumBaseAmount: symbols[x].BaseMaxSize.Float64(), + MinimumQuoteAmount: symbols[x].QuoteMinSize.Float64(), + MaximumQuoteAmount: symbols[x].QuoteMaxSize.Float64(), + Listed: symbols[x].TradingStartTime.Time(), }) } case asset.Futures: @@ -2346,21 +2285,36 @@ func (e *Exchange) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) if err != nil { return err } + l = make([]limits.MinMaxLevel, 0, len(contract)) for x := range contract { - pair, enabled, err := e.MatchSymbolCheckEnabled(contract[x].Symbol, a, false) - if err != nil && !errors.Is(err, currency.ErrPairNotFound) { + pair, err := e.MatchSymbolWithAvailablePairs(contract[x].Symbol, a, false) + if err != nil { return err } - if !enabled { - continue + + priceDivisor := 1.0 + if contract[x].Symbol[:2] == "10" { // handle 1000SHIBUSDT, 1000PEPEUSDT etc; exclude 1INCHUSDT + for _, r := range contract[x].Symbol[1:] { + if r != '0' { + break + } + priceDivisor *= 10 + } } + l = append(l, limits.MinMaxLevel{ Key: key.NewExchangeAssetPair(e.Name, a, pair), AmountStepIncrementSize: contract[x].LotSize, QuoteStepIncrementSize: contract[x].TickSize, + MinimumBaseAmount: contract[x].LotSize, MaximumBaseAmount: contract[x].MaxOrderQty, MaximumQuoteAmount: contract[x].MaxPrice, + MultiplierDecimal: contract[x].Multiplier, + Listed: contract[x].FirstOpenDate.Time(), + Delisted: contract[x].ExpireDate.Time(), + Expiry: contract[x].SettleDate.Time(), + PriceDivisor: priceDivisor, }) } } @@ -2382,18 +2336,13 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f } resp := make([]futures.OpenInterest, 0, len(contracts)) for i := range contracts { - var symbol currency.Pair - var enabled bool - symbol, enabled, err = e.MatchSymbolCheckEnabled(contracts[i].Symbol, asset.Futures, true) + pair, err := e.MatchSymbolWithAvailablePairs(contracts[i].Symbol, asset.Futures, true) if err != nil && !errors.Is(err, currency.ErrPairNotFound) { return nil, err } - if !enabled { - continue - } var appendData bool for j := range k { - if k[j].Pair().Equal(symbol) { + if k[j].Pair().Equal(pair) { appendData = true break } @@ -2402,7 +2351,7 @@ func (e *Exchange) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]f continue } resp = append(resp, futures.OpenInterest{ - Key: key.NewExchangeAssetPair(e.Name, asset.Futures, symbol), + Key: key.NewExchangeAssetPair(e.Name, asset.Futures, pair), OpenInterest: contracts[i].OpenInterest.Float64(), }) } diff --git a/exchanges/kucoin/kucoin_wrapper_test.go b/exchanges/kucoin/kucoin_wrapper_test.go new file mode 100644 index 00000000..f4de7531 --- /dev/null +++ b/exchanges/kucoin/kucoin_wrapper_test.go @@ -0,0 +1,27 @@ +package kucoin + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange" +) + +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.GetAvailablePairs(a) + 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.AmountStepIncrementSize, "AmountStepIncrementSize should not be zero") + } + }) + } +} diff --git a/types/number.go b/types/number.go index 452760fe..f866dfb3 100644 --- a/types/number.go +++ b/types/number.go @@ -16,7 +16,10 @@ type Number float64 // UnmarshalJSON implements json.Unmarshaler func (f *Number) UnmarshalJSON(data []byte) error { switch c := data[0]; c { // From json.decode literalInterface - case 'n', 't', 'f': // null, true, false + case 'n': // null + *f = Number(0) + return nil + case 't', 'f': // true, false return fmt.Errorf("%w: %s", errInvalidNumberValue, data) case '"': // string if len(data) < 2 || data[len(data)-1] != '"' { @@ -40,7 +43,6 @@ func (f *Number) UnmarshalJSON(data []byte) error { } *f = Number(val) - return nil } diff --git a/types/number_test.go b/types/number_test.go index 78e8b4e1..b74f48c5 100644 --- a/types/number_test.go +++ b/types/number_test.go @@ -22,12 +22,16 @@ func TestNumberUnmarshalJSON(t *testing.T) { assert.NoError(t, err, "Unmarshal should not error") assert.Zero(t, n.Float64(), "UnmarshalJSON should parse empty as 0") + err = n.UnmarshalJSON([]byte(`null`)) + assert.NoError(t, err, "Unmarshal should not error") + assert.Zero(t, n.Float64(), "UnmarshalJSON should parse empty as 0") + err = n.UnmarshalJSON([]byte(`1337.37`)) assert.NoError(t, err, "Unmarshal should not error on number types") assert.Equal(t, 1337.37, n.Float64(), "UnmarshalJSON should handle raw numerics") // Invalid value checking - for _, i := range []string{`"MEOW"`, `null`, `false`, `true`, `"1337.37`} { + for _, i := range []string{`"MEOW"`, `false`, `true`, `"1337.37`} { err = n.UnmarshalJSON([]byte(i)) assert.ErrorIsf(t, err, errInvalidNumberValue, "UnmarshalJSON should error with invalid Value for %q", i) }