diff --git a/.github/workflows/proto-lint.yml b/.github/workflows/proto-lint.yml
index fb45f7d8af5..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.47.2
+ - uses: bufbuild/buf-setup-action@v1.50.0
- name: buf generate
working-directory: ./gctrpc
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
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/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/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/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/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/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/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/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/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/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/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:
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/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/exchange.go b/exchanges/exchange.go
index 7d5b1df4594..63ecb7517f5 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.go b/exchanges/gateio/gateio.go
index e4187d606fa..a0658d44ac7 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"
@@ -776,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)
@@ -792,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() {
@@ -1843,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
}
@@ -2429,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
}
@@ -2624,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
}
@@ -2657,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
}
@@ -2678,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)
}
@@ -2792,7 +2790,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 +2807,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 +2825,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 +2918,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 == "" {
@@ -2942,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
}
@@ -3408,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 80f7a81b099..5c0468567ff 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) {
@@ -1142,29 +1141,25 @@ 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) {
+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) {
@@ -1225,7 +1220,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")
}
@@ -1372,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) {
@@ -1450,16 +1445,23 @@ 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) {
+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) {
@@ -1527,6 +1529,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 +1540,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")
}
@@ -1758,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) {
@@ -3177,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) {
@@ -3519,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 80a2c8b8df3..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,14 +1995,14 @@ 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"`
+// WSResponse represents generalized websocket push data from the server.
+type WSResponse struct {
+ ID int64 `json:"id"`
+ Time time.Time `json:"time"`
+ Channel string `json:"channel"`
+ Event string `json:"event"`
+ Result json.RawMessage `json:"result"`
+ RequestID string `json:"request_id"`
}
// WsTicker websocket ticker information.
@@ -2032,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"`
@@ -2053,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"`
@@ -2064,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"`
@@ -2076,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"`
@@ -2109,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.
@@ -2121,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"`
@@ -2135,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"`
@@ -2160,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
@@ -2215,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"`
@@ -2236,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"`
@@ -2252,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"`
@@ -2276,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"`
@@ -2302,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
@@ -2327,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"`
}
@@ -2339,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"`
}
@@ -2351,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
@@ -2365,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"`
}
@@ -2379,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"`
}
@@ -2402,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"`
}
@@ -2452,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.
@@ -2491,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.
@@ -2520,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.
@@ -2564,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.
@@ -2608,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.
@@ -2622,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 0733a8e7ae7..1d30cd05cca 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"
@@ -102,6 +103,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))
@@ -113,32 +172,32 @@ 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)
+ push, err := parseWSHeader(respRaw)
if 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.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:
@@ -163,6 +222,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 {
@@ -209,7 +307,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,
@@ -270,7 +368,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()}},
@@ -325,7 +423,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,
@@ -362,7 +460,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,
@@ -410,8 +508,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
@@ -440,7 +538,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_ws_delivery_futures.go b/exchanges/gateio/gateio_websocket_delivery_futures.go
similarity index 100%
rename from exchanges/gateio/gateio_ws_delivery_futures.go
rename to exchanges/gateio/gateio_websocket_delivery_futures.go
diff --git a/exchanges/gateio/gateio_ws_futures.go b/exchanges/gateio/gateio_websocket_futures.go
similarity index 98%
rename from exchanges/gateio/gateio_ws_futures.go
rename to exchanges/gateio/gateio_websocket_futures.go
index ba393277553..628824a08ca 100644
--- a/exchanges/gateio/gateio_ws_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_ws_option.go b/exchanges/gateio/gateio_websocket_option.go
similarity index 98%
rename from exchanges/gateio/gateio_ws_option.go
rename to exchanges/gateio/gateio_websocket_option.go
index 091a9ad1f0f..7ec39737e6e 100644
--- a/exchanges/gateio/gateio_ws_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_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 bb28b848ea7..239e3179245 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,
})
}
@@ -520,21 +526,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 +634,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 +834,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])
@@ -934,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
}
@@ -957,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
}
@@ -984,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
}
@@ -1007,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
}
@@ -1103,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
@@ -1486,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
@@ -1694,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,
@@ -1721,6 +1725,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)
@@ -1824,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
}
@@ -1861,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
@@ -1885,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
}
@@ -2111,61 +2119,54 @@ 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
+ }
+ resp := make([]futures.Contract, len(contracts))
+ for i := range contracts {
+ name, err := currency.NewPairFromString(contracts[i].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[i].Underlying)
+ if err != nil {
+ return nil, err
+ }
+ // 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 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())
+ 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())
+ }
+ resp[i] = futures.Contract{
+ Exchange: g.Name,
+ Name: name,
+ Underlying: underlying,
+ Asset: item,
+ StartDate: s,
+ EndDate: e,
+ SettlementType: futures.Linear,
+ IsActive: !contracts[i].InDelisting,
+ Type: ct,
+ SettlementCurrencies: currency.Currencies{currency.USDT},
+ MarginCurrency: currency.Code{},
+ Multiplier: contracts[i].QuantoMultiplier.Float64(),
+ MaxLeverage: contracts[i].LeverageMax.Float64(),
}
- resp = append(resp, contractsToAdd...)
}
return resp, nil
}
@@ -2324,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
}
@@ -2410,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
}
@@ -2432,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/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/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")
}
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/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 8b4aa535e26..a1c43bb68d5 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] {
@@ -1074,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,
@@ -1690,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 ""
}
@@ -1753,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 }}
`
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)
}
}
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
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..d35d8a5e62e 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
}
@@ -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
}
diff --git a/go.mod b/go.mod
index 73799fb7ad0..80cb89f4f6d 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
@@ -24,14 +24,14 @@ 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.32.0
- golang.org/x/term v0.27.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.8.0
- google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697
- google.golang.org/grpc v1.69.0
- google.golang.org/protobuf v1.35.2
+ golang.org/x/time v0.9.0
+ google.golang.org/genproto/googleapis/api v0.0.0-20241219192143-6b3ec007d9bb
+ google.golang.org/grpc v1.69.4
+ google.golang.org/protobuf v1.36.3
)
require (
@@ -66,9 +66,9 @@ 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-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 e3504750ac0..b92aa8170c6 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=
@@ -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.32.0 h1:ZqPmj8Kzc+Y6e0+skZsuACbx+wzMgo5MQsJh9Qd6aYI=
-golang.org/x/net v0.32.0/go.mod h1:CwU0IoeOlnQQWJ6ioyFrfRuomB8GKF6KbYXZVyeXNfs=
+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=
@@ -325,18 +325,18 @@ 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=
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=
@@ -358,20 +358,20 @@ 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=
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/protobuf v1.35.2 h1:8Ar7bF+apOIoThw1EdZl0p1oWvMqTHmpA2fRTyZO8io=
-google.golang.org/protobuf v1.35.2/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+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=
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=