Skip to content

Commit

Permalink
Kucoin: Subscription configuration
Browse files Browse the repository at this point in the history
* Simplify GenerateDefaultSubs
* Improve TestGenSubs coverage
* Test Candle Sub generation
* Support Candle intervals
* Full responsibility for formatting Channel name on GenerateDefaultSubs
  OR consumer of Subscribe
* Simplify generatePayloads as a result
  • Loading branch information
gbjk committed Nov 8, 2023
1 parent a9393e2 commit 5495fcf
Show file tree
Hide file tree
Showing 6 changed files with 328 additions and 378 deletions.
8 changes: 8 additions & 0 deletions currency/currencies.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ func NewCurrenciesFromStringArray(currencies []string) Currencies {
// Currencies define a range of supported currency codes
type Currencies []Code

// Add adds a currency to the list if it doesn't exist
func (c Currencies) Add(a Code) Currencies {
if !c.Contains(a) {
c = append(c, a)
}
return c
}

// Strings returns an array of currency strings
func (c Currencies) Strings() []string {
list := make([]string, len(c))
Expand Down
12 changes: 12 additions & 0 deletions currency/currencies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package currency
import (
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"
)

func TestCurrenciesUnmarshalJSON(t *testing.T) {
Expand Down Expand Up @@ -62,3 +64,13 @@ func TestMatch(t *testing.T) {
t.Fatal("should not match")
}
}

func TestCurrenciesAdd(t *testing.T) {
c := Currencies{}
c = c.Add(BTC)
assert.Len(t, c, 1, "Should have one currency")
c = c.Add(ETH)
assert.Len(t, c, 2, "Should have one currency")
c = c.Add(BTC)
assert.Len(t, c, 2, "Adding a duplicate should not change anything")
}
148 changes: 129 additions & 19 deletions exchanges/kucoin/kucoin_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"errors"
"log"
"os"
"strings"
"testing"
"time"

Expand All @@ -24,6 +25,7 @@ import (
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues"
"github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/portfolio/withdraw"
)

Expand Down Expand Up @@ -1922,7 +1924,7 @@ var websocketPushDatas = map[string]string{
"SymbolTickerPushDataJSON": `{"type": "message","topic": "/market/ticker:FET-BTC","subject": "trade.ticker","data": {"bestAsk": "0.000018679","bestAskSize": "258.4609","bestBid": "0.000018622","bestBidSize": "68.5961","price": "0.000018628","sequence": "38509148","size": "8.943","time": 1677321643926}}`,
"AllSymbolsTickerPushDataJSON": `{"type": "message","topic": "/market/ticker:all","subject": "FTM-ETH","data": {"bestAsk": "0.0002901","bestAskSize": "3514.4978","bestBid": "0.0002894","bestBidSize": "65.536","price": "0.0002894","sequence": "186911324","size": "150","time": 1677320967673}}`,
"MarketTradeSnapshotPushDataJSON": `{"type": "message","topic": "/market/snapshot:BTC","subject": "trade.snapshot","data": {"sequence": "5701753771","data": {"averagePrice": 21736.73225440,"baseCurrency": "BTC","board": 1,"buy": 21423,"changePrice": -556.80000000000000000000,"changeRate": -0.0253,"close": 21423.1,"datetime": 1676310802092,"high": 22030.70000000000000000000,"lastTradedPrice": 21423.1,"low": 21407.00000000000000000000,"makerCoefficient": 1.000000,"makerFeeRate": 0.001,"marginTrade": true,"mark": 0,"market": "USDS","markets": ["USDS"],"open": 21979.90000000000000000000,"quoteCurrency": "USDT","sell": 21423.1,"sort": 100,"symbol": "BTC-USDT","symbolCode": "BTC-USDT","takerCoefficient": 1.000000,"takerFeeRate": 0.001,"trading": true,"vol": 6179.80570155000000000000,"volValue": 133988049.45570351500000000000}}}`,
"Orderbook Level 2 PushDataJSON": `{"type": "message","topic": "/spotMarket/level2Depth5:ETH-USDT","subject": "level2","data": {"asks": [[ "21612.7", "0.32307467"],[ "21613.1", "0.1581911"],[ "21613.2", "1.37156153"],[ "21613.3", "2.58327302"],[ "21613.4", "0.00302088"]],"bids": [[ "21612.6", "2.34316818"],[ "21612.3", "0.5771615"],[ "21612.2", "0.21605964"],[ "21612.1", "0.22894841"],[ "21611.6", "0.29251003"]],"timestamp": 1676319909635}}`,
"Orderbook Level 2 PushDataJSON": `{"type": "message","topic": "/spotMarket/level2Depth5:ETH-USDT","subject": "level2","data": {"asks": [["21612.7","0.32307467"],["21613.1","0.1581911"],["21613.2","1.37156153"],["21613.3","2.58327302"],["21613.4","0.00302088"]],"bids": [["21612.6","2.34316818"],["21612.3","0.5771615"],["21612.2","0.21605964"],["21612.1","0.22894841"],["21611.6","0.29251003"]],"timestamp": 1676319909635}}`,
"TradeCandlesUpdatePushDataJSON": `{"type":"message","topic":"/market/candles:BTC-USDT_1hour","subject":"trade.candles.update","data":{"symbol":"BTC-USDT","candles":["1589968800","9786.9","9740.8","9806.1","9732","27.45649579","268280.09830877"],"time":1589970010253893337}}`,
"SymbolSnapshotPushDataJSON": `{"type": "message","topic": "/market/snapshot:KCS-BTC","subject": "trade.snapshot","data": {"sequence": "1545896669291","data": {"trading": true,"symbol": "KCS-BTC","buy": 0.00011,"sell": 0.00012, "sort": 100, "volValue": 3.13851792584, "baseCurrency": "KCS", "market": "BTC", "quoteCurrency": "BTC", "symbolCode": "KCS-BTC", "datetime": 1548388122031, "high": 0.00013, "vol": 27514.34842, "low": 0.0001, "changePrice": -1.0e-5, "changeRate": -0.0769, "lastTradedPrice": 0.00012, "board": 0, "mark": 0 } }}`,
"MatchExecutionPushDataJSON": `{"type":"message","topic":"/market/match:BTC-USDT","subject":"trade.l3match","data":{"sequence":"1545896669145","type":"match","symbol":"BTC-USDT","side":"buy","price":"0.08200000000000000000","size":"0.01022222000000000000","tradeId":"5c24c5da03aa673885cd67aa","takerOrderId":"5c24c5d903aa6772d55b371e","makerOrderId":"5c2187d003aa677bd09d5c93","time":"1545913818099033203"}}`,
Expand All @@ -1945,7 +1947,7 @@ var websocketPushDatas = map[string]string{
"Public Futures TickerV1PushDataJSON": `{"subject": "ticker","topic": "/contractMarket/ticker:ETHUSDCM","data": {"symbol": "ETHUSDCM","sequence": 45,"side": "sell","price": 3600.00,"size": 16,"tradeId": "5c9dcf4170744d6f5a3d32fb","bestBidSize": 795,"bestBidPrice": 3200.00,"bestAskPrice": 3600.00,"bestAskSize": 284,"ts": 1553846081210004941}}`,
"Public Futures Level2OrderbookPushDataJSON": `{"subject": "level2", "topic": "/contractMarket/level2:ETHUSDCM", "type": "message", "data": { "sequence": 18, "change": "5000.0,sell,83","timestamp": 1551770400000}}`,
"Public Futures ExecutionDataJSON": `{"type": "message","topic": "/contractMarket/execution:ETHUSDCM","subject": "match","data": {"makerUserId": "6287c3015c27f000017d0c2f","symbol": "ETHUSDCM","sequence": 31443494,"side": "buy","size": 35,"price": 23083.00000000,"takerOrderId": "63f94040839d00000193264b","makerOrderId": "63f94036839d0000019310c3","takerUserId": "6133f817230d8d000607b941","tradeId": "63f940400000650065f4996f","ts": 1677279296134648869}}`,
"PublicFuturesOrderbookWithDepth5PushDataJSON": `{ "type": "message", "topic": "/contractMarket/level2Depth5:ETHUSDCM", "subject": "level2", "data": { "sequence": 1672332328701, "asks": [[ 23149, 13703],[ 23150, 1460],[ 23151.00000000, 941],[ 23152, 4591],[ 23153, 4107] ], "bids": [[ 23148.00000000, 22801],[23147.0,4766],[ 23146, 1388],[ 23145.00000000, 2593],[ 23144.00000000, 6286] ], "ts": 1677280435684, "timestamp": 1677280435684 }}`,
"PublicFuturesOrderbookWithDepth5PushDataJSON": `{ "type": "message", "topic": "/contractMarket/level2Depth5:ETHUSDCM", "subject": "level2", "data": { "sequence": 1672332328701, "asks": [[23149,13703],[23150,1460],[23151.00000000,941],[23152,4591],[23153,4107] ], "bids": [[23148.00000000,22801],[23147.0,4766],[23146,1388],[23145.00000000,2593],[23144.00000000,6286] ], "ts": 1677280435684, "timestamp": 1677280435684 }}`,
"Private PositionSettlementPushDataJSON": `{"userId": "xbc453tg732eba53a88ggyt8c","topic": "/contract/position:ETHUSDCM","subject": "position.settlement","data": {"fundingTime": 1551770400000,"qty": 100,"markPrice": 3610.85,"fundingRate": -0.002966,"fundingFee": -296,"ts": 1547697294838004923,"settleCurrency": "XBT"}}`,
"Futures PositionChangePushDataJSON": `{ "userId": "5cd3f1a7b7ebc19ae9558591","topic": "/contract/position:ETHUSDCM", "subject": "position.change", "data": {"markPrice": 7947.83,"markValue": 0.00251640,"maintMargin": 0.00252044,"realLeverage": 10.06,"unrealisedPnl": -0.00014735,"unrealisedRoePcnt": -0.0553,"unrealisedPnlPcnt": -0.0553,"delevPercentage": 0.52,"currentTimestamp": 1558087175068,"settleCurrency": "XBT"}}`,
"Futures PositionChangeWithChangeReasonPushDataJSON": `{ "type": "message","userId": "5c32d69203aa676ce4b543c7","channelType": "private","topic": "/contract/position:ETHUSDCM", "subject": "position.change", "data": {"realisedGrossPnl": 0E-8,"symbol":"ETHUSDCM","crossMode": false,"liquidationPrice": 1000000.0,"posLoss": 0E-8,"avgEntryPrice": 7508.22,"unrealisedPnl": -0.00014735,"markPrice": 7947.83,"posMargin": 0.00266779,"autoDeposit": false,"riskLimit": 100000,"unrealisedCost": 0.00266375,"posComm": 0.00000392,"posMaint": 0.00001724,"posCost": 0.00266375,"maintMarginReq": 0.005,"bankruptPrice": 1000000.0,"realisedCost": 0.00000271,"markValue": 0.00251640,"posInit": 0.00266375,"realisedPnl": -0.00000253,"maintMargin": 0.00252044,"realLeverage": 1.06,"changeReason": "positionChange","currentCost": 0.00266375,"openingTimestamp": 1558433191000,"currentQty": -20,"delevPercentage": 0.52,"currentComm": 0.00000271,"realisedGrossCost": 0E-8,"isOpen": true,"posCross": 1.2E-7,"currentTimestamp": 1558506060394,"unrealisedRoePcnt": -0.0553,"unrealisedPnlPcnt": -0.0553,"settleCurrency": "XBT"}}`,
Expand All @@ -1971,10 +1973,133 @@ func TestPushData(t *testing.T) {
}
}

func verifySubs(tb testing.TB, subs []subscription.Subscription, a asset.Item, prefix string, expected ...string) {
tb.Helper()
var sub *subscription.Subscription
for i, s := range subs {
if s.Asset == a && strings.HasPrefix(s.Channel, prefix) {
if len(expected) == 1 && !strings.Contains(s.Channel, expected[0]) {
continue
}
if sub != nil {
assert.Failf(tb, "Too many subs with prefix", "Asset %s; Prefix %s", a.String(), prefix)
return
}
sub = &subs[i]
}
}
if assert.NotNil(tb, sub, "Should find a sub for asset %s with prefix %s", a.String(), prefix) {
suffix := strings.TrimPrefix(sub.Channel, prefix)
if len(expected) == 0 {
assert.Empty(tb, suffix, "Sub for asset %s with prefix %s should have no symbol suffix", a.String(), prefix)
} else {
currs := strings.Split(suffix, ",")
assert.ElementsMatch(tb, currs, expected, "Currencies should match in sub for asset %s with prefix %s", a.String(), prefix)
}
}
}

func TestGenerateDefaultSubscriptions(t *testing.T) {
t.Parallel()
if _, err := ku.GenerateDefaultSubscriptions(); err != nil {
t.Error(err)

subs, err := ku.GenerateDefaultSubscriptions()
assert.NoError(t, err, "GenerateDefaultSubscriptions should not error")

if assert.Len(t, subs, 12, "Should generate the correct number of subs when not logged in") {
for _, p := range []string{"ticker", "match", "level2"} {
verifySubs(t, subs, asset.Spot, "/market/"+p+":", "BTC-USDT", "ETH-USDT", "LTC-USDT", "AVA-USDT")
verifySubs(t, subs, asset.Margin, "/market/"+p+":", "FET-BTC", "FET-ETH", "ANKR-BTC")
}
for _, c := range []string{"ETHUSDCM", "XBTUSDCM", "SOLUSDTM"} {
verifySubs(t, subs, asset.Futures, "/contractMarket/tickerV2:", c)
verifySubs(t, subs, asset.Futures, "/contractMarket/level2Depth50:", c)
}
}
}

func TestGenerateAuthSubscriptions(t *testing.T) {
t.Parallel()

// Create a parallel safe Kucoin to mess with
nu := new(Kucoin)
nu.Base.Features = ku.Base.Features
assert.NoError(t, nu.CurrencyPairs.Load(&ku.CurrencyPairs), "Loading Pairs should not error")
nu.Websocket = sharedtestvalues.NewTestWebsocket()
nu.Websocket.SetCanUseAuthenticatedEndpoints(true)

subs, err := nu.GenerateDefaultSubscriptions()
assert.NoError(t, err, "GenerateDefaultSubscriptions with Auth should not error")

if assert.Len(t, subs, 25, "Should generate the correct number of subs when logged in") {
for _, p := range []string{"ticker", "match", "level2"} {
verifySubs(t, subs, asset.Spot, "/market/"+p+":", "BTC-USDT", "ETH-USDT", "LTC-USDT", "AVA-USDT")
verifySubs(t, subs, asset.Margin, "/market/"+p+":", "FET-BTC", "FET-ETH", "ANKR-BTC")
}
for _, c := range []string{"ETHUSDCM", "XBTUSDCM", "SOLUSDTM"} {
verifySubs(t, subs, asset.Futures, "/contractMarket/tickerV2:", c)
verifySubs(t, subs, asset.Futures, "/contractMarket/level2Depth50:", c)
}
for _, c := range []string{"AVA", "FET", "BTC", "ETH", "ANKR", "LTC", "USDT"} {
verifySubs(t, subs, asset.Margin, "/margin/loan:", c)
}
verifySubs(t, subs, asset.Spot, "/account/balance")
verifySubs(t, subs, asset.Margin, "/margin/position")
verifySubs(t, subs, asset.Margin, "/margin/fundingBook:", "AVA", "FET", "BTC", "ETH", "ANKR", "LTC", "USDT")
verifySubs(t, subs, asset.Futures, "/contractAccount/wallet")
verifySubs(t, subs, asset.Futures, "/contractMarket/advancedOrders")
verifySubs(t, subs, asset.Futures, "/contractMarket/tradeOrders")
}
}

func TestGenerateCandleSubscription(t *testing.T) {
t.Parallel()

// Create a parallel safe Kucoin to mess with
nu := new(Kucoin)
nu.Base.Features = ku.Base.Features
nu.Websocket = sharedtestvalues.NewTestWebsocket()
assert.NoError(t, nu.CurrencyPairs.Load(&ku.CurrencyPairs), "Loading Pairs should not error")

nu.Features.Enabled.Subscriptions = []*subscription.Subscription{
{Channel: subscription.CandlesChannel, Interval: kline.FourHour},
}

subs, err := nu.GenerateDefaultSubscriptions()
assert.NoError(t, err, "GenerateDefaultSubscriptions with Candles should not error")

if assert.Len(t, subs, 7, "Should generate the correct number of subs for candles") {
for _, c := range []string{"BTC-USDT", "ETH-USDT", "LTC-USDT", "AVA-USDT"} {
verifySubs(t, subs, asset.Spot, "/market/candles:", c+"_4hour")
}
for _, c := range []string{"FET-BTC", "FET-ETH", "ANKR-BTC"} {
verifySubs(t, subs, asset.Margin, "/market/candles:", c+"_4hour")
}
}
}

func TestGenerateMarketSubscription(t *testing.T) {
t.Parallel()

// Create a parallel safe Kucoin to mess with
nu := new(Kucoin)
nu.Base.Features = ku.Base.Features
nu.Websocket = sharedtestvalues.NewTestWebsocket()
assert.NoError(t, nu.CurrencyPairs.Load(&ku.CurrencyPairs), "Loading Pairs should not error")

nu.Features.Enabled.Subscriptions = []*subscription.Subscription{
{Channel: marketSnapshotChannel},
}

subs, err := nu.GenerateDefaultSubscriptions()
assert.NoError(t, err, "GenerateDefaultSubscriptions with MarketSnapshot should not error")

if assert.Len(t, subs, 7, "Should generate the correct number of subs for snapshot") {
for _, c := range []string{"AVA", "BTC", "ETH", "LTC", "USDT"} {
verifySubs(t, subs, asset.Spot, "/market/snapshot:", c)
}
for _, c := range []string{"FET", "ANKR"} {
verifySubs(t, subs, asset.Margin, "/market/snapshot:", c)
}
}
}

Expand Down Expand Up @@ -2152,21 +2277,6 @@ func TestCancelAllOrders(t *testing.T) {
}
}

func TestGeneratePayloads(t *testing.T) {
t.Parallel()
subscriptions, err := ku.GenerateDefaultSubscriptions()
if err != nil {
t.Error(err)
}
payload, err := ku.generatePayloads(subscriptions, "subscribe")
if err != nil {
t.Error(err)
}
if len(payload) != len(subscriptions) {
t.Error("derived payload is not same as generated channel subscription instances")
}
}

const (
subUserResponseJSON = `{"userId":"635002438793b80001dcc8b3", "uid":62356, "subName":"margin01", "status":2, "type":4, "access":"Margin", "createdAt":1666187844000, "remarks":null }`
positionSettlementPushData = `{"userId": "xbc453tg732eba53a88ggyt8c", "topic": "/contract/position:XBTUSDM", "subject": "position.settlement", "data": { "fundingTime": 1551770400000, "qty": 100, "markPrice": 3610.85, "fundingRate": -0.002966, "fundingFee": -296, "ts": 1547697294838004923, "settleCurrency": "XBT" } }`
Expand Down
Loading

0 comments on commit 5495fcf

Please sign in to comment.