Files
gocryptotrader/exchanges/bybit/bybit_websocket.go
Gareth Kirwan bda9bbec66 websocket: Remove GenerateMessageID (#2008)
* Exchanges: Remove example BespokeGenerateMessageID

* Okx: Replace conn.RequestIDGenerator with MesssageID

Continued overall direction to remove the closed-loop of e => conn => e
roundtrip for message ids

* Exchanges: Add MessageSequence

This method removes the either/or nature of message id generation.
We don't tie the message ids to connections, or to anything.
Consumers just call whichever they want, or even combine them as they
want.
Anything more complicated will need a separate installation anyway

* GateIO: Split usage of MessageID and MessageSequence

* Binance: Switch to UUID message IDs

* Kraken: Switch to e.MessageSequence

* Kucoin: Switch to MessageID

* HitBTC: Switch to UUIDv7 for ws message ID

* Bybit: Switch to UUIDv7 for ws message ID

* Bitfinex: Switch to UUIDv7 and MessageSequence

Tested CID - It accepts 53 bits only for an int, so MessageSequence
makes sense. Can't use MessageID

* Websocket: Remove now unused MessageID function

Moved all MessageID usage into funcs and onto base methods, to remove
the closed loop of message IDs

* Docs: Update guidance for message signatures
2025-10-24 11:14:24 +11:00

888 lines
28 KiB
Go

package bybit
import (
"context"
"encoding/hex"
"errors"
"fmt"
"net/http"
"strconv"
"strings"
"text/template"
"time"
"github.com/buger/jsonparser"
gws "github.com/gorilla/websocket"
"github.com/thrasher-corp/gocryptotrader/common"
"github.com/thrasher-corp/gocryptotrader/common/crypto"
"github.com/thrasher-corp/gocryptotrader/currency"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchange/websocket"
"github.com/thrasher-corp/gocryptotrader/exchanges/account"
"github.com/thrasher-corp/gocryptotrader/exchanges/asset"
"github.com/thrasher-corp/gocryptotrader/exchanges/fill"
"github.com/thrasher-corp/gocryptotrader/exchanges/kline"
"github.com/thrasher-corp/gocryptotrader/exchanges/order"
"github.com/thrasher-corp/gocryptotrader/exchanges/orderbook"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
"github.com/thrasher-corp/gocryptotrader/exchanges/subscription"
"github.com/thrasher-corp/gocryptotrader/exchanges/ticker"
"github.com/thrasher-corp/gocryptotrader/exchanges/trade"
"github.com/thrasher-corp/gocryptotrader/log"
)
const (
bybitWebsocketTimer = 20 * time.Second
// Public v5 channels
chanOrderbook = "orderbook"
chanPublicTrade = "publicTrade"
chanPublicTicker = "tickers"
chanKline = "kline"
chanLiquidation = "liquidation"
chanLeverageTokenKline = "kline_lt"
chanLeverageTokenTicker = "tickers_lt"
chanLeverageTokenNav = "lt"
// Private v5 channels
chanPositions = "position"
chanExecution = "execution"
chanOrder = "order"
chanWallet = "wallet"
chanGreeks = "greeks"
// TODO: Implement DCP (Disconnection Protect) subscription
spotPublic = "wss://stream.bybit.com/v5/public/spot"
linearPublic = "wss://stream.bybit.com/v5/public/linear" // USDT, USDC perpetual & USDC Futures
inversePublic = "wss://stream.bybit.com/v5/public/inverse" // Inverse contract
optionPublic = "wss://stream.bybit.com/v5/public/option" // USDC Option
// Main-net private
websocketPrivate = "wss://stream.bybit.com/v5/private"
websocketTrade = "wss://stream.bybit.com/v5/trade"
)
var defaultSubscriptions = subscription.List{
{Enabled: true, Asset: asset.Spot, Channel: subscription.TickerChannel},
{Enabled: true, Asset: asset.Spot, Channel: subscription.OrderbookChannel, Levels: 50},
{Enabled: true, Asset: asset.Spot, Channel: subscription.AllTradesChannel},
{Enabled: true, Asset: asset.Spot, Channel: subscription.CandlesChannel, Interval: kline.OneHour},
// Authenticated channels are currently being managed by the `generateAuthSubscriptions` method for the private connection
// TODO: expand subscription template generation to handle authenticated subscriptions across all assets
}
var subscriptionNames = map[string]string{
subscription.TickerChannel: chanPublicTicker,
subscription.OrderbookChannel: chanOrderbook,
subscription.AllTradesChannel: chanPublicTrade,
subscription.MyOrdersChannel: chanOrder,
subscription.MyWalletChannel: chanWallet,
subscription.MyTradesChannel: chanExecution,
subscription.CandlesChannel: chanKline,
}
var (
errUnhandledStreamData = errors.New("unhandled stream data")
errUnsupportedCategory = errors.New("unsupported category")
)
// WsConnect connects to a websocket feed
func (e *Exchange) WsConnect(ctx context.Context, conn websocket.Connection) error {
if err := conn.Dial(ctx, &gws.Dialer{}, http.Header{}); err != nil {
return err
}
conn.SetupPingHandler(request.Unset, websocket.PingHandler{
MessageType: gws.TextMessage,
Message: []byte(`{"op": "ping"}`),
Delay: bybitWebsocketTimer,
})
return nil
}
// WebsocketAuthenticatePrivateConnection sends an authentication message to the private websocket for inbound account
// data
func (e *Exchange) WebsocketAuthenticatePrivateConnection(ctx context.Context, conn websocket.Connection) error {
req, err := e.GetAuthenticationPayload(ctx, e.MessageID())
if err != nil {
return err
}
resp, err := conn.SendMessageReturnResponse(ctx, wsSubscriptionEPL, req.RequestID, req)
if err != nil {
return err
}
var response SubscriptionResponse
if err := json.Unmarshal(resp, &response); err != nil {
return err
}
if !response.Success {
return fmt.Errorf("%s with request ID %s msg: %s", response.Operation, response.RequestID, response.ReturnMessage)
}
return nil
}
// WebsocketAuthenticateTradeConnection sends an authentication message to the private trade websocket for outbound
// account data
func (e *Exchange) WebsocketAuthenticateTradeConnection(ctx context.Context, conn websocket.Connection) error {
// request ID is not returned with the response, a workaround in the trade connection handler monitors the response
// for the operation type "auth", which is then set in the response match key.
req, err := e.GetAuthenticationPayload(ctx, "auth")
if err != nil {
return err
}
resp, err := conn.SendMessageReturnResponse(ctx, wsSubscriptionEPL, req.RequestID, req)
if err != nil {
return err
}
var response struct {
ReturnCode int64 `json:"retCode"`
ReturnMessage string `json:"retMsg"`
Operation string `json:"op"`
ConnectionID string `json:"connId"`
}
if err := json.Unmarshal(resp, &response); err != nil {
return err
}
if response.ReturnCode != 0 {
c, ok := retCode[response.ReturnCode]
if !ok {
c = "unknown return error code"
}
return fmt.Errorf("%s failed - code:%d [%v] msg:%s", response.Operation, response.ReturnCode, c, response.ReturnMessage)
}
return nil
}
// GetAuthenticationPayload returns the authentication payload for the websocket connection to upgrade the connection.
func (e *Exchange) GetAuthenticationPayload(ctx context.Context, requestID string) (*Authenticate, error) {
creds, err := e.GetCredentials(ctx)
if err != nil {
return nil, err
}
expires := time.Now().Add(time.Hour * 6).UnixMilli()
hmac, err := crypto.GetHMAC(crypto.HashSHA256, []byte("GET/realtime"+strconv.FormatInt(expires, 10)), []byte(creds.Secret))
if err != nil {
return nil, err
}
return &Authenticate{
RequestID: requestID,
Operation: "auth",
Args: []any{creds.Key, expires, hex.EncodeToString(hmac)},
}, nil
}
func (e *Exchange) handleSubscriptions(_ websocket.Connection, operation string, subs subscription.List) (args []SubscriptionArgument, err error) {
subs, err = subs.ExpandTemplates(e)
if err != nil {
return
}
for _, list := range []subscription.List{subs.Public(), subs.Private()} {
for _, b := range common.Batch(list, 10) {
args = append(args, SubscriptionArgument{
auth: b[0].Authenticated,
Operation: operation,
RequestID: e.MessageID(),
Arguments: b.QualifiedChannels(),
associatedSubs: b,
})
}
}
return
}
// generateSubscriptions generates default subscription
func (e *Exchange) generateSubscriptions() (subscription.List, error) {
return e.Features.Subscriptions.ExpandTemplates(e)
}
// GetSubscriptionTemplate returns a subscription channel template
func (e *Exchange) GetSubscriptionTemplate(_ *subscription.Subscription) (*template.Template, error) {
return template.New("master.tmpl").Funcs(template.FuncMap{
"channelName": channelName,
"isSymbolChannel": isSymbolChannel,
"intervalToString": intervalToString,
"getCategoryName": getCategoryName,
}).Parse(subTplText)
}
func (e *Exchange) wsHandleTradeData(conn websocket.Connection, respRaw []byte) error {
var response struct {
RequestID string `json:"reqId"`
Operation string `json:"op"`
}
if err := json.Unmarshal(respRaw, &response); err != nil {
return err
}
if response.RequestID != "" {
return conn.RequireMatchWithData(response.RequestID, respRaw)
}
switch response.Operation {
case "auth": // When authenticating the connection there is no request ID, so a static value is used.
return conn.RequireMatchWithData(response.Operation, respRaw)
case "pong":
return nil
default:
return fmt.Errorf("%w for trade: %v", errUnhandledStreamData, string(respRaw))
}
}
func (e *Exchange) wsHandleData(conn websocket.Connection, assetType asset.Item, respRaw []byte) error {
var result WebsocketResponse
if err := json.Unmarshal(respRaw, &result); err != nil {
return err
}
if result.Topic == "" {
return e.handleNoTopicWebsocketResponse(conn, &result, respRaw)
}
topicSplit := strings.Split(result.Topic, ".")
switch topicSplit[0] {
case chanOrderbook:
return e.wsProcessOrderbook(assetType, &result)
case chanPublicTrade:
return e.wsProcessPublicTrade(assetType, &result)
case chanPublicTicker:
return e.wsProcessPublicTicker(assetType, &result)
case chanKline:
return e.wsProcessKline(assetType, &result, topicSplit)
case chanLiquidation:
return e.wsProcessLiquidation(&result)
case chanLeverageTokenKline:
return e.wsProcessLeverageTokenKline(assetType, &result, topicSplit)
case chanLeverageTokenTicker:
return e.wsProcessLeverageTokenTicker(assetType, &result)
case chanLeverageTokenNav:
return e.wsLeverageTokenNav(&result)
}
return fmt.Errorf("%w %s", errUnhandledStreamData, string(respRaw))
}
func (e *Exchange) wsHandleAuthenticatedData(ctx context.Context, conn websocket.Connection, respRaw []byte) error {
var result WebsocketResponse
if err := json.Unmarshal(respRaw, &result); err != nil {
return err
}
if result.Topic == "" {
return e.handleNoTopicWebsocketResponse(conn, &result, respRaw)
}
topicSplit := strings.Split(result.Topic, ".")
switch topicSplit[0] {
case chanPositions:
return e.wsProcessPosition(&result)
case chanExecution:
return e.wsProcessExecution(&result)
case chanOrder:
// Use first order's orderLinkId to match with an entire batch of order change requests
if id, err := jsonparser.GetString(respRaw, "data", "[0]", "orderLinkId"); err == nil {
if conn.IncomingWithData(id, respRaw) {
return nil // If the data has been routed, return
}
}
return e.wsProcessOrder(&result)
case chanWallet:
return e.wsProcessWalletPushData(ctx, respRaw)
case chanGreeks:
return e.wsProcessGreeks(respRaw)
}
return fmt.Errorf("%w %s", errUnhandledStreamData, string(respRaw))
}
func (e *Exchange) handleNoTopicWebsocketResponse(conn websocket.Connection, result *WebsocketResponse, respRaw []byte) error {
switch result.Operation {
case "subscribe", "unsubscribe", "auth":
if result.RequestID != "" {
return conn.RequireMatchWithData(result.RequestID, respRaw)
}
case "ping", "pong":
default:
e.Websocket.DataHandler <- websocket.UnhandledMessageWarning{Message: string(respRaw)}
}
return nil
}
func (e *Exchange) wsProcessGreeks(resp []byte) error {
var result GreeksResponse
if err := json.Unmarshal(resp, &result); err != nil {
return err
}
e.Websocket.DataHandler <- &result
return nil
}
func (e *Exchange) wsProcessWalletPushData(ctx context.Context, resp []byte) error {
var result WebsocketWallet
if err := json.Unmarshal(resp, &result); err != nil {
return err
}
creds, err := e.GetCredentials(ctx)
if err != nil {
return err
}
var changes []account.Change
for x := range result.Data {
for y := range result.Data[x].Coin {
changes = append(changes, account.Change{
AssetType: asset.Spot,
Balance: &account.Balance{
Currency: result.Data[x].Coin[y].Coin,
Total: result.Data[x].Coin[y].WalletBalance.Float64(),
Free: result.Data[x].Coin[y].WalletBalance.Float64(),
UpdatedAt: result.CreationTime.Time(),
},
})
}
}
e.Websocket.DataHandler <- changes
return account.ProcessChange(e.Name, changes, creds)
}
// wsProcessOrder the order stream to see changes to your orders in real-time.
func (e *Exchange) wsProcessOrder(resp *WebsocketResponse) error {
var result []WebsocketOrderDetails
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
execution := make([]order.Detail, len(result))
for x := range result {
cp, a, err := e.matchPairAssetFromResponse(result[x].Category, result[x].Symbol)
if err != nil {
return err
}
orderType, err := order.StringToOrderType(result[x].OrderType)
if err != nil {
return err
}
tif, err := order.StringToTimeInForce(result[x].TimeInForce)
if err != nil {
return err
}
execution[x] = order.Detail{
TimeInForce: tif,
Amount: result[x].Quantity.Float64(),
Exchange: e.Name,
OrderID: result[x].OrderID,
ClientOrderID: result[x].OrderLinkID,
Side: result[x].Side,
Type: orderType,
Pair: cp,
Cost: result[x].CumulativeExecutedQuantity.Float64() * result[x].AveragePrice.Float64(),
Fee: result[x].CumulativeExecutedFee.Float64(),
AssetType: a,
Status: StringToOrderStatus(result[x].OrderStatus),
Price: result[x].Price.Float64(),
ExecutedAmount: result[x].CumulativeExecutedQuantity.Float64(),
AverageExecutedPrice: result[x].AveragePrice.Float64(),
Date: result[x].CreatedTime.Time(),
LastUpdated: result[x].UpdatedTime.Time(),
}
}
e.Websocket.DataHandler <- execution
return nil
}
func (e *Exchange) wsProcessExecution(resp *WebsocketResponse) error {
var result WsExecutions
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
executions := make([]fill.Data, len(result))
for x := range result {
cp, a, err := e.matchPairAssetFromResponse(result[x].Category, result[x].Symbol)
if err != nil {
return err
}
side, err := order.StringToOrderSide(result[x].Side)
if err != nil {
return err
}
executions[x] = fill.Data{
ID: result[x].ExecID,
Timestamp: result[x].ExecTime.Time(),
Exchange: e.Name,
AssetType: a,
CurrencyPair: cp,
Side: side,
OrderID: result[x].OrderID,
ClientOrderID: result[x].OrderLinkID,
Price: result[x].ExecPrice.Float64(),
Amount: result[x].ExecQty.Float64(),
}
}
e.Websocket.DataHandler <- executions
return nil
}
func (e *Exchange) wsProcessPosition(resp *WebsocketResponse) error {
var result WsPositions
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
e.Websocket.DataHandler <- result
return nil
}
func (e *Exchange) wsLeverageTokenNav(resp *WebsocketResponse) error {
var result LTNav
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
e.Websocket.DataHandler <- result
return nil
}
func (e *Exchange) wsProcessLeverageTokenTicker(assetType asset.Item, resp *WebsocketResponse) error {
var result TickerWebsocket
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
cp, err := e.MatchSymbolWithAvailablePairs(result.Symbol, assetType, hasPotentialDelimiter(assetType))
if err != nil {
return err
}
e.Websocket.DataHandler <- &ticker.Price{
Last: result.LastPrice.Float64(),
High: result.HighPrice24H.Float64(),
Low: result.LowPrice24H.Float64(),
Pair: cp,
ExchangeName: e.Name,
AssetType: assetType,
LastUpdated: resp.PushTimestamp.Time(),
}
return nil
}
func (e *Exchange) wsProcessLeverageTokenKline(assetType asset.Item, resp *WebsocketResponse, topicSplit []string) error {
var result LTKlines
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
cp, err := e.MatchSymbolWithAvailablePairs(topicSplit[2], assetType, hasPotentialDelimiter(assetType))
if err != nil {
return err
}
ltKline := make([]websocket.KlineData, len(result))
for x := range result {
interval, err := stringToInterval(result[x].Interval)
if err != nil {
return err
}
ltKline[x] = websocket.KlineData{
Timestamp: result[x].Timestamp.Time(),
Pair: cp,
AssetType: assetType,
Exchange: e.Name,
StartTime: result[x].Start.Time(),
CloseTime: result[x].End.Time(),
Interval: interval.String(),
OpenPrice: result[x].Open.Float64(),
ClosePrice: result[x].Close.Float64(),
HighPrice: result[x].High.Float64(),
LowPrice: result[x].Low.Float64(),
}
}
e.Websocket.DataHandler <- result
return nil
}
func (e *Exchange) wsProcessLiquidation(resp *WebsocketResponse) error {
var result WebsocketLiquidation
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
e.Websocket.DataHandler <- result
return nil
}
func (e *Exchange) wsProcessKline(assetType asset.Item, resp *WebsocketResponse, topicSplit []string) error {
var result WsKlines
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
cp, err := e.MatchSymbolWithAvailablePairs(topicSplit[2], assetType, hasPotentialDelimiter(assetType))
if err != nil {
return err
}
spotCandlesticks := make([]websocket.KlineData, len(result))
for x := range result {
interval, err := stringToInterval(result[x].Interval)
if err != nil {
return err
}
spotCandlesticks[x] = websocket.KlineData{
Timestamp: result[x].Timestamp.Time(),
Pair: cp,
AssetType: assetType,
Exchange: e.Name,
StartTime: result[x].Start.Time(),
CloseTime: result[x].End.Time(),
Interval: interval.String(),
OpenPrice: result[x].Open.Float64(),
ClosePrice: result[x].Close.Float64(),
HighPrice: result[x].High.Float64(),
LowPrice: result[x].Low.Float64(),
Volume: result[x].Volume.Float64(),
}
}
e.Websocket.DataHandler <- spotCandlesticks
return nil
}
func (e *Exchange) wsProcessPublicTicker(assetType asset.Item, resp *WebsocketResponse) error {
var tickResp TickerWebsocket
if err := json.Unmarshal(resp.Data, &tickResp); err != nil {
return err
}
p, err := e.MatchSymbolWithAvailablePairs(tickResp.Symbol, assetType, hasPotentialDelimiter(assetType))
if err != nil {
return err
}
tick := &ticker.Price{Pair: p, ExchangeName: e.Name, AssetType: assetType}
if resp.Type != "snapshot" {
// ticker updates may be partial, so we need to update the current ticker
tick, err = e.GetCachedTicker(p, assetType)
if err != nil {
return err
}
}
updateTicker(tick, &tickResp)
tick.LastUpdated = resp.PushTimestamp.Time()
if err := ticker.ProcessTicker(tick); err != nil {
return err
}
e.Websocket.DataHandler <- tick
return nil
}
func updateTicker(tick *ticker.Price, resp *TickerWebsocket) {
if resp.LastPrice.Float64() != 0 {
tick.Last = resp.LastPrice.Float64()
}
if resp.HighPrice24H.Float64() != 0 {
tick.High = resp.HighPrice24H.Float64()
}
if resp.LowPrice24H.Float64() != 0 {
tick.Low = resp.LowPrice24H.Float64()
}
if resp.Volume24H.Float64() != 0 {
tick.Volume = resp.Volume24H.Float64()
}
if tick.AssetType == asset.Spot {
return
}
if resp.MarkPrice.Float64() != 0 {
tick.MarkPrice = resp.MarkPrice.Float64()
}
if resp.IndexPrice.Float64() != 0 {
tick.IndexPrice = resp.IndexPrice.Float64()
}
if resp.OpenInterest.Float64() != 0 {
tick.OpenInterest = resp.OpenInterest.Float64()
}
switch tick.AssetType {
case asset.Options:
if resp.BidPrice.Float64() != 0 {
tick.Bid = resp.BidPrice.Float64()
}
if resp.BidSize.Float64() != 0 {
tick.BidSize = resp.BidSize.Float64()
}
if resp.AskPrice.Float64() != 0 {
tick.Ask = resp.AskPrice.Float64()
}
if resp.AskSize.Float64() != 0 {
tick.AskSize = resp.AskSize.Float64()
}
case asset.USDCMarginedFutures, asset.USDTMarginedFutures, asset.CoinMarginedFutures:
if resp.Bid1Price.Float64() != 0 {
tick.Bid = resp.Bid1Price.Float64()
}
if resp.Bid1Size.Float64() != 0 {
tick.BidSize = resp.Bid1Size.Float64()
}
if resp.Ask1Price.Float64() != 0 {
tick.Ask = resp.Ask1Price.Float64()
}
if resp.Ask1Size.Float64() != 0 {
tick.AskSize = resp.Ask1Size.Float64()
}
}
}
func (e *Exchange) wsProcessPublicTrade(assetType asset.Item, resp *WebsocketResponse) error {
var result WebsocketPublicTrades
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
tradeDatas := make([]trade.Data, len(result))
for x := range result {
cp, err := e.MatchSymbolWithAvailablePairs(result[x].Symbol, assetType, hasPotentialDelimiter(assetType))
if err != nil {
return err
}
side, err := order.StringToOrderSide(result[x].Side)
if err != nil {
return err
}
tradeDatas[x] = trade.Data{
Timestamp: result[x].OrderFillTimestamp.Time(),
CurrencyPair: cp,
AssetType: assetType,
Exchange: e.Name,
Price: result[x].Price.Float64(),
Amount: result[x].Size.Float64(),
Side: side,
TID: result[x].TradeID,
}
}
return trade.AddTradesToBuffer(tradeDatas...)
}
func (e *Exchange) wsProcessOrderbook(assetType asset.Item, resp *WebsocketResponse) error {
var result WsOrderbookDetail
if err := json.Unmarshal(resp.Data, &result); err != nil {
return err
}
cp, err := e.MatchSymbolWithAvailablePairs(result.Symbol, assetType, hasPotentialDelimiter(assetType))
if err != nil {
return err
}
if resp.Type == "snapshot" {
return e.Websocket.Orderbook.LoadSnapshot(&orderbook.Book{
Pair: cp,
Exchange: e.Name,
Asset: assetType,
LastUpdated: resp.OrderbookLastUpdated.Time(),
LastUpdateID: result.UpdateID,
LastPushed: resp.PushTimestamp.Time(),
Asks: result.Asks.Levels(),
Bids: result.Bids.Levels(),
})
}
return e.Websocket.Orderbook.Update(&orderbook.Update{
Pair: cp,
Asks: result.Asks.Levels(),
Bids: result.Bids.Levels(),
Asset: assetType,
UpdateID: result.UpdateID,
UpdateTime: resp.OrderbookLastUpdated.Time(),
LastPushed: resp.PushTimestamp.Time(),
AllowEmpty: true,
})
}
// channelName converts global channel names to exchange specific names
func channelName(s *subscription.Subscription) string {
if name, ok := subscriptionNames[s.Channel]; ok {
return name
}
return s.Channel
}
// isSymbolChannel returns whether the channel accepts a symbol parameter
func isSymbolChannel(name string) bool {
switch name {
case chanPositions, chanExecution, chanOrder, chanWallet:
return false
}
return true
}
const subTplText = `
{{ with $name := channelName $.S }}
{{- range $asset, $pairs := $.AssetPairs }}
{{- if isSymbolChannel $name }}
{{- range $p := $pairs }}
{{- $name -}} .
{{- if eq $name "orderbook" -}} {{- $.S.Levels -}} . {{- end }}
{{- if eq $name "kline" -}} {{- intervalToString $.S.Interval -}} . {{- end }}
{{- $p }}
{{- $.PairSeparator }}
{{- end }}
{{- end }}
{{- end }}
{{- $.AssetSeparator }}
{{- end }}
`
// hasPotentialDelimiter returns if the asset has a potential delimiter on the pairs being returned.
func hasPotentialDelimiter(a asset.Item) bool {
return a == asset.Options || a == asset.USDCMarginedFutures
}
// TODO: Remove this function when template expansion is across all assets
func (e *Exchange) submitDirectSubscription(ctx context.Context, conn websocket.Connection, a asset.Item, operation string, channelsToSubscribe subscription.List) error {
payloads, err := e.directSubscriptionPayload(a, operation, channelsToSubscribe)
if err != nil {
return err
}
op := e.Websocket.AddSubscriptions
if operation == "unsubscribe" {
op = e.Websocket.RemoveSubscriptions
}
for _, payload := range payloads {
if a == asset.Options {
// The options connection does not send the subscription request id back with the subscription notification payload
// therefore the code doesn't wait for the response to check whether the subscription is successful or not.
if err := conn.SendJSONMessage(ctx, wsSubscriptionEPL, payload); err != nil {
return err
}
} else {
response, err := conn.SendMessageReturnResponse(ctx, wsSubscriptionEPL, payload.RequestID, payload)
if err != nil {
return err
}
var resp SubscriptionResponse
if err := json.Unmarshal(response, &resp); err != nil {
return err
}
if !resp.Success {
return fmt.Errorf("%s with request ID %s msg: %s", resp.Operation, resp.RequestID, resp.ReturnMessage)
}
}
if err := op(conn, payload.associatedSubs...); err != nil {
return err
}
}
return nil
}
// TODO: Remove this function when template expansion is across all assets
func (e *Exchange) directSubscriptionPayload(assetType asset.Item, operation string, channelsToSubscribe subscription.List) ([]SubscriptionArgument, error) {
var args []SubscriptionArgument
arg := SubscriptionArgument{
Operation: operation,
RequestID: e.MessageID(),
Arguments: []string{},
}
authArg := SubscriptionArgument{
auth: true,
Operation: operation,
RequestID: e.MessageID(),
Arguments: []string{},
}
chanMap := map[string]bool{}
pairFmt, err := e.GetPairFormat(assetType, true)
if err != nil {
return nil, err
}
for _, s := range channelsToSubscribe {
var pair currency.Pair
if len(s.Pairs) > 1 {
return nil, subscription.ErrNotSinglePair
}
if len(s.Pairs) == 1 {
pair = s.Pairs[0]
}
switch s.Channel {
case chanOrderbook:
arg.Arguments = append(arg.Arguments, fmt.Sprintf("%s.%d.%s", s.Channel, 50, pairFmt.Format(pair)))
arg.associatedSubs = append(arg.associatedSubs, s)
case chanPublicTrade, chanPublicTicker, chanLiquidation, chanLeverageTokenTicker, chanLeverageTokenNav:
arg.Arguments = append(arg.Arguments, s.Channel+"."+pairFmt.Format(pair))
arg.associatedSubs = append(arg.associatedSubs, s)
case chanKline, chanLeverageTokenKline:
interval, err := intervalToString(kline.FiveMin)
if err != nil {
return nil, err
}
arg.Arguments = append(arg.Arguments, s.Channel+"."+interval+"."+pairFmt.Format(pair))
arg.associatedSubs = append(arg.associatedSubs, s)
case chanPositions, chanExecution, chanOrder, chanWallet, chanGreeks:
if chanMap[s.Channel] {
continue
}
authArg.Arguments = append(authArg.Arguments, s.Channel)
// add channel name to map so we only subscribe to channel once
chanMap[s.Channel] = true
authArg.associatedSubs = append(authArg.associatedSubs, s)
}
if len(arg.Arguments) >= 10 {
args = append(args, arg)
arg = SubscriptionArgument{
Operation: operation,
RequestID: e.MessageID(),
Arguments: []string{},
}
}
}
if len(arg.Arguments) != 0 {
args = append(args, arg)
}
if len(authArg.Arguments) != 0 {
args = append(args, authArg)
}
return args, nil
}
// generateAuthSubscriptions generates default subscription for the dedicated auth websocket connection. These are
// agnostic to the asset type and pair as all account level data will be routed through this connection.
// TODO: Remove this function when template expansion is across all assets
func (e *Exchange) generateAuthSubscriptions() (subscription.List, error) {
if !e.Websocket.CanUseAuthenticatedEndpoints() {
return nil, nil
}
for _, configSub := range e.Config.Features.Subscriptions.Enabled() {
if configSub.Authenticated {
log.Warnf(log.WebsocketMgr, "%s has an authenticated subscription %q in config which is not supported. Please remove.", e.Name, configSub.Channel)
configSub.Enabled = false
}
}
var subscriptions subscription.List
// TODO: Implement DCP (Disconnection Protect) subscription
for _, channel := range []string{chanPositions, chanExecution, chanOrder, chanWallet} {
subscriptions = append(subscriptions, &subscription.Subscription{Channel: channel, Asset: asset.All})
}
return subscriptions, nil
}
func (e *Exchange) authSubscribe(ctx context.Context, conn websocket.Connection, channelSubscriptions subscription.List) error {
return e.submitDirectSubscription(ctx, conn, asset.Spot, "subscribe", channelSubscriptions)
}
func (e *Exchange) authUnsubscribe(ctx context.Context, conn websocket.Connection, channelSubscriptions subscription.List) error {
return e.submitDirectSubscription(ctx, conn, asset.Spot, "unsubscribe", channelSubscriptions)
}
// matchPairAssetFromResponse returns the currency pair and asset type based on the category and symbol. Used with a dedicated
// auth connection where multiple asset type changes are piped through a single connection.
func (e *Exchange) matchPairAssetFromResponse(category, symbol string) (currency.Pair, asset.Item, error) {
assets := make([]asset.Item, 0, 2)
switch category {
case cSpot:
assets = append(assets, asset.Spot)
case cInverse:
assets = append(assets, asset.CoinMarginedFutures)
case cLinear:
assets = append(assets, asset.USDTMarginedFutures, asset.USDCMarginedFutures)
case cOption:
assets = append(assets, asset.Options)
default:
return currency.EMPTYPAIR, 0, fmt.Errorf("incoming symbol %q %w: %q", symbol, errUnsupportedCategory, category)
}
for _, a := range assets {
cp, err := e.MatchSymbolWithAvailablePairs(symbol, a, hasPotentialDelimiter(a))
if err != nil {
if !errors.Is(err, currency.ErrPairNotFound) {
return currency.EMPTYPAIR, 0, fmt.Errorf("%w for symbol %q: %q", err, category, symbol)
}
continue
}
return cp, a, nil
}
return currency.EMPTYPAIR, 0, currency.ErrPairNotFound
}