Kucoin: Margin subscription fix and improvements (#1761)

As a GCT user with spot and margin assets enabled, but only margin asset enabled websocket subscriptions,
I should still get subscriptions for all the pairs in margin which are also in spot
Currently it only works when spot subscriptions are enabled. Otherwise the spot pairs are ignored.

Fixes #1755
This commit is contained in:
Gareth Kirwan
2025-01-10 03:38:56 +00:00
committed by GitHub
parent 1efd8e0db0
commit e4479557de
6 changed files with 184 additions and 116 deletions

View File

@@ -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 |

View File

@@ -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

View File

@@ -1,22 +1,22 @@
# GoCryptoTrader package Currency state manager
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![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
<img src="/common/gctlogo.png?raw=true" width="350px" height="350px" hspace="70">
[![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.
## 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
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
## Donations
<img src="https://github.com/thrasher-corp/gocryptotrader/blob/master/web/src/assets/donate.png?raw=true" hspace="70">
If this framework helped you in any way, or you would like to support the developers working on it, please donate Bitcoin to:
***bc1qk0jareu4jytc0cfrhr5wgshsq8282awpavfahc***

View File

@@ -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

View File

@@ -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(),
{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.Margin, Pairs: subPairs[6:8], QualifiedChannel: "/spotMarket/level2Depth5:" + subPairs[6:8].Join(),
{Channel: subscription.OrderbookChannel, Asset: asset.Futures, Pairs: pairs["futures"], QualifiedChannel: "/contractMarket/level2Depth5:" + pairs["futures"].Join(),
Interval: kline.HundredMilliseconds},
{Channel: subscription.OrderbookChannel, Asset: asset.Futures, Pairs: subPairs[8:], QualifiedChannel: "/contractMarket/level2Depth5:" + subPairs[8:].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),

View File

@@ -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 }}
`