From 4fcee8489e7c58a871f86f045e150ffcf33b3e09 Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Thu, 2 Jan 2025 21:55:58 +0000 Subject: [PATCH 01/11] Deribit: Add subscription configuration (#1636) * Kline: Fix Raw Short, Marshal and Unmarshal * Deribit: Rename GenerateDefaultSubs * Deribit: Remove custom GetDefaultConfig Moved to exchange base by #1472 * Deribit: Straight Rename of eps to endpoints Since I had to ask what this abbreviation meant, I think we should abandon it * Deribit: Add Subscription configuration * Deribit: Fix race on Setup with optionsRegex Calling Setup twice would race on the assignment to this package var. There was an option to just move the assignment to the package var declaration, but this change improves the performance and allocations: ``` BenchmarkOptionPairToString-8 1000000 1239 ns/op 485 B/op 10 allocs/op BenchmarkOptionPairToString2-8 3473804 656.2 ns/op 348 B/op 7 allocs/op ``` I've also removed the t.Run because even success the -v output from tests would be very noisy, and I don't think we were getting any benefit from it at all: ``` === RUN TestOptionPairToString === PAUSE TestOptionPairToString === CONT TestOptionPairToString === RUN TestOptionPairToString/BTC-30MAY24-61000-C === PAUSE TestOptionPairToString/BTC-30MAY24-61000-C === RUN TestOptionPairToString/ETH-1JUN24-3200-P === PAUSE TestOptionPairToString/ETH-1JUN24-3200-P === RUN TestOptionPairToString/SOL_USDC-31MAY24-162-P === PAUSE TestOptionPairToString/SOL_USDC-31MAY24-162-P === RUN TestOptionPairToString/MATIC_USDC-6APR24-0d98-P === PAUSE TestOptionPairToString/MATIC_USDC-6APR24-0d98-P === CONT TestOptionPairToString/BTC-30MAY24-61000-C === CONT TestOptionPairToString/SOL_USDC-31MAY24-162-P === CONT TestOptionPairToString/ETH-1JUN24-3200-P === CONT TestOptionPairToString/MATIC_USDC-6APR24-0d98-P --- PASS: TestOptionPairToString (0.00s) --- PASS: TestOptionPairToString/BTC-30MAY24-61000-C (0.00s) --- PASS: TestOptionPairToString/ETH-1JUN24-3200-P (0.00s) --- PASS: TestOptionPairToString/SOL_USDC-31MAY24-162-P (0.00s) --- PASS: TestOptionPairToString/MATIC_USDC-6APR24-0d98-P (0.00s) ``` ( And that got worse with me adding more tests ) --- .../exchanges_templates/deribit.tmpl | 21 +- exchanges/deribit/README.md | 21 +- exchanges/deribit/deribit.go | 33 +- exchanges/deribit/deribit_test.go | 92 ++- exchanges/deribit/deribit_websocket.go | 607 +++++------------- exchanges/deribit/deribit_wrapper.go | 28 +- ...bsocket_eps.go => deribit_ws_endpoints.go} | 0 exchanges/kline/kline.go | 7 + exchanges/kline/kline_test.go | 27 +- 9 files changed, 308 insertions(+), 528 deletions(-) rename exchanges/deribit/{deribit_websocket_eps.go => deribit_ws_endpoints.go} (100%) diff --git a/cmd/documentation/exchanges_templates/deribit.tmpl b/cmd/documentation/exchanges_templates/deribit.tmpl index 9e04bfa9c36..1277ed1a3e7 100644 --- a/cmd/documentation/exchanges_templates/deribit.tmpl +++ b/cmd/documentation/exchanges_templates/deribit.tmpl @@ -93,12 +93,23 @@ if err != nil { } ``` -### How to do Websocket public/private calls +### Subscriptions -```go - // Exchanges will be abstracted out in further updates and examples will be - // supplied then -``` +All default subscriptions are for all enabled assets. + +Default Public Subscriptions: +- Candles ( Timeframe: 1 day ) +- Orderbook ( Full depth @ Interval: 100ms ) +- Ticker ( Interval: 100ms ) +- All Trades ( Interval: 100ms ) + +Default Authenticated Subscriptions: +- My Account Orders +- My Account Trades + +kline.Raw Interval configurable for a raw orderbook subscription when authenticated + +Subscriptions are subject to enabled assets and pairs. ### Please click GoDocs chevron above to view current GoDoc information for this package {{template "contributions"}} diff --git a/exchanges/deribit/README.md b/exchanges/deribit/README.md index 57e04fdcaee..3e6595258f7 100644 --- a/exchanges/deribit/README.md +++ b/exchanges/deribit/README.md @@ -111,12 +111,23 @@ if err != nil { } ``` -### How to do Websocket public/private calls +### Subscriptions -```go - // Exchanges will be abstracted out in further updates and examples will be - // supplied then -``` +All default subscriptions are for all enabled assets. + +Default Public Subscriptions: +- Candles ( Timeframe: 1 day ) +- Orderbook ( Full depth @ Interval: 100ms ) +- Ticker ( Interval: 100ms ) +- All Trades ( Interval: 100ms ) + +Default Authenticated Subscriptions: +- My Account Orders +- My Account Trades + +kline.Raw Interval configurable for a raw orderbook subscription when authenticated + +Subscriptions are subject to enabled assets and pairs. ### Please click GoDocs chevron above to view current GoDoc information for this package diff --git a/exchanges/deribit/deribit.go b/exchanges/deribit/deribit.go index f4787df6be4..c13c24fcef1 100644 --- a/exchanges/deribit/deribit.go +++ b/exchanges/deribit/deribit.go @@ -7,7 +7,6 @@ import ( "fmt" "net/http" "net/url" - "regexp" "strconv" "strings" "time" @@ -28,12 +27,6 @@ type Deribit struct { exchange.Base } -var ( - // optionRegex compiles optionDecimalRegex at startup and is used to help set - // option currency lower-case d eg MATIC-USDC-3JUN24-0d64-P - optionRegex *regexp.Regexp -) - const ( deribitAPIVersion = "/api/v2" tradeBaseURL = "https://www.deribit.com/" @@ -43,8 +36,7 @@ const ( tradeFuturesCombo = "futures-spreads/" tradeOptionsCombo = "combos/" - perpString = "PERPETUAL" - optionDecimalRegex = `\d+(D)\d+` + perpString = "PERPETUAL" // Public endpoints // Market Data @@ -2837,20 +2829,19 @@ func (d *Deribit) formatFuturesTradablePair(pair currency.Pair) string { return instrumentID } -// optionPairToString to format and return an Options currency pairs with the following format: MATIC_USDC-6APR24-0d98-P -// it has both uppercase or lowercase characters, which we can not achieve with the Upper=true or Upper=false +// optionPairToString formats an options pair as: SYMBOL-EXPIRE-STRIKE-TYPE +// SYMBOL may be a currency (BTC) or a pair (XRP_USDC) +// EXPIRE is DDMMMYY +// STRIKE may include a d for decimal point in linear options +// TYPE is Call or Put func (d *Deribit) optionPairToString(pair currency.Pair) string { - subCodes := strings.Split(pair.Quote.String(), currency.DashDelimiter) initialDelimiter := currency.DashDelimiter - if subCodes[0] == "USDC" { + q := pair.Quote.String() + if strings.HasPrefix(q, "USDC") && len(q) > 11 { // Linear option initialDelimiter = currency.UnderscoreDelimiter + // Replace a capital D with d for decimal place in Strike price + // Char 11 is either the hyphen before Strike price or first digit + q = q[:11] + strings.Replace(q[11:], "D", "d", -1) } - for i := range subCodes { - if match := optionRegex.MatchString(subCodes[i]); match { - subCodes[i] = strings.ToLower(subCodes[i]) - break - } - } - - return pair.Base.String() + initialDelimiter + strings.Join(subCodes, currency.DashDelimiter) + return pair.Base.String() + initialDelimiter + q } diff --git a/exchanges/deribit/deribit_test.go b/exchanges/deribit/deribit_test.go index 84b92c7b2d2..51a505b337f 100644 --- a/exchanges/deribit/deribit_test.go +++ b/exchanges/deribit/deribit_test.go @@ -25,9 +25,11 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" + "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange" + testsubs "github.com/thrasher-corp/gocryptotrader/internal/testing/subscriptions" "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) @@ -3266,11 +3268,82 @@ func setupWs() { } } -func TestGenerateDefaultSubscriptions(t *testing.T) { +func TestGenerateSubscriptions(t *testing.T) { t.Parallel() - result, err := d.GenerateDefaultSubscriptions() + + d := new(Deribit) //nolint:govet // Intentional lexical scope shadow + require.NoError(t, testexch.Setup(d), "Test instance Setup must not error") + + d.Websocket.SetCanUseAuthenticatedEndpoints(true) + subs, err := d.generateSubscriptions() require.NoError(t, err) - assert.NotNil(t, result) + exp := subscription.List{} + for _, s := range d.Features.Subscriptions { + for _, a := range d.GetAssetTypes(true) { + if !d.IsAssetWebsocketSupported(a) { + continue + } + pairs, err := d.GetEnabledPairs(a) + require.NoErrorf(t, err, "GetEnabledPairs %s must not error", a) + s := s.Clone() //nolint:govet // Intentional lexical scope shadow + s.Asset = a + if isSymbolChannel(s) { + for i, p := range pairs { + s := s.Clone() //nolint:govet // Intentional lexical scope shadow + s.QualifiedChannel = channelName(s) + "." + p.String() + if s.Interval != 0 { + s.QualifiedChannel += "." + channelInterval(s) + } + s.Pairs = pairs[i : i+1] + exp = append(exp, s) + } + } else { + s.Pairs = pairs + s.QualifiedChannel = channelName(s) + exp = append(exp, s) + } + } + } + testsubs.EqualLists(t, exp, subs) +} + +func TestChannelInterval(t *testing.T) { + t.Parallel() + + for _, i := range []int64{1, 3, 5, 10, 15, 30, 60, 120, 180, 360, 720} { + a := channelInterval(&subscription.Subscription{Channel: subscription.CandlesChannel, Interval: kline.Interval(i * int64(time.Minute))}) + assert.Equal(t, strconv.Itoa(int(i)), a) + } + + a := channelInterval(&subscription.Subscription{Channel: subscription.CandlesChannel, Interval: kline.OneDay}) + assert.Equal(t, "1D", a) + + assert.Panics(t, func() { + channelInterval(&subscription.Subscription{Channel: subscription.CandlesChannel, Interval: kline.OneMonth}) + }) + + a = channelInterval(&subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.ThousandMilliseconds}) + assert.Equal(t, "agg2", a, "1 second should expand to agg2") + + a = channelInterval(&subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.HundredMilliseconds}) + assert.Equal(t, "100ms", a, "100ms should expand correctly") + + a = channelInterval(&subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.Raw}) + assert.Equal(t, "raw", a, "raw should expand correctly") + + assert.Panics(t, func() { + channelInterval(&subscription.Subscription{Channel: subscription.OrderbookChannel, Interval: kline.OneMonth}) + }) + + a = channelInterval(&subscription.Subscription{Channel: userAccessLogChannel}) + assert.Empty(t, a, "Anything else should return empty") +} + +func TestChannelName(t *testing.T) { + t.Parallel() + assert.Equal(t, tickerChannel, channelName(&subscription.Subscription{Channel: subscription.TickerChannel})) + assert.Equal(t, userLockChannel, channelName(&subscription.Subscription{Channel: userLockChannel})) + assert.Panics(t, func() { channelName(&subscription.Subscription{Channel: "wibble"}) }, "Unknown channels should panic") } func TestFetchTicker(t *testing.T) { @@ -3681,18 +3754,15 @@ func TestFormatFuturesTradablePair(t *testing.T) { func TestOptionPairToString(t *testing.T) { t.Parallel() - optionsList := map[currency.Pair]string{ + for pair, exp := range map[currency.Pair]string{ {Delimiter: currency.DashDelimiter, Base: currency.BTC, Quote: currency.NewCode("30MAY24-61000-C")}: "BTC-30MAY24-61000-C", {Delimiter: currency.DashDelimiter, Base: currency.ETH, Quote: currency.NewCode("1JUN24-3200-P")}: "ETH-1JUN24-3200-P", {Delimiter: currency.DashDelimiter, Base: currency.SOL, Quote: currency.NewCode("USDC-31MAY24-162-P")}: "SOL_USDC-31MAY24-162-P", {Delimiter: currency.DashDelimiter, Base: currency.MATIC, Quote: currency.NewCode("USDC-6APR24-0d98-P")}: "MATIC_USDC-6APR24-0d98-P", - } - for pair, instrumentID := range optionsList { - t.Run(instrumentID, func(t *testing.T) { - t.Parallel() - instrument := d.optionPairToString(pair) - require.Equal(t, instrumentID, instrument) - }) + {Delimiter: currency.DashDelimiter, Base: currency.MATIC, Quote: currency.NewCode("USDC-8JUN24-0D99-P")}: "MATIC_USDC-8JUN24-0d99-P", + {Delimiter: currency.DashDelimiter, Base: currency.MATIC, Quote: currency.NewCode("USDC-6DEC29-0D87-C")}: "MATIC_USDC-6DEC29-0d87-C", + } { + assert.Equal(t, exp, d.optionPairToString(pair), "optionPairToString should return correctly") } } diff --git a/exchanges/deribit/deribit_websocket.go b/exchanges/deribit/deribit_websocket.go index d99dc86293d..21e7088bb7b 100644 --- a/exchanges/deribit/deribit_websocket.go +++ b/exchanges/deribit/deribit_websocket.go @@ -3,10 +3,12 @@ package deribit import ( "context" "encoding/json" + "errors" "fmt" "net/http" "strconv" "strings" + "text/template" "time" "github.com/gorilla/websocket" @@ -50,33 +52,59 @@ const ( platformStatePublicMethodsStateChannel = "platform_state.public_methods_state" quoteChannel = "quote" requestForQuoteChannel = "rfq" - tickerChannel = "ticker." - tradesChannel = "trades." - tradesWithKindChannel = "trades" + tickerChannel = "ticker" + tradesChannel = "trades" // private websocket channels - userAccessLogChannel = "user.access_log" - userChangesInstrumentsChannel = "user.changes." - userChangesCurrencyChannel = "user.changes" - userLockChannel = "user.lock" - userMMPTriggerChannel = "user.mmp_trigger" - rawUserOrdersChannel = "user.orders.%s.raw" - userOrdersWithIntervalChannel = "user.orders." - rawUsersOrdersKindCurrencyChannel = "user.orders.%s.%s.raw" - rawUsersOrdersWithKindCurrencyAndIntervalChannel = "user.orders" - userPortfolioChannel = "user.portfolio" - userTradesChannelByInstrument = "user.trades." - userTradesByKindCurrencyAndIntervalChannel = "user.trades" + userAccessLogChannel = "user.access_log" + userChangesInstrumentsChannel = "user.changes." + userChangesCurrencyChannel = "user.changes" + userLockChannel = "user.lock" + userMMPTriggerChannel = "user.mmp_trigger" + userOrdersChannel = "user.orders" + userTradesChannel = "user.trades" + userPortfolioChannel = "user.portfolio" ) -var ( - defaultSubscriptions = []string{ - chartTradesChannel, // chart trades channel to fetch candlestick data. - orderbookChannel, - tickerChannel, - tradesWithKindChannel, - } +var subscriptionNames = map[string]string{ + subscription.TickerChannel: tickerChannel, + subscription.OrderbookChannel: orderbookChannel, + subscription.CandlesChannel: chartTradesChannel, + subscription.AllTradesChannel: tradesChannel, + subscription.MyTradesChannel: userTradesChannel, + subscription.MyOrdersChannel: userOrdersChannel, + announcementsChannel: announcementsChannel, + priceIndexChannel: priceIndexChannel, + priceRankingChannel: priceRankingChannel, + priceStatisticsChannel: priceStatisticsChannel, + volatilityIndexChannel: volatilityIndexChannel, + estimatedExpirationPriceChannel: estimatedExpirationPriceChannel, + incrementalTickerChannel: incrementalTickerChannel, + instrumentStateChannel: instrumentStateChannel, + markPriceOptionsChannel: markPriceOptionsChannel, + perpetualChannel: perpetualChannel, + platformStateChannel: platformStateChannel, + platformStatePublicMethodsStateChannel: platformStatePublicMethodsStateChannel, + quoteChannel: quoteChannel, + requestForQuoteChannel: requestForQuoteChannel, + userAccessLogChannel: userAccessLogChannel, + userChangesInstrumentsChannel: userChangesInstrumentsChannel, + userChangesCurrencyChannel: userChangesCurrencyChannel, + userLockChannel: userLockChannel, + userMMPTriggerChannel: userMMPTriggerChannel, + userPortfolioChannel: userPortfolioChannel, +} +var defaultSubscriptions = subscription.List{ + {Enabled: true, Asset: asset.All, Channel: subscription.CandlesChannel, Interval: kline.OneDay}, + {Enabled: true, Asset: asset.All, Channel: subscription.OrderbookChannel, Interval: kline.HundredMilliseconds}, // Raw is available for authenticated users + {Enabled: true, Asset: asset.All, Channel: subscription.TickerChannel, Interval: kline.HundredMilliseconds}, + {Enabled: true, Asset: asset.All, Channel: subscription.AllTradesChannel, Interval: kline.HundredMilliseconds}, + {Enabled: true, Asset: asset.All, Channel: subscription.MyOrdersChannel, Interval: kline.HundredMilliseconds, Authenticated: true}, + {Enabled: true, Asset: asset.All, Channel: subscription.MyTradesChannel, Interval: kline.HundredMilliseconds, Authenticated: true}, +} + +var ( indexENUMS = []string{"ada_usd", "algo_usd", "avax_usd", "bch_usd", "bnb_usd", "btc_usd", "doge_usd", "dot_usd", "eth_usd", "link_usd", "ltc_usd", "luna_usd", "matic_usd", "near_usd", "shib_usd", "sol_usd", "trx_usd", "uni_usd", "usdc_usd", "xrp_usd", "ada_usdc", "bch_usdc", "algo_usdc", "avax_usdc", "btc_usdc", "doge_usdc", "dot_usdc", "bch_usdc", "bnb_usdc", "eth_usdc", "link_usdc", "ltc_usdc", "luna_usdc", "matic_usdc", "near_usdc", "shib_usdc", "sol_usdc", "trx_usdc", "uni_usdc", "xrp_usdc", "btcdvol_usdc", "ethdvol_usdc"} pingMessage = WsSubscriptionInput{ @@ -740,446 +768,75 @@ func (d *Deribit) processOrderbook(respRaw []byte, channels []string) error { return nil } -// GenerateDefaultSubscriptions Adds default subscriptions to websocket to be handled by ManageSubscriptions() -func (d *Deribit) GenerateDefaultSubscriptions() (subscription.List, error) { - var subscriptions subscription.List - assets := d.GetAssetTypes(true) - subscriptionChannels := defaultSubscriptions - if d.Websocket.CanUseAuthenticatedEndpoints() { - subscriptionChannels = append( - subscriptionChannels, - - // authenticated subscriptions - rawUsersOrdersKindCurrencyChannel, - rawUsersOrdersWithKindCurrencyAndIntervalChannel, - userTradesByKindCurrencyAndIntervalChannel, - ) - } - var err error - assetPairs := make(map[asset.Item][]currency.Pair, len(assets)) - for _, a := range assets { - assetPairs[a], err = d.GetEnabledPairs(a) - if err != nil { - return nil, err - } - if len(assetPairs[a]) > 5 { - assetPairs[a] = assetPairs[a][:5] - } - } - for x := range subscriptionChannels { - switch subscriptionChannels[x] { - case chartTradesChannel: - for _, a := range assets { - for z := range assetPairs[a] { - if ((assetPairs[a][z].Quote.Upper().String() == perpString || - !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString)) && - a == asset.Futures) || (a != asset.Spot && a != asset.Futures) { - continue - } - subscriptions = append(subscriptions, - &subscription.Subscription{ - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{assetPairs[a][z]}, - Params: map[string]interface{}{ - "resolution": "1D", - }, - Asset: a, - }) - } - } - case incrementalTickerChannel, - quoteChannel, - rawUserOrdersChannel: - for _, a := range assets { - for z := range assetPairs[a] { - if ((assetPairs[a][z].Quote.Upper().String() == perpString || - !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString)) && - a == asset.Futures) || (a != asset.Spot && a != asset.Futures) { - continue - } - subscriptions = append(subscriptions, &subscription.Subscription{ - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{assetPairs[a][z]}, - Asset: a, - }) - } - } - case orderbookChannel: - for _, a := range assets { - for z := range assetPairs[a] { - if ((assetPairs[a][z].Quote.Upper().String() == perpString || - !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString)) && - a == asset.Futures) || (a != asset.Spot && a != asset.Futures) { - continue - } - subscriptions = append(subscriptions, - &subscription.Subscription{ - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{assetPairs[a][z]}, - // if needed, group and depth of orderbook can be passed as follow "group": "250", "depth": "20", - Interval: kline.HundredMilliseconds, - Asset: a, - Params: map[string]interface{}{ - "group": "none", - "depth": "10", - }, - }, - ) - if d.Websocket.CanUseAuthenticatedEndpoints() { - subscriptions = append(subscriptions, &subscription.Subscription{ - Channel: orderbookChannel, - Pairs: currency.Pairs{assetPairs[a][z]}, - Asset: a, - Interval: kline.Interval(0), - Params: map[string]interface{}{ - "group": "none", - "depth": "10", - }, - }) - } - } - } - case tickerChannel, - tradesChannel: - for _, a := range assets { - for z := range assetPairs[a] { - if ((assetPairs[a][z].Quote.Upper().String() != perpString && - !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString)) && - a == asset.Futures) || (a != asset.Spot && a != asset.Futures) { - continue - } - subscriptions = append(subscriptions, - &subscription.Subscription{ - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{assetPairs[a][z]}, - Interval: kline.HundredMilliseconds, - Asset: a, - }) - } - } - case perpetualChannel, - userChangesInstrumentsChannel, - userTradesChannelByInstrument: - for _, a := range assets { - for z := range assetPairs[a] { - if subscriptionChannels[x] == perpetualChannel && !strings.Contains(assetPairs[a][z].Quote.Upper().String(), perpString) { - continue - } - subscriptions = append(subscriptions, - &subscription.Subscription{ - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{assetPairs[a][z]}, - Interval: kline.HundredMilliseconds, - Asset: a, - }) - } - } - case instrumentStateChannel, - rawUsersOrdersKindCurrencyChannel: - var okay bool - for _, a := range assets { - currencyPairsName := make(map[currency.Code]bool, 2*len(assetPairs[a])) - for z := range assetPairs[a] { - if okay = currencyPairsName[assetPairs[a][z].Base]; !okay { - subscriptions = append(subscriptions, &subscription.Subscription{ - Asset: a, - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{currency.Pair{Base: assetPairs[a][z].Base}}, - }) - currencyPairsName[assetPairs[a][z].Base] = true - } - if okay = currencyPairsName[assetPairs[a][z].Quote]; !okay { - subscriptions = append(subscriptions, &subscription.Subscription{ - Asset: a, - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{currency.Pair{Base: assetPairs[a][z].Quote}}, - }) - currencyPairsName[assetPairs[a][z].Quote] = true - } - } - } - case userChangesCurrencyChannel, - userOrdersWithIntervalChannel, - rawUsersOrdersWithKindCurrencyAndIntervalChannel, - userTradesByKindCurrencyAndIntervalChannel, - tradesWithKindChannel: - for _, a := range assets { - currencyPairsName := make(map[currency.Code]bool, 2*len(assetPairs[a])) - var okay bool - for z := range assetPairs[a] { - if okay = currencyPairsName[assetPairs[a][z].Base]; !okay { - subscriptions = append(subscriptions, &subscription.Subscription{ - Asset: a, - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{currency.Pair{Base: assetPairs[a][z].Base}}, - Interval: kline.HundredMilliseconds, - }) - currencyPairsName[assetPairs[a][z].Base] = true - } - if okay = currencyPairsName[assetPairs[a][z].Quote]; !okay { - subscriptions = append(subscriptions, &subscription.Subscription{ - Asset: a, - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{currency.Pair{Base: assetPairs[a][z].Quote}}, - Interval: kline.HundredMilliseconds, - }) - currencyPairsName[assetPairs[a][z].Quote] = true - } - } - } - case requestForQuoteChannel, - userMMPTriggerChannel, - userPortfolioChannel: - for _, a := range assets { - currencyPairsName := make(map[currency.Code]bool, 2*len(assetPairs[a])) - var okay bool - for z := range assetPairs[a] { - if okay = currencyPairsName[assetPairs[a][z].Base]; !okay { - subscriptions = append(subscriptions, &subscription.Subscription{ - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{currency.Pair{Base: assetPairs[a][z].Base}}, - Asset: a, - }) - currencyPairsName[assetPairs[a][z].Base] = true - } - if okay = currencyPairsName[assetPairs[a][z].Quote]; !okay { - subscriptions = append(subscriptions, &subscription.Subscription{ - Channel: subscriptionChannels[x], - Pairs: currency.Pairs{currency.Pair{Base: assetPairs[a][z].Quote}}, - Asset: a, - }) - currencyPairsName[assetPairs[a][z].Quote] = true - } - } - } - case announcementsChannel, - userAccessLogChannel, - platformStateChannel, - userLockChannel, - platformStatePublicMethodsStateChannel: - subscriptions = append(subscriptions, &subscription.Subscription{ - Channel: subscriptionChannels[x], - }) - case priceIndexChannel, - priceRankingChannel, - priceStatisticsChannel, - volatilityIndexChannel, - markPriceOptionsChannel, - estimatedExpirationPriceChannel: - for i := range indexENUMS { - subscriptions = append(subscriptions, &subscription.Subscription{ - Channel: subscriptionChannels[x], - Params: map[string]interface{}{ - "index_name": indexENUMS[i], - }, - }) - } - } - } - return subscriptions, nil +// generateSubscriptions returns a list of configured subscriptions +func (d *Deribit) generateSubscriptions() (subscription.List, error) { + return d.Features.Subscriptions.ExpandTemplates(d) } -func (d *Deribit) generatePayloadFromSubscriptionInfos(operation string, subscs subscription.List) ([]WsSubscriptionInput, error) { - subscriptionPayloads := make([]WsSubscriptionInput, len(subscs)) - for x := range subscs { - if len(subscs[x].Pairs) > 1 { - return nil, subscription.ErrNotSinglePair - } - sub := WsSubscriptionInput{ - JSONRPCVersion: rpcVersion, - ID: d.Websocket.Conn.GenerateMessageID(false), - Method: "public/" + operation, - Params: map[string][]string{}, - } - switch subscs[x].Channel { - case userAccessLogChannel, userChangesInstrumentsChannel, userChangesCurrencyChannel, userLockChannel, userMMPTriggerChannel, rawUserOrdersChannel, - userOrdersWithIntervalChannel, rawUsersOrdersKindCurrencyChannel, userPortfolioChannel, userTradesChannelByInstrument, userTradesByKindCurrencyAndIntervalChannel: - if !d.Websocket.CanUseAuthenticatedEndpoints() { - continue - } - sub.Method = "private/" + operation - } - var instrumentID string - if len(subscs[x].Pairs) == 1 { - pairFormat, err := d.GetPairFormat(subscs[x].Asset, true) - if err != nil { - return nil, err - } - subscs[x].Pairs = subscs[x].Pairs.Format(pairFormat) - if subscs[x].Asset == asset.Futures { - instrumentID = d.formatFuturesTradablePair(subscs[x].Pairs[0]) - } else { - instrumentID = subscs[x].Pairs.Join() - } - } - switch subscs[x].Channel { - case announcementsChannel, - userAccessLogChannel, - platformStateChannel, - platformStatePublicMethodsStateChannel, - userLockChannel: - sub.Params["channels"] = []string{subscs[x].Channel} - case orderbookChannel: - if len(subscs[x].Pairs) != 1 { - return nil, currency.ErrCurrencyPairEmpty - } - intervalString, err := d.GetResolutionFromInterval(subscs[x].Interval) - if err != nil { - return nil, err - } - group, okay := subscs[x].Params["group"].(string) - if !okay { - sub.Params["channels"] = []string{orderbookChannel + "." + instrumentID + "." + intervalString} - break - } - depth, okay := subscs[x].Params["depth"].(string) - if !okay { - sub.Params["channels"] = []string{orderbookChannel + "." + instrumentID + "." + intervalString} - break - } - sub.Params["channels"] = []string{orderbookChannel + "." + instrumentID + "." + group + "." + depth + "." + intervalString} - case chartTradesChannel: - if len(subscs[x].Pairs) != 1 { - return nil, currency.ErrCurrencyPairEmpty - } - resolution, okay := subscs[x].Params["resolution"].(string) - if !okay { - resolution = "1D" - } - sub.Params["channels"] = []string{chartTradesChannel + "." + d.formatFuturesTradablePair(subscs[x].Pairs[0]) + "." + resolution} - case priceIndexChannel, - priceRankingChannel, - priceStatisticsChannel, - volatilityIndexChannel, - markPriceOptionsChannel, - estimatedExpirationPriceChannel: - indexName, okay := subscs[x].Params["index_name"].(string) - if !okay { - return nil, errUnsupportedIndexName - } - sub.Params["channels"] = []string{subscs[x].Channel + "." + indexName} - case instrumentStateChannel: - if len(subscs[x].Pairs) != 1 { - return nil, currency.ErrCurrencyPairEmpty - } - kind := d.GetAssetKind(subscs[x].Asset) - currencyCode := getValidatedCurrencyCode(subscs[x].Pairs[0]) - sub.Params["channels"] = []string{"instrument.state." + kind + "." + currencyCode} - case rawUsersOrdersKindCurrencyChannel: - if len(subscs[x].Pairs) != 1 { - return nil, currency.ErrCurrencyPairEmpty - } - kind := d.GetAssetKind(subscs[x].Asset) - currencyCode := getValidatedCurrencyCode(subscs[x].Pairs[0]) - sub.Params["channels"] = []string{"user.orders." + kind + "." + currencyCode + ".raw"} - case quoteChannel, - incrementalTickerChannel: - if len(subscs[x].Pairs) != 1 { - return nil, currency.ErrCurrencyPairEmpty - } - sub.Params["channels"] = []string{subscs[x].Channel + "." + instrumentID} - case rawUserOrdersChannel: - if len(subscs[x].Pairs) != 1 { - return nil, currency.ErrCurrencyPairEmpty - } - sub.Params["channels"] = []string{"user.orders." + instrumentID + ".raw"} - case requestForQuoteChannel, - userMMPTriggerChannel, - userPortfolioChannel: - if len(subscs[x].Pairs) != 1 { - return nil, currency.ErrCurrencyPairEmpty - } - currencyCode := getValidatedCurrencyCode(subscs[x].Pairs[0]) - sub.Params["channels"] = []string{subscs[x].Channel + "." + currencyCode} - case tradesChannel, - userChangesInstrumentsChannel, - userOrdersWithIntervalChannel, - tickerChannel, - perpetualChannel, - userTradesChannelByInstrument: - if len(subscs[x].Pairs) != 1 { - return nil, currency.ErrCurrencyPairEmpty - } - if subscs[x].Interval.Duration() == 0 { - sub.Params["channels"] = []string{subscs[x].Channel + instrumentID} - continue - } - intervalString, err := d.GetResolutionFromInterval(subscs[x].Interval) - if err != nil { - return nil, err - } - sub.Params["channels"] = []string{subscs[x].Channel + instrumentID + "." + intervalString} - case userChangesCurrencyChannel, - tradesWithKindChannel, - rawUsersOrdersWithKindCurrencyAndIntervalChannel, - userTradesByKindCurrencyAndIntervalChannel: - kind := d.GetAssetKind(subscs[x].Asset) - if len(subscs[x].Pairs) != 1 { - return nil, currency.ErrCurrencyPairEmpty - } - currencyCode := getValidatedCurrencyCode(subscs[x].Pairs[0]) - if subscs[x].Interval.Duration() == 0 { - sub.Params["channels"] = []string{subscs[x].Channel + "." + kind + "." + currencyCode} - continue - } - intervalString, err := d.GetResolutionFromInterval(subscs[x].Interval) - if err != nil { - return nil, err - } - sub.Params["channels"] = []string{subscs[x].Channel + "." + kind + "." + currencyCode + "." + intervalString} - default: - return nil, errUnsupportedChannel - } - subscriptionPayloads[x] = sub - } - return filterSubscriptionPayloads(subscriptionPayloads), nil +// GetSubscriptionTemplate returns a subscription channel template +func (d *Deribit) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) { + return template.New("master.tmpl").Funcs(template.FuncMap{ + "channelName": channelName, + "interval": channelInterval, + "isSymbolChannel": isSymbolChannel, + }). + Parse(subTplText) } // Subscribe sends a websocket message to receive data from the channel -func (d *Deribit) Subscribe(channelsToSubscribe subscription.List) error { - return d.handleSubscription("subscribe", channelsToSubscribe) +func (d *Deribit) Subscribe(subs subscription.List) error { + errs := d.handleSubscription("public/subscribe", subs.Public()) + return common.AppendError(errs, + d.handleSubscription("private/subscribe", subs.Private()), + ) } // Unsubscribe sends a websocket message to stop receiving data from the channel -func (d *Deribit) Unsubscribe(channelsToUnsubscribe subscription.List) error { - return d.handleSubscription("unsubscribe", channelsToUnsubscribe) +func (d *Deribit) Unsubscribe(subs subscription.List) error { + errs := d.handleSubscription("public/unsubscribe", subs.Public()) + return common.AppendError(errs, + d.handleSubscription("private/unsubscribe", subs.Private()), + ) } -func filterSubscriptionPayloads(subscription []WsSubscriptionInput) []WsSubscriptionInput { - newSubscriptionsMap := map[string]bool{} - newSubscs := make([]WsSubscriptionInput, 0, len(subscription)) - for x := range subscription { - if len(subscription[x].Params["channels"]) == 0 { - continue - } - if !newSubscriptionsMap[subscription[x].Params["channels"][0]] { - newSubscriptionsMap[subscription[x].Params["channels"][0]] = true - newSubscs = append(newSubscs, subscription[x]) - } +func (d *Deribit) handleSubscription(method string, subs subscription.List) error { + var err error + subs, err = subs.ExpandTemplates(d) + if err != nil || len(subs) == 0 { + return err } - return newSubscs -} - -func (d *Deribit) handleSubscription(operation string, channels subscription.List) error { - payloads, err := d.generatePayloadFromSubscriptionInfos(operation, channels) + r := WsSubscriptionInput{ + JSONRPCVersion: rpcVersion, + ID: d.Websocket.Conn.GenerateMessageID(false), + Method: method, + Params: map[string][]string{ + "channels": subs.QualifiedChannels(), + }, + } + data, err := d.Websocket.Conn.SendMessageReturnResponse(context.TODO(), request.Unset, r.ID, r) if err != nil { return err } - for x := range payloads { - data, err := d.Websocket.Conn.SendMessageReturnResponse(context.TODO(), request.Unset, payloads[x].ID, payloads[x]) - if err != nil { - return err - } - var response wsSubscriptionResponse - err = json.Unmarshal(data, &response) - if err != nil { - return fmt.Errorf("%v %v", d.Name, err) - } - if payloads[x].ID == response.ID && len(response.Result) == 0 { - log.Errorf(log.ExchangeSys, "subscription to channel %s was not successful", payloads[x].Params["channels"][0]) + var response wsSubscriptionResponse + err = json.Unmarshal(data, &response) + if err != nil { + return fmt.Errorf("%v %v", d.Name, err) + } + subAck := map[string]bool{} + for _, c := range response.Result { + subAck[c] = true + } + if len(subAck) != len(subs) { + err = common.ErrUnknownError + } + for _, s := range subs { + if _, ok := subAck[s.QualifiedChannel]; ok { + err = common.AppendError(err, d.Websocket.AddSuccessfulSubscriptions(d.Websocket.Conn, s)) + } else { + err = common.AppendError(err, errors.New(s.String())) } } - return nil + return err } func getValidatedCurrencyCode(pair currency.Pair) string { @@ -1199,3 +856,61 @@ func getValidatedCurrencyCode(pair currency.Pair) string { return "any" } } + +func channelName(s *subscription.Subscription) string { + if name, ok := subscriptionNames[s.Channel]; ok { + return name + } + panic(fmt.Errorf("%w: %s", subscription.ErrNotSupported, s.Channel)) +} + +// channelInterval converts an interval to an exchange specific interval +// We convert 1s to agg2; Docs do not explain agg2 but support explained that it may vary under load but is currently 1 second +func channelInterval(s *subscription.Subscription) string { + if s.Interval != 0 { + if channelName(s) == chartTradesChannel { + if s.Interval == kline.OneDay { + return "1D" + } + m := s.Interval.Duration().Minutes() + switch m { + case 1, 3, 5, 10, 15, 30, 60, 120, 180, 360, 720: // Valid Minute intervals + return strconv.Itoa(int(m)) + } + panic(fmt.Errorf("%w: %s", kline.ErrUnsupportedInterval, s.Interval)) + } + switch s.Interval { + case kline.ThousandMilliseconds: + return "agg2" + case kline.HundredMilliseconds, kline.Raw: + return s.Interval.Short() + } + panic(fmt.Errorf("%w: %s", kline.ErrUnsupportedInterval, s.Interval)) + } + return "" +} + +func isSymbolChannel(s *subscription.Subscription) bool { + switch channelName(s) { + case orderbookChannel, chartTradesChannel, tickerChannel, tradesChannel, perpetualChannel, quoteChannel, + userChangesInstrumentsChannel, incrementalTickerChannel, userOrdersChannel, userTradesChannel: + return true + } + return false +} + +const subTplText = ` +{{- if isSymbolChannel $.S -}} + {{- range $asset, $pairs := $.AssetPairs }} + {{- range $p := $pairs }} + {{- channelName $.S -}} . {{- $p }} + {{- with $i := interval $.S -}} . {{- $i }}{{ end }} + {{- $.PairSeparator }} + {{- end }} + {{- $.AssetSeparator }} + {{- end }} +{{- else }} + {{- channelName $.S -}} + {{- with $i := interval $.S -}} . {{- $i }}{{ end }} +{{- end }} +` diff --git a/exchanges/deribit/deribit_wrapper.go b/exchanges/deribit/deribit_wrapper.go index 38023875caa..c3bce491083 100644 --- a/exchanges/deribit/deribit_wrapper.go +++ b/exchanges/deribit/deribit_wrapper.go @@ -4,7 +4,6 @@ import ( "context" "errors" "fmt" - "regexp" "sort" "strconv" "strings" @@ -34,27 +33,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/portfolio/withdraw" ) -// GetDefaultConfig returns a default exchange config -func (d *Deribit) GetDefaultConfig(ctx context.Context) (*config.Exchange, error) { - d.SetDefaults() - exchCfg, err := d.GetStandardConfig() - if err != nil { - return nil, err - } - - err = d.SetupDefaults(exchCfg) - if err != nil { - return nil, err - } - if d.Features.Supports.RESTCapabilities.AutoPairUpdates { - err := d.UpdateTradablePairs(ctx, true) - if err != nil { - return nil, err - } - } - return exchCfg, nil -} - // SetDefaults sets the basic defaults for Deribit func (d *Deribit) SetDefaults() { d.Name = "Deribit" @@ -148,6 +126,7 @@ func (d *Deribit) SetDefaults() { GlobalResultLimit: 500, }, }, + Subscriptions: defaultSubscriptions.Clone(), } d.Requester, err = request.New(d.Name, common.NewHTTPClientWithTimeout(exchange.DefaultHTTPTimeout), @@ -197,7 +176,7 @@ func (d *Deribit) Setup(exch *config.Exchange) error { Connector: d.WsConnect, Subscriber: d.Subscribe, Unsubscriber: d.Unsubscribe, - GenerateSubscriptions: d.GenerateDefaultSubscriptions, + GenerateSubscriptions: d.generateSubscriptions, Features: &d.Features.Supports.WebsocketCapabilities, OrderbookBufferConfig: buffer.Config{ SortBuffer: true, @@ -208,9 +187,6 @@ func (d *Deribit) Setup(exch *config.Exchange) error { return err } - // setup option decimal regex at startup to make constant checks more efficient - optionRegex = regexp.MustCompile(optionDecimalRegex) - return d.Websocket.SetupNewConnection(&stream.ConnectionSetup{ URL: d.Websocket.GetWebsocketURL(), ResponseCheckTimeout: exch.WebsocketResponseCheckTimeout, diff --git a/exchanges/deribit/deribit_websocket_eps.go b/exchanges/deribit/deribit_ws_endpoints.go similarity index 100% rename from exchanges/deribit/deribit_websocket_eps.go rename to exchanges/deribit/deribit_ws_endpoints.go diff --git a/exchanges/kline/kline.go b/exchanges/kline/kline.go index 5a8ea21ebb1..a972af3af18 100644 --- a/exchanges/kline/kline.go +++ b/exchanges/kline/kline.go @@ -135,6 +135,9 @@ func (i Interval) Duration() time.Duration { // Short returns short string version of interval func (i Interval) Short() string { + if i == Raw { + return "raw" + } s := i.String() if strings.HasSuffix(s, "m0s") { s = s[:len(s)-2] @@ -149,6 +152,10 @@ func (i Interval) Short() string { // It does not validate the duration is aligned, only that it is a parsable duration func (i *Interval) UnmarshalJSON(text []byte) error { text = bytes.Trim(text, `"`) + if string(text) == "raw" { + *i = Raw + return nil + } if len(bytes.TrimLeft(text, `0123456789`)) > 0 { // contains non-numerics, ParseDuration can handle errors d, err := time.ParseDuration(string(text)) if err != nil { diff --git a/exchanges/kline/kline_test.go b/exchanges/kline/kline_test.go index a17e21637e1..cf29e9bffb1 100644 --- a/exchanges/kline/kline_test.go +++ b/exchanges/kline/kline_test.go @@ -143,9 +143,9 @@ func TestKlineDuration(t *testing.T) { func TestKlineShort(t *testing.T) { t.Parallel() - if OneDay.Short() != "24h" { - t.Fatalf("unexpected result: %v", OneDay.Short()) - } + assert.Equal(t, "24h", OneDay.Short(), "One day should show as 24h") + assert.Equal(t, "1h", OneHour.Short(), "One hour should truncate 0m0s suffix") + assert.Equal(t, "raw", Raw.Short(), "Raw should return raw") } func TestDurationToWord(t *testing.T) { @@ -1403,16 +1403,15 @@ func TestGetIntervalResultLimit(t *testing.T) { } func TestUnmarshalJSON(t *testing.T) { - i := new(Interval) - err := i.UnmarshalJSON([]byte(`"3m"`)) - assert.NoError(t, err, "UnmarshalJSON should not error") - assert.Equal(t, time.Minute*3, i.Duration(), "Interval should have correct value") - err = i.UnmarshalJSON([]byte(`"15s"`)) - assert.NoError(t, err, "UnmarshalJSON should not error") - assert.Equal(t, time.Second*15, i.Duration(), "Interval should have correct value") - err = i.UnmarshalJSON([]byte(`720000000000`)) - assert.NoError(t, err, "UnmarshalJSON should not error") - assert.Equal(t, time.Minute*12, i.Duration(), "Interval should have correct value") - err = i.UnmarshalJSON([]byte(`"6hedgehogs"`)) + t.Parallel() + var i Interval + for _, tt := range []struct { + in string + exp Interval + }{{`"3m"`, ThreeMin}, {`"15s"`, FifteenSecond}, {`720000000000`, OneMin * 12}, {`"-1ns"`, Raw}, {`"raw"`, Raw}} { + err := i.UnmarshalJSON([]byte(tt.in)) + assert.NoErrorf(t, err, "UnmarshalJSON should not error on %q", tt.in) + } + err := i.UnmarshalJSON([]byte(`"6hedgehogs"`)) assert.ErrorContains(t, err, "unknown unit", "UnmarshalJSON should error") } From 6da0b7dce542c158fac7dc039c27b28be4be9899 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:07:30 +1100 Subject: [PATCH 02/11] build(deps): Bump golang.org/x/time from 0.8.0 to 0.9.0 (#1768) Bumps [golang.org/x/time](https://github.com/golang/time) from 0.8.0 to 0.9.0. - [Commits](https://github.com/golang/time/compare/v0.8.0...v0.9.0) --- updated-dependencies: - dependency-name: golang.org/x/time dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 0c866fe14aa..310b1caa855 100644 --- a/go.mod +++ b/go.mod @@ -28,7 +28,7 @@ require ( golang.org/x/net v0.33.0 golang.org/x/term v0.27.0 golang.org/x/text v0.21.0 - golang.org/x/time v0.8.0 + golang.org/x/time v0.9.0 google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb google.golang.org/grpc v1.69.2 google.golang.org/protobuf v1.36.1 diff --git a/go.sum b/go.sum index aa402b4a5f9..c838c51e7df 100644 --- a/go.sum +++ b/go.sum @@ -335,8 +335,8 @@ golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.8.0 h1:9i3RxcPv3PZnitoVGMPDKZSq1xW1gK1Xy3ArNOGZfEg= -golang.org/x/time v0.8.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= +golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= +golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= From 5ae149a0636767efb3bf89519fbdf60b78337da2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 6 Jan 2025 17:33:36 +1100 Subject: [PATCH 03/11] build(deps): Bump golang.org/x/term from 0.27.0 to 0.28.0 (#1767) Bumps [golang.org/x/term](https://github.com/golang/term) from 0.27.0 to 0.28.0. - [Commits](https://github.com/golang/term/compare/v0.27.0...v0.28.0) --- updated-dependencies: - dependency-name: golang.org/x/term dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 310b1caa855..1a235db3f1d 100644 --- a/go.mod +++ b/go.mod @@ -26,7 +26,7 @@ require ( github.com/volatiletech/null v8.0.0+incompatible golang.org/x/crypto v0.31.0 golang.org/x/net v0.33.0 - golang.org/x/term v0.27.0 + golang.org/x/term v0.28.0 golang.org/x/text v0.21.0 golang.org/x/time v0.9.0 google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb @@ -66,7 +66,7 @@ require ( go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.9.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect - golang.org/x/sys v0.28.0 // indirect + golang.org/x/sys v0.29.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb // indirect gopkg.in/ini.v1 v1.67.0 // indirect diff --git a/go.sum b/go.sum index c838c51e7df..7cac7b14d29 100644 --- a/go.sum +++ b/go.sum @@ -325,10 +325,10 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190927073244-c990c680b611/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA= -golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q= -golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM= +golang.org/x/sys v0.29.0 h1:TPYlXGxvx1MGTn2GiZDhnjPA9wZzZeGKHHmKhHYvgaU= +golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.28.0 h1:/Ts8HFuMR2E6IP/jlo7QVLZHggjKQbhu/7H0LJFr3Gg= +golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= From fcd78add9694a4a5b539ff58a043076d60ae1d7d Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Tue, 7 Jan 2025 12:03:32 +1100 Subject: [PATCH 04/11] bybit: Add protected subtype for account checking to reduce outbound requests (#1739) * bybit: Add protected sub type for account checking to reduce outbound requests * add type and string method plus fix linter with comment * linter: fix * whoops * Update exchanges/bybit/bybit.go Co-authored-by: Scott * Update exchanges/bybit/bybit.go Co-authored-by: Scott * glorious: nits --------- Co-authored-by: Ryan O'Hara-Reid Co-authored-by: Scott --- exchanges/bybit/bybit.go | 67 ++++++++++++++++++++---------- exchanges/bybit/bybit_live_test.go | 4 +- exchanges/bybit/bybit_test.go | 33 ++++++++++++--- exchanges/bybit/bybit_types.go | 24 +++++++++++ exchanges/bybit/bybit_wrapper.go | 4 +- 5 files changed, 100 insertions(+), 32 deletions(-) diff --git a/exchanges/bybit/bybit.go b/exchanges/bybit/bybit.go index 2a5503a73de..6ff82ea5f0a 100644 --- a/exchanges/bybit/bybit.go +++ b/exchanges/bybit/bybit.go @@ -27,10 +27,7 @@ import ( // Bybit is the overarching type across this package type Bybit struct { exchange.Base - - // AccountType holds information about whether the account to which the api key belongs is a unified margin account or not. - // 0: unified, and 1: for normal account - AccountType int64 + account accountTypeHolder } const ( @@ -45,8 +42,8 @@ const ( cSpot, cLinear, cOption, cInverse = "spot", "linear", "option", "inverse" - accountTypeNormal = 0 // 0: regular account - accountTypeUnified = 1 // 1: unified trade account + accountTypeNormal AccountType = 1 + accountTypeUnified AccountType = 2 longDatedFormat = "02Jan06" ) @@ -1176,8 +1173,9 @@ func (by *Bybit) GetPreUpgradeOrderHistory(ctx context.Context, category, symbol if err != nil { return nil, err } - if by.AccountType == accountTypeNormal { - return nil, errAPIKeyIsNotUnified + err = by.RequiresUnifiedAccount(ctx) + if err != nil { + return nil, err } var resp *TradeOrders return resp, by.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/pre-upgrade/order/history", params, nil, &resp, defaultEPL) @@ -1189,8 +1187,9 @@ func (by *Bybit) GetPreUpgradeTradeHistory(ctx context.Context, category, symbol if err != nil { return nil, err } - if by.AccountType == accountTypeNormal { - return nil, errAPIKeyIsNotUnified + err = by.RequiresUnifiedAccount(ctx) + if err != nil { + return nil, err } if executionType != "" { params.Set("executionType", executionType) @@ -1205,8 +1204,9 @@ func (by *Bybit) GetPreUpgradeClosedPnL(ctx context.Context, category, symbol, c if err != nil { return nil, err } - if by.AccountType == accountTypeNormal { - return nil, errAPIKeyIsNotUnified + err = by.RequiresUnifiedAccount(ctx) + if err != nil { + return nil, err } var resp *ClosedProfitAndLossResponse return resp, by.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/pre-upgrade/position/closed-pnl", params, nil, &resp, defaultEPL) @@ -1218,8 +1218,9 @@ func (by *Bybit) GetPreUpgradeTransactionLog(ctx context.Context, category, base if err != nil { return nil, err } - if by.AccountType == accountTypeNormal { - return nil, errAPIKeyIsNotUnified + err = by.RequiresUnifiedAccount(ctx) + if err != nil { + return nil, err } if transactionType != "" { params.Set("type", transactionType) @@ -1234,8 +1235,9 @@ func (by *Bybit) GetPreUpgradeOptionDeliveryRecord(ctx context.Context, category if err != nil { return nil, err } - if by.AccountType == accountTypeNormal { - return nil, errAPIKeyIsNotUnified + err = by.RequiresUnifiedAccount(ctx) + if err != nil { + return nil, err } if !expiryDate.IsZero() { params.Set("expData", expiryDate.Format(longDatedFormat)) @@ -1250,8 +1252,9 @@ func (by *Bybit) GetPreUpgradeUSDCSessionSettlement(ctx context.Context, categor if err != nil { return nil, err } - if by.AccountType == accountTypeNormal { - return nil, errAPIKeyIsNotUnified + err = by.RequiresUnifiedAccount(ctx) + if err != nil { + return nil, err } var resp *SettlementSession return resp, by.SendAuthHTTPRequestV5(ctx, exchange.RestSpot, http.MethodGet, "/v5/pre-upgrade/asset/settlement-record", params, nil, &resp, defaultEPL) @@ -2699,13 +2702,31 @@ func getSign(sign, secret string) (string, error) { return crypto.HexEncodeToString(hmacSigned), nil } -// RetrieveAndSetAccountType retrieve to set account type information -func (by *Bybit) RetrieveAndSetAccountType(ctx context.Context) error { - accInfo, err := by.GetAPIKeyInformation(ctx) +// FetchAccountType if not set fetches the account type from the API, stores it and returns it. Else returns the stored account type. +func (by *Bybit) FetchAccountType(ctx context.Context) (AccountType, error) { + by.account.m.Lock() + defer by.account.m.Unlock() + if by.account.accountType == 0 { + accInfo, err := by.GetAPIKeyInformation(ctx) + if err != nil { + return 0, err + } + // From endpoint 0:regular account; 1:unified trade account + // + 1 to make it 1 and 2 so that a zero value can be used to check if the account type has been set or not. + by.account.accountType = AccountType(accInfo.IsUnifiedTradeAccount + 1) + } + return by.account.accountType, nil +} + +// RequiresUnifiedAccount checks account type and returns error if not unified +func (by *Bybit) RequiresUnifiedAccount(ctx context.Context) error { + at, err := by.FetchAccountType(ctx) if err != nil { - return err + return nil //nolint:nilerr // if we can't get the account type, we can't check if it's unified or not, fail on call + } + if at != accountTypeUnified { + return fmt.Errorf("%w, account type: %s", errAPIKeyIsNotUnified, at) } - by.AccountType = accInfo.IsUnifiedTradeAccount // 0:regular account; 1:unified trade account return nil } diff --git a/exchanges/bybit/bybit_live_test.go b/exchanges/bybit/bybit_live_test.go index fc792be0955..299e9e2dc42 100644 --- a/exchanges/bybit/bybit_live_test.go +++ b/exchanges/bybit/bybit_live_test.go @@ -31,8 +31,8 @@ func TestMain(m *testing.M) { } if b.API.AuthenticatedSupport { - if err := b.RetrieveAndSetAccountType(context.Background()); err != nil { - log.Printf("%s unable to RetrieveAndSetAccountType: %v", b.Name, err) + if _, err := b.FetchAccountType(context.Background()); err != nil { + log.Printf("%s unable to FetchAccountType: %v", b.Name, err) } } diff --git a/exchanges/bybit/bybit_test.go b/exchanges/bybit/bybit_test.go index 4259eab31a7..9fa89c6fa13 100644 --- a/exchanges/bybit/bybit_test.go +++ b/exchanges/bybit/bybit_test.go @@ -3548,12 +3548,35 @@ func TestStringToOrderStatus(t *testing.T) { } } -func TestRetrieveAndSetAccountType(t *testing.T) { - sharedtestvalues.SkipTestIfCredentialsUnset(t, b, canManipulateRealOrders) - err := b.RetrieveAndSetAccountType(context.Background()) - if err != nil { - t.Fatal(err) +func TestFetchAccountType(t *testing.T) { + t.Parallel() + if !mockTests { + sharedtestvalues.SkipTestIfCredentialsUnset(t, b) } + val, err := b.FetchAccountType(context.Background()) + require.NoError(t, err) + require.NotZero(t, val) +} + +func TestAccountTypeString(t *testing.T) { + t.Parallel() + require.Equal(t, "unset", AccountType(0).String()) + require.Equal(t, "unified", accountTypeUnified.String()) + require.Equal(t, "normal", accountTypeNormal.String()) + require.Equal(t, "unknown", AccountType(3).String()) +} + +func TestRequiresUnifiedAccount(t *testing.T) { + t.Parallel() + if !mockTests { + sharedtestvalues.SkipTestIfCredentialsUnset(t, b) + } + err := b.RequiresUnifiedAccount(context.Background()) + require.NoError(t, err) + b := &Bybit{} //nolint:govet // Intentional shadow to avoid future copy/paste mistakes. Also stops race below. + b.account.accountType = accountTypeNormal + err = b.RequiresUnifiedAccount(context.Background()) + require.ErrorIs(t, err, errAPIKeyIsNotUnified) } func TestGetLatestFundingRates(t *testing.T) { diff --git a/exchanges/bybit/bybit_types.go b/exchanges/bybit/bybit_types.go index 656a4d2ffa7..16114557684 100644 --- a/exchanges/bybit/bybit_types.go +++ b/exchanges/bybit/bybit_types.go @@ -2,6 +2,7 @@ package bybit import ( "encoding/json" + "sync" "time" "github.com/gofrs/uuid" @@ -2035,3 +2036,26 @@ type Error struct { ExtCode string `json:"ext_code"` ExtMsg string `json:"ext_info"` } + +// accountTypeHolder holds the account type associated with the loaded API key. +type accountTypeHolder struct { + accountType AccountType + m sync.Mutex +} + +// AccountType constants +type AccountType uint8 + +// String returns the account type as a string +func (a AccountType) String() string { + switch a { + case 0: + return "unset" + case accountTypeNormal: + return "normal" + case accountTypeUnified: + return "unified" + default: + return "unknown" + } +} diff --git a/exchanges/bybit/bybit_wrapper.go b/exchanges/bybit/bybit_wrapper.go index acb3ae66949..1d473a47830 100644 --- a/exchanges/bybit/bybit_wrapper.go +++ b/exchanges/bybit/bybit_wrapper.go @@ -562,7 +562,7 @@ func (by *Bybit) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a var acc account.SubAccount var accountType string info.Exchange = by.Name - err := by.RetrieveAndSetAccountType(ctx) + at, err := by.FetchAccountType(ctx) if err != nil { return info, err } @@ -570,7 +570,7 @@ func (by *Bybit) UpdateAccountInfo(ctx context.Context, assetType asset.Item) (a case asset.Spot, asset.Options, asset.USDCMarginedFutures, asset.USDTMarginedFutures: - switch by.AccountType { + switch at { case accountTypeUnified: accountType = "UNIFIED" case accountTypeNormal: From b3e640ef5e10850579f7d7b500502cd0e047589d Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Fri, 10 Jan 2025 11:17:10 +1100 Subject: [PATCH 05/11] CI: Add stale activity check for issues and PRs GHA (#1689) --- .github/workflows/stale.yml | 42 +++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/stale.yml diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 00000000000..db3748580ea --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,42 @@ +name: stale-checks +on: + schedule: + - cron: '0 0 * * 1-5' + workflow_dispatch: + +permissions: + contents: read + +env: + DAYS_BEFORE_STALE: ${{ vars.DAYS_BEFORE_STALE }} + DAYS_BEFORE_CLOSE: ${{ vars.DAYS_BEFORE_CLOSE }} + EXEMPT_ISSUE_LABELS: ${{ vars.EXEMPT_ISSUE_LABELS }} + EXEMPT_PR_LABELS: ${{ vars.EXEMPT_PR_LABELS }} + +jobs: + stale: + name: Stale issues and PRs check + runs-on: ubuntu-latest + environment: ci + permissions: + issues: write + pull-requests: write + steps: + - uses: actions/stale@v9 + with: + # General settings + days-before-stale: ${{ env.DAYS_BEFORE_STALE }} + days-before-close: ${{ env.DAYS_BEFORE_CLOSE }} + exempt-issue-labels: ${{ env.EXEMPT_ISSUE_LABELS }} + exempt-pr-labels: ${{ env.EXEMPT_PR_LABELS }} + enable-statistics: true + + # Issue settings + stale-issue-message: 'This issue is stale because it has been open ${{ env.DAYS_BEFORE_STALE }} days with no activity. Please provide an update or this issue will be automatically closed in ${{ env.DAYS_BEFORE_CLOSE }} days.' + close-issue-message: 'This issue was closed because it has been stalled for ${{ env.DAYS_BEFORE_CLOSE }} days with no activity.' + stale-issue-label: 'stale' + + # PR settings + stale-pr-message: 'This PR is stale because it has been open ${{ env.DAYS_BEFORE_STALE }} days with no activity. Please provide an update on the progress of this PR.' + days-before-pr-close: -1 + stale-pr-label: 'stale' \ No newline at end of file From 1efd8e0db0551b8e70f06d698823202650da7217 Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Fri, 10 Jan 2025 00:21:02 +0000 Subject: [PATCH 06/11] Subscriptions: Fix IBotExchange not implementing sub.IExchange (#1765) Thusfar ExpandTemplates had only been called internally. All methods consumers might want to get, in this case GetPairFormat, need to be in the interface we're passing out, otherwise users can't call them (or things that use them, like ExpandTemplates) --- exchanges/interfaces.go | 1 + exchanges/subscription/interface_test.go | 16 ++++++++++++++++ exchanges/subscription/list.go | 7 ++++--- exchanges/subscription/template.go | 4 ++-- 4 files changed, 23 insertions(+), 5 deletions(-) create mode 100644 exchanges/subscription/interface_test.go diff --git a/exchanges/interfaces.go b/exchanges/interfaces.go index 0f6071a864d..6ef32727d63 100644 --- a/exchanges/interfaces.go +++ b/exchanges/interfaces.go @@ -51,6 +51,7 @@ type IBotExchange interface { UpdateTradablePairs(ctx context.Context, forceUpdate bool) error GetEnabledPairs(a asset.Item) (currency.Pairs, error) GetAvailablePairs(a asset.Item) (currency.Pairs, error) + GetPairFormat(asset.Item, bool) (currency.PairFormat, error) SetPairs(pairs currency.Pairs, a asset.Item, enabled bool) error GetAssetTypes(enabled bool) asset.Items GetRecentTrades(ctx context.Context, p currency.Pair, a asset.Item) ([]trade.Data, error) diff --git a/exchanges/subscription/interface_test.go b/exchanges/subscription/interface_test.go new file mode 100644 index 00000000000..c608c97c046 --- /dev/null +++ b/exchanges/subscription/interface_test.go @@ -0,0 +1,16 @@ +package subscription_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + exchange "github.com/thrasher-corp/gocryptotrader/exchanges" + shared "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" + "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" +) + +// TestIExchange ensures that IExchange is a subset of IBotExchange, so when an exchange is passed by interface, it can still use ExpandTemplates +func TestIExchange(t *testing.T) { + assert.Implements(t, (*subscription.IExchange)(nil), exchange.IBotExchange(&shared.CustomEx{})) + var _ subscription.IExchange = exchange.IBotExchange(nil) +} diff --git a/exchanges/subscription/list.go b/exchanges/subscription/list.go index 6dc871a3cb9..5cadd716c4a 100644 --- a/exchanges/subscription/list.go +++ b/exchanges/subscription/list.go @@ -14,7 +14,8 @@ type List []*Subscription type assetPairs map[asset.Item]currency.Pairs -type iExchange interface { +// IExchange provides method requirements for exchanges to use subscription templating +type IExchange interface { GetAssetTypes(enabled bool) asset.Items GetEnabledPairs(asset.Item) (currency.Pairs, error) GetPairFormat(asset.Item, bool) (currency.PairFormat, error) @@ -93,7 +94,7 @@ func (l List) SetStates(state State) error { return err } -func fillAssetPairs(ap assetPairs, a asset.Item, e iExchange) error { +func fillAssetPairs(ap assetPairs, a asset.Item, e IExchange) error { p, err := e.GetEnabledPairs(a) if err != nil { return err @@ -107,7 +108,7 @@ func fillAssetPairs(ap assetPairs, a asset.Item, e iExchange) error { } // assetPairs returns a map of enabled pairs for the subscriptions in the list, formatted for the asset -func (l List) assetPairs(e iExchange) (assetPairs, error) { +func (l List) assetPairs(e IExchange) (assetPairs, error) { at := []asset.Item{} for _, a := range e.GetAssetTypes(true) { if e.IsAssetWebsocketSupported(a) { diff --git a/exchanges/subscription/template.go b/exchanges/subscription/template.go index ad48f3b462e..c076a6c8fe9 100644 --- a/exchanges/subscription/template.go +++ b/exchanges/subscription/template.go @@ -42,7 +42,7 @@ type tplCtx struct { // Calls e.GetSubscriptionTemplate to find a template for each subscription // Filters out Authenticated subscriptions if !e.CanUseAuthenticatedEndpoints // See README.md for more details -func (l List) ExpandTemplates(e iExchange) (List, error) { +func (l List) ExpandTemplates(e IExchange) (List, error) { if !slices.ContainsFunc(l, func(s *Subscription) bool { return s.QualifiedChannel == "" }) { // Empty list, or already processed return slices.Clone(l), nil @@ -82,7 +82,7 @@ func (l List) ExpandTemplates(e iExchange) (List, error) { return subs, err } -func expandTemplate(e iExchange, s *Subscription, ap assetPairs, assets asset.Items) (List, error) { +func expandTemplate(e IExchange, s *Subscription, ap assetPairs, assets asset.Items) (List, error) { if s.QualifiedChannel != "" { return List{s}, nil } From e4479557ded10cbded9a5cf89bdd0fbb8c7b9043 Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Fri, 10 Jan 2025 03:38:56 +0000 Subject: [PATCH 07/11] 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 --- README.md | 10 +- .../exchanges_templates/kucoin.tmpl | 3 + engine/currency_state_manager.md | 74 +++++----- exchanges/kucoin/README.md | 3 + exchanges/kucoin/kucoin_test.go | 137 +++++++++++------- exchanges/kucoin/kucoin_websocket.go | 75 +++++++--- 6 files changed, 185 insertions(+), 117 deletions(-) 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 }} ` From 87641a43a80284565a9e9bbbd12e8d30690082a2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:43:06 +1100 Subject: [PATCH 08/11] build(deps): Bump golang.org/x/net from 0.33.0 to 0.34.0 (#1773) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.33.0 to 0.34.0. - [Commits](https://github.com/golang/net/compare/v0.33.0...v0.34.0) --- updated-dependencies: - dependency-name: golang.org/x/net dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 4 ++-- go.sum | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/go.mod b/go.mod index 1a235db3f1d..15bc59adffc 100644 --- a/go.mod +++ b/go.mod @@ -24,8 +24,8 @@ require ( github.com/thrasher-corp/sqlboiler v1.0.1-0.20191001234224-71e17f37a85e github.com/urfave/cli/v2 v2.27.5 github.com/volatiletech/null v8.0.0+incompatible - golang.org/x/crypto v0.31.0 - golang.org/x/net v0.33.0 + golang.org/x/crypto v0.32.0 + golang.org/x/net v0.34.0 golang.org/x/term v0.28.0 golang.org/x/text v0.21.0 golang.org/x/time v0.9.0 diff --git a/go.sum b/go.sum index 7cac7b14d29..1f26e16dd0e 100644 --- a/go.sum +++ b/go.sum @@ -281,8 +281,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190325154230-a5d413f7728c/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U= -golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk= +golang.org/x/crypto v0.32.0 h1:euUpcYgM8WcP71gNpTqQCn6rC2t6ULUPiOzfWaXVVfc= +golang.org/x/crypto v0.32.0/go.mod h1:ZnnJkOaASj8g0AjIduWNlq2NRxL0PlBrbKVyZ6V/Ugc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= @@ -303,8 +303,8 @@ golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I= -golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4= +golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0= +golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= From 7677d07cd7c101a9234f10c37b0f0e02b87d6cff Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:44:36 +1100 Subject: [PATCH 09/11] build(deps): Bump bufbuild/buf-setup-action from 1.48.0 to 1.49.0 (#1772) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.48.0 to 1.49.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.48.0...v1.49.0) --- updated-dependencies: - dependency-name: bufbuild/buf-setup-action dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/proto-lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml index 9cf0e8056e7..bf98f79022d 100644 --- a/.github/workflows/proto-lint.yml +++ b/.github/workflows/proto-lint.yml @@ -21,7 +21,7 @@ jobs: go install google.golang.org/protobuf/cmd/protoc-gen-go@latest go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest - - uses: bufbuild/buf-setup-action@v1.48.0 + - uses: bufbuild/buf-setup-action@v1.49.0 - name: buf generate working-directory: ./gctrpc From 861054ceb08e94c3d91cb0b30c2f1eea5961e194 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 13 Jan 2025 18:45:25 +1100 Subject: [PATCH 10/11] build(deps): Bump google.golang.org/protobuf from 1.36.1 to 1.36.2 (#1775) Bumps google.golang.org/protobuf from 1.36.1 to 1.36.2. --- updated-dependencies: - dependency-name: google.golang.org/protobuf dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 15bc59adffc..cc526512197 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( golang.org/x/time v0.9.0 google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb google.golang.org/grpc v1.69.2 - google.golang.org/protobuf v1.36.1 + google.golang.org/protobuf v1.36.2 ) require ( diff --git a/go.sum b/go.sum index 1f26e16dd0e..bad962ba073 100644 --- a/go.sum +++ b/go.sum @@ -370,8 +370,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= -google.golang.org/protobuf v1.36.1 h1:yBPeRvTftaleIgM3PZ/WBIZ7XM/eEYAaEyCwvyjq/gk= -google.golang.org/protobuf v1.36.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= +google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= From 4c7f48ae0ee21367781e6de5ed27fdd9ecd5fe07 Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Tue, 14 Jan 2025 04:19:17 +0000 Subject: [PATCH 11/11] GateIO: Fix GetFuturesContractDetails for delivery futures and minor other fixes (#1766) * GateIO: Fix GetFuturesContractDetails for Deliveries Was returning the product of all the contracts, so 1444 instead of 38 contracts. * GateIO: Fix GetOpenInterest returning asset.ErrNotEnabled Using wrong error for pair not enabled * GateIO: Rename GetSingleContract and GetSingleDeliveryContracts Especially fixes GetSingleContract, which seems misleading to not say Futures. There's a load of `GetSingle*` here that should probably also be fixed, but these two justified a dyno * GateIO: Rename GateIOGetPersonalTradingHistory to GetMySpotTradingHistory * GateIO: Rename GetMyPersonalTradingHistory to GetMyFuturesTradingHistory * GateIO: Remove duplicate DeliveryTradingHistory * GateIO: Rename Get*PersonalTradingHistory to GetMy*TradingHistory * Linter: Disable shadow linting for err It's been a year, and I'm still getting caught out by govet demanding I don't shadow a var I was deliberately shadowing. Made worse by an increase in clashes with stylecheck when they both want opposite things on the same line. * GateIO: Add missing Futures and tradinghistory fields * GateIO: Improve WS Header parsing This unifies handling for time_ms and time in response headers, since options and delivery have only time, but spot has time_ms as well. We use the better of the two results. Also [improves performance 2x](https://gist.github.com/gbjk/7cacb63b9a256e745534bb05ca853c48) * GateIO: Use time_ms WS fields where available Removes the deprecated _time json fields and populates our Time fields with the time_ms values --- .golangci.yml | 3 + currency/pairs.go | 9 +- engine/rpcserver.go | 5 +- exchanges/btse/btse_wrapper.go | 2 +- exchanges/gateio/gateio.go | 35 ++- exchanges/gateio/gateio_test.go | 90 +++--- exchanges/gateio/gateio_types.go | 304 ++++++++----------- exchanges/gateio/gateio_websocket.go | 66 +++- exchanges/gateio/gateio_websocket_futures.go | 19 +- exchanges/gateio/gateio_websocket_option.go | 11 +- exchanges/gateio/gateio_wrapper.go | 81 +++-- exchanges/subscription/template.go | 2 +- 12 files changed, 316 insertions(+), 311 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index b59ae9635b3..c577a1f7d5d 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -150,6 +150,9 @@ issues: - text: "Expect WriteFile permissions to be 0600 or less" linters: - gosec + - text: 'shadow: declaration of "err" shadows declaration at' + linters: [ govet ] + exclude-dirs: - vendor diff --git a/currency/pairs.go b/currency/pairs.go index cc5e2f6db52..99398cd1e0d 100644 --- a/currency/pairs.go +++ b/currency/pairs.go @@ -9,14 +9,15 @@ import ( "strings" ) +// Public Errors +var ( + ErrPairDuplication = errors.New("currency pair duplication") +) + var ( errSymbolEmpty = errors.New("symbol is empty") errNoDelimiter = errors.New("no delimiter was supplied") errPairFormattingInconsistent = errors.New("pair formatting is inconsistent") - - // ErrPairDuplication defines an error when there is multiple of the same - // currency pairs found. - ErrPairDuplication = errors.New("currency pair duplication") ) // NewPairsFromStrings takes in currency pair strings and returns a currency diff --git a/engine/rpcserver.go b/engine/rpcserver.go index 72e7e76e8b6..76fdf12579a 100644 --- a/engine/rpcserver.go +++ b/engine/rpcserver.go @@ -79,7 +79,6 @@ var ( errGRPCShutdownSignalIsNil = errors.New("cannot shutdown, gRPC shutdown channel is nil") errInvalidStrategy = errors.New("invalid strategy") errSpecificPairNotEnabled = errors.New("specified pair is not enabled") - errPairNotEnabled = errors.New("pair is not enabled") ) // RPCServer struct @@ -4723,7 +4722,7 @@ func (s *RPCServer) GetFundingRates(ctx context.Context, r *gctrpc.GetFundingRat } if !pairs.Contains(cp, true) { - return nil, fmt.Errorf("%w %v", errPairNotEnabled, cp) + return nil, fmt.Errorf("%w %v", currency.ErrPairNotEnabled, cp) } funding, err := exch.GetHistoricalFundingRates(ctx, &fundingrate.HistoricalRatesRequest{ @@ -4821,7 +4820,7 @@ func (s *RPCServer) GetLatestFundingRate(ctx context.Context, r *gctrpc.GetLates } if !pairs.Contains(cp, true) { - return nil, fmt.Errorf("%w %v", errPairNotEnabled, cp) + return nil, fmt.Errorf("%w %v", currency.ErrPairNotEnabled, cp) } fundingRates, err := exch.GetLatestFundingRates(ctx, &fundingrate.LatestRateRequest{ diff --git a/exchanges/btse/btse_wrapper.go b/exchanges/btse/btse_wrapper.go index 72d25f42925..54fa94d2a79 100644 --- a/exchanges/btse/btse_wrapper.go +++ b/exchanges/btse/btse_wrapper.go @@ -1281,7 +1281,7 @@ func (b *BTSE) UpdateOrderExecutionLimits(ctx context.Context, a asset.Item) err var errs error limits := make([]order.MinMaxLevel, 0, len(summary)) for _, marketInfo := range summary { - p, err := marketInfo.Pair() //nolint:govet // Deliberately shadow err + p, err := marketInfo.Pair() if err != nil { errs = common.AppendError(err, fmt.Errorf("%s: %w", p, err)) continue diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index 68ae378dee0..a0658d44ac7 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -775,12 +775,11 @@ func (g *Gateio) CancelSingleSpotOrder(ctx context.Context, orderID, currencyPai return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, spotCancelSingleOrderEPL, http.MethodDelete, gateioSpotOrders+"/"+orderID, params, nil, &response) } -// GateIOGetPersonalTradingHistory retrieves personal trading history -func (g *Gateio) GateIOGetPersonalTradingHistory(ctx context.Context, currencyPair currency.Pair, - orderID string, page, limit uint64, crossMarginAccount bool, from, to time.Time) ([]SpotPersonalTradeHistory, error) { +// GetMySpotTradingHistory retrieves personal trading history +func (g *Gateio) GetMySpotTradingHistory(ctx context.Context, p currency.Pair, orderID string, page, limit uint64, crossMargin bool, from, to time.Time) ([]SpotPersonalTradeHistory, error) { params := url.Values{} - if currencyPair.IsPopulated() { - params.Set("currency_pair", currencyPair.String()) + if p.IsPopulated() { + params.Set("currency_pair", p.String()) } if orderID != "" { params.Set("order_id", orderID) @@ -791,7 +790,7 @@ func (g *Gateio) GateIOGetPersonalTradingHistory(ctx context.Context, currencyPa if page > 0 { params.Set("page", strconv.FormatUint(page, 10)) } - if crossMarginAccount { + if crossMargin { params.Set("account", asset.CrossMargin.String()) } if !from.IsZero() { @@ -1842,8 +1841,8 @@ func (g *Gateio) GetAllFutureContracts(ctx context.Context, settle currency.Code return contracts, g.SendHTTPRequest(ctx, exchange.RestSpot, publicFuturesContractsEPL, futuresPath+settle.Item.Lower+"/contracts", &contracts) } -// GetSingleContract returns a single contract info for the specified settle and Currency Pair (contract << in this case) -func (g *Gateio) GetSingleContract(ctx context.Context, settle currency.Code, contract string) (*FuturesContract, error) { +// GetFuturesContract returns a single futures contract info for the specified settle and Currency Pair (contract << in this case) +func (g *Gateio) GetFuturesContract(ctx context.Context, settle currency.Code, contract string) (*FuturesContract, error) { if contract == "" { return nil, currency.ErrCurrencyPairEmpty } @@ -2428,8 +2427,8 @@ func (g *Gateio) AmendFuturesOrder(ctx context.Context, settle currency.Code, or return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, perpetualAmendOrderEPL, http.MethodPut, futuresPath+settle.Item.Lower+"/orders/"+orderID, nil, &arg, &response) } -// GetMyPersonalTradingHistory retrieves my personal trading history -func (g *Gateio) GetMyPersonalTradingHistory(ctx context.Context, settle currency.Code, lastID, orderID string, contract currency.Pair, limit, offset, countTotal uint64) ([]TradingHistoryItem, error) { +// GetMyFuturesTradingHistory retrieves authenticated account's futures trading history +func (g *Gateio) GetMyFuturesTradingHistory(ctx context.Context, settle currency.Code, lastID, orderID string, contract currency.Pair, limit, offset, countTotal uint64) ([]TradingHistoryItem, error) { if settle.IsEmpty() { return nil, errEmptyOrInvalidSettlementCurrency } @@ -2623,8 +2622,8 @@ func (g *Gateio) GetAllDeliveryContracts(ctx context.Context, settle currency.Co return contracts, g.SendHTTPRequest(ctx, exchange.RestSpot, publicDeliveryContractsEPL, deliveryPath+settle.Item.Lower+"/contracts", &contracts) } -// GetSingleDeliveryContracts retrieves a single delivery contract instance. -func (g *Gateio) GetSingleDeliveryContracts(ctx context.Context, settle currency.Code, contract currency.Pair) (*DeliveryContract, error) { +// GetDeliveryContract retrieves a single delivery contract instance +func (g *Gateio) GetDeliveryContract(ctx context.Context, settle currency.Code, contract currency.Pair) (*DeliveryContract, error) { if settle.IsEmpty() { return nil, errEmptyOrInvalidSettlementCurrency } @@ -2656,7 +2655,7 @@ func (g *Gateio) GetDeliveryOrderbook(ctx context.Context, settle currency.Code, } // GetDeliveryTradingHistory retrieves futures trading history -func (g *Gateio) GetDeliveryTradingHistory(ctx context.Context, settle currency.Code, lastID string, contract currency.Pair, limit uint64, from, to time.Time) ([]DeliveryTradingHistory, error) { +func (g *Gateio) GetDeliveryTradingHistory(ctx context.Context, settle currency.Code, lastID string, contract currency.Pair, limit uint64, from, to time.Time) ([]TradingHistoryItem, error) { if settle.IsEmpty() { return nil, errEmptyOrInvalidSettlementCurrency } @@ -2677,7 +2676,7 @@ func (g *Gateio) GetDeliveryTradingHistory(ctx context.Context, settle currency. if lastID != "" { params.Set("last_id", lastID) } - var histories []DeliveryTradingHistory + var histories []TradingHistoryItem return histories, g.SendHTTPRequest(ctx, exchange.RestSpot, publicTradingHistoryDeliveryEPL, common.EncodeURLValues(deliveryPath+settle.Item.Lower+"/trades", params), &histories) } @@ -2941,8 +2940,8 @@ func (g *Gateio) CancelSingleDeliveryOrder(ctx context.Context, settle currency. return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, deliveryCancelOrderEPL, http.MethodDelete, deliveryPath+settle.Item.Lower+"/orders/"+orderID, nil, nil, &response) } -// GetDeliveryPersonalTradingHistory retrieves personal trading history -func (g *Gateio) GetDeliveryPersonalTradingHistory(ctx context.Context, settle currency.Code, orderID string, contract currency.Pair, limit, offset, countTotal uint64, lastID string) ([]TradingHistoryItem, error) { +// GetMyDeliveryTradingHistory retrieves authenticated account delivery futures trading history +func (g *Gateio) GetMyDeliveryTradingHistory(ctx context.Context, settle currency.Code, orderID string, contract currency.Pair, limit, offset, countTotal uint64, lastID string) ([]TradingHistoryItem, error) { if settle.IsEmpty() { return nil, errEmptyOrInvalidSettlementCurrency } @@ -3407,8 +3406,8 @@ func (g *Gateio) CancelOptionSingleOrder(ctx context.Context, orderID string) (* return response, g.SendAuthenticatedHTTPRequest(ctx, exchange.RestSpot, optionsCancelOrderEPL, http.MethodDelete, "options/orders/"+orderID, nil, nil, &response) } -// GetOptionsPersonalTradingHistory retrieves personal tradign histories given the underlying{Required}, contract, and other pagination params. -func (g *Gateio) GetOptionsPersonalTradingHistory(ctx context.Context, underlying string, contract currency.Pair, offset, limit uint64, from, to time.Time) ([]OptionTradingHistory, error) { +// GetMyOptionsTradingHistory retrieves authenticated account's option trading history +func (g *Gateio) GetMyOptionsTradingHistory(ctx context.Context, underlying string, contract currency.Pair, offset, limit uint64, from, to time.Time) ([]OptionTradingHistory, error) { if underlying == "" { return nil, errInvalidUnderlying } diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index d5d4bfe6c98..469a4ee9d37 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -439,12 +439,11 @@ func TestCancelSingleSpotOrder(t *testing.T) { } } -func TestGetPersonalTradingHistory(t *testing.T) { +func TestGetMySpotTradingHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - if _, err := g.GateIOGetPersonalTradingHistory(context.Background(), currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, "", 0, 0, false, time.Time{}, time.Time{}); err != nil { - t.Errorf("%s GetPersonalTradingHistory() error %v", g.Name, err) - } + _, err := g.GetMySpotTradingHistory(context.Background(), currency.Pair{Base: currency.BTC, Quote: currency.USDT, Delimiter: currency.UnderscoreDelimiter}, "", 0, 0, false, time.Time{}, time.Time{}) + require.NoError(t, err) } func TestGetServerTime(t *testing.T) { @@ -968,12 +967,12 @@ func TestGetAllFutureContracts(t *testing.T) { } } -func TestGetSingleContract(t *testing.T) { +func TestGetFuturesContract(t *testing.T) { t.Parallel() settle, err := getSettlementFromCurrency(getPair(t, asset.Futures)) require.NoError(t, err, "getSettlementFromCurrency must not error") - _, err = g.GetSingleContract(context.Background(), settle, getPair(t, asset.Futures).String()) - assert.NoError(t, err, "GetSingleContract should not error") + _, err = g.GetFuturesContract(context.Background(), settle, getPair(t, asset.Futures).String()) + assert.NoError(t, err, "GetFuturesContract should not error") } func TestGetFuturesOrderbook(t *testing.T) { @@ -1156,11 +1155,11 @@ func TestCancelSingleDeliveryOrder(t *testing.T) { assert.NoError(t, err, "CancelSingleDeliveryOrder should not error") } -func TestGetDeliveryPersonalTradingHistory(t *testing.T) { +func TestGetMyDeliveryTradingHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - _, err := g.GetDeliveryPersonalTradingHistory(context.Background(), currency.USDT, "", getPair(t, asset.DeliveryFutures), 0, 0, 1, "") - assert.NoError(t, err, "GetDeliveryPersonalTradingHistory should not error") + _, err := g.GetMyDeliveryTradingHistory(context.Background(), currency.USDT, "", getPair(t, asset.DeliveryFutures), 0, 0, 1, "") + assert.NoError(t, err, "GetMyDeliveryTradingHistory should not error") } func TestGetDeliveryPositionCloseHistory(t *testing.T) { @@ -1368,11 +1367,11 @@ func TestAmendFuturesOrder(t *testing.T) { assert.NoError(t, err, "AmendFuturesOrder should not error") } -func TestGetMyPersonalTradingHistory(t *testing.T) { +func TestGetMyFuturesTradingHistory(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - _, err := g.GetMyPersonalTradingHistory(context.Background(), currency.BTC, "", "", getPair(t, asset.Futures), 0, 0, 0) - assert.NoError(t, err, "GetMyPersonalTradingHistory should not error") + _, err := g.GetMyFuturesTradingHistory(context.Background(), currency.BTC, "", "", getPair(t, asset.Futures), 0, 0, 0) + assert.NoError(t, err, "GetMyFuturesTradingHistory should not error") } func TestGetFuturesPositionCloseHistory(t *testing.T) { @@ -1457,12 +1456,12 @@ func TestGetAllDeliveryContracts(t *testing.T) { } } -func TestGetSingleDeliveryContracts(t *testing.T) { +func TestGetDeliveryContract(t *testing.T) { t.Parallel() settle, err := getSettlementFromCurrency(getPair(t, asset.DeliveryFutures)) require.NoError(t, err, "getSettlementFromCurrency must not error") - _, err = g.GetSingleDeliveryContracts(context.Background(), settle, getPair(t, asset.DeliveryFutures)) - assert.NoError(t, err, "GetSingleDeliveryContracts should not error") + _, err = g.GetDeliveryContract(context.Background(), settle, getPair(t, asset.DeliveryFutures)) + assert.NoError(t, err, "GetDeliveryContract should not error") } func TestGetDeliveryOrderbook(t *testing.T) { @@ -1767,12 +1766,12 @@ func TestCancelSingleOrder(t *testing.T) { } } -func TestGetOptionsPersonalTradingHistory(t *testing.T) { +func TestGetMyOptionsTradingHistory(t *testing.T) { t.Parallel() + sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - if _, err := g.GetOptionsPersonalTradingHistory(context.Background(), "BTC_USDT", currency.EMPTYPAIR, 0, 0, time.Time{}, time.Time{}); err != nil { - t.Errorf("%s GetOptionPersonalTradingHistory() error %v", g.Name, err) - } + _, err := g.GetMyOptionsTradingHistory(context.Background(), "BTC_USDT", currency.EMPTYPAIR, 0, 0, time.Time{}, time.Time{}) + require.NoError(t, err) } func TestWithdrawCurrency(t *testing.T) { @@ -3186,22 +3185,20 @@ func TestForceFileStandard(t *testing.T) { func TestGetFuturesContractDetails(t *testing.T) { t.Parallel() _, err := g.GetFuturesContractDetails(context.Background(), asset.Spot) - if !errors.Is(err, futures.ErrNotFuturesAsset) { - t.Error(err) - } + require.ErrorIs(t, err, futures.ErrNotFuturesAsset) + _, err = g.GetFuturesContractDetails(context.Background(), asset.PerpetualContract) - if !errors.Is(err, asset.ErrNotSupported) { - t.Error(err) - } + require.ErrorIs(t, err, asset.ErrNotSupported) - _, err = g.GetFuturesContractDetails(context.Background(), asset.DeliveryFutures) - if !errors.Is(err, nil) { - t.Error(err) - } - _, err = g.GetFuturesContractDetails(context.Background(), asset.Futures) - if !errors.Is(err, nil) { - t.Error(err) - } + exp, err := g.GetAllDeliveryContracts(context.Background(), currency.USDT) + require.NoError(t, err, "GetAllDeliveryContracts must not error") + c, err := g.GetFuturesContractDetails(context.Background(), asset.DeliveryFutures) + require.NoError(t, err, "GetFuturesContractDetails must not error for DeliveryFutures") + assert.Equal(t, len(exp), len(c), "GetFuturesContractDetails should return same number of Delivery contracts as exist") + + c, err = g.GetFuturesContractDetails(context.Background(), asset.Futures) + require.NoError(t, err, "GetFuturesContractDetails must not error for DeliveryFutures") + assert.NotEmpty(t, c, "GetFuturesContractDetails should return same number of Future contracts as exist") } func TestGetLatestFundingRates(t *testing.T) { @@ -3528,3 +3525,28 @@ func TestHandleSubscriptions(t *testing.T) { }) require.NoError(t, err) } + +func TestParseWSHeader(t *testing.T) { + in := []string{ + `{"time":1726121320,"time_ms":1726121320745,"id":1,"channel":"spot.tickers","event":"subscribe","result":{"status":"success"},"request_id":"a4"}`, + `{"time_ms":1726121320746,"id":2,"channel":"spot.tickers","event":"subscribe","result":{"status":"success"},"request_id":"a4"}`, + `{"time":1726121321,"id":3,"channel":"spot.tickers","event":"subscribe","result":{"status":"success"},"request_id":"a4"}`, + } + for _, i := range in { + h, err := parseWSHeader([]byte(i)) + require.NoError(t, err) + require.NotEmpty(t, h.ID) + assert.Equal(t, "a4", h.RequestID) + assert.Equal(t, "spot.tickers", h.Channel) + assert.Equal(t, "subscribe", h.Event) + assert.NotEmpty(t, h.Result) + switch h.ID { + case 1: + assert.Equal(t, int64(1726121320745), h.Time.UnixMilli()) + case 2: + assert.Equal(t, int64(1726121320746), h.Time.UnixMilli()) + case 3: + assert.Equal(t, int64(1726121321), h.Time.Unix()) + } + } +} diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index 0577f6d2d11..3e2d7520237 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -585,18 +585,17 @@ type Orderbook struct { // Trade represents market trade. type Trade struct { - ID int64 `json:"id,string"` - TradingTime types.Time `json:"create_time"` - CreateTimeMs types.Time `json:"create_time_ms"` - OrderID string `json:"order_id"` - Side string `json:"side"` - Role string `json:"role"` - Amount types.Number `json:"amount"` - Price types.Number `json:"price"` - Fee types.Number `json:"fee"` - FeeCurrency string `json:"fee_currency"` - PointFee string `json:"point_fee"` - GtFee string `json:"gt_fee"` + ID int64 `json:"id,string"` + CreateTime types.Time `json:"create_time_ms"` + OrderID string `json:"order_id"` + Side string `json:"side"` + Role string `json:"role"` + Amount types.Number `json:"amount"` + Price types.Number `json:"price"` + Fee types.Number `json:"fee"` + FeeCurrency string `json:"fee_currency"` + PointFee string `json:"point_fee"` + GtFee string `json:"gt_fee"` } // Candlestick represents candlestick data point detail. @@ -679,17 +678,22 @@ type FuturesContract struct { OrdersLimit int64 `json:"orders_limit"` TradeID int64 `json:"trade_id"` OrderbookID int64 `json:"orderbook_id"` + EnableBonus bool `json:"enable_bonus"` + EnableCredit bool `json:"enable_credit"` + CreateTime types.Time `json:"create_time"` + FundingCapRatio types.Number `json:"funding_cap_ratio"` + VoucherLeverage types.Number `json:"voucher_leverage"` } // TradingHistoryItem represents futures trading history item. type TradingHistoryItem struct { ID int64 `json:"id"` - CreateTime types.Time `json:"create_time"` + CreateTime types.Time `json:"create_time_ms"` Contract string `json:"contract"` Text string `json:"text"` Size float64 `json:"size"` Price types.Number `json:"price"` - // Added for Derived market trade history datas. + // Added for Derived market trade history data Fee types.Number `json:"fee"` PointFee types.Number `json:"point_fee"` Role string `json:"role"` @@ -704,9 +708,7 @@ type FuturesCandlestick struct { LowestPrice types.Number `json:"l"` OpenPrice types.Number `json:"o"` Sum types.Number `json:"sum"` // Trading volume (unit: Quote currency) - - // Added for websocket push data - Name string `json:"n,omitempty"` + Name string `json:"n,omitempty"` } // FuturesPremiumIndexKLineResponse represents premium index K-Line information. @@ -836,15 +838,6 @@ type DeliveryContract struct { InDelisting bool `json:"in_delisting"` } -// DeliveryTradingHistory represents futures trading history -type DeliveryTradingHistory struct { - ID int64 `json:"id"` - CreateTime types.Time `json:"create_time"` - Contract string `json:"contract"` - Size float64 `json:"size"` - Price types.Number `json:"price"` -} - // OptionUnderlying represents option underlying and it's index price. type OptionUnderlying struct { Name string `json:"name"` @@ -1223,8 +1216,7 @@ type AccountBalanceInformation struct { // MarginAccountBalanceChangeInfo represents margin account balance type MarginAccountBalanceChangeInfo struct { ID string `json:"id"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time types.Time `json:"time_ms"` Currency string `json:"currency"` CurrencyPair string `json:"currency_pair"` AmountChanged string `json:"change"` @@ -1393,10 +1385,8 @@ type SpotOrder struct { Succeeded bool `json:"succeeded"` ErrorLabel string `json:"label,omitempty"` Message string `json:"message,omitempty"` - CreateTime types.Time `json:"create_time,omitempty"` - CreateTimeMs types.Time `json:"create_time_ms,omitempty"` - UpdateTime types.Time `json:"update_time,omitempty"` - UpdateTimeMs types.Time `json:"update_time_ms,omitempty"` + CreateTime types.Time `json:"create_time_ms,omitempty"` + UpdateTime types.Time `json:"update_time_ms,omitempty"` CurrencyPair string `json:"currency_pair,omitempty"` Status string `json:"status,omitempty"` Type string `json:"type,omitempty"` @@ -1457,8 +1447,7 @@ type CancelOrderByIDResponse struct { // SpotPersonalTradeHistory represents personal trading history. type SpotPersonalTradeHistory struct { TradeID string `json:"id"` - CreateTime types.Time `json:"create_time"` - CreateTimeMs types.Time `json:"create_time_ms"` + CreateTime types.Time `json:"create_time_ms"` CurrencyPair string `json:"currency_pair"` OrderID string `json:"order_id"` Side string `json:"side"` @@ -2006,11 +1995,10 @@ type WsEventResponse struct { } } -// WsResponse represents generalized websocket push data from the server. -type WsResponse struct { +// WSResponse represents generalized websocket push data from the server. +type WSResponse struct { ID int64 `json:"id"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time time.Time `json:"time"` Channel string `json:"channel"` Event string `json:"event"` Result json.RawMessage `json:"result"` @@ -2033,8 +2021,7 @@ type WsTicker struct { // WsTrade represents a websocket push data response for a trade type WsTrade struct { ID int64 `json:"id"` - CreateTime types.Time `json:"create_time"` - CreateTimeMs types.Time `json:"create_time_ms"` + CreateTime types.Time `json:"create_time_ms"` Side string `json:"side"` CurrencyPair currency.Pair `json:"currency_pair"` Amount types.Number `json:"amount"` @@ -2054,7 +2041,7 @@ type WsCandlesticks struct { // WsOrderbookTickerData represents the websocket orderbook best bid or best ask push data type WsOrderbookTickerData struct { - UpdateTimeMS types.Time `json:"t"` + UpdateTime types.Time `json:"t"` UpdateOrderID int64 `json:"u"` CurrencyPair currency.Pair `json:"s"` BestBidPrice types.Number `json:"b"` @@ -2065,9 +2052,7 @@ type WsOrderbookTickerData struct { // WsOrderbookUpdate represents websocket orderbook update push data type WsOrderbookUpdate struct { - UpdateTimeMs types.Time `json:"t"` - IgnoreField string `json:"e"` - UpdateTime types.Time `json:"E"` + UpdateTime types.Time `json:"t"` CurrencyPair currency.Pair `json:"s"` FirstOrderbookUpdatedID int64 `json:"U"` // First update order book id in this event since last update LastOrderbookUpdatedID int64 `json:"u"` @@ -2077,7 +2062,7 @@ type WsOrderbookUpdate struct { // WsOrderbookSnapshot represents a websocket orderbook snapshot push data type WsOrderbookSnapshot struct { - UpdateTimeMs types.Time `json:"t"` + UpdateTime types.Time `json:"t"` LastUpdateID int64 `json:"lastUpdateId"` CurrencyPair currency.Pair `json:"s"` Bids [][2]types.Number `json:"bids"` @@ -2110,10 +2095,8 @@ type WsSpotOrder struct { RebatedFee string `json:"rebated_fee,omitempty"` RebatedFeeCurrency string `json:"rebated_fee_currency,omitempty"` Event string `json:"event"` - CreateTime types.Time `json:"create_time,omitempty"` - CreateTimeMs types.Time `json:"create_time_ms,omitempty"` - UpdateTime types.Time `json:"update_time,omitempty"` - UpdateTimeMs types.Time `json:"update_time_ms,omitempty"` + CreateTime types.Time `json:"create_time_ms,omitempty"` + UpdateTime types.Time `json:"update_time_ms,omitempty"` } // WsUserPersonalTrade represents a user's personal trade pushed through the websocket connection. @@ -2122,8 +2105,7 @@ type WsUserPersonalTrade struct { UserID int64 `json:"user_id"` OrderID string `json:"order_id"` CurrencyPair currency.Pair `json:"currency_pair"` - CreateTime types.Time `json:"create_time"` - CreateTimeMs types.Time `json:"create_time_ms"` + CreateTime types.Time `json:"create_time_ms"` Side string `json:"side"` Amount types.Number `json:"amount"` Role string `json:"role"` @@ -2136,19 +2118,17 @@ type WsUserPersonalTrade struct { // WsSpotBalance represents a spot balance. type WsSpotBalance struct { - Timestamp types.Time `json:"timestamp"` - TimestampMs types.Time `json:"timestamp_ms"` - User string `json:"user"` - Currency string `json:"currency"` - Change types.Number `json:"change"` - Total types.Number `json:"total"` - Available types.Number `json:"available"` + Timestamp types.Time `json:"timestamp_ms"` + User string `json:"user"` + Currency string `json:"currency"` + Change types.Number `json:"change"` + Total types.Number `json:"total"` + Available types.Number `json:"available"` } // WsMarginBalance represents margin account balance push data type WsMarginBalance struct { - Timestamp types.Time `json:"timestamp"` - TimestampMs types.Time `json:"timestamp_ms"` + Timestamp types.Time `json:"timestamp_ms"` User string `json:"user"` CurrencyPair string `json:"currency_pair"` Currency string `json:"currency"` @@ -2161,24 +2141,22 @@ type WsMarginBalance struct { // WsFundingBalance represents funding balance push data. type WsFundingBalance struct { - Timestamp types.Time `json:"timestamp"` - TimestampMs types.Time `json:"timestamp_ms"` - User string `json:"user"` - Currency string `json:"currency"` - Change string `json:"change"` - Freeze string `json:"freeze"` - Lent string `json:"lent"` + Timestamp types.Time `json:"timestamp_ms"` + User string `json:"user"` + Currency string `json:"currency"` + Change string `json:"change"` + Freeze string `json:"freeze"` + Lent string `json:"lent"` } // WsCrossMarginBalance represents a cross margin balance detail type WsCrossMarginBalance struct { - Timestamp types.Time `json:"timestamp"` - TimestampMs types.Time `json:"timestamp_ms"` - User string `json:"user"` - Currency string `json:"currency"` - Change string `json:"change"` - Total types.Number `json:"total"` - Available types.Number `json:"available"` + Timestamp types.Time `json:"timestamp_ms"` + User string `json:"user"` + Currency string `json:"currency"` + Change string `json:"change"` + Total types.Number `json:"total"` + Available types.Number `json:"available"` } // WsCrossMarginLoan represents a cross margin loan push data @@ -2216,17 +2194,16 @@ type WsFutureTicker struct { // WsFuturesTrades represents a list of trades push data type WsFuturesTrades struct { - Size float64 `json:"size"` - ID int64 `json:"id"` - CreateTime types.Time `json:"create_time"` - CreateTimeMs types.Time `json:"create_time_ms"` - Price types.Number `json:"price"` - Contract currency.Pair `json:"contract"` + Size float64 `json:"size"` + ID int64 `json:"id"` + CreateTime types.Time `json:"create_time_ms"` + Price types.Number `json:"price"` + Contract currency.Pair `json:"contract"` } // WsFuturesOrderbookTicker represents the orderbook ticker push data type WsFuturesOrderbookTicker struct { - TimestampMs types.Time `json:"t"` + Timestamp types.Time `json:"t"` UpdateID int64 `json:"u"` CurrencyPair string `json:"s"` BestBidPrice types.Number `json:"b"` @@ -2237,7 +2214,7 @@ type WsFuturesOrderbookTicker struct { // WsFuturesAndOptionsOrderbookUpdate represents futures and options account orderbook update push data type WsFuturesAndOptionsOrderbookUpdate struct { - TimestampInMs types.Time `json:"t"` + Timestamp types.Time `json:"t"` ContractName currency.Pair `json:"s"` FirstUpdatedID int64 `json:"U"` LastUpdatedID int64 `json:"u"` @@ -2253,10 +2230,10 @@ type WsFuturesAndOptionsOrderbookUpdate struct { // WsFuturesOrderbookSnapshot represents a futures orderbook snapshot push data type WsFuturesOrderbookSnapshot struct { - TimestampInMs types.Time `json:"t"` - Contract currency.Pair `json:"contract"` - OrderbookID int64 `json:"id"` - Asks []struct { + Timestamp types.Time `json:"t"` + Contract currency.Pair `json:"contract"` + OrderbookID int64 `json:"id"` + Asks []struct { Price types.Number `json:"p"` Size float64 `json:"s"` } `json:"asks"` @@ -2277,12 +2254,10 @@ type WsFuturesOrderbookUpdateEvent struct { // WsFuturesOrder represents futures order type WsFuturesOrder struct { Contract currency.Pair `json:"contract"` - CreateTime types.Time `json:"create_time"` - CreateTimeMs types.Time `json:"create_time_ms"` + CreateTime types.Time `json:"create_time_ms"` FillPrice float64 `json:"fill_price"` FinishAs string `json:"finish_as"` - FinishTime types.Time `json:"finish_time"` - FinishTimeMs types.Time `json:"finish_time_ms"` + FinishTime types.Time `json:"finish_time_ms"` Iceberg int64 `json:"iceberg"` ID int64 `json:"id"` IsClose bool `json:"is_close"` @@ -2303,17 +2278,16 @@ type WsFuturesOrder struct { // WsFuturesUserTrade represents a futures account user trade push data type WsFuturesUserTrade struct { - ID string `json:"id"` - CreateTime types.Time `json:"create_time"` - CreateTimeMs types.Time `json:"create_time_ms"` - Contract currency.Pair `json:"contract"` - OrderID string `json:"order_id"` - Size float64 `json:"size"` - Price types.Number `json:"price"` - Role string `json:"role"` - Text string `json:"text"` - Fee float64 `json:"fee"` - PointFee int64 `json:"point_fee"` + ID string `json:"id"` + CreateTime types.Time `json:"create_time_ms"` + Contract currency.Pair `json:"contract"` + OrderID string `json:"order_id"` + Size float64 `json:"size"` + Price types.Number `json:"price"` + Role string `json:"role"` + Text string `json:"text"` + Fee float64 `json:"fee"` + PointFee int64 `json:"point_fee"` } // WsFuturesLiquidationNotification represents a liquidation notification push data @@ -2328,8 +2302,7 @@ type WsFuturesLiquidationNotification struct { OrderID int64 `json:"order_id"` OrderPrice float64 `json:"order_price"` Size float64 `json:"size"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time types.Time `json:"time_ms"` Contract string `json:"contract"` User string `json:"user"` } @@ -2340,8 +2313,7 @@ type WsFuturesAutoDeleveragesNotification struct { FillPrice float64 `json:"fill_price"` PositionSize int64 `json:"position_size"` TradeSize int64 `json:"trade_size"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time types.Time `json:"time_ms"` Contract string `json:"contract"` User string `json:"user"` } @@ -2352,8 +2324,7 @@ type WsPositionClose struct { ProfitAndLoss float64 `json:"pnl,omitempty"` Side string `json:"side"` Text string `json:"text"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time types.Time `json:"time_ms"` User string `json:"user"` // Added in options close position push datas @@ -2366,8 +2337,7 @@ type WsBalance struct { Balance float64 `json:"balance"` Change float64 `json:"change"` Text string `json:"text"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time types.Time `json:"time_ms"` Type string `json:"type"` User string `json:"user"` } @@ -2380,8 +2350,7 @@ type WsFuturesReduceRiskLimitNotification struct { LiqPrice float64 `json:"liq_price"` MaintenanceRate float64 `json:"maintenance_rate"` RiskLimit int64 `json:"risk_limit"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time types.Time `json:"time_ms"` User string `json:"user"` } @@ -2403,8 +2372,7 @@ type WsFuturesPosition struct { RealisedPoint float64 `json:"realised_point"` RiskLimit float64 `json:"risk_limit"` Size float64 `json:"size"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time types.Time `json:"time_ms"` User string `json:"user"` } @@ -2453,31 +2421,26 @@ type WsOptionUnderlyingTicker struct { // WsOptionsTrades represents options trades for websocket push data. type WsOptionsTrades struct { ID int64 `json:"id"` - CreateTime types.Time `json:"create_time"` + CreateTime types.Time `json:"create_time_ms"` Contract currency.Pair `json:"contract"` Size float64 `json:"size"` Price float64 `json:"price"` - - // Added in options websocket push data - CreateTimeMs types.Time `json:"create_time_ms"` - Underlying string `json:"underlying"` - IsCall bool `json:"is_call"` // added in underlying trades + Underlying string `json:"underlying"` + IsCall bool `json:"is_call"` // added in underlying trades } // WsOptionsUnderlyingPrice represents the underlying price. type WsOptionsUnderlyingPrice struct { - Underlying string `json:"underlying"` - Price float64 `json:"price"` - UpdateTime types.Time `json:"time"` - UpdateTimeMs types.Time `json:"time_ms"` + Underlying string `json:"underlying"` + Price float64 `json:"price"` + UpdateTime types.Time `json:"time_ms"` } // WsOptionsMarkPrice represents options mark price push data. type WsOptionsMarkPrice struct { - Contract string `json:"contract"` - Price float64 `json:"price"` - UpdateTimeMs types.Time `json:"time_ms"` - UpdateTime types.Time `json:"time"` + Contract string `json:"contract"` + Price float64 `json:"price"` + UpdateTime types.Time `json:"time_ms"` } // WsOptionsSettlement represents a options settlement push data. @@ -2492,8 +2455,7 @@ type WsOptionsSettlement struct { TradeID int64 `json:"trade_id"` TradeSize int64 `json:"trade_size"` Underlying string `json:"underlying"` - UpdateTime types.Time `json:"time"` - UpdateTimeMs types.Time `json:"time_ms"` + UpdateTime types.Time `json:"time_ms"` } // WsOptionsContract represents an option contract push data. @@ -2521,8 +2483,7 @@ type WsOptionsContract struct { Tag string `json:"tag"` TakerFeeRate float64 `json:"taker_fee_rate"` Underlying string `json:"underlying"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time types.Time `json:"time_ms"` } // WsOptionsContractCandlestick represents an options contract candlestick push data. @@ -2565,42 +2526,40 @@ type WsOptionsOrderbookSnapshot struct { // WsOptionsOrder represents options order push data. type WsOptionsOrder struct { - ID int64 `json:"id"` - Contract currency.Pair `json:"contract"` - CreateTime types.Time `json:"create_time"` - FillPrice float64 `json:"fill_price"` - FinishAs string `json:"finish_as"` - Iceberg float64 `json:"iceberg"` - IsClose bool `json:"is_close"` - IsLiq bool `json:"is_liq"` - IsReduceOnly bool `json:"is_reduce_only"` - Left float64 `json:"left"` - Mkfr float64 `json:"mkfr"` - Price float64 `json:"price"` - Refr float64 `json:"refr"` - Refu float64 `json:"refu"` - Size float64 `json:"size"` - Status string `json:"status"` - Text string `json:"text"` - Tif string `json:"tif"` - Tkfr float64 `json:"tkfr"` - Underlying string `json:"underlying"` - User string `json:"user"` - CreationTime types.Time `json:"time"` - CreationTimeMs types.Time `json:"time_ms"` + ID int64 `json:"id"` + Contract currency.Pair `json:"contract"` + CreateTime types.Time `json:"create_time"` + FillPrice float64 `json:"fill_price"` + FinishAs string `json:"finish_as"` + Iceberg float64 `json:"iceberg"` + IsClose bool `json:"is_close"` + IsLiq bool `json:"is_liq"` + IsReduceOnly bool `json:"is_reduce_only"` + Left float64 `json:"left"` + Mkfr float64 `json:"mkfr"` + Price float64 `json:"price"` + Refr float64 `json:"refr"` + Refu float64 `json:"refu"` + Size float64 `json:"size"` + Status string `json:"status"` + Text string `json:"text"` + Tif string `json:"tif"` + Tkfr float64 `json:"tkfr"` + Underlying string `json:"underlying"` + User string `json:"user"` + CreationTime types.Time `json:"time_ms"` } // WsOptionsUserTrade represents user's personal trades of option account. type WsOptionsUserTrade struct { - ID string `json:"id"` - Underlying string `json:"underlying"` - OrderID string `json:"order"` - Contract currency.Pair `json:"contract"` - CreateTime types.Time `json:"create_time"` - CreateTimeMs types.Time `json:"create_time_ms"` - Price types.Number `json:"price"` - Role string `json:"role"` - Size float64 `json:"size"` + ID string `json:"id"` + Underlying string `json:"underlying"` + OrderID string `json:"order"` + Contract currency.Pair `json:"contract"` + CreateTime types.Time `json:"create_time_ms"` + Price types.Number `json:"price"` + Role string `json:"role"` + Size float64 `json:"size"` } // WsOptionsLiquidates represents the liquidates push data of option account. @@ -2609,8 +2568,7 @@ type WsOptionsLiquidates struct { InitMargin float64 `json:"init_margin"` MaintMargin float64 `json:"maint_margin"` OrderMargin float64 `json:"order_margin"` - Time types.Time `json:"time"` - TimeMs types.Time `json:"time_ms"` + Time types.Time `json:"time_ms"` } // WsOptionsUserSettlement represents user's personal settlements push data of options account. @@ -2623,19 +2581,17 @@ type WsOptionsUserSettlement struct { Size float64 `json:"size"` StrikePrice float64 `json:"strike_price"` Underlying string `json:"underlying"` - SettleTime types.Time `json:"time"` - SettleTimeMs types.Time `json:"time_ms"` + SettleTime types.Time `json:"time_ms"` } // WsOptionsPosition represents positions push data for options account. type WsOptionsPosition struct { - EntryPrice float64 `json:"entry_price"` - RealisedPnl float64 `json:"realised_pnl"` - Size float64 `json:"size"` - Contract string `json:"contract"` - User string `json:"user"` - UpdateTime types.Time `json:"time"` - UpdateTimeMs types.Time `json:"time_ms"` + EntryPrice float64 `json:"entry_price"` + RealisedPnl float64 `json:"realised_pnl"` + Size float64 `json:"size"` + Contract string `json:"contract"` + User string `json:"user"` + UpdateTime types.Time `json:"time_ms"` } // InterSubAccountTransferParams represents parameters to transfer funds between sub-accounts. diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index ea53f12029a..5849e72d706 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -15,6 +15,7 @@ import ( "time" "github.com/Masterminds/sprig/v3" + "github.com/buger/jsonparser" "github.com/gorilla/websocket" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/currency" @@ -166,8 +167,8 @@ func (g *Gateio) generateWsSignature(secret, event, channel string, t int64) (st // WsHandleSpotData handles spot data func (g *Gateio) WsHandleSpotData(_ context.Context, respRaw []byte) error { - var push WsResponse - if err := json.Unmarshal(respRaw, &push); err != nil { + push, err := parseWSHeader(respRaw) + if err != nil { return err } @@ -181,17 +182,17 @@ func (g *Gateio) WsHandleSpotData(_ context.Context, respRaw []byte) error { switch push.Channel { // TODO: Convert function params below to only use push.Result case spotTickerChannel: - return g.processTicker(push.Result, push.TimeMs.Time()) + return g.processTicker(push.Result, push.Time) case spotTradesChannel: return g.processTrades(push.Result) case spotCandlesticksChannel: return g.processCandlestick(push.Result) case spotOrderbookTickerChannel: - return g.processOrderbookTicker(push.Result, push.TimeMs.Time()) + return g.processOrderbookTicker(push.Result, push.Time) case spotOrderbookUpdateChannel: - return g.processOrderbookUpdate(push.Result, push.TimeMs.Time()) + return g.processOrderbookUpdate(push.Result, push.Time) case spotOrderbookChannel: - return g.processOrderbookSnapshot(push.Result, push.TimeMs.Time()) + return g.processOrderbookSnapshot(push.Result, push.Time) case spotOrdersChannel: return g.processSpotOrders(respRaw) case spotUserTradesChannel: @@ -216,6 +217,45 @@ func (g *Gateio) WsHandleSpotData(_ context.Context, respRaw []byte) error { return nil } +func parseWSHeader(msg []byte) (r *WSResponse, errs error) { + r = &WSResponse{} + paths := [][]string{{"time_ms"}, {"time"}, {"channel"}, {"event"}, {"request_id"}, {"id"}, {"result"}} + jsonparser.EachKey(msg, func(idx int, v []byte, _ jsonparser.ValueType, _ error) { + switch idx { + case 0: // time_ms + if ts, err := strconv.ParseInt(string(v), 10, 64); err != nil { + errs = common.AppendError(errs, fmt.Errorf("%w parsing `time_ms`", err)) + } else { + r.Time = time.UnixMilli(ts) + } + case 1: // time + if r.Time.IsZero() { + if ts, err := strconv.ParseInt(string(v), 10, 64); err != nil { + errs = common.AppendError(errs, fmt.Errorf("%w parsing `time`", err)) + } else { + r.Time = time.Unix(ts, 0) + } + } + case 2: + r.Channel = string(v) + case 3: + r.Event = string(v) + case 4: + r.RequestID = string(v) + case 5: + if id, err := strconv.ParseInt(string(v), 10, 64); err != nil { + errs = common.AppendError(errs, fmt.Errorf("%w parsing `id`", err)) + } else { + r.ID = id + } + case 6: + r.Result = json.RawMessage(v) + } + }, paths...) + + return r, errs +} + func (g *Gateio) processTicker(incoming []byte, pushTime time.Time) error { var data WsTicker if err := json.Unmarshal(incoming, &data); err != nil { @@ -262,7 +302,7 @@ func (g *Gateio) processTrades(incoming []byte) error { for _, a := range standardMarginAssetTypes { if enabled, _ := g.CurrencyPairs.IsPairEnabled(data.CurrencyPair, a); enabled { if err := g.Websocket.Trade.Update(saveTradeData, trade.Data{ - Timestamp: data.CreateTimeMs.Time(), + Timestamp: data.CreateTime.Time(), CurrencyPair: data.CurrencyPair, AssetType: a, Exchange: g.Name, @@ -323,7 +363,7 @@ func (g *Gateio) processOrderbookTicker(incoming []byte, updatePushedAt time.Tim Exchange: g.Name, Pair: data.CurrencyPair, Asset: asset.Spot, - LastUpdated: data.UpdateTimeMS.Time(), + LastUpdated: data.UpdateTime.Time(), UpdatePushedAt: updatePushedAt, Bids: []orderbook.Tranche{{Price: data.BestBidPrice.Float64(), Amount: data.BestBidAmount.Float64()}}, Asks: []orderbook.Tranche{{Price: data.BestAskPrice.Float64(), Amount: data.BestAskAmount.Float64()}}, @@ -378,7 +418,7 @@ func (g *Gateio) processOrderbookUpdate(incoming []byte, updatePushedAt time.Tim for _, a := range enabledAssets { if err := g.Websocket.Orderbook.Update(&orderbook.Update{ - UpdateTime: data.UpdateTimeMs.Time(), + UpdateTime: data.UpdateTime.Time(), UpdatePushedAt: updatePushedAt, Pair: data.CurrencyPair, Asset: a, @@ -415,7 +455,7 @@ func (g *Gateio) processOrderbookSnapshot(incoming []byte, updatePushedAt time.T Exchange: g.Name, Pair: data.CurrencyPair, Asset: a, - LastUpdated: data.UpdateTimeMs.Time(), + LastUpdated: data.UpdateTime.Time(), UpdatePushedAt: updatePushedAt, Bids: bids, Asks: asks, @@ -463,8 +503,8 @@ func (g *Gateio) processSpotOrders(data []byte) error { AssetType: a, Price: resp.Result[x].Price.Float64(), ExecutedAmount: resp.Result[x].Amount.Float64() - resp.Result[x].Left.Float64(), - Date: resp.Result[x].CreateTimeMs.Time(), - LastUpdated: resp.Result[x].UpdateTimeMs.Time(), + Date: resp.Result[x].CreateTime.Time(), + LastUpdated: resp.Result[x].UpdateTime.Time(), } } g.Websocket.DataHandler <- details @@ -493,7 +533,7 @@ func (g *Gateio) processUserPersonalTrades(data []byte) error { return err } fills[x] = fill.Data{ - Timestamp: resp.Result[x].CreateTimeMs.Time(), + Timestamp: resp.Result[x].CreateTime.Time(), Exchange: g.Name, CurrencyPair: resp.Result[x].CurrencyPair, Side: side, diff --git a/exchanges/gateio/gateio_websocket_futures.go b/exchanges/gateio/gateio_websocket_futures.go index ba393277553..628824a08ca 100644 --- a/exchanges/gateio/gateio_websocket_futures.go +++ b/exchanges/gateio/gateio_websocket_futures.go @@ -149,8 +149,7 @@ func (g *Gateio) FuturesUnsubscribe(ctx context.Context, conn stream.Connection, // WsHandleFuturesData handles futures websocket data func (g *Gateio) WsHandleFuturesData(_ context.Context, respRaw []byte, a asset.Item) error { - var push WsResponse - err := json.Unmarshal(respRaw, &push) + push, err := parseWSHeader(respRaw) if err != nil { return err } @@ -168,7 +167,7 @@ func (g *Gateio) WsHandleFuturesData(_ context.Context, respRaw []byte, a asset. case futuresTradesChannel: return g.processFuturesTrades(respRaw, a) case futuresOrderbookChannel: - return g.processFuturesOrderbookSnapshot(push.Event, push.Result, a, push.TimeMs.Time()) + return g.processFuturesOrderbookSnapshot(push.Event, push.Result, a, push.Time) case futuresOrderbookTickerChannel: return g.processFuturesOrderbookTicker(push.Result) case futuresOrderbookUpdateChannel: @@ -353,7 +352,7 @@ func (g *Gateio) processFuturesTrades(data []byte, assetType asset.Item) error { trades := make([]trade.Data, len(resp.Result)) for x := range resp.Result { trades[x] = trade.Data{ - Timestamp: resp.Result[x].CreateTimeMs.Time(), + Timestamp: resp.Result[x].CreateTime.Time(), CurrencyPair: resp.Result[x].Contract, AssetType: assetType, Exchange: g.Name, @@ -439,7 +438,7 @@ func (g *Gateio) processFuturesAndOptionsOrderbookUpdate(incoming []byte, assetT } } updates := orderbook.Update{ - UpdateTime: data.TimestampInMs.Time(), + UpdateTime: data.Timestamp.Time(), Pair: data.ContractName, Asset: assetType, } @@ -470,7 +469,7 @@ func (g *Gateio) processFuturesOrderbookSnapshot(event string, incoming []byte, Asset: assetType, Exchange: g.Name, Pair: data.Contract, - LastUpdated: data.TimestampInMs.Time(), + LastUpdated: data.Timestamp.Time(), UpdatePushedAt: updatePushedAt, VerifyOrderbook: g.CanVerifyOrderbook, } @@ -574,13 +573,13 @@ func (g *Gateio) processFuturesOrdersPushData(data []byte, assetType asset.Item) OrderID: strconv.FormatInt(resp.Result[x].ID, 10), Status: status, Pair: resp.Result[x].Contract, - LastUpdated: resp.Result[x].FinishTimeMs.Time(), - Date: resp.Result[x].CreateTimeMs.Time(), + LastUpdated: resp.Result[x].FinishTime.Time(), + Date: resp.Result[x].CreateTime.Time(), ExecutedAmount: resp.Result[x].Size - resp.Result[x].Left, Price: resp.Result[x].Price, AssetType: assetType, AccountID: resp.Result[x].User, - CloseTime: resp.Result[x].FinishTimeMs.Time(), + CloseTime: resp.Result[x].FinishTime.Time(), } } return orderDetails, nil @@ -604,7 +603,7 @@ func (g *Gateio) procesFuturesUserTrades(data []byte, assetType asset.Item) erro fills := make([]fill.Data, len(resp.Result)) for x := range resp.Result { fills[x] = fill.Data{ - Timestamp: resp.Result[x].CreateTimeMs.Time(), + Timestamp: resp.Result[x].CreateTime.Time(), Exchange: g.Name, CurrencyPair: resp.Result[x].Contract, OrderID: resp.Result[x].OrderID, diff --git a/exchanges/gateio/gateio_websocket_option.go b/exchanges/gateio/gateio_websocket_option.go index 091a9ad1f0f..7ec39737e6e 100644 --- a/exchanges/gateio/gateio_websocket_option.go +++ b/exchanges/gateio/gateio_websocket_option.go @@ -294,8 +294,7 @@ func (g *Gateio) OptionsUnsubscribe(ctx context.Context, conn stream.Connection, // WsHandleOptionsData handles options websocket data func (g *Gateio) WsHandleOptionsData(_ context.Context, respRaw []byte) error { - var push WsResponse - err := json.Unmarshal(respRaw, &push) + push, err := parseWSHeader(respRaw) if err != nil { return err } @@ -327,7 +326,7 @@ func (g *Gateio) WsHandleOptionsData(_ context.Context, respRaw []byte) error { optionsUnderlyingCandlesticksChannel: return g.processOptionsCandlestickPushData(respRaw) case optionsOrderbookChannel: - return g.processOptionsOrderbookSnapshotPushData(push.Event, push.Result, push.Time.Time()) + return g.processOptionsOrderbookSnapshotPushData(push.Event, push.Result, push.Time) case optionsOrderbookTickerChannel: return g.processOrderbookTickerPushData(respRaw) case optionsOrderbookUpdateChannel: @@ -402,7 +401,7 @@ func (g *Gateio) processOptionsTradesPushData(data []byte) error { trades := make([]trade.Data, len(resp.Result)) for x := range resp.Result { trades[x] = trade.Data{ - Timestamp: resp.Result[x].CreateTimeMs.Time(), + Timestamp: resp.Result[x].CreateTime.Time(), CurrencyPair: resp.Result[x].Contract, AssetType: asset.Options, Exchange: g.Name, @@ -606,7 +605,7 @@ func (g *Gateio) processOptionsOrderPushData(data []byte) error { OrderID: strconv.FormatInt(resp.Result[x].ID, 10), Status: status, Pair: resp.Result[x].Contract, - Date: resp.Result[x].CreationTimeMs.Time(), + Date: resp.Result[x].CreationTime.Time(), ExecutedAmount: resp.Result[x].Size - resp.Result[x].Left, Price: resp.Result[x].Price, AssetType: asset.Options, @@ -634,7 +633,7 @@ func (g *Gateio) processOptionsUserTradesPushData(data []byte) error { fills := make([]fill.Data, len(resp.Result)) for x := range resp.Result { fills[x] = fill.Data{ - Timestamp: resp.Result[x].CreateTimeMs.Time(), + Timestamp: resp.Result[x].CreateTime.Time(), Exchange: g.Name, CurrencyPair: resp.Result[x].Contract, OrderID: resp.Result[x].OrderID, diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index 9d9929f6e56..239e3179245 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -945,18 +945,16 @@ func (g *Gateio) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.I var resp []trade.Data switch a { case asset.Spot, asset.Margin, asset.CrossMargin: - var tradeData []Trade if p.IsEmpty() { return nil, currency.ErrCurrencyPairEmpty } - tradeData, err = g.GetMarketTrades(ctx, p, 0, "", false, time.Time{}, time.Time{}, 0) + tradeData, err := g.GetMarketTrades(ctx, p, 0, "", false, time.Time{}, time.Time{}, 0) if err != nil { return nil, err } resp = make([]trade.Data, len(tradeData)) for i := range tradeData { - var side order.Side - side, err = order.StringToOrderSide(tradeData[i].Side) + side, err := order.StringToOrderSide(tradeData[i].Side) if err != nil { return nil, err } @@ -968,17 +966,15 @@ func (g *Gateio) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.I Side: side, Price: tradeData[i].Price.Float64(), Amount: tradeData[i].Amount.Float64(), - Timestamp: tradeData[i].CreateTimeMs.Time(), + Timestamp: tradeData[i].CreateTime.Time(), } } case asset.Futures: - var settle currency.Code - settle, err = getSettlementFromCurrency(p) + settle, err := getSettlementFromCurrency(p) if err != nil { return nil, err } - var futuresTrades []TradingHistoryItem - futuresTrades, err = g.GetFuturesTradingHistory(ctx, settle, p, 0, 0, "", time.Time{}, time.Time{}) + futuresTrades, err := g.GetFuturesTradingHistory(ctx, settle, p, 0, 0, "", time.Time{}, time.Time{}) if err != nil { return nil, err } @@ -995,13 +991,11 @@ func (g *Gateio) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.I } } case asset.DeliveryFutures: - var settle currency.Code - settle, err = getSettlementFromCurrency(p) + settle, err := getSettlementFromCurrency(p) if err != nil { return nil, err } - var deliveryTrades []DeliveryTradingHistory - deliveryTrades, err = g.GetDeliveryTradingHistory(ctx, settle, "", p.Upper(), 0, time.Time{}, time.Time{}) + deliveryTrades, err := g.GetDeliveryTradingHistory(ctx, settle, "", p.Upper(), 0, time.Time{}, time.Time{}) if err != nil { return nil, err } @@ -1018,8 +1012,7 @@ func (g *Gateio) GetRecentTrades(ctx context.Context, p currency.Pair, a asset.I } } case asset.Options: - var trades []TradingHistoryItem - trades, err = g.GetOptionsTradeHistory(ctx, p.Upper(), "", 0, 0, time.Time{}, time.Time{}) + trades, err := g.GetOptionsTradeHistory(ctx, p.Upper(), "", 0, 0, time.Time{}, time.Time{}) if err != nil { return nil, err } @@ -1114,8 +1107,8 @@ func (g *Gateio) SubmitOrder(ctx context.Context, s *order.Submit) (*order.Submi response.Pair = s.Pair response.Date = sOrder.CreateTime.Time() response.ClientOrderID = sOrder.Text - response.Date = sOrder.CreateTimeMs.Time() - response.LastUpdated = sOrder.UpdateTimeMs.Time() + response.Date = sOrder.CreateTime.Time() + response.LastUpdated = sOrder.UpdateTime.Time() return response, nil case asset.Futures: // TODO: See https://www.gate.io/docs/developers/apiv4/en/#create-a-futures-order @@ -1497,8 +1490,8 @@ func (g *Gateio) GetOrderInfo(ctx context.Context, orderID string, pair currency Status: orderStatus, Price: spotOrder.Price.Float64(), ExecutedAmount: spotOrder.Amount.Float64() - spotOrder.Left.Float64(), - Date: spotOrder.CreateTimeMs.Time(), - LastUpdated: spotOrder.UpdateTimeMs.Time(), + Date: spotOrder.CreateTime.Time(), + LastUpdated: spotOrder.UpdateTime.Time(), }, nil case asset.Futures, asset.DeliveryFutures: var settle currency.Code @@ -1705,8 +1698,8 @@ func (g *Gateio) GetActiveOrders(ctx context.Context, req *order.MultiOrderReque RemainingAmount: spotOrders[x].Orders[y].Left.Float64(), Price: spotOrders[x].Orders[y].Price.Float64(), AverageExecutedPrice: spotOrders[x].Orders[y].AverageFillPrice.Float64(), - Date: spotOrders[x].Orders[y].CreateTimeMs.Time(), - LastUpdated: spotOrders[x].Orders[y].UpdateTimeMs.Time(), + Date: spotOrders[x].Orders[y].CreateTime.Time(), + LastUpdated: spotOrders[x].Orders[y].UpdateTime.Time(), Exchange: g.Name, AssetType: req.AssetType, ClientOrderID: spotOrders[x].Orders[y].Text, @@ -1840,7 +1833,7 @@ func (g *Gateio) GetOrderHistory(ctx context.Context, req *order.MultiOrderReque for x := range req.Pairs { fPair := req.Pairs[x].Format(format) var spotOrders []SpotPersonalTradeHistory - spotOrders, err = g.GateIOGetPersonalTradingHistory(ctx, fPair, req.FromOrderID, 0, 0, req.AssetType == asset.CrossMargin, req.StartTime, req.EndTime) + spotOrders, err = g.GetMySpotTradingHistory(ctx, fPair, req.FromOrderID, 0, 0, req.AssetType == asset.CrossMargin, req.StartTime, req.EndTime) if err != nil { return nil, err } @@ -1877,9 +1870,9 @@ func (g *Gateio) GetOrderHistory(ctx context.Context, req *order.MultiOrderReque } var futuresOrder []TradingHistoryItem if req.AssetType == asset.Futures { - futuresOrder, err = g.GetMyPersonalTradingHistory(ctx, settle, "", req.FromOrderID, fPair, 0, 0, 0) + futuresOrder, err = g.GetMyFuturesTradingHistory(ctx, settle, "", req.FromOrderID, fPair, 0, 0, 0) } else { - futuresOrder, err = g.GetDeliveryPersonalTradingHistory(ctx, settle, req.FromOrderID, fPair, 0, 0, 0, "") + futuresOrder, err = g.GetMyDeliveryTradingHistory(ctx, settle, req.FromOrderID, fPair, 0, 0, 0, "") } if err != nil { return nil, err @@ -1901,8 +1894,7 @@ func (g *Gateio) GetOrderHistory(ctx context.Context, req *order.MultiOrderReque case asset.Options: for x := range req.Pairs { fPair := req.Pairs[x].Format(format) - var optionOrders []OptionTradingHistory - optionOrders, err = g.GetOptionsPersonalTradingHistory(ctx, fPair.String(), fPair.Upper(), 0, 0, req.StartTime, req.EndTime) + optionOrders, err := g.GetMyOptionsTradingHistory(ctx, fPair.String(), fPair.Upper(), 0, 0, req.StartTime, req.EndTime) if err != nil { return nil, err } @@ -2127,28 +2119,26 @@ func (g *Gateio) GetFuturesContractDetails(ctx context.Context, item asset.Item) } return resp, nil case asset.DeliveryFutures: - var resp []futures.Contract contracts, err := g.GetAllDeliveryContracts(ctx, currency.USDT) if err != nil { return nil, err } - contractsToAdd := make([]futures.Contract, len(contracts)) - for j := range contracts { - var name, underlying currency.Pair - name, err = currency.NewPairFromString(contracts[j].Name) + resp := make([]futures.Contract, len(contracts)) + for i := range contracts { + name, err := currency.NewPairFromString(contracts[i].Name) if err != nil { return nil, err } - underlying, err = currency.NewPairFromString(contracts[j].Underlying) + underlying, err := currency.NewPairFromString(contracts[i].Underlying) if err != nil { return nil, err } - var ct futures.ContractType // no start information, inferring it based on contract type // gateio also reuses contracts for kline data, cannot use a lookup to see the first trade - var s, e time.Time - e = contracts[j].ExpireTime.Time() - switch contracts[j].Cycle { + var s time.Time + e := contracts[i].ExpireTime.Time() + ct := futures.LongDated + switch contracts[i].Cycle { case "WEEKLY": ct = futures.Weekly s = e.Add(-kline.OneWeek.Duration()) @@ -2161,10 +2151,8 @@ func (g *Gateio) GetFuturesContractDetails(ctx context.Context, item asset.Item) case "BI-QUARTERLY": ct = futures.HalfYearly s = e.Add(-kline.SixMonth.Duration()) - default: - ct = futures.LongDated } - contractsToAdd[j] = futures.Contract{ + resp[i] = futures.Contract{ Exchange: g.Name, Name: name, Underlying: underlying, @@ -2172,14 +2160,13 @@ func (g *Gateio) GetFuturesContractDetails(ctx context.Context, item asset.Item) StartDate: s, EndDate: e, SettlementType: futures.Linear, - IsActive: !contracts[j].InDelisting, + IsActive: !contracts[i].InDelisting, Type: ct, SettlementCurrencies: currency.Currencies{currency.USDT}, MarginCurrency: currency.Code{}, - Multiplier: contracts[j].QuantoMultiplier.Float64(), - MaxLeverage: contracts[j].LeverageMax.Float64(), + Multiplier: contracts[i].QuantoMultiplier.Float64(), + MaxLeverage: contracts[i].LeverageMax.Float64(), } - resp = append(resp, contractsToAdd...) } return resp, nil } @@ -2338,7 +2325,7 @@ func (g *Gateio) GetLatestFundingRates(ctx context.Context, r *fundingrate.Lates if err != nil { return nil, err } - contract, err := g.GetSingleContract(ctx, settle, fPair.String()) + contract, err := g.GetFuturesContract(ctx, settle, fPair.String()) if err != nil { return nil, err } @@ -2424,11 +2411,11 @@ func (g *Gateio) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]fut return nil, err } if !isEnabled { - return nil, fmt.Errorf("%w %v", asset.ErrNotEnabled, k[0].Pair()) + return nil, fmt.Errorf("%w: %v", currency.ErrPairNotEnabled, k[0].Pair()) } switch k[0].Asset { case asset.DeliveryFutures: - contractResp, err := g.GetSingleDeliveryContracts(ctx, currency.USDT, p) + contractResp, err := g.GetDeliveryContract(ctx, currency.USDT, p) if err != nil { return nil, err } @@ -2446,7 +2433,7 @@ func (g *Gateio) GetOpenInterest(ctx context.Context, k ...key.PairAsset) ([]fut }, nil case asset.Futures: for _, s := range settlementCurrencies { - contractResp, err := g.GetSingleContract(ctx, s, p.String()) + contractResp, err := g.GetFuturesContract(ctx, s, p.String()) if err != nil { continue } diff --git a/exchanges/subscription/template.go b/exchanges/subscription/template.go index c076a6c8fe9..d35d8a5e62e 100644 --- a/exchanges/subscription/template.go +++ b/exchanges/subscription/template.go @@ -129,7 +129,7 @@ func expandTemplate(e IExchange, s *Subscription, ap assetPairs, assets asset.It } buf := &bytes.Buffer{} - if err := t.Execute(buf, subCtx); err != nil { //nolint:govet // Shadow, or gocritic will complain sloppyReassign + if err := t.Execute(buf, subCtx); err != nil { return nil, err }