From 143e336c033f0b839d687cece50fbc0afac527ec Mon Sep 17 00:00:00 2001 From: Adrian Gallagher Date: Fri, 20 Dec 2024 12:07:46 +1100 Subject: [PATCH 01/21] exchanges: Fix GateIO/Coinbase test failures and OKX race (#1753) * exchanges: Fix gateio/coinbase test failures * OKX: Fix TestGetAssetsFromInstrumentTypeOrID race * GateIO: Add/improve comments * GateIO: Rid additional API call for FetchTradablePairs and provide additional context for test * GateIO: Prompt test reviewers to take action if BTC settlement is supported again --- exchanges/coinbasepro/coinbasepro_test.go | 36 +------ exchanges/gateio/gateio.go | 9 +- exchanges/gateio/gateio_test.go | 37 ++++--- exchanges/gateio/gateio_wrapper.go | 124 ++++++++++++---------- exchanges/okx/okx_test.go | 85 ++++----------- 5 files changed, 114 insertions(+), 177 deletions(-) diff --git a/exchanges/coinbasepro/coinbasepro_test.go b/exchanges/coinbasepro/coinbasepro_test.go index bf88705f5af..80d5e95e343 100644 --- a/exchanges/coinbasepro/coinbasepro_test.go +++ b/exchanges/coinbasepro/coinbasepro_test.go @@ -3,7 +3,6 @@ package coinbasepro import ( "context" "errors" - "log" "net/http" "os" "testing" @@ -14,7 +13,6 @@ import ( "github.com/stretchr/testify/require" "github.com/thrasher-corp/gocryptotrader/common" "github.com/thrasher-corp/gocryptotrader/common/convert" - "github.com/thrasher-corp/gocryptotrader/config" "github.com/thrasher-corp/gocryptotrader/core" "github.com/thrasher-corp/gocryptotrader/currency" exchange "github.com/thrasher-corp/gocryptotrader/exchanges" @@ -42,33 +40,11 @@ const ( canManipulateRealOrders = false ) -func TestMain(m *testing.M) { - c.SetDefaults() - cfg := config.GetConfig() - err := cfg.LoadConfig("../../testdata/configtest.json", true) - if err != nil { - log.Fatal("coinbasepro load config error", err) - } - gdxConfig, err := cfg.GetExchangeConfig("CoinbasePro") - if err != nil { - log.Fatal("coinbasepro Setup() init error") - } - gdxConfig.API.Credentials.Key = apiKey - gdxConfig.API.Credentials.Secret = apiSecret - gdxConfig.API.Credentials.ClientID = clientID - gdxConfig.API.AuthenticatedSupport = true - gdxConfig.API.AuthenticatedWebsocketSupport = true - c.Websocket = sharedtestvalues.NewTestWebsocket() - err = c.Setup(gdxConfig) - if err != nil { - log.Fatal("CoinbasePro setup error", err) - } - os.Exit(m.Run()) +func TestMain(_ *testing.M) { + os.Exit(0) // Disable full test suite until PR #1381 is merged as more API endpoints have been deprecated over time } func TestGetProducts(t *testing.T) { - t.Skip("API is deprecated") - _, err := c.GetProducts(context.Background()) if err != nil { t.Errorf("Coinbase, GetProducts() Error: %s", err) @@ -76,8 +52,6 @@ func TestGetProducts(t *testing.T) { } func TestGetOrderbook(t *testing.T) { - t.Skip("API is deprecated") - _, err := c.GetOrderbook(context.Background(), testPair.String(), 2) if err != nil { t.Error(err) @@ -89,8 +63,6 @@ func TestGetOrderbook(t *testing.T) { } func TestGetTicker(t *testing.T) { - t.Skip("API is deprecated") - _, err := c.GetTicker(context.Background(), testPair.String()) if err != nil { t.Error("GetTicker() error", err) @@ -105,8 +77,6 @@ func TestGetTrades(t *testing.T) { } func TestGetHistoricRatesGranularityCheck(t *testing.T) { - t.Skip("API is deprecated") - end := time.Now() start := end.Add(-time.Hour * 2) _, err := c.GetHistoricCandles(context.Background(), @@ -117,8 +87,6 @@ func TestGetHistoricRatesGranularityCheck(t *testing.T) { } func TestCoinbasePro_GetHistoricCandlesExtended(t *testing.T) { - t.Skip("API is deprecated") - start := time.Unix(1546300800, 0) end := time.Unix(1577836799, 0) diff --git a/exchanges/gateio/gateio.go b/exchanges/gateio/gateio.go index e4187d606fa..68ae378dee0 100644 --- a/exchanges/gateio/gateio.go +++ b/exchanges/gateio/gateio.go @@ -10,7 +10,6 @@ import ( "fmt" "net/http" "net/url" - "slices" "strconv" "strings" "time" @@ -2792,7 +2791,7 @@ func (g *Gateio) GetSingleDeliveryPosition(ctx context.Context, settle currency. // UpdateDeliveryPositionMargin updates position margin func (g *Gateio) UpdateDeliveryPositionMargin(ctx context.Context, settle currency.Code, change float64, contract currency.Pair) (*Position, error) { - if !slices.Contains(settlementCurrencies, settle) { + if settle.IsEmpty() { return nil, errEmptyOrInvalidSettlementCurrency } if contract.IsInvalid() { @@ -2809,7 +2808,7 @@ func (g *Gateio) UpdateDeliveryPositionMargin(ctx context.Context, settle curren // UpdateDeliveryPositionLeverage updates position leverage func (g *Gateio) UpdateDeliveryPositionLeverage(ctx context.Context, settle currency.Code, contract currency.Pair, leverage float64) (*Position, error) { - if !slices.Contains(settlementCurrencies, settle) { + if settle.IsEmpty() { return nil, errEmptyOrInvalidSettlementCurrency } if contract.IsInvalid() { @@ -2827,7 +2826,7 @@ func (g *Gateio) UpdateDeliveryPositionLeverage(ctx context.Context, settle curr // UpdateDeliveryPositionRiskLimit update position risk limit func (g *Gateio) UpdateDeliveryPositionRiskLimit(ctx context.Context, settle currency.Code, contract currency.Pair, riskLimit uint64) (*Position, error) { - if !slices.Contains(settlementCurrencies, settle) { + if settle.IsEmpty() { return nil, errEmptyOrInvalidSettlementCurrency } if contract.IsInvalid() { @@ -2920,7 +2919,7 @@ func (g *Gateio) CancelMultipleDeliveryOrders(ctx context.Context, contract curr // GetSingleDeliveryOrder Get a single order // Zero-filled order cannot be retrieved 10 minutes after order cancellation func (g *Gateio) GetSingleDeliveryOrder(ctx context.Context, settle currency.Code, orderID string) (*Order, error) { - if !slices.Contains(settlementCurrencies, settle) { + if settle.IsEmpty() { return nil, errEmptyOrInvalidSettlementCurrency } if orderID == "" { diff --git a/exchanges/gateio/gateio_test.go b/exchanges/gateio/gateio_test.go index 96dd3eb954e..d5d4bfe6c98 100644 --- a/exchanges/gateio/gateio_test.go +++ b/exchanges/gateio/gateio_test.go @@ -1142,22 +1142,18 @@ func TestCancelMultipleDeliveryOrders(t *testing.T) { func TestGetSingleDeliveryOrder(t *testing.T) { t.Parallel() - _, err := g.GetSingleDeliveryOrder(context.Background(), currency.USD, "123456") + _, err := g.GetSingleDeliveryOrder(context.Background(), currency.EMPTYCODE, "123456") assert.ErrorIs(t, err, errEmptyOrInvalidSettlementCurrency, "GetSingleDeliveryOrder should return errEmptyOrInvalidSettlementCurrency") sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - for _, settle := range settlementCurrencies { - _, err := g.GetSingleDeliveryOrder(context.Background(), settle, "123456") - assert.NoErrorf(t, err, "GetSingleDeliveryOrder %s should not error", settle) - } + _, err = g.GetSingleDeliveryOrder(context.Background(), currency.USDT, "123456") + assert.NoError(t, err, "GetSingleDeliveryOrder should not error") } func TestCancelSingleDeliveryOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) - for _, settle := range settlementCurrencies { - _, err := g.CancelSingleDeliveryOrder(context.Background(), settle, "123456") - assert.NoErrorf(t, err, "CancelSingleDeliveryOrder %s should not error", settle) - } + _, err := g.CancelSingleDeliveryOrder(context.Background(), currency.USDT, "123456") + assert.NoError(t, err, "CancelSingleDeliveryOrder should not error") } func TestGetDeliveryPersonalTradingHistory(t *testing.T) { @@ -1225,7 +1221,7 @@ func TestCancelAllDeliveryPriceTriggeredOrder(t *testing.T) { func TestGetSingleDeliveryPriceTriggeredOrder(t *testing.T) { t.Parallel() sharedtestvalues.SkipTestIfCredentialsUnset(t, g) - _, err := g.GetSingleDeliveryPriceTriggeredOrder(context.Background(), currency.BTC, "12345") + _, err := g.GetSingleDeliveryPriceTriggeredOrder(context.Background(), currency.USDT, "12345") assert.NoError(t, err, "GetSingleDeliveryPriceTriggeredOrder should not error") } @@ -1450,8 +1446,15 @@ func TestCancelAllFuturesOpenOrders(t *testing.T) { func TestGetAllDeliveryContracts(t *testing.T) { t.Parallel() - _, err := g.GetAllDeliveryContracts(context.Background(), currency.USDT) - assert.NoError(t, err, "GetAllDeliveryContracts should not error") + r, err := g.GetAllDeliveryContracts(context.Background(), currency.USDT) + require.NoError(t, err, "GetAllDeliveryContracts must not error") + assert.NotEmpty(t, r, "GetAllDeliveryContracts should return data") + r, err = g.GetAllDeliveryContracts(context.Background(), currency.BTC) + require.NoError(t, err, "GetAllDeliveryContracts must not error") + // The test below will fail if support for BTC settlement is added. This is intentional, as it ensures we are alerted when it's time to reintroduce support + if !assert.Empty(t, r, "GetAllDeliveryContracts should not return any data with unsupported settlement currency BTC") { + t.Error("BTC settlement for delivery futures appears to be supported again by the API. Please raise an issue to reintroduce BTC support for this exchange") + } } func TestGetSingleDeliveryContracts(t *testing.T) { @@ -1527,6 +1530,8 @@ func TestGetSingleDeliveryPosition(t *testing.T) { func TestUpdateDeliveryPositionMargin(t *testing.T) { t.Parallel() + _, err := g.UpdateDeliveryPositionMargin(context.Background(), currency.EMPTYCODE, 0.001, currency.Pair{}) + assert.ErrorIs(t, err, errEmptyOrInvalidSettlementCurrency) sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) settle, err := getSettlementFromCurrency(getPair(t, asset.DeliveryFutures)) require.NoError(t, err, "getSettlementFromCurrency must not error") @@ -1536,15 +1541,19 @@ func TestUpdateDeliveryPositionMargin(t *testing.T) { func TestUpdateDeliveryPositionLeverage(t *testing.T) { t.Parallel() + _, err := g.UpdateDeliveryPositionLeverage(context.Background(), currency.EMPTYCODE, currency.Pair{}, 0.001) + assert.ErrorIs(t, err, errEmptyOrInvalidSettlementCurrency) sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) - _, err := g.UpdateDeliveryPositionLeverage(context.Background(), currency.USDT, getPair(t, asset.DeliveryFutures), 0.001) + _, err = g.UpdateDeliveryPositionLeverage(context.Background(), currency.USDT, getPair(t, asset.DeliveryFutures), 0.001) assert.NoError(t, err, "UpdateDeliveryPositionLeverage should not error") } func TestUpdateDeliveryPositionRiskLimit(t *testing.T) { t.Parallel() + _, err := g.UpdateDeliveryPositionRiskLimit(context.Background(), currency.EMPTYCODE, currency.Pair{}, 0) + assert.ErrorIs(t, err, errEmptyOrInvalidSettlementCurrency) sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) - _, err := g.UpdateDeliveryPositionRiskLimit(context.Background(), currency.USDT, getPair(t, asset.DeliveryFutures), 30) + _, err = g.UpdateDeliveryPositionRiskLimit(context.Background(), currency.USDT, getPair(t, asset.DeliveryFutures), 30) assert.NoError(t, err, "UpdateDeliveryPositionRiskLimit should not error") } diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index bb28b848ea7..dad42e4dfd3 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -520,21 +520,16 @@ func (g *Gateio) FetchTradablePairs(ctx context.Context, a asset.Item) (currency } return pairs, nil case asset.DeliveryFutures: - btcContracts, err := g.GetAllDeliveryContracts(ctx, currency.BTC) - if err != nil { - return nil, err - } usdtContracts, err := g.GetAllDeliveryContracts(ctx, currency.USDT) if err != nil { return nil, err } - btcContracts = append(btcContracts, usdtContracts...) - pairs := make([]currency.Pair, 0, len(btcContracts)) - for x := range btcContracts { - if btcContracts[x].InDelisting { + pairs := make([]currency.Pair, 0, len(usdtContracts)) + for x := range usdtContracts { + if usdtContracts[x].InDelisting { continue } - p := strings.ToUpper(btcContracts[x].Name) + p := strings.ToUpper(usdtContracts[x].Name) if !g.IsValidPairString(p) { continue } @@ -633,6 +628,11 @@ func (g *Gateio) UpdateTickers(ctx context.Context, a asset.Item) error { var tickers []FuturesTicker var ticks []FuturesTicker for _, settle := range settlementCurrencies { + // All delivery futures are settled in USDT only, despite the API accepting a settlement currency parameter for all delivery futures endpoints + if a == asset.DeliveryFutures && !settle.Equal(currency.USDT) { + continue + } + if a == asset.Futures { ticks, err = g.GetFuturesTickers(ctx, settle, currency.EMPTYPAIR) } else { @@ -828,6 +828,11 @@ func (g *Gateio) UpdateAccountInfo(ctx context.Context, a asset.Item) (account.H case asset.Futures, asset.DeliveryFutures: currencies := make([]account.Balance, 0, 2) for x := range settlementCurrencies { + // All delivery futures are settled in USDT only, despite the API accepting a settlement currency parameter for all delivery futures endpoints + if a == asset.DeliveryFutures && !settlementCurrencies[x].Equal(currency.USDT) { + continue + } + var balance *FuturesAccount if a == asset.Futures { balance, err = g.QueryFuturesAccount(ctx, settlementCurrencies[x]) @@ -1721,6 +1726,11 @@ func (g *Gateio) GetActiveOrders(ctx context.Context, req *order.MultiOrderReque } for settlement := range settlements { + // All delivery futures are settled in USDT only, despite the API accepting a settlement currency parameter for all delivery futures endpoints + if req.AssetType == asset.DeliveryFutures && !settlement.Equal(currency.USDT) { + continue + } + var futuresOrders []Order if req.AssetType == asset.Futures { futuresOrders, err = g.GetFuturesOrders(ctx, currency.EMPTYPAIR, "open", "", settlement, 0, 0, 0) @@ -2112,58 +2122,56 @@ func (g *Gateio) GetFuturesContractDetails(ctx context.Context, item asset.Item) return resp, nil case asset.DeliveryFutures: var resp []futures.Contract - for k := range settlementCurrencies { - contracts, err := g.GetAllDeliveryContracts(ctx, settlementCurrencies[k]) + 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) 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) - if err != nil { - return nil, err - } - underlying, err = currency.NewPairFromString(contracts[j].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 { - case "WEEKLY": - ct = futures.Weekly - s = e.Add(-kline.OneWeek.Duration()) - case "BI-WEEKLY": - ct = futures.Fortnightly - s = e.Add(-kline.TwoWeek.Duration()) - case "QUARTERLY": - ct = futures.Quarterly - s = e.Add(-kline.ThreeMonth.Duration()) - case "BI-QUARTERLY": - ct = futures.HalfYearly - s = e.Add(-kline.SixMonth.Duration()) - default: - ct = futures.LongDated - } - contractsToAdd[j] = futures.Contract{ - Exchange: g.Name, - Name: name, - Underlying: underlying, - Asset: item, - StartDate: s, - EndDate: e, - SettlementType: futures.Linear, - IsActive: !contracts[j].InDelisting, - Type: ct, - SettlementCurrencies: currency.Currencies{settlementCurrencies[k]}, - MarginCurrency: currency.Code{}, - Multiplier: contracts[j].QuantoMultiplier.Float64(), - MaxLeverage: contracts[j].LeverageMax.Float64(), - } + underlying, err = currency.NewPairFromString(contracts[j].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 { + case "WEEKLY": + ct = futures.Weekly + s = e.Add(-kline.OneWeek.Duration()) + case "BI-WEEKLY": + ct = futures.Fortnightly + s = e.Add(-kline.TwoWeek.Duration()) + case "QUARTERLY": + ct = futures.Quarterly + s = e.Add(-kline.ThreeMonth.Duration()) + case "BI-QUARTERLY": + ct = futures.HalfYearly + s = e.Add(-kline.SixMonth.Duration()) + default: + ct = futures.LongDated + } + contractsToAdd[j] = futures.Contract{ + Exchange: g.Name, + Name: name, + Underlying: underlying, + Asset: item, + StartDate: s, + EndDate: e, + SettlementType: futures.Linear, + IsActive: !contracts[j].InDelisting, + Type: ct, + SettlementCurrencies: currency.Currencies{currency.USDT}, + MarginCurrency: currency.Code{}, + Multiplier: contracts[j].QuantoMultiplier.Float64(), + MaxLeverage: contracts[j].LeverageMax.Float64(), } resp = append(resp, contractsToAdd...) } diff --git a/exchanges/okx/okx_test.go b/exchanges/okx/okx_test.go index a5d86fde9f7..14138bbea33 100644 --- a/exchanges/okx/okx_test.go +++ b/exchanges/okx/okx_test.go @@ -3380,80 +3380,33 @@ func TestIsPerpetualFutureCurrency(t *testing.T) { func TestGetAssetsFromInstrumentTypeOrID(t *testing.T) { t.Parallel() - _, err := ok.GetAssetsFromInstrumentTypeOrID("", "") - if !errors.Is(err, errEmptyArgument) { - t.Error(err) - } - assets, err := ok.GetAssetsFromInstrumentTypeOrID("SPOT", "") - if !errors.Is(err, nil) { - t.Error(err) - } - if len(assets) != 1 { - t.Errorf("received %v expected %v", len(assets), 1) - } - if assets[0] != asset.Spot { - t.Errorf("received %v expected %v", assets[0], asset.Spot) - } + ok := new(Okx) //nolint:govet // Intentional shadow + require.NoError(t, testexch.Setup(ok), "Setup must not error") - assets, err = ok.GetAssetsFromInstrumentTypeOrID("", ok.CurrencyPairs.Pairs[asset.Futures].Enabled[0].String()) - if !errors.Is(err, nil) { - t.Error(err) - } - if len(assets) != 1 { - t.Errorf("received %v expected %v", len(assets), 1) - } - if assets[0] != asset.Futures { - t.Errorf("received %v expected %v", assets[0], asset.Futures) - } + _, err := ok.GetAssetsFromInstrumentTypeOrID("", "") + assert.ErrorIs(t, err, errEmptyArgument) - assets, err = ok.GetAssetsFromInstrumentTypeOrID("", ok.CurrencyPairs.Pairs[asset.PerpetualSwap].Enabled[0].String()) - if !errors.Is(err, nil) { - t.Error(err) - } - if len(assets) != 1 { - t.Errorf("received %v expected %v", len(assets), 1) - } - if assets[0] != asset.PerpetualSwap { - t.Errorf("received %v expected %v", assets[0], asset.PerpetualSwap) + for _, a := range []asset.Item{asset.Spot, asset.Futures, asset.PerpetualSwap, asset.Options} { + symbol := "" + if a != asset.Spot { + symbol = ok.CurrencyPairs.Pairs[a].Enabled[0].String() + } + assets, err2 := ok.GetAssetsFromInstrumentTypeOrID(a.String(), symbol) + require.NoErrorf(t, err2, "GetAssetsFromInstrumentTypeOrID must not error for asset: %s", a) + require.Len(t, assets, 1) + assert.Equalf(t, a, assets[0], "Should contain asset: %s", a) } _, err = ok.GetAssetsFromInstrumentTypeOrID("", "test") - if !errors.Is(err, currency.ErrCurrencyNotSupported) { - t.Error(err) - } - + assert.ErrorIs(t, err, currency.ErrCurrencyNotSupported) _, err = ok.GetAssetsFromInstrumentTypeOrID("", "test-test") - if !errors.Is(err, asset.ErrNotSupported) { - t.Error(err) - } - - assets, err = ok.GetAssetsFromInstrumentTypeOrID("", ok.CurrencyPairs.Pairs[asset.Margin].Enabled[0].String()) - if !errors.Is(err, nil) { - t.Error(err) - } - var found bool - for i := range assets { - if assets[i] == asset.Margin { - found = true - } - } - if !found { - t.Errorf("received %v expected %v", assets, asset.Margin) - } + assert.ErrorIs(t, err, asset.ErrNotSupported) - assets, err = ok.GetAssetsFromInstrumentTypeOrID("", ok.CurrencyPairs.Pairs[asset.Spot].Enabled[0].String()) - if !errors.Is(err, nil) { - t.Error(err) - } - found = false - for i := range assets { - if assets[i] == asset.Spot { - found = true - } - } - if !found { - t.Errorf("received %v expected %v", assets, asset.Spot) + for _, a := range []asset.Item{asset.Margin, asset.Spot} { + assets, err2 := ok.GetAssetsFromInstrumentTypeOrID("", ok.CurrencyPairs.Pairs[a].Enabled[0].String()) + require.NoErrorf(t, err2, "GetAssetsFromInstrumentTypeOrID must not error for asset: %s", a) + assert.Contains(t, assets, a) } } From 50448ec6a04731e5876884c734dcf634e8a68e08 Mon Sep 17 00:00:00 2001 From: Ryan O'Hara-Reid Date: Fri, 20 Dec 2024 13:50:31 +1100 Subject: [PATCH 02/21] websocket/gateio: Add request functions for websocket multi-connection [SPOT] (#1598) * gateio: Add multi asset websocket support WIP. * meow * Add tests and shenanigans * integrate flushing and for enabling/disabling pairs from rpc shenanigans * some changes * linter: fixes strikes again. * Change name ConnectionAssociation -> ConnectionCandidate for better clarity on purpose. Change connections map to point to candidate to track subscriptions for future dynamic connections holder and drop struct ConnectionDetails. * Add subscription tests (state functional) * glorious:nits + proxy handling * Spelling * linter: fixerino * instead of nil, dont do nil. * clean up nils * cya nils * don't need to set URL or check if its running * stream match update * update tests * linter: fix * glorious: nits + handle context cancellations * stop ping handler routine leak * * Fix bug where reader routine on error that is not a disconnection error but websocket frame error or anything really makes the reader routine return and then connection never cycles and the buffer gets filled. * Handle reconnection via an errors.Is check which is simpler and in that scope allow for quick disconnect reconnect without waiting for connection cycle. * Dial now uses code from DialContext but just calls context.Background() * Don't allow reader to return on parse binary response error. Just output error and return a non nil response * Allow rollback on connect on any error across all connections * fix shadow jutsu * glorious/gk: nitters - adds in ws mock server * linter: fix * fix deadlock on connection as the previous channel had no reader and would hang connection reader for eternity. * glorious: whooops * gk: nits * Leak issue and edge case * Websocket: Add SendMessageReturnResponses * whooooooopsie * gk: nitssssss * Update exchanges/stream/stream_match.go Co-authored-by: Gareth Kirwan * Update exchanges/stream/stream_match_test.go Co-authored-by: Gareth Kirwan * linter: appease the linter gods * gk: nits * gk: drain brain * started * more changes before merge match pr * gateio: still building out * gateio: finish spot * fix up tests in gateio * Add tests for stream package * rm unused field * glorious: nits * rn files, specifically set function names to asset and offload routing to websocket type. * linter: fix * glorious: nits * add counter and update gateio * fix collision issue * Update exchanges/stream/websocket.go Co-authored-by: Scott * glorious: nits * add tests * linter: fix * After merge * Add error connection info * upgrade to upstream merge * Fix edge case where it does not reconnect made by an already closed connection * stream coverage * glorious: nits * glorious: nits removed asset error handling in stream package * linter: fix * rm block * Add basic readme * fix asset enabled flush cycle for multi connection * spella: fix * linter: fix * Add glorious suggestions, fix some race thing * reinstate name before any routine gets spawned * stop on error in mock tests * glorious: nits * glorious: nits found in CI build * Add test for drain, bumped wait times as there seems to be something happening on macos CI builds, used context.WithTimeout because its instant. * mutex across shutdown and connect for protection * lint: fix * test time withoffset, reinstate stop * fix whoops * const trafficCheckInterval; rm testmain * y * fix lint * bump time check window * stream: fix intermittant test failures while testing routines and remove code that is not needed. * spells * cant do what I did * protect race due to routine. * update testURL * use mock websocket connection instead of test URL's * linter: fix * remove url because its throwing errors on CI builds * connections drop all the time, don't need to worry about not being able to echo back ws data as it can be easily reviewed _test file side. * remove another superfluous url thats not really set up for this * spawn overwatch routine when there is no errors, inline checker instead of waiting for a time period, add sleep inline with echo handler as this is really quick and wanted to ensure that latency is handing correctly * linter: fixerino uperino * glorious: panix * linter: things * whoops * dont need to make consecutive Unix() calls * websocket: fix potential panic on error and no responses and adding waitForResponses * rm json parser and handle in json package instead * linter: fix * linter: fix again * * change field name OutboundRequestSignature to WrapperDefinedConnectionSignature for agnostic inbound and outbound connections. * change method name GetOutboundConnection to GetConnection for agnostic inbound and outbound connections. * drop outbound field map for improved performance just using a range and field check (less complex as well) * change field name connections to connectionToWrapper for better clarity * spells and magic and wands * glorious: nits * comparable check for signature * mv err var * glorious: nits and stuff * attempt to fix race * glorious: nits * gk: nits; engine log cleanup * gk: nits; OCD * gk: nits; move function change file names * gk: nits; :rocket: * gk: nits; convert variadic function and message inspection to interface and include a specific function for that handling so as to not need nil on every call * gk: nits; continued * gk: engine nits; rm loaded exchange * gk: nits; drop WebsocketLoginResponse * stream: Add match method EnsureMatchWithData * gk: nits; rn Inspect to IsFinal * gk: nits; rn to MessageFilter * linter: fix * gateio: update rate limit definitions (cherry-pick) * Add test and missing * Shared REST rate limit definitions with Websocket service, set lookup item to nil for systems that do not require rate limiting; add glorious nit * integrate rate limits for websocket trading spot * bitstamp: fix issue * glorious: nits * ch name and commentary * fix bug add test * rm a thing * fix test * Update engine/engine.go Co-authored-by: Adrian Gallagher * thrasher: nits * Update exchanges/stream/stream_match_test.go Co-authored-by: Adrian Gallagher * Update exchanges/stream/stream_match_test.go Co-authored-by: Adrian Gallagher * GK: nits rn websocket functions * explicit function names for single to multi outbound orders * linter: fix --------- Co-authored-by: shazbert Co-authored-by: Gareth Kirwan Co-authored-by: Scott Co-authored-by: Adrian Gallagher --- engine/engine.go | 46 +--- exchanges/bitfinex/bitfinex_websocket.go | 24 +- exchanges/bitmex/bitmex_websocket.go | 5 +- exchanges/bitstamp/bitstamp_websocket.go | 5 +- exchanges/exchange.go | 8 +- exchanges/gateio/gateio_types.go | 13 +- exchanges/gateio/gateio_websocket.go | 72 ++++- ...o => gateio_websocket_delivery_futures.go} | 3 +- ...futures.go => gateio_websocket_futures.go} | 3 +- ...s_option.go => gateio_websocket_option.go} | 3 +- .../gateio/gateio_websocket_request_spot.go | 224 ++++++++++++++++ .../gateio_websocket_request_spot_test.go | 248 ++++++++++++++++++ .../gateio/gateio_websocket_request_types.go | 143 ++++++++++ exchanges/gateio/gateio_wrapper.go | 6 + exchanges/kraken/kraken_websocket.go | 2 +- exchanges/kucoin/kucoin_websocket.go | 8 +- exchanges/stream/stream_match.go | 13 + exchanges/stream/stream_match_test.go | 15 ++ exchanges/stream/stream_types.go | 13 + exchanges/stream/websocket.go | 69 ++++- exchanges/stream/websocket_connection.go | 41 ++- exchanges/stream/websocket_test.go | 88 ++++++- exchanges/stream/websocket_types.go | 2 +- 23 files changed, 951 insertions(+), 103 deletions(-) rename exchanges/gateio/{gateio_ws_delivery_futures.go => gateio_websocket_delivery_futures.go} (98%) rename exchanges/gateio/{gateio_ws_futures.go => gateio_websocket_futures.go} (99%) rename exchanges/gateio/{gateio_ws_option.go => gateio_websocket_option.go} (99%) create mode 100644 exchanges/gateio/gateio_websocket_request_spot.go create mode 100644 exchanges/gateio/gateio_websocket_request_spot_test.go create mode 100644 exchanges/gateio/gateio_websocket_request_types.go diff --git a/engine/engine.go b/engine/engine.go index bd9dba18aec..8ad14d87ed6 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -791,17 +791,11 @@ func (bot *Engine) LoadExchange(name string) error { localWG.Wait() if !bot.Settings.EnableExchangeHTTPRateLimiter { - gctlog.Warnf(gctlog.ExchangeSys, - "Loaded exchange %s rate limiting has been turned off.\n", - exch.GetName(), - ) err = exch.DisableRateLimiter() if err != nil { - gctlog.Errorf(gctlog.ExchangeSys, - "Loaded exchange %s rate limiting cannot be turned off: %s.\n", - exch.GetName(), - err, - ) + gctlog.Errorf(gctlog.ExchangeSys, "%s error disabling rate limiter: %v", exch.GetName(), err) + } else { + gctlog.Warnf(gctlog.ExchangeSys, "%s rate limiting has been turned off", exch.GetName()) } } @@ -820,29 +814,18 @@ func (bot *Engine) LoadExchange(name string) error { return err } - base := exch.GetBase() - if base.API.AuthenticatedSupport || - base.API.AuthenticatedWebsocketSupport { - assetTypes := base.GetAssetTypes(false) - var useAsset asset.Item - for a := range assetTypes { - err = base.CurrencyPairs.IsAssetEnabled(assetTypes[a]) - if err != nil { - continue - } - useAsset = assetTypes[a] - break - } - err = exch.ValidateAPICredentials(context.TODO(), useAsset) + b := exch.GetBase() + if b.API.AuthenticatedSupport || b.API.AuthenticatedWebsocketSupport { + err = exch.ValidateAPICredentials(context.TODO(), asset.Spot) if err != nil { - gctlog.Warnf(gctlog.ExchangeSys, - "%s: Cannot validate credentials, authenticated support has been disabled, Error: %s\n", - base.Name, - err) - base.API.AuthenticatedSupport = false - base.API.AuthenticatedWebsocketSupport = false + gctlog.Warnf(gctlog.ExchangeSys, "%s: Error validating credentials: %v", b.Name, err) + b.API.AuthenticatedSupport = false + b.API.AuthenticatedWebsocketSupport = false exchCfg.API.AuthenticatedSupport = false exchCfg.API.AuthenticatedWebsocketSupport = false + if b.Websocket != nil { + b.Websocket.SetCanUseAuthenticatedEndpoints(false) + } } } @@ -854,10 +837,7 @@ func (bot *Engine) dryRunParamInteraction(param string) { return } - gctlog.Warnf(gctlog.Global, - "Command line argument '-%s' induces dry run mode."+ - " Set -dryrun=false if you wish to override this.", - param) + gctlog.Warnf(gctlog.Global, "Command line argument '-%s' induces dry run mode. Set -dryrun=false if you wish to override this.", param) if !bot.Settings.EnableDryRun { bot.Settings.EnableDryRun = true diff --git a/exchanges/bitfinex/bitfinex_websocket.go b/exchanges/bitfinex/bitfinex_websocket.go index 74c04afc2d3..ee1b854b0b0 100644 --- a/exchanges/bitfinex/bitfinex_websocket.go +++ b/exchanges/bitfinex/bitfinex_websocket.go @@ -456,17 +456,20 @@ func (b *Bitfinex) handleWSEvent(respRaw []byte) error { if err != nil { return fmt.Errorf("%w 'chanId': %w from message: %s", errParsingWSField, err, respRaw) } - if !b.Websocket.Match.IncomingWithData("unsubscribe:"+chanID, respRaw) { - return fmt.Errorf("%w: unsubscribe:%v", stream.ErrNoMessageListener, chanID) + err = b.Websocket.Match.RequireMatchWithData("unsubscribe:"+chanID, respRaw) + if err != nil { + return fmt.Errorf("%w: unsubscribe:%v", err, chanID) } case wsEventError: if subID, err := jsonparser.GetUnsafeString(respRaw, "subId"); err == nil { - if !b.Websocket.Match.IncomingWithData("subscribe:"+subID, respRaw) { - return fmt.Errorf("%w: subscribe:%v", stream.ErrNoMessageListener, subID) + err = b.Websocket.Match.RequireMatchWithData("subscribe:"+subID, respRaw) + if err != nil { + return fmt.Errorf("%w: subscribe:%v", err, subID) } } else if chanID, err := jsonparser.GetUnsafeString(respRaw, "chanId"); err == nil { - if !b.Websocket.Match.IncomingWithData("unsubscribe:"+chanID, respRaw) { - return fmt.Errorf("%w: unsubscribe:%v", stream.ErrNoMessageListener, chanID) + err = b.Websocket.Match.RequireMatchWithData("unsubscribe:"+chanID, respRaw) + if err != nil { + return fmt.Errorf("%w: unsubscribe:%v", err, chanID) } } else { return fmt.Errorf("unknown channel error; Message: %s", respRaw) @@ -531,17 +534,16 @@ func (b *Bitfinex) handleWSSubscribed(respRaw []byte) error { c.Key = int(chanID) // subscribeToChan removes the old subID keyed Subscription - if err := b.Websocket.AddSuccessfulSubscriptions(b.Websocket.Conn, c); err != nil { + err = b.Websocket.AddSuccessfulSubscriptions(b.Websocket.Conn, c) + if err != nil { return fmt.Errorf("%w: %w subID: %s", stream.ErrSubscriptionFailure, err, subID) } if b.Verbose { log.Debugf(log.ExchangeSys, "%s Subscribed to Channel: %s Pair: %s ChannelID: %d\n", b.Name, c.Channel, c.Pairs, chanID) } - if !b.Websocket.Match.IncomingWithData("subscribe:"+subID, respRaw) { - return fmt.Errorf("%w: subscribe:%v", stream.ErrNoMessageListener, subID) - } - return nil + + return b.Websocket.Match.RequireMatchWithData("subscribe:"+subID, respRaw) } func (b *Bitfinex) handleWSChannelUpdate(s *subscription.Subscription, eventType string, d []interface{}) error { diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index f539787cd1e..d322d5a6713 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -170,8 +170,9 @@ func (b *Bitmex) wsHandleData(respRaw []byte) error { if e2 != nil { return fmt.Errorf("%w parsing stream", e2) } - if !b.Websocket.Match.IncomingWithData(op+":"+streamID, msg) { - return fmt.Errorf("%w: %s:%s", stream.ErrNoMessageListener, op, streamID) + err = b.Websocket.Match.RequireMatchWithData(op+":"+streamID, msg) + if err != nil { + return fmt.Errorf("%w: %s:%s", err, op, streamID) } return nil } diff --git a/exchanges/bitstamp/bitstamp_websocket.go b/exchanges/bitstamp/bitstamp_websocket.go index de45f2fba95..8fe4f2fc1fa 100644 --- a/exchanges/bitstamp/bitstamp_websocket.go +++ b/exchanges/bitstamp/bitstamp_websocket.go @@ -135,10 +135,7 @@ func (b *Bitstamp) handleWSSubscription(event string, respRaw []byte) error { return fmt.Errorf("%w `channel`: %w", errParsingWSField, err) } event = strings.TrimSuffix(event, "scription_succeeded") - if !b.Websocket.Match.IncomingWithData(event+":"+channel, respRaw) { - return fmt.Errorf("%w: %s", stream.ErrNoMessageListener, event+":"+channel) - } - return nil + return b.Websocket.Match.RequireMatchWithData(event+":"+channel, respRaw) } func (b *Bitstamp) handleWSTrade(msg []byte) error { diff --git a/exchanges/exchange.go b/exchanges/exchange.go index 938923e2332..4369b1dd110 100644 --- a/exchanges/exchange.go +++ b/exchanges/exchange.go @@ -975,8 +975,7 @@ func (b *Base) SupportsAsset(a asset.Item) bool { // PrintEnabledPairs prints the exchanges enabled asset pairs func (b *Base) PrintEnabledPairs() { for k, v := range b.CurrencyPairs.Pairs { - log.Infof(log.ExchangeSys, "%s Asset type %v:\n\t Enabled pairs: %v", - b.Name, strings.ToUpper(k.String()), v.Enabled) + log.Infof(log.ExchangeSys, "%s Asset type %v:\n\t Enabled pairs: %v", b.Name, strings.ToUpper(k.String()), v.Enabled) } } @@ -987,10 +986,7 @@ func (b *Base) GetBase() *Base { return b } // for validation of API credentials func (b *Base) CheckTransientError(err error) error { if _, ok := err.(net.Error); ok { - log.Warnf(log.ExchangeSys, - "%s net error captured, will not disable authentication %s", - b.Name, - err) + log.Warnf(log.ExchangeSys, "%s net error captured, will not disable authentication %s", b.Name, err) return nil } return err diff --git a/exchanges/gateio/gateio_types.go b/exchanges/gateio/gateio_types.go index 80a2c8b8df3..0577f6d2d11 100644 --- a/exchanges/gateio/gateio_types.go +++ b/exchanges/gateio/gateio_types.go @@ -2008,12 +2008,13 @@ type WsEventResponse 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"` - Channel string `json:"channel"` - Event string `json:"event"` - Result json.RawMessage `json:"result"` + ID int64 `json:"id"` + Time types.Time `json:"time"` + TimeMs types.Time `json:"time_ms"` + Channel string `json:"channel"` + Event string `json:"event"` + Result json.RawMessage `json:"result"` + RequestID string `json:"request_id"` } // WsTicker websocket ticker information. diff --git a/exchanges/gateio/gateio_websocket.go b/exchanges/gateio/gateio_websocket.go index 932768ec2a6..ea53f12029a 100644 --- a/exchanges/gateio/gateio_websocket.go +++ b/exchanges/gateio/gateio_websocket.go @@ -97,6 +97,64 @@ func (g *Gateio) WsConnectSpot(ctx context.Context, conn stream.Connection) erro return nil } +// authenticateSpot sends an authentication message to the websocket connection +func (g *Gateio) authenticateSpot(ctx context.Context, conn stream.Connection) error { + return g.websocketLogin(ctx, conn, "spot.login") +} + +// websocketLogin authenticates the websocket connection +func (g *Gateio) websocketLogin(ctx context.Context, conn stream.Connection, channel string) error { + if conn == nil { + return fmt.Errorf("%w: %T", common.ErrNilPointer, conn) + } + + if channel == "" { + return errChannelEmpty + } + + creds, err := g.GetCredentials(ctx) + if err != nil { + return err + } + + tn := time.Now().Unix() + msg := "api\n" + channel + "\n" + "\n" + strconv.FormatInt(tn, 10) + mac := hmac.New(sha512.New, []byte(creds.Secret)) + if _, err = mac.Write([]byte(msg)); err != nil { + return err + } + signature := hex.EncodeToString(mac.Sum(nil)) + + payload := WebsocketPayload{ + RequestID: strconv.FormatInt(conn.GenerateMessageID(false), 10), + APIKey: creds.Key, + Signature: signature, + Timestamp: strconv.FormatInt(tn, 10), + } + + req := WebsocketRequest{Time: tn, Channel: channel, Event: "api", Payload: payload} + + resp, err := conn.SendMessageReturnResponse(ctx, websocketRateLimitNotNeededEPL, req.Payload.RequestID, req) + if err != nil { + return err + } + + var inbound WebsocketAPIResponse + if err := json.Unmarshal(resp, &inbound); err != nil { + return err + } + + if inbound.Header.Status != "200" { + var wsErr WebsocketErrors + if err := json.Unmarshal(inbound.Data, &wsErr.Errors); err != nil { + return err + } + return fmt.Errorf("%s: %s", wsErr.Errors.Label, wsErr.Errors.Message) + } + + return nil +} + func (g *Gateio) generateWsSignature(secret, event, channel string, t int64) (string, error) { msg := "channel=" + channel + "&event=" + event + "&time=" + strconv.FormatInt(t, 10) mac := hmac.New(sha512.New, []byte(secret)) @@ -109,21 +167,21 @@ 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 - err := json.Unmarshal(respRaw, &push) - if err != nil { + if err := json.Unmarshal(respRaw, &push); err != nil { return err } + if push.RequestID != "" { + return g.Websocket.Match.RequireMatchWithData(push.RequestID, respRaw) + } + if push.Event == subscribeEvent || push.Event == unsubscribeEvent { - if !g.Websocket.Match.IncomingWithData(push.ID, respRaw) { - return fmt.Errorf("couldn't match subscription message with ID: %d", push.ID) - } - return nil + return g.Websocket.Match.RequireMatchWithData(push.ID, respRaw) } switch push.Channel { // TODO: Convert function params below to only use push.Result case spotTickerChannel: - return g.processTicker(push.Result, push.Time.Time()) + return g.processTicker(push.Result, push.TimeMs.Time()) case spotTradesChannel: return g.processTrades(push.Result) case spotCandlesticksChannel: diff --git a/exchanges/gateio/gateio_ws_delivery_futures.go b/exchanges/gateio/gateio_websocket_delivery_futures.go similarity index 98% rename from exchanges/gateio/gateio_ws_delivery_futures.go rename to exchanges/gateio/gateio_websocket_delivery_futures.go index d4e1c8abb33..eeb1a706053 100644 --- a/exchanges/gateio/gateio_ws_delivery_futures.go +++ b/exchanges/gateio/gateio_websocket_delivery_futures.go @@ -13,7 +13,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/account" "github.com/thrasher-corp/gocryptotrader/exchanges/asset" "github.com/thrasher-corp/gocryptotrader/exchanges/kline" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" ) @@ -55,7 +54,7 @@ func (g *Gateio) WsDeliveryFuturesConnect(ctx context.Context, conn stream.Conne if err != nil { return err } - conn.SetupPingHandler(request.Unset, stream.PingHandler{ + conn.SetupPingHandler(websocketRateLimitNotNeededEPL, stream.PingHandler{ Websocket: true, Delay: time.Second * 5, MessageType: websocket.PingMessage, diff --git a/exchanges/gateio/gateio_ws_futures.go b/exchanges/gateio/gateio_websocket_futures.go similarity index 99% rename from exchanges/gateio/gateio_ws_futures.go rename to exchanges/gateio/gateio_websocket_futures.go index 520eb816d58..ba393277553 100644 --- a/exchanges/gateio/gateio_ws_futures.go +++ b/exchanges/gateio/gateio_websocket_futures.go @@ -19,7 +19,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" @@ -76,7 +75,7 @@ func (g *Gateio) WsFuturesConnect(ctx context.Context, conn stream.Connection) e if err != nil { return err } - conn.SetupPingHandler(request.Unset, stream.PingHandler{ + conn.SetupPingHandler(websocketRateLimitNotNeededEPL, stream.PingHandler{ Websocket: true, MessageType: websocket.PingMessage, Delay: time.Second * 15, diff --git a/exchanges/gateio/gateio_ws_option.go b/exchanges/gateio/gateio_websocket_option.go similarity index 99% rename from exchanges/gateio/gateio_ws_option.go rename to exchanges/gateio/gateio_websocket_option.go index 40bc63fe336..091a9ad1f0f 100644 --- a/exchanges/gateio/gateio_ws_option.go +++ b/exchanges/gateio/gateio_websocket_option.go @@ -18,7 +18,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/kline" "github.com/thrasher-corp/gocryptotrader/exchanges/order" "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" - "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" @@ -85,7 +84,7 @@ func (g *Gateio) WsOptionsConnect(ctx context.Context, conn stream.Connection) e if err != nil { return err } - conn.SetupPingHandler(request.Unset, stream.PingHandler{ + conn.SetupPingHandler(websocketRateLimitNotNeededEPL, stream.PingHandler{ Websocket: true, Delay: time.Second * 5, MessageType: websocket.PingMessage, diff --git a/exchanges/gateio/gateio_websocket_request_spot.go b/exchanges/gateio/gateio_websocket_request_spot.go new file mode 100644 index 00000000000..25ac1ea1689 --- /dev/null +++ b/exchanges/gateio/gateio_websocket_request_spot.go @@ -0,0 +1,224 @@ +package gateio + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "strconv" + "strings" + "time" + + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/request" +) + +var ( + errOrdersEmpty = errors.New("orders cannot be empty") + errNoOrdersToCancel = errors.New("no orders to cancel") + errChannelEmpty = errors.New("channel cannot be empty") +) + +// WebsocketSpotSubmitOrder submits an order via the websocket connection +func (g *Gateio) WebsocketSpotSubmitOrder(ctx context.Context, order *WebsocketOrder) ([]WebsocketOrderResponse, error) { + return g.WebsocketSpotSubmitOrders(ctx, []WebsocketOrder{*order}) +} + +// WebsocketSpotSubmitOrders submits orders via the websocket connection. You can +// send multiple orders in a single request. But only for one asset route. +func (g *Gateio) WebsocketSpotSubmitOrders(ctx context.Context, orders []WebsocketOrder) ([]WebsocketOrderResponse, error) { + if len(orders) == 0 { + return nil, errOrdersEmpty + } + + for i := range orders { + if orders[i].Text == "" { + // API requires Text field, or it will be rejected + orders[i].Text = "t-" + strconv.FormatInt(g.Counter.IncrementAndGet(), 10) + } + if orders[i].CurrencyPair == "" { + return nil, currency.ErrCurrencyPairEmpty + } + if orders[i].Side == "" { + return nil, order.ErrSideIsInvalid + } + if orders[i].Amount == "" { + return nil, errInvalidAmount + } + if orders[i].Type == "limit" && orders[i].Price == "" { + return nil, errInvalidPrice + } + } + + if len(orders) == 1 { + var singleResponse WebsocketOrderResponse + return []WebsocketOrderResponse{singleResponse}, g.SendWebsocketRequest(ctx, spotPlaceOrderEPL, "spot.order_place", asset.Spot, orders[0], &singleResponse, 2) + } + var resp []WebsocketOrderResponse + return resp, g.SendWebsocketRequest(ctx, spotBatchOrdersEPL, "spot.order_place", asset.Spot, orders, &resp, 2) +} + +// WebsocketSpotCancelOrder cancels an order via the websocket connection +func (g *Gateio) WebsocketSpotCancelOrder(ctx context.Context, orderID string, pair currency.Pair, account string) (*WebsocketOrderResponse, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } + if pair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + + params := &WebsocketOrderRequest{OrderID: orderID, Pair: pair.String(), Account: account} + + var resp WebsocketOrderResponse + return &resp, g.SendWebsocketRequest(ctx, spotCancelSingleOrderEPL, "spot.order_cancel", asset.Spot, params, &resp, 1) +} + +// WebsocketSpotCancelAllOrdersByIDs cancels multiple orders via the websocket +func (g *Gateio) WebsocketSpotCancelAllOrdersByIDs(ctx context.Context, o []WebsocketOrderBatchRequest) ([]WebsocketCancellAllResponse, error) { + if len(o) == 0 { + return nil, errNoOrdersToCancel + } + + for i := range o { + if o[i].OrderID == "" { + return nil, order.ErrOrderIDNotSet + } + if o[i].Pair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + } + + var resp []WebsocketCancellAllResponse + return resp, g.SendWebsocketRequest(ctx, spotCancelBatchOrdersEPL, "spot.order_cancel_ids", asset.Spot, o, &resp, 2) +} + +// WebsocketSpotCancelAllOrdersByPair cancels all orders for a specific pair +func (g *Gateio) WebsocketSpotCancelAllOrdersByPair(ctx context.Context, pair currency.Pair, side order.Side, account string) ([]WebsocketOrderResponse, error) { + if !pair.IsEmpty() && side == order.UnknownSide { + // This case will cancel all orders for every pair, this can be introduced later + return nil, fmt.Errorf("'%v' %w while pair is set", side, order.ErrSideIsInvalid) + } + + sideStr := "" + if side != order.UnknownSide { + sideStr = side.Lower() + } + + params := &WebsocketCancelParam{ + Pair: pair, + Side: sideStr, + Account: account, + } + + var resp []WebsocketOrderResponse + return resp, g.SendWebsocketRequest(ctx, spotCancelAllOpenOrdersEPL, "spot.order_cancel_cp", asset.Spot, params, &resp, 1) +} + +// WebsocketSpotAmendOrder amends an order via the websocket connection +func (g *Gateio) WebsocketSpotAmendOrder(ctx context.Context, amend *WebsocketAmendOrder) (*WebsocketOrderResponse, error) { + if amend == nil { + return nil, fmt.Errorf("%w: %T", common.ErrNilPointer, amend) + } + + if amend.OrderID == "" { + return nil, order.ErrOrderIDNotSet + } + + if amend.Pair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + + if amend.Amount == "" && amend.Price == "" { + return nil, fmt.Errorf("%w: amount or price must be set", errInvalidAmount) + } + + var resp WebsocketOrderResponse + return &resp, g.SendWebsocketRequest(ctx, spotAmendOrderEPL, "spot.order_amend", asset.Spot, amend, &resp, 1) +} + +// WebsocketSpotGetOrderStatus gets the status of an order via the websocket connection +func (g *Gateio) WebsocketSpotGetOrderStatus(ctx context.Context, orderID string, pair currency.Pair, account string) (*WebsocketOrderResponse, error) { + if orderID == "" { + return nil, order.ErrOrderIDNotSet + } + if pair.IsEmpty() { + return nil, currency.ErrCurrencyPairEmpty + } + + params := &WebsocketOrderRequest{OrderID: orderID, Pair: pair.String(), Account: account} + + var resp WebsocketOrderResponse + return &resp, g.SendWebsocketRequest(ctx, spotGetOrdersEPL, "spot.order_status", asset.Spot, params, &resp, 1) +} + +// funnelResult is used to unmarshal the result of a websocket request back to the required caller type +type funnelResult struct { + Result any `json:"result"` +} + +// SendWebsocketRequest sends a websocket request to the exchange +func (g *Gateio) SendWebsocketRequest(ctx context.Context, epl request.EndpointLimit, channel string, connSignature, params, result any, expectedResponses int) error { + paramPayload, err := json.Marshal(params) + if err != nil { + return err + } + + conn, err := g.Websocket.GetConnection(connSignature) + if err != nil { + return err + } + + tn := time.Now().Unix() + req := &WebsocketRequest{ + Time: tn, + Channel: channel, + Event: "api", + Payload: WebsocketPayload{ + // This request ID associated with the payload is the match to the + // response. + RequestID: strconv.FormatInt(conn.GenerateMessageID(false), 10), + RequestParam: paramPayload, + Timestamp: strconv.FormatInt(tn, 10), + }, + } + + responses, err := conn.SendMessageReturnResponsesWithInspector(ctx, epl, req.Payload.RequestID, req, expectedResponses, wsRespAckInspector{}) + if err != nil { + return err + } + + if len(responses) == 0 { + return common.ErrNoResponse + } + + var inbound WebsocketAPIResponse + // The last response is the one we want to unmarshal, the other is just + // an ack. If the request fails on the ACK then we can unmarshal the error + // from that as the next response won't come anyway. + endResponse := responses[len(responses)-1] + + if err := json.Unmarshal(endResponse, &inbound); err != nil { + return err + } + + if inbound.Header.Status != "200" { + var wsErr WebsocketErrors + if err := json.Unmarshal(inbound.Data, &wsErr); err != nil { + return err + } + return fmt.Errorf("%s: %s", wsErr.Errors.Label, wsErr.Errors.Message) + } + + return json.Unmarshal(inbound.Data, &funnelResult{Result: result}) +} + +type wsRespAckInspector struct{} + +// IsFinal checks the payload for an ack, it returns true if the payload does not contain an ack. +// This will force the cancellation of further waiting for responses. +func (wsRespAckInspector) IsFinal(data []byte) bool { + return !strings.Contains(string(data), "ack") +} diff --git a/exchanges/gateio/gateio_websocket_request_spot_test.go b/exchanges/gateio/gateio_websocket_request_spot_test.go new file mode 100644 index 00000000000..7933c117294 --- /dev/null +++ b/exchanges/gateio/gateio_websocket_request_spot_test.go @@ -0,0 +1,248 @@ +package gateio + +import ( + "context" + "strings" + "testing" + + "github.com/stretchr/testify/require" + "github.com/thrasher-corp/gocryptotrader/common" + "github.com/thrasher-corp/gocryptotrader/config" + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/exchanges/asset" + "github.com/thrasher-corp/gocryptotrader/exchanges/order" + "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" + "github.com/thrasher-corp/gocryptotrader/exchanges/stream" + testexch "github.com/thrasher-corp/gocryptotrader/internal/testing/exchange" +) + +func TestWebsocketLogin(t *testing.T) { + t.Parallel() + err := g.websocketLogin(context.Background(), nil, "") + require.ErrorIs(t, err, common.ErrNilPointer) + + err = g.websocketLogin(context.Background(), &stream.WebsocketConnection{}, "") + require.ErrorIs(t, err, errChannelEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + + testexch.UpdatePairsOnce(t, g) + g := getWebsocketInstance(t, g) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + demonstrationConn, err := g.Websocket.GetConnection(asset.Spot) + require.NoError(t, err) + + err = g.websocketLogin(context.Background(), demonstrationConn, "spot.login") + require.NoError(t, err) +} + +func TestWebsocketSpotSubmitOrder(t *testing.T) { + t.Parallel() + _, err := g.WebsocketSpotSubmitOrder(context.Background(), &WebsocketOrder{}) + require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + out := &WebsocketOrder{CurrencyPair: "BTC_USDT"} + _, err = g.WebsocketSpotSubmitOrder(context.Background(), out) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + out.Side = strings.ToLower(order.Buy.String()) + _, err = g.WebsocketSpotSubmitOrder(context.Background(), out) + require.ErrorIs(t, err, errInvalidAmount) + out.Amount = "0.0003" + out.Type = "limit" + _, err = g.WebsocketSpotSubmitOrder(context.Background(), out) + require.ErrorIs(t, err, errInvalidPrice) + out.Price = "20000" + + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + + testexch.UpdatePairsOnce(t, g) + g := getWebsocketInstance(t, g) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + got, err := g.WebsocketSpotSubmitOrder(context.Background(), out) + require.NoError(t, err) + require.NotEmpty(t, got) +} + +func TestWebsocketSpotSubmitOrders(t *testing.T) { + t.Parallel() + _, err := g.WebsocketSpotSubmitOrders(context.Background(), nil) + require.ErrorIs(t, err, errOrdersEmpty) + _, err = g.WebsocketSpotSubmitOrders(context.Background(), make([]WebsocketOrder, 1)) + require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + out := WebsocketOrder{CurrencyPair: "BTC_USDT"} + _, err = g.WebsocketSpotSubmitOrders(context.Background(), []WebsocketOrder{out}) + require.ErrorIs(t, err, order.ErrSideIsInvalid) + out.Side = strings.ToLower(order.Buy.String()) + _, err = g.WebsocketSpotSubmitOrders(context.Background(), []WebsocketOrder{out}) + require.ErrorIs(t, err, errInvalidAmount) + out.Amount = "0.0003" + out.Type = "limit" + _, err = g.WebsocketSpotSubmitOrders(context.Background(), []WebsocketOrder{out}) + require.ErrorIs(t, err, errInvalidPrice) + out.Price = "20000" + + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + + testexch.UpdatePairsOnce(t, g) + g := getWebsocketInstance(t, g) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + // test single order + got, err := g.WebsocketSpotSubmitOrders(context.Background(), []WebsocketOrder{out}) + require.NoError(t, err) + require.NotEmpty(t, got) + + // test batch orders + got, err = g.WebsocketSpotSubmitOrders(context.Background(), []WebsocketOrder{out, out}) + require.NoError(t, err) + require.NotEmpty(t, got) +} + +func TestWebsocketSpotCancelOrder(t *testing.T) { + t.Parallel() + _, err := g.WebsocketSpotCancelOrder(context.Background(), "", currency.EMPTYPAIR, "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + _, err = g.WebsocketSpotCancelOrder(context.Background(), "1337", currency.EMPTYPAIR, "") + require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + btcusdt, err := currency.NewPairFromString("BTC_USDT") + require.NoError(t, err) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + + testexch.UpdatePairsOnce(t, g) + g := getWebsocketInstance(t, g) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + got, err := g.WebsocketSpotCancelOrder(context.Background(), "644913098758", btcusdt, "") + require.NoError(t, err) + require.NotEmpty(t, got) +} + +func TestWebsocketSpotCancelAllOrdersByIDs(t *testing.T) { + t.Parallel() + _, err := g.WebsocketSpotCancelAllOrdersByIDs(context.Background(), []WebsocketOrderBatchRequest{}) + require.ErrorIs(t, err, errNoOrdersToCancel) + out := WebsocketOrderBatchRequest{} + _, err = g.WebsocketSpotCancelAllOrdersByIDs(context.Background(), []WebsocketOrderBatchRequest{out}) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + out.OrderID = "1337" + _, err = g.WebsocketSpotCancelAllOrdersByIDs(context.Background(), []WebsocketOrderBatchRequest{out}) + require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + out.Pair, err = currency.NewPairFromString("BTC_USDT") + require.NoError(t, err) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + + testexch.UpdatePairsOnce(t, g) + g := getWebsocketInstance(t, g) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + out.OrderID = "644913101755" + got, err := g.WebsocketSpotCancelAllOrdersByIDs(context.Background(), []WebsocketOrderBatchRequest{out}) + require.NoError(t, err) + require.NotEmpty(t, got) +} + +func TestWebsocketSpotCancelAllOrdersByPair(t *testing.T) { + t.Parallel() + pair, err := currency.NewPairFromString("LTC_USDT") + require.NoError(t, err) + + _, err = g.WebsocketSpotCancelAllOrdersByPair(context.Background(), pair, 0, "") + require.ErrorIs(t, err, order.ErrSideIsInvalid) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + + testexch.UpdatePairsOnce(t, g) + g := getWebsocketInstance(t, g) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + got, err := g.WebsocketSpotCancelAllOrdersByPair(context.Background(), currency.EMPTYPAIR, order.Buy, "") + require.NoError(t, err) + require.NotEmpty(t, got) +} + +func TestWebsocketSpotAmendOrder(t *testing.T) { + t.Parallel() + + _, err := g.WebsocketSpotAmendOrder(context.Background(), nil) + require.ErrorIs(t, err, common.ErrNilPointer) + + amend := &WebsocketAmendOrder{} + _, err = g.WebsocketSpotAmendOrder(context.Background(), amend) + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + amend.OrderID = "1337" + _, err = g.WebsocketSpotAmendOrder(context.Background(), amend) + require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + amend.Pair, err = currency.NewPairFromString("BTC_USDT") + require.NoError(t, err) + + _, err = g.WebsocketSpotAmendOrder(context.Background(), amend) + require.ErrorIs(t, err, errInvalidAmount) + + amend.Amount = "0.0004" + + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + + testexch.UpdatePairsOnce(t, g) + g := getWebsocketInstance(t, g) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + amend.OrderID = "645029162673" + got, err := g.WebsocketSpotAmendOrder(context.Background(), amend) + require.NoError(t, err) + require.NotEmpty(t, got) +} + +func TestWebsocketSpotGetOrderStatus(t *testing.T) { + t.Parallel() + + _, err := g.WebsocketSpotGetOrderStatus(context.Background(), "", currency.EMPTYPAIR, "") + require.ErrorIs(t, err, order.ErrOrderIDNotSet) + + _, err = g.WebsocketSpotGetOrderStatus(context.Background(), "1337", currency.EMPTYPAIR, "") + require.ErrorIs(t, err, currency.ErrCurrencyPairEmpty) + + sharedtestvalues.SkipTestIfCredentialsUnset(t, g, canManipulateRealOrders) + + testexch.UpdatePairsOnce(t, g) + g := getWebsocketInstance(t, g) //nolint:govet // Intentional shadow to avoid future copy/paste mistakes + + pair, err := currency.NewPairFromString("BTC_USDT") + require.NoError(t, err) + + got, err := g.WebsocketSpotGetOrderStatus(context.Background(), "644999650452", pair, "") + require.NoError(t, err) + require.NotEmpty(t, got) +} + +// getWebsocketInstance returns a websocket instance copy for testing. +// This restricts the pairs to a single pair per asset type to reduce test time. +func getWebsocketInstance(t *testing.T, g *Gateio) *Gateio { + t.Helper() + + cpy := new(Gateio) + cpy.SetDefaults() + gConf, err := config.GetConfig().GetExchangeConfig("GateIO") + require.NoError(t, err) + gConf.API.AuthenticatedSupport = true + gConf.API.AuthenticatedWebsocketSupport = true + gConf.API.Credentials.Key = apiKey + gConf.API.Credentials.Secret = apiSecret + + require.NoError(t, cpy.Setup(gConf), "Test instance Setup must not error") + cpy.CurrencyPairs.Load(&g.CurrencyPairs) + + for _, a := range cpy.GetAssetTypes(true) { + if a != asset.Spot { + require.NoError(t, cpy.CurrencyPairs.SetAssetEnabled(a, false)) + continue + } + avail, err := cpy.GetAvailablePairs(a) + require.NoError(t, err) + if len(avail) > 1 { + avail = avail[:1] + } + require.NoError(t, cpy.SetPairs(avail, a, true)) + } + require.NoError(t, cpy.Websocket.Connect()) + return cpy +} diff --git a/exchanges/gateio/gateio_websocket_request_types.go b/exchanges/gateio/gateio_websocket_request_types.go new file mode 100644 index 00000000000..165eea41cba --- /dev/null +++ b/exchanges/gateio/gateio_websocket_request_types.go @@ -0,0 +1,143 @@ +package gateio + +import ( + "encoding/json" + + "github.com/thrasher-corp/gocryptotrader/currency" + "github.com/thrasher-corp/gocryptotrader/types" +) + +// WebsocketAPIResponse defines a general websocket response for api calls +type WebsocketAPIResponse struct { + Header Header `json:"header"` + Data json.RawMessage `json:"data"` +} + +// Header defines a websocket header +type Header struct { + ResponseTime types.Time `json:"response_time"` + Status string `json:"status"` + Channel string `json:"channel"` + Event string `json:"event"` + ClientID string `json:"client_id"` + ConnectionID string `json:"conn_id"` + TraceID string `json:"trace_id"` +} + +// WebsocketRequest defines a websocket request +type WebsocketRequest struct { + Time int64 `json:"time,omitempty"` + ID int64 `json:"id,omitempty"` + Channel string `json:"channel"` + Event string `json:"event"` + Payload WebsocketPayload `json:"payload"` +} + +// WebsocketPayload defines an individualised websocket payload +type WebsocketPayload struct { + RequestID string `json:"req_id,omitempty"` + // APIKey and signature are only required in the initial login request + // which is done when the connection is established. + APIKey string `json:"api_key,omitempty"` + Timestamp string `json:"timestamp,omitempty"` + Signature string `json:"signature,omitempty"` + RequestParam json.RawMessage `json:"req_param,omitempty"` +} + +// WebsocketErrors defines a websocket error +type WebsocketErrors struct { + Errors struct { + Label string `json:"label"` + Message string `json:"message"` + } `json:"errs"` +} + +// WebsocketOrder defines a websocket order +type WebsocketOrder struct { + Text string `json:"text"` + CurrencyPair string `json:"currency_pair,omitempty"` + Type string `json:"type,omitempty"` + Account string `json:"account,omitempty"` + Side string `json:"side,omitempty"` + Amount string `json:"amount,omitempty"` + Price string `json:"price,omitempty"` + TimeInForce string `json:"time_in_force,omitempty"` + Iceberg string `json:"iceberg,omitempty"` + AutoBorrow bool `json:"auto_borrow,omitempty"` + AutoRepay bool `json:"auto_repay,omitempty"` + StpAct string `json:"stp_act,omitempty"` +} + +// WebsocketOrderResponse defines a websocket order response +type WebsocketOrderResponse struct { + Left types.Number `json:"left"` + UpdateTime types.Time `json:"update_time"` + Amount types.Number `json:"amount"` + CreateTime types.Time `json:"create_time"` + Price types.Number `json:"price"` + FinishAs string `json:"finish_as"` + TimeInForce string `json:"time_in_force"` + CurrencyPair currency.Pair `json:"currency_pair"` + Type string `json:"type"` + Account string `json:"account"` + Side string `json:"side"` + AmendText string `json:"amend_text"` + Text string `json:"text"` + Status string `json:"status"` + Iceberg types.Number `json:"iceberg"` + FilledTotal types.Number `json:"filled_total"` + ID string `json:"id"` + FillPrice types.Number `json:"fill_price"` + UpdateTimeMs types.Time `json:"update_time_ms"` + CreateTimeMs types.Time `json:"create_time_ms"` + Fee types.Number `json:"fee"` + FeeCurrency currency.Code `json:"fee_currency"` + PointFee types.Number `json:"point_fee"` + GTFee types.Number `json:"gt_fee"` + GTMakerFee types.Number `json:"gt_maker_fee"` + GTTakerFee types.Number `json:"gt_taker_fee"` + GTDiscount bool `json:"gt_discount"` + RebatedFee types.Number `json:"rebated_fee"` + RebatedFeeCurrency currency.Code `json:"rebated_fee_currency"` + STPID int `json:"stp_id"` + STPAct string `json:"stp_act"` +} + +// WebsocketOrderBatchRequest defines a websocket order batch request +type WebsocketOrderBatchRequest struct { + OrderID string `json:"id"` // This require id tag not order_id + Pair currency.Pair `json:"currency_pair"` + Account string `json:"account,omitempty"` +} + +// WebsocketOrderRequest defines a websocket order request +type WebsocketOrderRequest struct { + OrderID string `json:"order_id"` // This requires order_id tag + Pair string `json:"pair"` + Account string `json:"account,omitempty"` +} + +// WebsocketCancellAllResponse defines a websocket order cancel response +type WebsocketCancellAllResponse struct { + Pair currency.Pair `json:"currency_pair"` + Label string `json:"label"` + Message string `json:"message"` + Succeeded bool `json:"succeeded"` +} + +// WebsocketCancelParam is a struct to hold the parameters for cancelling orders +type WebsocketCancelParam struct { + Pair currency.Pair `json:"pair"` + Side string `json:"side"` + Account string `json:"account,omitempty"` +} + +// WebsocketAmendOrder defines a websocket amend order +type WebsocketAmendOrder struct { + OrderID string `json:"order_id"` + Pair currency.Pair `json:"currency_pair"` + Account string `json:"account,omitempty"` + AmendText string `json:"amend_text,omitempty"` + Price string `json:"price,omitempty"` + Amount string `json:"amount,omitempty"` +} diff --git a/exchanges/gateio/gateio_wrapper.go b/exchanges/gateio/gateio_wrapper.go index dad42e4dfd3..9d9929f6e56 100644 --- a/exchanges/gateio/gateio_wrapper.go +++ b/exchanges/gateio/gateio_wrapper.go @@ -218,6 +218,8 @@ func (g *Gateio) Setup(exch *config.Exchange) error { Unsubscriber: g.Unsubscribe, GenerateSubscriptions: g.generateSubscriptionsSpot, Connector: g.WsConnectSpot, + Authenticate: g.authenticateSpot, + MessageFilter: asset.Spot, BespokeGenerateMessageID: g.GenerateWebsocketMessageID, }) if err != nil { @@ -235,6 +237,7 @@ func (g *Gateio) Setup(exch *config.Exchange) error { Unsubscriber: g.FuturesUnsubscribe, GenerateSubscriptions: func() (subscription.List, error) { return g.GenerateFuturesDefaultSubscriptions(currency.USDT) }, Connector: g.WsFuturesConnect, + MessageFilter: asset.USDTMarginedFutures, BespokeGenerateMessageID: g.GenerateWebsocketMessageID, }) if err != nil { @@ -253,6 +256,7 @@ func (g *Gateio) Setup(exch *config.Exchange) error { Unsubscriber: g.FuturesUnsubscribe, GenerateSubscriptions: func() (subscription.List, error) { return g.GenerateFuturesDefaultSubscriptions(currency.BTC) }, Connector: g.WsFuturesConnect, + MessageFilter: asset.CoinMarginedFutures, BespokeGenerateMessageID: g.GenerateWebsocketMessageID, }) if err != nil { @@ -272,6 +276,7 @@ func (g *Gateio) Setup(exch *config.Exchange) error { Unsubscriber: g.DeliveryFuturesUnsubscribe, GenerateSubscriptions: g.GenerateDeliveryFuturesDefaultSubscriptions, Connector: g.WsDeliveryFuturesConnect, + MessageFilter: asset.DeliveryFutures, BespokeGenerateMessageID: g.GenerateWebsocketMessageID, }) if err != nil { @@ -288,6 +293,7 @@ func (g *Gateio) Setup(exch *config.Exchange) error { Unsubscriber: g.OptionsUnsubscribe, GenerateSubscriptions: g.GenerateOptionsDefaultSubscriptions, Connector: g.WsOptionsConnect, + MessageFilter: asset.Options, BespokeGenerateMessageID: g.GenerateWebsocketMessageID, }) } diff --git a/exchanges/kraken/kraken_websocket.go b/exchanges/kraken/kraken_websocket.go index 22088590463..5b276055181 100644 --- a/exchanges/kraken/kraken_websocket.go +++ b/exchanges/kraken/kraken_websocket.go @@ -231,7 +231,7 @@ func (k *Kraken) wsHandleData(respRaw []byte) error { return nil case krakenWsCancelOrderStatus, krakenWsCancelAllOrderStatus, krakenWsAddOrderStatus, krakenWsSubscriptionStatus: // All of these should have found a listener already - return fmt.Errorf("%w: %s %v", stream.ErrNoMessageListener, event, reqID) + return fmt.Errorf("%w: %s %v", stream.ErrSignatureNotMatched, event, reqID) case krakenWsSystemStatus: return k.wsProcessSystemStatus(respRaw) default: diff --git a/exchanges/kucoin/kucoin_websocket.go b/exchanges/kucoin/kucoin_websocket.go index 8b4aa535e26..e364b2b84fc 100644 --- a/exchanges/kucoin/kucoin_websocket.go +++ b/exchanges/kucoin/kucoin_websocket.go @@ -215,18 +215,14 @@ func (ku *Kucoin) wsReadData() { // wsHandleData processes a websocket incoming data. func (ku *Kucoin) wsHandleData(respData []byte) error { resp := WsPushData{} - err := json.Unmarshal(respData, &resp) - if err != nil { + if err := json.Unmarshal(respData, &resp); err != nil { return err } if resp.Type == "pong" || resp.Type == "welcome" { return nil } if resp.ID != "" { - if !ku.Websocket.Match.IncomingWithData("msgID:"+resp.ID, respData) { - return fmt.Errorf("%w: %s", stream.ErrNoMessageListener, resp.ID) - } - return nil + return ku.Websocket.Match.RequireMatchWithData("msgID:"+resp.ID, respData) } topicInfo := strings.Split(resp.Topic, ":") switch topicInfo[0] { diff --git a/exchanges/stream/stream_match.go b/exchanges/stream/stream_match.go index a7b8a10bfab..430688a816b 100644 --- a/exchanges/stream/stream_match.go +++ b/exchanges/stream/stream_match.go @@ -2,9 +2,13 @@ package stream import ( "errors" + "fmt" "sync" ) +// ErrSignatureNotMatched is returned when a signature does not match a request +var ErrSignatureNotMatched = errors.New("websocket response to request signature not matched") + var ( errSignatureCollision = errors.New("signature collision") errInvalidBufferSize = errors.New("buffer size must be positive") @@ -47,6 +51,15 @@ func (m *Match) IncomingWithData(signature any, data []byte) bool { return true } +// RequireMatchWithData validates that incoming data matches a request's signature. +// If a match is found, the data is processed; otherwise, it returns an error. +func (m *Match) RequireMatchWithData(signature any, data []byte) error { + if m.IncomingWithData(signature, data) { + return nil + } + return fmt.Errorf("'%v' %w with data %v", signature, ErrSignatureNotMatched, string(data)) +} + // Set the signature response channel for incoming data func (m *Match) Set(signature any, bufSize int) (<-chan []byte, error) { if bufSize <= 0 { diff --git a/exchanges/stream/stream_match_test.go b/exchanges/stream/stream_match_test.go index b7a21b23c05..8053cb37934 100644 --- a/exchanges/stream/stream_match_test.go +++ b/exchanges/stream/stream_match_test.go @@ -51,3 +51,18 @@ func TestRemoveSignature(t *testing.T) { t.Fatal("Should be able to read from a closed channel") } } + +func TestRequireMatchWithData(t *testing.T) { + t.Parallel() + match := NewMatch() + err := match.RequireMatchWithData("hello", []byte("world")) + require.ErrorIs(t, err, ErrSignatureNotMatched, "Must error on unmatched signature") + assert.Contains(t, err.Error(), "world", "Should contain the data in the error message") + assert.Contains(t, err.Error(), "hello", "Should contain the signature in the error message") + + ch, err := match.Set("hello", 1) + require.NoError(t, err, "Set must not error") + err = match.RequireMatchWithData("hello", []byte("world")) + require.NoError(t, err, "Must not error on matched signature") + assert.Equal(t, "world", string(<-ch)) +} diff --git a/exchanges/stream/stream_types.go b/exchanges/stream/stream_types.go index 2cbf0a2fe04..832e74c5526 100644 --- a/exchanges/stream/stream_types.go +++ b/exchanges/stream/stream_types.go @@ -27,6 +27,8 @@ type Connection interface { SendMessageReturnResponse(ctx context.Context, epl request.EndpointLimit, signature any, request any) ([]byte, error) // SendMessageReturnResponses will send a WS message to the connection and wait for N responses SendMessageReturnResponses(ctx context.Context, epl request.EndpointLimit, signature any, request any, expected int) ([][]byte, error) + // SendMessageReturnResponsesWithInspector will send a WS message to the connection and wait for N responses with message inspection + SendMessageReturnResponsesWithInspector(ctx context.Context, epl request.EndpointLimit, signature any, request any, expected int, messageInspector Inspector) ([][]byte, error) // SendRawMessage sends a message over the connection without JSON encoding it SendRawMessage(ctx context.Context, epl request.EndpointLimit, messageType int, message []byte) error // SendJSONMessage sends a JSON encoded message over the connection @@ -37,6 +39,12 @@ type Connection interface { Shutdown() error } +// Inspector is used to verify messages via SendMessageReturnResponsesWithInspection +// It inspects the []bytes websocket message and returns true if the message is the final message in a sequence of expected messages +type Inspector interface { + IsFinal([]byte) bool +} + // Response defines generalised data from the stream connection type Response struct { Type int @@ -76,6 +84,11 @@ type ConnectionSetup struct { // This is useful for when an exchange connection requires a unique or // structured message ID for each message sent. BespokeGenerateMessageID func(highPrecision bool) int64 + // Authenticate will be called to authenticate the connection + Authenticate func(ctx context.Context, conn Connection) error + // MessageFilter defines the criteria used to match messages to a specific connection. + // The filter enables precise routing and handling of messages for distinct connection contexts. + MessageFilter any } // ConnectionWrapper contains the connection setup details to be used when diff --git a/exchanges/stream/websocket.go b/exchanges/stream/websocket.go index 309db9a79d4..737c0eadc7f 100644 --- a/exchanges/stream/websocket.go +++ b/exchanges/stream/websocket.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "net/url" + "reflect" "slices" "sync" "time" @@ -27,8 +28,10 @@ var ( ErrUnsubscribeFailure = errors.New("unsubscribe failure") ErrAlreadyDisabled = errors.New("websocket already disabled") ErrNotConnected = errors.New("websocket is not connected") - ErrNoMessageListener = errors.New("websocket listener not found for message") ErrSignatureTimeout = errors.New("websocket timeout waiting for response with signature") + ErrRequestRouteNotFound = errors.New("request route not found") + ErrSignatureNotSet = errors.New("signature not set") + ErrRequestPayloadNotSet = errors.New("request payload not set") ) // Private websocket errors @@ -64,6 +67,9 @@ var ( errConnectionWrapperDuplication = errors.New("connection wrapper duplication") errCannotChangeConnectionURL = errors.New("cannot change connection URL when using multi connection management") errExchangeConfigEmpty = errors.New("exchange config is empty") + errCannotObtainOutboundConnection = errors.New("cannot obtain outbound connection") + errMessageFilterNotSet = errors.New("message filter not set") + errMessageFilterNotComparable = errors.New("message filter is not comparable") ) var globalReporter Reporter @@ -259,13 +265,19 @@ func (w *Websocket) SetupNewConnection(c *ConnectionSetup) error { return fmt.Errorf("%w: %w", errConnSetup, errWebsocketDataHandlerUnset) } + if c.MessageFilter != nil && !reflect.TypeOf(c.MessageFilter).Comparable() { + return errMessageFilterNotComparable + } + for x := range w.connectionManager { - if w.connectionManager[x].Setup.URL == c.URL { + // Below allows for multiple connections to the same URL with different outbound request signatures. This + // allows for easier determination of inbound and outbound messages. e.g. Gateio cross_margin, margin on + // a spot connection. + if w.connectionManager[x].Setup.URL == c.URL && c.MessageFilter == w.connectionManager[x].Setup.MessageFilter { return fmt.Errorf("%w: %w", errConnSetup, errConnectionWrapperDuplication) } } - - w.connectionManager = append(w.connectionManager, ConnectionWrapper{ + w.connectionManager = append(w.connectionManager, &ConnectionWrapper{ Setup: c, Subscriptions: subscription.NewStore(), }) @@ -422,12 +434,21 @@ func (w *Websocket) connect() error { break } - w.connections[conn] = &w.connectionManager[i] + w.connections[conn] = w.connectionManager[i] w.connectionManager[i].Connection = conn w.Wg.Add(1) go w.Reader(context.TODO(), conn, w.connectionManager[i].Setup.Handler) + if w.connectionManager[i].Setup.Authenticate != nil && w.CanUseAuthenticatedEndpoints() { + err = w.connectionManager[i].Setup.Authenticate(context.TODO(), conn) + if err != nil { + // Opted to not fail entirely here for POC. This should be + // revisited and handled more gracefully. + log.Errorf(log.WebsocketMgr, "%s websocket: [conn:%d] [URL:%s] failed to authenticate %v", w.exchangeName, i+1, conn.URL, err) + } + } + err = w.connectionManager[i].Setup.Subscriber(context.TODO(), conn, subs) if err != nil { multiConnectFatalError = fmt.Errorf("%v Error subscribing %w", w.exchangeName, err) @@ -633,7 +654,7 @@ func (w *Websocket) FlushChannels() error { } w.Wg.Add(1) go w.Reader(context.TODO(), conn, w.connectionManager[x].Setup.Handler) - w.connections[conn] = &w.connectionManager[x] + w.connections[conn] = w.connectionManager[x] w.connectionManager[x].Connection = conn } @@ -1064,7 +1085,7 @@ func (w *Websocket) checkSubscriptions(conn Connection, subs subscription.List) if s.State() == subscription.ResubscribingState { continue } - if found := w.subscriptions.Get(s); found != nil { + if found := subscriptionStore.Get(s); found != nil { return fmt.Errorf("%w: %s", subscription.ErrDuplicate, s) } } @@ -1241,3 +1262,37 @@ func signalReceived(ch chan struct{}) bool { return false } } + +// GetConnection returns a connection by message filter (defined in exchange package _wrapper.go websocket connection) +// for request and response handling in a multi connection context. +func (w *Websocket) GetConnection(messageFilter any) (Connection, error) { + if w == nil { + return nil, fmt.Errorf("%w: %T", common.ErrNilPointer, w) + } + + if messageFilter == nil { + return nil, errMessageFilterNotSet + } + + w.m.Lock() + defer w.m.Unlock() + + if !w.useMultiConnectionManagement { + return nil, fmt.Errorf("%s: multi connection management not enabled %w please use exported Conn and AuthConn fields", w.exchangeName, errCannotObtainOutboundConnection) + } + + if !w.IsConnected() { + return nil, ErrNotConnected + } + + for _, wrapper := range w.connectionManager { + if wrapper.Setup.MessageFilter == messageFilter { + if wrapper.Connection == nil { + return nil, fmt.Errorf("%s: %s %w associated with message filter: '%v'", w.exchangeName, wrapper.Setup.URL, ErrNotConnected, messageFilter) + } + return wrapper.Connection, nil + } + } + + return nil, fmt.Errorf("%s: %w associated with message filter: '%v'", w.exchangeName, ErrRequestRouteNotFound, messageFilter) +} diff --git a/exchanges/stream/websocket_connection.go b/exchanges/stream/websocket_connection.go index 1f6f5e6019a..55fd71682e6 100644 --- a/exchanges/stream/websocket_connection.go +++ b/exchanges/stream/websocket_connection.go @@ -304,6 +304,12 @@ func (w *WebsocketConnection) SendMessageReturnResponse(ctx context.Context, epl // SendMessageReturnResponses will send a WS message to the connection and wait for N responses // An error of ErrSignatureTimeout can be ignored if individual responses are being otherwise tracked func (w *WebsocketConnection) SendMessageReturnResponses(ctx context.Context, epl request.EndpointLimit, signature, payload any, expected int) ([][]byte, error) { + return w.SendMessageReturnResponsesWithInspector(ctx, epl, signature, payload, expected, nil) +} + +// SendMessageReturnResponsesWithInspector will send a WS message to the connection and wait for N responses +// An error of ErrSignatureTimeout can be ignored if individual responses are being otherwise tracked +func (w *WebsocketConnection) SendMessageReturnResponsesWithInspector(ctx context.Context, epl request.EndpointLimit, signature, payload any, expected int, messageInspector Inspector) ([][]byte, error) { outbound, err := json.Marshal(payload) if err != nil { return nil, fmt.Errorf("error marshaling json for %s: %w", signature, err) @@ -320,28 +326,43 @@ func (w *WebsocketConnection) SendMessageReturnResponses(ctx context.Context, ep return nil, err } + resps, err := w.waitForResponses(ctx, signature, ch, expected, messageInspector) + if err != nil { + return nil, err + } + + if w.Reporter != nil { + w.Reporter.Latency(w.ExchangeName, outbound, time.Since(start)) + } + + return resps, err +} + +// waitForResponses waits for N responses from a channel +func (w *WebsocketConnection) waitForResponses(ctx context.Context, signature any, ch <-chan []byte, expected int, messageInspector Inspector) ([][]byte, error) { timeout := time.NewTimer(w.ResponseMaxLimit * time.Duration(expected)) + defer timeout.Stop() resps := make([][]byte, 0, expected) - for err == nil && len(resps) < expected { +inspection: + for range expected { select { case resp := <-ch: resps = append(resps, resp) + // Checks recently received message to determine if this is in fact the final message in a sequence of messages. + if messageInspector != nil && messageInspector.IsFinal(resp) { + w.Match.RemoveSignature(signature) + break inspection + } case <-timeout.C: w.Match.RemoveSignature(signature) - err = fmt.Errorf("%s %w %v", w.ExchangeName, ErrSignatureTimeout, signature) + return nil, fmt.Errorf("%s %w %v", w.ExchangeName, ErrSignatureTimeout, signature) case <-ctx.Done(): w.Match.RemoveSignature(signature) - err = ctx.Err() + return nil, ctx.Err() } } - timeout.Stop() - - if err == nil && w.Reporter != nil { - w.Reporter.Latency(w.ExchangeName, outbound, time.Since(start)) - } - // Only check context verbosity. If the exchange is verbose, it will log the responses in the ReadMessage() call. if request.IsVerbose(ctx, false) { for i := range resps { @@ -349,7 +370,7 @@ func (w *WebsocketConnection) SendMessageReturnResponses(ctx context.Context, ep } } - return resps, err + return resps, nil } func removeURLQueryString(url string) string { diff --git a/exchanges/stream/websocket_test.go b/exchanges/stream/websocket_test.go index 2904bccadee..b6f3a762404 100644 --- a/exchanges/stream/websocket_test.go +++ b/exchanges/stream/websocket_test.go @@ -223,13 +223,16 @@ func TestConnectionMessageErrors(t *testing.T) { assert.ErrorIs(t, err, errNoPendingConnections, "Connect should error correctly") ws.useMultiConnectionManagement = true + ws.SetCanUseAuthenticatedEndpoints(true) mock := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { mockws.WsMockUpgrader(t, w, r, mockws.EchoHandler) })) defer mock.Close() - ws.connectionManager = []ConnectionWrapper{{Setup: &ConnectionSetup{URL: "ws" + mock.URL[len("http"):] + "/ws"}}} + ws.connectionManager = []*ConnectionWrapper{{Setup: &ConnectionSetup{URL: "ws" + mock.URL[len("http"):] + "/ws"}}} err = ws.Connect() require.ErrorIs(t, err, errWebsocketSubscriptionsGeneratorUnset) + ws.connectionManager[0].Setup.Authenticate = func(context.Context, Connection) error { return errDastardlyReason } + ws.connectionManager[0].Setup.GenerateSubscriptions = func() (subscription.List, error) { return nil, errDastardlyReason } @@ -371,7 +374,7 @@ func TestWebsocket(t *testing.T) { ws.useMultiConnectionManagement = true - ws.connectionManager = []ConnectionWrapper{{Setup: &ConnectionSetup{URL: "ws://demos.kaazing.com/echo"}, Connection: &WebsocketConnection{}}} + ws.connectionManager = []*ConnectionWrapper{{Setup: &ConnectionSetup{URL: "ws://demos.kaazing.com/echo"}, Connection: &WebsocketConnection{}}} err = ws.SetProxyAddress("https://192.168.0.1:1337") require.NoError(t, err) } @@ -464,7 +467,7 @@ func TestSubscribeUnsubscribe(t *testing.T) { amazingConn := multi.getConnectionFromSetup(amazingCandidate) multi.connections = map[Connection]*ConnectionWrapper{ - amazingConn: &multi.connectionManager[0], + amazingConn: multi.connectionManager[0], } subs, err = amazingCandidate.GenerateSubscriptions() @@ -761,8 +764,43 @@ func TestSendMessageReturnResponse(t *testing.T) { wc.ResponseMaxLimit = 1 _, err = wc.SendMessageReturnResponse(context.Background(), request.Unset, "123", req) assert.ErrorIs(t, err, ErrSignatureTimeout, "SendMessageReturnResponse should error when request ID not found") + + _, err = wc.SendMessageReturnResponsesWithInspector(context.Background(), request.Unset, "123", req, 1, inspection{}) + assert.ErrorIs(t, err, ErrSignatureTimeout, "SendMessageReturnResponse should error when request ID not found") } +func TestWaitForResponses(t *testing.T) { + t.Parallel() + dummy := &WebsocketConnection{ + ResponseMaxLimit: time.Nanosecond, + Match: NewMatch(), + } + _, err := dummy.waitForResponses(context.Background(), "silly", nil, 1, inspection{}) + require.ErrorIs(t, err, ErrSignatureTimeout) + + dummy.ResponseMaxLimit = time.Second + ctx, cancel := context.WithCancel(context.Background()) + cancel() + _, err = dummy.waitForResponses(ctx, "silly", nil, 1, inspection{}) + require.ErrorIs(t, err, context.Canceled) + + // test break early and hit verbose path + ch := make(chan []byte, 1) + ch <- []byte("hello") + ctx = request.WithVerbose(context.Background()) + + got, err := dummy.waitForResponses(ctx, "silly", ch, 2, inspection{breakEarly: true}) + require.NoError(t, err) + require.Len(t, got, 1) + assert.Equal(t, "hello", string(got[0])) +} + +type inspection struct { + breakEarly bool +} + +func (i inspection) IsFinal([]byte) bool { return i.breakEarly } + type reporter struct { name string msg []byte @@ -1229,6 +1267,11 @@ func TestSetupNewConnection(t *testing.T) { require.ErrorIs(t, err, errWebsocketDataHandlerUnset) connSetup.Handler = func(context.Context, []byte) error { return nil } + connSetup.MessageFilter = []string{"slices are super naughty and not comparable"} + err = multi.SetupNewConnection(connSetup) + require.ErrorIs(t, err, errMessageFilterNotComparable) + + connSetup.MessageFilter = "comparable string signature" err = multi.SetupNewConnection(connSetup) require.NoError(t, err) @@ -1484,3 +1527,42 @@ func TestMonitorTraffic(t *testing.T) { ws.TrafficAlert <- struct{}{} require.False(t, innerShell()) } + +func TestGetConnection(t *testing.T) { + t.Parallel() + var ws *Websocket + _, err := ws.GetConnection(nil) + require.ErrorIs(t, err, common.ErrNilPointer) + + ws = &Websocket{} + + _, err = ws.GetConnection(nil) + require.ErrorIs(t, err, errMessageFilterNotSet) + + _, err = ws.GetConnection("testURL") + require.ErrorIs(t, err, errCannotObtainOutboundConnection) + + ws.useMultiConnectionManagement = true + + _, err = ws.GetConnection("testURL") + require.ErrorIs(t, err, ErrNotConnected) + + ws.setState(connectedState) + + _, err = ws.GetConnection("testURL") + require.ErrorIs(t, err, ErrRequestRouteNotFound) + + ws.connectionManager = []*ConnectionWrapper{{ + Setup: &ConnectionSetup{MessageFilter: "testURL", URL: "testURL"}, + }} + + _, err = ws.GetConnection("testURL") + require.ErrorIs(t, err, ErrNotConnected) + + expected := &WebsocketConnection{} + ws.connectionManager[0].Connection = expected + + conn, err := ws.GetConnection("testURL") + require.NoError(t, err) + assert.Same(t, expected, conn) +} diff --git a/exchanges/stream/websocket_types.go b/exchanges/stream/websocket_types.go index 27a5c81963f..26b20f1ee74 100644 --- a/exchanges/stream/websocket_types.go +++ b/exchanges/stream/websocket_types.go @@ -54,7 +54,7 @@ type Websocket struct { // For example, separate connections can be used for Spot, Margin, and Futures trading. This structure is especially useful // for exchanges that differentiate between trading pairs by using different connection endpoints or protocols for various asset classes. // If an exchange does not require such differentiation, all connections may be managed under a single ConnectionWrapper. - connectionManager []ConnectionWrapper + connectionManager []*ConnectionWrapper // connections holds a look up table for all connections to their corresponding ConnectionWrapper and subscription holder connections map[Connection]*ConnectionWrapper From 3bc0aa730a6c23ba5232d7b6252fda3278e3c4d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 09:28:50 +1100 Subject: [PATCH 03/21] build(deps): Bump golang.org/x/net from 0.32.0 to 0.33.0 (#1759) Bumps [golang.org/x/net](https://github.com/golang/net) from 0.32.0 to 0.33.0. - [Commits](https://github.com/golang/net/compare/v0.32.0...v0.33.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 | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 73799fb7ad0..c9712f82bab 100644 --- a/go.mod +++ b/go.mod @@ -25,7 +25,7 @@ require ( 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.32.0 + 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 diff --git a/go.sum b/go.sum index e3504750ac0..c7cd5ddd74c 100644 --- a/go.sum +++ b/go.sum @@ -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.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI= -golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs= +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/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 737fb31902751d2fa9d28abeef1090822ce80c33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 09:29:56 +1100 Subject: [PATCH 04/21] build(deps): Bump bufbuild/buf-setup-action from 1.47.2 to 1.48.0 (#1756) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.47.2 to 1.48.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.47.2...v1.48.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 fb45f7d8af5..9cf0e8056e7 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.47.2 + - uses: bufbuild/buf-setup-action@v1.48.0 - name: buf generate working-directory: ./gctrpc From 9b4e63c523718cb7b9ae88a411acaca69f7f2719 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 09:54:01 +1100 Subject: [PATCH 05/21] build(deps): Bump google.golang.org/grpc from 1.69.0 to 1.69.2 (#1758) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.69.0 to 1.69.2. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.69.0...v1.69.2) --- updated-dependencies: - dependency-name: google.golang.org/grpc 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 c9712f82bab..df5f5e08b18 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( golang.org/x/text v0.21.0 golang.org/x/time v0.8.0 google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 - google.golang.org/grpc v1.69.0 + google.golang.org/grpc v1.69.2 google.golang.org/protobuf v1.35.2 ) diff --git a/go.sum b/go.sum index c7cd5ddd74c..7a55e40a4f4 100644 --- a/go.sum +++ b/go.sum @@ -368,8 +368,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.69.0 h1:quSiOM1GJPmPH5XtU+BCoVXcDVJJAzNcoyfC2cCjGkI= -google.golang.org/grpc v1.69.0/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= +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.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= From 0bc9f23aea66803e1d3a8136be896add6807b8b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Dec 2024 10:07:39 +1100 Subject: [PATCH 06/21] build(deps): Bump github.com/grpc-ecosystem/grpc-gateway/v2 (#1760) Bumps [github.com/grpc-ecosystem/grpc-gateway/v2](https://github.com/grpc-ecosystem/grpc-gateway) from 2.24.0 to 2.25.1. - [Release notes](https://github.com/grpc-ecosystem/grpc-gateway/releases) - [Changelog](https://github.com/grpc-ecosystem/grpc-gateway/blob/main/.goreleaser.yml) - [Commits](https://github.com/grpc-ecosystem/grpc-gateway/compare/v2.24.0...v2.25.1) --- updated-dependencies: - dependency-name: github.com/grpc-ecosystem/grpc-gateway/v2 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 | 8 ++++---- go.sum | 16 ++++++++-------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/go.mod b/go.mod index df5f5e08b18..e0ced2ee6fd 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/gorilla/mux v1.8.1 github.com/gorilla/websocket v1.5.3 github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 - github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 + github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 github.com/kat-co/vala v0.0.0-20170210184112-42e1d8b61f12 github.com/lib/pq v1.10.9 github.com/mattn/go-sqlite3 v1.14.23 @@ -29,9 +29,9 @@ require ( golang.org/x/term v0.27.0 golang.org/x/text v0.21.0 golang.org/x/time v0.8.0 - google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 + google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb google.golang.org/grpc v1.69.2 - google.golang.org/protobuf v1.35.2 + google.golang.org/protobuf v1.36.0 ) require ( @@ -68,7 +68,7 @@ require ( golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/sys v0.28.0 // indirect golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 7a55e40a4f4..3411718eab5 100644 --- a/go.sum +++ b/go.sum @@ -107,8 +107,8 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDa github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.9.0/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1 h1:VNqngBF40hVlDloBruUehVYC3ArSgIyScOAyMRqBxRg= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.25.1/go.mod h1:RBRO7fro65R6tjKzYgLAFo0t1QEXY1Dp+i/bvpRiqiQ= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= @@ -358,10 +358,10 @@ google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697 h1:pgr/4QbFyktUv9CtQ/Fq4gzEE6/Xs7iCXbktaGzLHbQ= -google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697/go.mod h1:+D9ySVjN8nY8YCVjc5O7PZDIdZporIDY3KaGfJunh88= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697 h1:LWZqQOEjDyONlF1H6afSWpAL/znlREo2tHfLoe+8LMA= -google.golang.org/genproto/googleapis/rpc v0.0.0-20241118233622-e639e219e697/go.mod h1:5uTbfoYQed2U9p3KIj2/Zzm02PYhndfdmML0qC3q3FU= +google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb h1:B7GIB7sr443wZ/EAEl7VZjmh1V6qzkt5V+RYcUYtS1U= +google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:E5//3O5ZIG2l71Xnt+P/CYUY8Bxs8E7WMoZ9tlcMbAY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb h1:3oy2tynMOP1QbTC0MsNNAV+Se8M2Bd0A5+x1QHyw+pI= +google.golang.org/genproto/googleapis/rpc v0.0.0-20241219192143-6b3ec007d9bb/go.mod h1:lcTa1sDdWEIHMWlITnIczmw5w60CF9ffkb8Z+DVmmjA= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= @@ -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.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io= -google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= +google.golang.org/protobuf v1.36.0/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 b209b0e1a1227bf39fa8cd257a4875ac9ce88b33 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 30 Dec 2024 11:27:37 +1100 Subject: [PATCH 07/21] build(deps): Bump google.golang.org/protobuf from 1.35.2 to 1.36.1 (#1763) Bumps google.golang.org/protobuf from 1.35.2 to 1.36.1. --- updated-dependencies: - dependency-name: google.golang.org/protobuf 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 e0ced2ee6fd..0c866fe14aa 100644 --- a/go.mod +++ b/go.mod @@ -31,7 +31,7 @@ require ( golang.org/x/time v0.8.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.0 + google.golang.org/protobuf v1.36.1 ) require ( diff --git a/go.sum b/go.sum index 3411718eab5..aa402b4a5f9 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.0 h1:mjIs9gYtt56AzC4ZaffQuh88TZurBGhIJMBZGSxNerQ= -google.golang.org/protobuf v1.36.0/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +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= 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 4fcee8489e7c58a871f86f045e150ffcf33b3e09 Mon Sep 17 00:00:00 2001 From: Gareth Kirwan Date: Thu, 2 Jan 2025 21:55:58 +0000 Subject: [PATCH 08/21] 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 09/21] 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 10/21] 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 11/21] 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 12/21] 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 13/21] 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 14/21] 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 15/21] 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 16/21] 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 17/21] 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 18/21] 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 } From f419f6e61482a20c564647f3dd892966555fb9a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:40:02 +1100 Subject: [PATCH 19/21] build(deps): Bump google.golang.org/protobuf from 1.36.2 to 1.36.3 (#1779) Bumps google.golang.org/protobuf from 1.36.2 to 1.36.3. --- 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 cc526512197..7b3c368c3ca 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.2 + google.golang.org/protobuf v1.36.3 ) require ( diff --git a/go.sum b/go.sum index bad962ba073..d51f2c1b5d1 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.2 h1:R8FeyR1/eLmkutZOM5CWghmo5itiG9z0ktFlTVLuTmU= -google.golang.org/protobuf v1.36.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= +google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= +google.golang.org/protobuf v1.36.3/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 d412ae74adde7b8f52462ffaa7b3a976fbd55e4f Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 16:53:31 +1100 Subject: [PATCH 20/21] build(deps): Bump google.golang.org/grpc from 1.69.2 to 1.69.4 (#1780) Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.69.2 to 1.69.4. - [Release notes](https://github.com/grpc/grpc-go/releases) - [Commits](https://github.com/grpc/grpc-go/compare/v1.69.2...v1.69.4) --- updated-dependencies: - dependency-name: google.golang.org/grpc 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 7b3c368c3ca..80cb89f4f6d 100644 --- a/go.mod +++ b/go.mod @@ -30,7 +30,7 @@ require ( 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 - google.golang.org/grpc v1.69.2 + google.golang.org/grpc v1.69.4 google.golang.org/protobuf v1.36.3 ) diff --git a/go.sum b/go.sum index d51f2c1b5d1..b92aa8170c6 100644 --- a/go.sum +++ b/go.sum @@ -368,8 +368,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= 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/grpc v1.69.4 h1:MF5TftSMkd8GLw/m0KM6V8CMOCY6NZ1NQDPGFgbTt4A= +google.golang.org/grpc v1.69.4/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= google.golang.org/protobuf v1.36.3 h1:82DV7MYdb8anAVi3qge1wSnMDrnKK7ebr+I0hHRN1BU= google.golang.org/protobuf v1.36.3/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= From f0c785de8999a25e620b930f586b52f9226ec86e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 20 Jan 2025 17:07:24 +1100 Subject: [PATCH 21/21] build(deps): Bump bufbuild/buf-setup-action from 1.49.0 to 1.50.0 (#1781) Bumps [bufbuild/buf-setup-action](https://github.com/bufbuild/buf-setup-action) from 1.49.0 to 1.50.0. - [Release notes](https://github.com/bufbuild/buf-setup-action/releases) - [Commits](https://github.com/bufbuild/buf-setup-action/compare/v1.49.0...v1.50.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 bf98f79022d..6b86a966810 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.49.0 + - uses: bufbuild/buf-setup-action@v1.50.0 - name: buf generate working-directory: ./gctrpc