diff --git a/README.md b/README.md index 16819ec0f39..b76c4bc3ade 100644 --- a/README.md +++ b/README.md @@ -148,11 +148,11 @@ Binaries will be published once the codebase reaches a stable condition. |User|Contribution Amount| |--|--| -| [thrasher-](https://github.com/thrasher-) | 700 | -| [shazbert](https://github.com/shazbert) | 345 | -| [dependabot[bot]](https://github.com/apps/dependabot) | 317 | -| [gloriousCode](https://github.com/gloriousCode) | 234 | -| [gbjk](https://github.com/gbjk) | 93 | +| [thrasher-](https://github.com/thrasher-) | 703 | +| [shazbert](https://github.com/shazbert) | 355 | +| [dependabot[bot]](https://github.com/apps/dependabot) | 331 | +| [gloriousCode](https://github.com/gloriousCode) | 236 | +| [gbjk](https://github.com/gbjk) | 107 | | [dependabot-preview[bot]](https://github.com/apps/dependabot-preview) | 88 | | [xtda](https://github.com/xtda) | 47 | | [lrascao](https://github.com/lrascao) | 27 | diff --git a/cmd/documentation/exchanges_templates/kucoin.tmpl b/cmd/documentation/exchanges_templates/kucoin.tmpl index 1eb9ca856c9..1ae61cc437c 100644 --- a/cmd/documentation/exchanges_templates/kucoin.tmpl +++ b/cmd/documentation/exchanges_templates/kucoin.tmpl @@ -23,6 +23,9 @@ Default Authenticated Subscriptions: Subscriptions are subject to enabled assets and pairs. +Margin subscriptions for ticker, orderbook and All trades are merged into Spot subscriptions because duplicates are not allowed, +unless Spot subscription does not exist, i.e. Spot asset not enabled, or subscription configured only for Margin + Limitations: - 100 symbols per subscription - 300 symbols per connection diff --git a/engine/currency_state_manager.md b/engine/currency_state_manager.md index 5e938e72fab..cb6a44d9a57 100644 --- a/engine/currency_state_manager.md +++ b/engine/currency_state_manager.md @@ -1,22 +1,22 @@ -# GoCryptoTrader package Currency state manager - - - - -[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml) -[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) -[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/currency_state_manager) -[![Coverage Status](https://codecov.io/gh/thrasher-corp/gocryptotrader/graph/badge.svg?token=41784B23TS)](https://codecov.io/gh/thrasher-corp/gocryptotrader) -[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) - - -This currency_state_manager package is part of the GoCryptoTrader codebase. - -## This is still in active development - -You can track ideas, planned features and what's in progress on our [GoCryptoTrader Kanban board](https://github.com/orgs/thrasher-corp/projects/3). - -Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) +# GoCryptoTrader package Currency state manager + + + + +[![Build Status](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml/badge.svg?branch=master)](https://github.com/thrasher-corp/gocryptotrader/actions/workflows/tests.yml) +[![Software License](https://img.shields.io/badge/License-MIT-orange.svg?style=flat-square)](https://github.com/thrasher-corp/gocryptotrader/blob/master/LICENSE) +[![GoDoc](https://godoc.org/github.com/thrasher-corp/gocryptotrader?status.svg)](https://godoc.org/github.com/thrasher-corp/gocryptotrader/engine/currency_state_manager) +[![Coverage Status](https://codecov.io/gh/thrasher-corp/gocryptotrader/graph/badge.svg?token=41784B23TS)](https://codecov.io/gh/thrasher-corp/gocryptotrader) +[![Go Report Card](https://goreportcard.com/badge/github.com/thrasher-corp/gocryptotrader)](https://goreportcard.com/report/github.com/thrasher-corp/gocryptotrader) + + +This currency_state_manager package is part of the GoCryptoTrader codebase. + +## This is still in active development + +You can track ideas, planned features and what's in progress on our [GoCryptoTrader Kanban board](https://github.com/orgs/thrasher-corp/projects/3). + +Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader Slack](https://join.slack.com/t/gocryptotrader/shared_invite/enQtNTQ5NDAxMjA2Mjc5LTc5ZDE1ZTNiOGM3ZGMyMmY1NTAxYWZhODE0MWM5N2JlZDk1NDU0YTViYzk4NTk3OTRiMDQzNGQ1YTc4YmRlMTk) ## Current Features for Currency state manager + The state manager keeps currency states up to date, which include: @@ -27,22 +27,22 @@ Join our slack to discuss all things related to GoCryptoTrader! [GoCryptoTrader + This allows for an internal state check to compliment internal and external strategies. - -## Contribution - -Please feel free to submit any pull requests or suggest any desired features to be added. - -When submitting a PR, please abide by our coding guidelines: - -+ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). -+ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. -+ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). -+ Pull requests need to be based on and opened against the `master` branch. - -## Donations - - - -If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: - + +## Contribution + +Please feel free to submit any pull requests or suggest any desired features to be added. + +When submitting a PR, please abide by our coding guidelines: + ++ Code must adhere to the official Go [formatting](https://golang.org/doc/effective_go.html#formatting) guidelines (i.e. uses [gofmt](https://golang.org/cmd/gofmt/)). ++ Code must be documented adhering to the official Go [commentary](https://golang.org/doc/effective_go.html#commentary) guidelines. ++ Code must adhere to our [coding style](https://github.com/thrasher-corp/gocryptotrader/blob/master/doc/coding_style.md). ++ Pull requests need to be based on and opened against the `master` branch. + +## Donations + + + +If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to: + ***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc*** diff --git a/exchanges/kucoin/README.md b/exchanges/kucoin/README.md index 3439d186f84..b027535efcd 100644 --- a/exchanges/kucoin/README.md +++ b/exchanges/kucoin/README.md @@ -41,6 +41,9 @@ Default Authenticated Subscriptions: Subscriptions are subject to enabled assets and pairs. +Margin subscriptions for ticker, orderbook and All trades are merged into Spot subscriptions because duplicates are not allowed, +unless Spot subscription does not exist, i.e. Spot asset not enabled, or subscription configured only for Margin + Limitations: - 100 symbols per subscription - 300 symbols per connection diff --git a/exchanges/kucoin/kucoin_test.go b/exchanges/kucoin/kucoin_test.go index 86c3ac38b33..aab3357280d 100644 --- a/exchanges/kucoin/kucoin_test.go +++ b/exchanges/kucoin/kucoin_test.go @@ -2266,52 +2266,53 @@ func TestGenerateSubscriptions(t *testing.T) { // Only in Spot: BTC-USDT, ETH-USDT // In Both: ETH-BTC, LTC-USDT // Only in Margin: TRX-BTC, SOL-USDC - subPairs := currency.Pairs{} - for _, pp := range [][]string{ - {"BTC", "USDT", "-"}, {"ETH", "BTC", "-"}, {"ETH", "USDT", "-"}, {"LTC", "USDT", "-"}, // Spot - {"ETH", "BTC", "-"}, {"LTC", "USDT", "-"}, {"SOL", "USDC", "-"}, {"TRX", "BTC", "-"}, // Margin - {"ETH", "USDCM", ""}, {"SOL", "USDTM", ""}, {"XBT", "USDCM", ""}, // Futures + pairs := map[string]currency.Pairs{} + for a, ss := range map[string][]string{ + "spot": {"BTC-USDT", "ETH-BTC", "ETH-USDT", "LTC-USDT"}, + "margin": {"ETH-BTC", "LTC-USDT", "SOL-USDC", "TRX-BTC"}, + "futures": {"ETHUSDCM", "SOLUSDTM", "XBTUSDCM"}, } { - subPairs = append(subPairs, currency.NewPairWithDelimiter(pp[0], pp[1], pp[2])) + for _, s := range ss { + p, err := currency.NewPairFromString(s) + require.NoError(t, err, "NewPairFromString must not error") + pairs[a] = pairs[a].Add(p) + } } + pairs["both"] = common.SortStrings(pairs["spot"].Add(pairs["margin"]...)) exp := subscription.List{ - {Channel: subscription.TickerChannel, Asset: asset.Spot, Pairs: subPairs[0:4], QualifiedChannel: "/market/ticker:" + subPairs[0:4].Join()}, - {Channel: subscription.TickerChannel, Asset: asset.Margin, Pairs: subPairs[6:8], QualifiedChannel: "/market/ticker:" + subPairs[6:8].Join()}, - {Channel: subscription.TickerChannel, Asset: asset.Futures, Pairs: subPairs[8:], QualifiedChannel: "/contractMarket/tickerV2:" + subPairs[8:].Join()}, - {Channel: subscription.OrderbookChannel, Asset: asset.Spot, Pairs: subPairs[0:4], QualifiedChannel: "/spotMarket/level2Depth5:" + subPairs[0:4].Join(), - Interval: kline.HundredMilliseconds}, - {Channel: subscription.OrderbookChannel, Asset: asset.Margin, Pairs: subPairs[6:8], QualifiedChannel: "/spotMarket/level2Depth5:" + subPairs[6:8].Join(), + {Channel: subscription.TickerChannel, Asset: asset.Spot, Pairs: pairs["both"], QualifiedChannel: "/market/ticker:" + pairs["both"].Join()}, + {Channel: subscription.TickerChannel, Asset: asset.Futures, Pairs: pairs["futures"], QualifiedChannel: "/contractMarket/tickerV2:" + pairs["futures"].Join()}, + {Channel: subscription.OrderbookChannel, Asset: asset.Spot, Pairs: pairs["both"], QualifiedChannel: "/spotMarket/level2Depth5:" + pairs["both"].Join(), Interval: kline.HundredMilliseconds}, - {Channel: subscription.OrderbookChannel, Asset: asset.Futures, Pairs: subPairs[8:], QualifiedChannel: "/contractMarket/level2Depth5:" + subPairs[8:].Join(), + {Channel: subscription.OrderbookChannel, Asset: asset.Futures, Pairs: pairs["futures"], QualifiedChannel: "/contractMarket/level2Depth5:" + pairs["futures"].Join(), Interval: kline.HundredMilliseconds}, - {Channel: subscription.AllTradesChannel, Asset: asset.Spot, Pairs: subPairs[0:4], QualifiedChannel: "/market/match:" + subPairs[0:4].Join()}, - {Channel: subscription.AllTradesChannel, Asset: asset.Margin, Pairs: subPairs[6:8], QualifiedChannel: "/market/match:" + subPairs[6:8].Join()}, + {Channel: subscription.AllTradesChannel, Asset: asset.Spot, Pairs: pairs["both"], QualifiedChannel: "/market/match:" + pairs["both"].Join()}, } subs, err := ku.generateSubscriptions() - assert.NoError(t, err, "generateSubscriptions must not error") + require.NoError(t, err, "generateSubscriptions must not error") testsubs.EqualLists(t, exp, subs) ku.Websocket.SetCanUseAuthenticatedEndpoints(true) var loanPairs currency.Pairs - loanCurrs := common.SortStrings(subPairs[0:8].GetCurrencies()) + loanCurrs := common.SortStrings(pairs["both"].GetCurrencies()) for _, c := range loanCurrs { loanPairs = append(loanPairs, currency.Pair{Base: c}) } exp = append(exp, subscription.List{ - {Asset: asset.Futures, Channel: futuresTradeOrderChannel, QualifiedChannel: "/contractMarket/tradeOrders", Pairs: subPairs[8:]}, - {Asset: asset.Futures, Channel: futuresStopOrdersLifecycleEventChannel, QualifiedChannel: "/contractMarket/advancedOrders", Pairs: subPairs[8:]}, - {Asset: asset.Futures, Channel: futuresAccountBalanceEventChannel, QualifiedChannel: "/contractAccount/wallet", Pairs: subPairs[8:]}, - {Asset: asset.Margin, Channel: marginPositionChannel, QualifiedChannel: "/margin/position", Pairs: subPairs[4:8]}, + {Asset: asset.Futures, Channel: futuresTradeOrderChannel, QualifiedChannel: "/contractMarket/tradeOrders", Pairs: pairs["futures"]}, + {Asset: asset.Futures, Channel: futuresStopOrdersLifecycleEventChannel, QualifiedChannel: "/contractMarket/advancedOrders", Pairs: pairs["futures"]}, + {Asset: asset.Futures, Channel: futuresAccountBalanceEventChannel, QualifiedChannel: "/contractAccount/wallet", Pairs: pairs["futures"]}, + {Asset: asset.Margin, Channel: marginPositionChannel, QualifiedChannel: "/margin/position", Pairs: pairs["margin"]}, {Asset: asset.Margin, Channel: marginLoanChannel, QualifiedChannel: "/margin/loan:" + loanCurrs.Join(), Pairs: loanPairs}, {Channel: accountBalanceChannel, QualifiedChannel: "/account/balance"}, }...) subs, err = ku.generateSubscriptions() - assert.NoError(t, err, "generateSubscriptions with Auth must not error") + require.NoError(t, err, "generateSubscriptions with Auth must not error") testsubs.EqualLists(t, exp, subs) } @@ -2320,21 +2321,16 @@ func TestGenerateTickerAllSub(t *testing.T) { ku := testInstance(t) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes avail, err := ku.GetAvailablePairs(asset.Spot) - assert.NoError(t, err, "GetAvailablePairs must not error") - for i := 0; i <= 10; i++ { - err = ku.CurrencyPairs.EnablePair(asset.Spot, avail[i]) - assert.NoError(t, common.ExcludeError(err, currency.ErrPairAlreadyEnabled), "EnablePair must not error") - } - - enabled, err := ku.GetEnabledPairs(asset.Spot) - assert.NoError(t, err, "GetEnabledPairs must not error") + require.NoError(t, err, "GetAvailablePairs must not error") + err = ku.CurrencyPairs.StorePairs(asset.Spot, avail[:11], true) + require.NoError(t, err, "StorePairs must not error") ku.Features.Subscriptions = subscription.List{{Channel: subscription.TickerChannel, Asset: asset.Spot}} exp := subscription.List{ - {Channel: subscription.TickerChannel, Asset: asset.Spot, QualifiedChannel: "/market/ticker:all", Pairs: enabled}, + {Channel: subscription.TickerChannel, Asset: asset.Spot, QualifiedChannel: "/market/ticker:all", Pairs: avail[:11]}, } subs, err := ku.generateSubscriptions() - assert.NoError(t, err, "generateSubscriptions with Auth must not error") + require.NoError(t, err, "generateSubscriptions with Auth must not error") testsubs.EqualLists(t, exp, subs) } @@ -2353,7 +2349,7 @@ func TestGenerateOtherSubscriptions(t *testing.T) { ku.Features.Subscriptions = subscription.List{s} got, err := ku.generateSubscriptions() assert.NoError(t, err, "generateSubscriptions should not error") - assert.Len(t, got, 1, "Should generate just one sub") + require.Len(t, got, 1, "Must generate just one sub") assert.NotEmpty(t, got[0].QualifiedChannel, "Qualified Channel should not be empty") if got[0].Channel == subscription.CandlesChannel { assert.Equal(t, "/market/candles:BTC-USDT_4hour,ETH-BTC_4hour,ETH-USDT_4hour,LTC-USDT_4hour", got[0].QualifiedChannel, "QualifiedChannel should be correct") @@ -2361,6 +2357,43 @@ func TestGenerateOtherSubscriptions(t *testing.T) { } } +// TestGenerateMarginSubscriptions is a regression test for #1755 and ensures margin subscriptions work without spot subs +func TestGenerateMarginSubscriptions(t *testing.T) { + t.Parallel() + + ku := testInstance(t) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + avail, err := ku.GetAvailablePairs(asset.Spot) + require.NoError(t, err, "GetAvailablePairs must not error storing spot pairs") + avail = common.SortStrings(avail) + err = ku.CurrencyPairs.StorePairs(asset.Margin, avail[:6], true) + require.NoError(t, err, "StorePairs must not error storing margin pairs") + err = ku.CurrencyPairs.StorePairs(asset.Spot, avail[:3], true) + require.NoError(t, err, "StorePairs must not error storing spot pairs") + + ku.Features.Subscriptions = subscription.List{{Channel: subscription.TickerChannel, Asset: asset.Margin}} + subs, err := ku.Features.Subscriptions.ExpandTemplates(ku) + require.NoError(t, err, "ExpandTemplates must not error") + require.Len(t, subs, 1, "Must generate just one sub") + assert.Equal(t, asset.Margin, subs[0].Asset, "Asset should be correct") + assert.Equal(t, "/market/ticker:"+avail[:6].Join(), subs[0].QualifiedChannel, "QualifiedChannel should be correct") + + require.NoError(t, ku.CurrencyPairs.SetAssetEnabled(asset.Margin, false), "SetAssetEnabled Spot must not error") + require.NoError(t, err, "SetAssetEnabled must not error") + ku.Features.Subscriptions = subscription.List{{Channel: subscription.TickerChannel, Asset: asset.All}} + subs, err = ku.Features.Subscriptions.ExpandTemplates(ku) + require.NoError(t, err, "mergeMarginPairs must not cause errAssetRecords by adding an empty asset when Margin is disabled") + require.NotEmpty(t, subs, "ExpandTemplates must return some subs") + + require.NoError(t, ku.CurrencyPairs.SetAssetEnabled(asset.Margin, true), "SetAssetEnabled Margin must not error") + require.NoError(t, ku.CurrencyPairs.SetAssetEnabled(asset.Spot, false), "SetAssetEnabled Spot must not error") + require.NoError(t, ku.CurrencyPairs.SetAssetEnabled(asset.Futures, false), "SetAssetEnabled Futures must not error") + ku.Features.Subscriptions = subscription.List{{Channel: subscription.TickerChannel, Asset: asset.All}} + subs, err = ku.Features.Subscriptions.ExpandTemplates(ku) + require.NoError(t, err, "mergeMarginPairs must not cause errAssetRecords by adding an empty asset when Spot is disabled") + require.NotEmpty(t, subs, "ExpandTemplates must return some subs") +} + // TestCheckSubscriptions ensures checkSubscriptions upgrades user config correctly func TestCheckSubscriptions(t *testing.T) { t.Parallel() @@ -2934,8 +2967,8 @@ func TestSubscribeBatches(t *testing.T) { } subs, err := ku.generateSubscriptions() - assert.NoError(t, err, "generateSubscriptions must not error") - assert.Len(t, subs, len(ku.Features.Subscriptions), "Must generate batched subscriptions") + require.NoError(t, err, "generateSubscriptions must not error") + require.Len(t, subs, len(ku.Features.Subscriptions), "Must generate batched subscriptions") err = ku.Subscribe(subs) assert.NoError(t, err, "Subscribe to small batches should not error") @@ -2953,32 +2986,32 @@ func TestSubscribeBatchLimit(t *testing.T) { testexch.SetupWs(t, ku) avail, err := ku.GetAvailablePairs(asset.Spot) - assert.NoError(t, err, "GetAvailablePairs must not error") + require.NoError(t, err, "GetAvailablePairs must not error") err = ku.CurrencyPairs.StorePairs(asset.Spot, avail[:299], true) - assert.NoError(t, err, "StorePairs must not error") + require.NoError(t, err, "StorePairs must not error") ku.Features.Subscriptions = subscription.List{{Asset: asset.Spot, Channel: subscription.AllTradesChannel}} subs, err := ku.generateSubscriptions() - assert.NoError(t, err, "generateSubscriptions must not error") - assert.Len(t, subs, 3, "Must get 3 subs") + require.NoError(t, err, "generateSubscriptions must not error") + require.Len(t, subs, 3, "Must get 3 subs") err = ku.Subscribe(subs) - assert.NoError(t, err, "Subscribe must not error") + require.NoError(t, err, "Subscribe must not error") err = ku.Unsubscribe(subs) - assert.NoError(t, err, "Unsubscribe must not error") + require.NoError(t, err, "Unsubscribe must not error") err = ku.CurrencyPairs.StorePairs(asset.Spot, avail[:320], true) - assert.NoError(t, err, "StorePairs must not error") + require.NoError(t, err, "StorePairs must not error") ku.Features.Subscriptions = subscription.List{{Asset: asset.Spot, Channel: subscription.AllTradesChannel}} subs, err = ku.generateSubscriptions() - assert.NoError(t, err, "generateSubscriptions must not error") - assert.Len(t, subs, 4, "Must get 4 subs") + require.NoError(t, err, "generateSubscriptions must not error") + require.Len(t, subs, 4, "Must get 4 subs") err = ku.Subscribe(subs) - assert.ErrorContains(t, err, "exceed max subscription count limitation of 300 per session", "Subscribe to MarketSnapshot must error above connection symbol limit") + assert.ErrorContains(t, err, "exceed max subscription count limitation of 300 per session", "Subscribe to MarketSnapshot should error above connection symbol limit") } func TestSubscribeTickerAll(t *testing.T) { @@ -2989,20 +3022,20 @@ func TestSubscribeTickerAll(t *testing.T) { testexch.SetupWs(t, ku) avail, err := ku.GetAvailablePairs(asset.Spot) - assert.NoError(t, err, "GetAvailablePairs must not error") + require.NoError(t, err, "GetAvailablePairs must not error") err = ku.CurrencyPairs.StorePairs(asset.Spot, avail[:500], true) - assert.NoError(t, err, "StorePairs must not error") + require.NoError(t, err, "StorePairs must not error") ku.Features.Subscriptions = subscription.List{{Asset: asset.Spot, Channel: subscription.TickerChannel}} subs, err := ku.generateSubscriptions() - assert.NoError(t, err, "generateSubscriptions must not error") - assert.Len(t, subs, 1, "Must generate one subscription") - assert.Equal(t, "/market/ticker:all", subs[0].QualifiedChannel, "QualifiedChannel must be correct") + require.NoError(t, err, "generateSubscriptions must not error") + require.Len(t, subs, 1, "Must generate one subscription") + assert.Equal(t, "/market/ticker:all", subs[0].QualifiedChannel, "QualifiedChannel should be correct") err = ku.Subscribe(subs) - assert.NoError(t, err, "Subscribe to must not error") + assert.NoError(t, err, "Subscribe to should not error") } func TestSeedLocalCache(t *testing.T) { @@ -3957,7 +3990,7 @@ func TestGetCurrencyTradeURL(t *testing.T) { func testInstance(tb testing.TB) *Kucoin { tb.Helper() kucoin := new(Kucoin) - assert.NoError(tb, testexch.Setup(kucoin), "Test instance Setup must not error") + require.NoError(tb, testexch.Setup(kucoin), "Test instance Setup must not error") kucoin.obm = &orderbookManager{ state: make(map[currency.Code]map[currency.Code]map[asset.Item]*update), jobs: make(chan job, maxWSOrderbookJobs), diff --git a/exchanges/kucoin/kucoin_websocket.go b/exchanges/kucoin/kucoin_websocket.go index e364b2b84fc..a1c43bb68d5 100644 --- a/exchanges/kucoin/kucoin_websocket.go +++ b/exchanges/kucoin/kucoin_websocket.go @@ -1070,11 +1070,8 @@ func (ku *Kucoin) generateSubscriptions() (subscription.List, error) { func (ku *Kucoin) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) { return template.New("master.tmpl"). Funcs(template.FuncMap{ - "channelName": channelName, - "removeSpotFromMargin": func(s *subscription.Subscription, ap map[asset.Item]currency.Pairs) string { - spotPairs, _ := ku.GetEnabledPairs(asset.Spot) - return removeSpotFromMargin(s, ap, spotPairs) - }, + "channelName": channelName, + "mergeMarginPairs": ku.mergeMarginPairs, "isCurrencyChannel": isCurrencyChannel, "isSymbolChannel": isSymbolChannel, "channelInterval": channelInterval, @@ -1686,13 +1683,45 @@ func channelName(s *subscription.Subscription, a asset.Item) string { return s.Channel } -// removeSpotFromMargin removes spot pairs from margin pairs in the supplied AssetPairs map for subscriptions to non-margin endpoints -func removeSpotFromMargin(s *subscription.Subscription, ap map[asset.Item]currency.Pairs, spotPairs currency.Pairs) string { +// mergeMarginPairs merges margin pairs into spot pairs for shared subs (ticker, orderbook, etc) if Spot asset and sub are enabled, +// because Kucoin errors on duplicate pairs in separate subs, and doesn't have separate subs for spot and margin +func (ku *Kucoin) mergeMarginPairs(s *subscription.Subscription, ap map[asset.Item]currency.Pairs) string { if strings.HasPrefix(s.Channel, "/margin") { return "" } - if p, ok := ap[asset.Margin]; ok { - ap[asset.Margin] = p.Remove(spotPairs...) + wantKey := &subscription.IgnoringAssetKey{Subscription: s} + switch s.Asset { + case asset.All: + _, marginEnabled := ap[asset.Margin] + _, spotEnabled := ap[asset.Spot] + if marginEnabled && spotEnabled { + marginPairs, _ := ku.GetEnabledPairs(asset.Margin) + ap[asset.Spot] = common.SortStrings(ap[asset.Spot].Add(marginPairs...)) + ap[asset.Margin] = currency.Pairs{} + } + case asset.Spot: + // If there's a margin sub then we should merge the pairs into spot + hasMarginSub := slices.ContainsFunc(ku.Features.Subscriptions, func(sB *subscription.Subscription) bool { + if sB.Asset != asset.Margin && sB.Asset != asset.All { + return false + } + return wantKey.Match(&subscription.IgnoringAssetKey{Subscription: sB}) + }) + if hasMarginSub { + marginPairs, _ := ku.GetEnabledPairs(asset.Margin) + ap[asset.Spot] = common.SortStrings(ap[asset.Spot].Add(marginPairs...)) + } + case asset.Margin: + // If there's a spot sub, all margin pairs are already merged, so empty the margin pairs + hasSpotSub := slices.ContainsFunc(ku.Features.Subscriptions, func(sB *subscription.Subscription) bool { + if sB.Asset != asset.Spot && sB.Asset != asset.All { + return false + } + return wantKey.Match(&subscription.IgnoringAssetKey{Subscription: sB}) + }) + if hasSpotSub { + ap[asset.Margin] = currency.Pairs{} + } } return "" } @@ -1749,27 +1778,27 @@ func joinPairsWithInterval(b currency.Pairs, s *subscription.Subscription) strin } const subTplText = ` -{{- removeSpotFromMargin $.S $.AssetPairs -}} +{{- mergeMarginPairs $.S $.AssetPairs }} {{- if isCurrencyChannel $.S }} - {{ channelName $.S $.S.Asset -}} : {{- (assetCurrencies $.S $.AssetPairs).Join -}} + {{- channelName $.S $.S.Asset -}} : {{- (assetCurrencies $.S $.AssetPairs).Join }} {{- else if isSymbolChannel $.S }} - {{ range $asset, $pairs := $.AssetPairs }} + {{- range $asset, $pairs := $.AssetPairs }} {{- with $name := channelName $.S $asset }} - {{- if and (eq $name "/market/ticker") (gt (len $pairs) 10) -}} + {{- if and (eq $name "/market/ticker") (gt (len $pairs) 10) }} {{- $name -}} :all - {{- with $i := channelInterval $.S -}}_{{- $i -}}{{- end -}} - {{- $.BatchSize -}} {{ len $pairs }} - {{- else -}} - {{- range $b := batch $pairs 100 -}} - {{- $name -}} : {{- joinPairsWithInterval $b $.S -}} - {{ $.PairSeparator }} - {{- end -}} + {{- with $i := channelInterval $.S }}_{{ $i }}{{ end }} + {{- $.BatchSize }} {{- len $pairs }} + {{- else }} + {{- range $b := batch $pairs 100 }} + {{- $name -}} : {{- joinPairsWithInterval $b $.S }} + {{- $.PairSeparator }} + {{- end }} {{- $.BatchSize -}} 100 {{- end }} {{- end }} - {{ $.AssetSeparator }} + {{- $.AssetSeparator }} {{- end }} -{{- else -}} - {{ channelName $.S $.S.Asset }} +{{- else }} + {{- channelName $.S $.S.Asset }} {{- end }} `