diff --git a/exchanges/bitmex/bitmex_test.go b/exchanges/bitmex/bitmex_test.go index 0471dcd019b..64ff57c5b65 100644 --- a/exchanges/bitmex/bitmex_test.go +++ b/exchanges/bitmex/bitmex_test.go @@ -25,7 +25,9 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/orderbook" "github.com/thrasher-corp/gocryptotrader/exchanges/sharedtestvalues" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" + "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" 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" ) @@ -1357,3 +1359,32 @@ func TestGetCurrencyTradeURL(t *testing.T) { assert.NotEmpty(t, resp) } } + +func TestGenerateSubscriptions(t *testing.T) { + t.Parallel() + + p := currency.Pairs{ + currency.NewPair(currency.ETH, currency.USD), + currency.NewPair(currency.BCH, currency.NewCode("Z19")), + } + + exp := subscription.List{ + {QualifiedChannel: bitmexWSAnnouncement, Channel: bitmexWSAnnouncement}, + {QualifiedChannel: bitmexWSOrderbookL2 + ":" + p[1].String(), Channel: bitmexWSOrderbookL2, Asset: asset.Futures, Pairs: p[1:2]}, + {QualifiedChannel: bitmexWSOrderbookL2 + ":" + p[0].String(), Channel: bitmexWSOrderbookL2, Asset: asset.PerpetualContract, Pairs: p[:1]}, + {QualifiedChannel: bitmexWSTrade + ":" + p[1].String(), Channel: bitmexWSTrade, Asset: asset.Futures, Pairs: p[1:2]}, + {QualifiedChannel: bitmexWSTrade + ":" + p[0].String(), Channel: bitmexWSTrade, Asset: asset.PerpetualContract, Pairs: p[:1]}, + {QualifiedChannel: bitmexWSAffiliate, Channel: bitmexWSAffiliate, Authenticated: true}, + {QualifiedChannel: bitmexWSOrder, Channel: bitmexWSOrder, Authenticated: true}, + {QualifiedChannel: bitmexWSMargin, Channel: bitmexWSMargin, Authenticated: true}, + {QualifiedChannel: bitmexWSPrivateNotifications, Channel: bitmexWSPrivateNotifications, Authenticated: true}, + {QualifiedChannel: bitmexWSTransact, Channel: bitmexWSTransact, Authenticated: true}, + {QualifiedChannel: bitmexWSWallet, Channel: bitmexWSWallet, Authenticated: true}, + {QualifiedChannel: bitmexWSExecution + ":" + p[0].String(), Channel: bitmexWSExecution, Authenticated: true, Asset: asset.PerpetualContract, Pairs: p[:1]}, + {QualifiedChannel: bitmexWSPosition + ":" + p[0].String(), Channel: bitmexWSPosition, Authenticated: true, Asset: asset.PerpetualContract, Pairs: p[:1]}, + } + + subs, err := b.generateSubscriptions() + require.NoError(t, err, "generateSubscriptions must not error") + testsubs.EqualLists(t, exp, subs) +} diff --git a/exchanges/bitmex/bitmex_websocket.go b/exchanges/bitmex/bitmex_websocket.go index 6779a97a1e9..1fd4d6600cf 100644 --- a/exchanges/bitmex/bitmex_websocket.go +++ b/exchanges/bitmex/bitmex_websocket.go @@ -8,6 +8,7 @@ import ( "net/http" "strconv" "strings" + "text/template" "time" "github.com/gorilla/websocket" @@ -65,9 +66,18 @@ const ( bitmexActionUpdateData = "update" ) -var subscriptionNames = map[string]string{ - subscription.OrderbookChannel: bitmexWSOrderbookL2, - subscription.AllTradesChannel: bitmexWSTrade, +var defaultSubscriptions = subscription.List{ + {Enabled: true, Channel: bitmexWSAnnouncement}, + {Enabled: true, Channel: bitmexWSOrderbookL2, Asset: asset.All}, + {Enabled: true, Channel: bitmexWSTrade, Asset: asset.All}, + {Enabled: true, Channel: bitmexWSAffiliate, Authenticated: true}, + {Enabled: true, Channel: bitmexWSOrder, Authenticated: true}, + {Enabled: true, Channel: bitmexWSMargin, Authenticated: true}, + {Enabled: true, Channel: bitmexWSPrivateNotifications, Authenticated: true}, + {Enabled: true, Channel: bitmexWSTransact, Authenticated: true}, + {Enabled: true, Channel: bitmexWSWallet, Authenticated: true}, + {Enabled: true, Channel: bitmexWSExecution, Authenticated: true, Asset: asset.PerpetualContract}, + {Enabled: true, Channel: bitmexWSPosition, Authenticated: true, Asset: asset.PerpetualContract}, } // WsConnect initiates a new websocket connection @@ -542,50 +552,16 @@ func (b *Bitmex) processOrderbook(data []OrderBookL2, action string, p currency. return nil } -// generateSubscriptions returns Adds default subscriptions to websocket to be handled by ManageSubscriptions() +// generateSubscriptions returns a list of subscriptions from the configured subscriptions feature func (b *Bitmex) generateSubscriptions() (subscription.List, error) { - authed := b.Websocket.CanUseAuthenticatedEndpoints() - - assetPairs := map[asset.Item]currency.Pairs{} - for _, a := range b.GetAssetTypes(true) { - p, err := b.GetEnabledPairs(a) - if err != nil { - return nil, err - } - f, err := b.GetPairFormat(a, true) - if err != nil { - return nil, err - } - assetPairs[a] = p.Format(f) - } - - subs := subscription.List{} - for _, baseSub := range b.Features.Subscriptions { - if !authed && baseSub.Authenticated { - continue - } - - if baseSub.Asset == asset.Empty { - // Skip pair handling for subs which don't have an asset - subs = append(subs, baseSub.Clone()) - continue - } - - for a, p := range assetPairs { - if baseSub.Channel == bitmexWSOrderbookL2 && a == asset.Index { - continue // There are no L2 orderbook for index assets - } - if baseSub.Asset != asset.All && baseSub.Asset != a { - continue - } - s := baseSub.Clone() - s.Asset = a - s.Pairs = p - subs = append(subs, s) - } - } + return b.Features.Subscriptions.ExpandTemplates(b) +} - return subs, nil +// GetSubscriptionTemplate returns a subscription channel template +func (b *Bitmex) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) { + return template.New("master.tmpl").Funcs(template.FuncMap{ + "channelName": channelName, + }).Parse(subTplText) } // Subscribe subscribes to a websocket channel @@ -594,9 +570,8 @@ func (b *Bitmex) Subscribe(subs subscription.List) error { Command: "subscribe", } for _, s := range subs { - for _, p := range s.Pairs { - cName := channelName(s.Channel) - req.Arguments = append(req.Arguments, cName+":"+p.String()) + for _, a := range strings.Split(s.QualifiedChannel, ",") { + req.Arguments = append(req.Arguments, a) } } err := b.Websocket.Conn.SendJSONMessage(req) @@ -611,11 +586,9 @@ func (b *Bitmex) Unsubscribe(subs subscription.List) error { req := WebsocketRequest{ Command: "unsubscribe", } - for _, s := range subs { - for _, p := range s.Pairs { - cName := channelName(s.Channel) - req.Arguments = append(req.Arguments, cName+":"+p.String()) + for _, a := range strings.Split(s.QualifiedChannel, ",") { + req.Arguments = append(req.Arguments, a) } } err := b.Websocket.Conn.SendJSONMessage(req) @@ -625,15 +598,6 @@ func (b *Bitmex) Unsubscribe(subs subscription.List) error { return err } -// channelName converts global channel Names used in config of channel input into bitmex channel names -// returns the name unchanged if no match is found -func channelName(name string) string { - if s, ok := subscriptionNames[name]; ok { - return s - } - return name -} - // WebsocketSendAuth sends an authenticated subscription func (b *Bitmex) websocketSendAuth(ctx context.Context) error { creds, err := b.GetCredentials(ctx) @@ -651,10 +615,10 @@ func (b *Bitmex) websocketSendAuth(ctx context.Context) error { } signature := crypto.HexEncodeToString(hmac) - var sendAuth WebsocketRequest - sendAuth.Command = "authKeyExpires" - sendAuth.Arguments = append(sendAuth.Arguments, creds.Key, timestamp, - signature) + sendAuth := WebsocketRequest{ + Command: "authKeyExpires", + Arguments: []any{creds.Key, timestamp, signature}, + } err = b.Websocket.Conn.SendJSONMessage(sendAuth) if err != nil { b.Websocket.SetCanUseAuthenticatedEndpoints(false) @@ -677,3 +641,33 @@ func (b *Bitmex) GetActionFromString(s string) (orderbook.Action, error) { } return 0, fmt.Errorf("%s %w", s, orderbook.ErrInvalidAction) } + +// channelName returns the correct channel name for the asset +func channelName(s *subscription.Subscription, a asset.Item) string { + switch s.Channel { + case subscription.OrderbookChannel: + if a == asset.Index { + return "" // There are no L2 orderbook for index assets + } + return bitmexWSOrderbookL2 + case subscription.AllTradesChannel: + return bitmexWSTrade + } + return s.Channel +} + +const subTplText = ` +{{- if $.S.Asset }} + {{ range $asset, $pairs := $.AssetPairs }} + {{- with $name := channelName $.S $asset }} + {{- range $i, $p := $pairs -}} + {{- if $i -}} , {{- end -}} + {{- $name -}} : {{- $p -}} + {{- end }} + {{- end }} + {{ $.AssetSeparator }} + {{- end }} +{{- else -}} + {{ channelName $.S $.S.Asset }} +{{- end }} +` diff --git a/exchanges/bitmex/bitmex_websocket_types.go b/exchanges/bitmex/bitmex_websocket_types.go index 26a2a0325bc..5ae86e38c46 100644 --- a/exchanges/bitmex/bitmex_websocket_types.go +++ b/exchanges/bitmex/bitmex_websocket_types.go @@ -1,11 +1,13 @@ package bitmex -import "time" +import ( + "time" +) // WebsocketRequest is the main request type type WebsocketRequest struct { - Command string `json:"op"` - Arguments []interface{} `json:"args"` + Command string `json:"op"` + Arguments []any `json:"args"` } // WebsocketErrorResponse main error response diff --git a/exchanges/bitmex/bitmex_wrapper.go b/exchanges/bitmex/bitmex_wrapper.go index 0d20a361665..87c89a6041e 100644 --- a/exchanges/bitmex/bitmex_wrapper.go +++ b/exchanges/bitmex/bitmex_wrapper.go @@ -28,7 +28,6 @@ import ( "github.com/thrasher-corp/gocryptotrader/exchanges/request" "github.com/thrasher-corp/gocryptotrader/exchanges/stream" "github.com/thrasher-corp/gocryptotrader/exchanges/stream/buffer" - "github.com/thrasher-corp/gocryptotrader/exchanges/subscription" "github.com/thrasher-corp/gocryptotrader/exchanges/ticker" "github.com/thrasher-corp/gocryptotrader/exchanges/trade" "github.com/thrasher-corp/gocryptotrader/log" @@ -137,19 +136,7 @@ func (b *Bitmex) SetDefaults() { Enabled: exchange.FeaturesEnabled{ AutoPairUpdates: true, }, - Subscriptions: subscription.List{ - {Enabled: true, Channel: bitmexWSAnnouncement}, - {Enabled: true, Channel: bitmexWSOrderbookL2, Asset: asset.All}, - {Enabled: true, Channel: bitmexWSTrade, Asset: asset.All}, - {Enabled: true, Channel: bitmexWSAffiliate, Authenticated: true}, - {Enabled: true, Channel: bitmexWSOrder, Authenticated: true}, - {Enabled: true, Channel: bitmexWSMargin, Authenticated: true}, - {Enabled: true, Channel: bitmexWSPrivateNotifications, Authenticated: true}, - {Enabled: true, Channel: bitmexWSTransact, Authenticated: true}, - {Enabled: true, Channel: bitmexWSWallet, Authenticated: true}, - {Enabled: true, Channel: bitmexWSExecution, Authenticated: true, Asset: asset.PerpetualContract}, - {Enabled: true, Channel: bitmexWSPosition, Authenticated: true, Asset: asset.PerpetualContract}, - }, + Subscriptions: defaultSubscriptions.Clone(), } b.Requester, err = request.New(b.Name,