Files
gocryptotrader/exchanges/bybit/bybit_websocket_requests.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

140 lines
4.4 KiB
Go

package bybit
import (
"context"
"fmt"
"strconv"
"time"
"github.com/gofrs/uuid"
"github.com/thrasher-corp/gocryptotrader/encoding/json"
"github.com/thrasher-corp/gocryptotrader/exchanges/request"
)
// Websocket request operation types
const (
OutboundTradeConnection = "PRIVATE_TRADE"
InboundPrivateConnection = "PRIVATE"
)
// WSCreateOrder creates an order through the websocket connection
func (e *Exchange) WSCreateOrder(ctx context.Context, r *PlaceOrderRequest) (*WebsocketOrderDetails, error) {
if err := r.Validate(); err != nil {
return nil, err
}
epl, err := getWSRateLimitEPLByCategory(r.Category)
if err != nil {
return nil, err
}
if r.OrderLinkID == "" {
r.OrderLinkID = uuid.Must(uuid.NewV7()).String()
}
return e.sendWebsocketTradeRequest(ctx, "order.create", r.OrderLinkID, r, epl)
}
// WSAmendOrder amends an order through the websocket connection
func (e *Exchange) WSAmendOrder(ctx context.Context, r *AmendOrderRequest) (*WebsocketOrderDetails, error) {
if err := r.Validate(); err != nil {
return nil, err
}
epl, err := getWSRateLimitEPLByCategory(r.Category)
if err != nil {
return nil, err
}
if r.OrderLinkID == "" {
r.OrderLinkID = uuid.Must(uuid.NewV7()).String()
}
return e.sendWebsocketTradeRequest(ctx, "order.amend", r.OrderLinkID, r, epl)
}
// WSCancelOrder cancels an order through the websocket connection
func (e *Exchange) WSCancelOrder(ctx context.Context, r *CancelOrderRequest) (*WebsocketOrderDetails, error) {
if err := r.Validate(); err != nil {
return nil, err
}
epl, err := getWSRateLimitEPLByCategory(r.Category)
if err != nil {
return nil, err
}
if r.OrderLinkID == "" {
r.OrderLinkID = uuid.Must(uuid.NewV7()).String()
}
return e.sendWebsocketTradeRequest(ctx, "order.cancel", r.OrderLinkID, r, epl)
}
// sendWebsocketTradeRequest sends a trade request to the exchange through the websocket connection
func (e *Exchange) sendWebsocketTradeRequest(ctx context.Context, op, orderLinkID string, payload any, limit request.EndpointLimit) (*WebsocketOrderDetails, error) {
// Get the outbound and inbound connections to send and receive the request. This makes sure both are live before
// sending the request.
outbound, err := e.Websocket.GetConnection(OutboundTradeConnection)
if err != nil {
return nil, err
}
inbound, err := e.Websocket.GetConnection(InboundPrivateConnection)
if err != nil {
return nil, err
}
// Set up a listener to wait for the response to come back from the inbound connection. The request is sent through
// the outbound trade connection, the response can come back through the inbound private connection before the
// outbound connection sends its acknowledgement.
ch, err := inbound.MatchReturnResponses(ctx, orderLinkID, 1)
if err != nil {
return nil, err
}
requestID := e.MessageID()
outResp, err := outbound.SendMessageReturnResponse(ctx, limit, requestID, WebsocketGeneralPayload{
RequestID: requestID,
Header: map[string]string{"X-BAPI-TIMESTAMP": strconv.FormatInt(time.Now().UnixMilli(), 10)},
Operation: op,
Arguments: []any{payload},
})
if err != nil {
return nil, err
}
var confirmation WebsocketConfirmation
if err := json.Unmarshal(outResp, &confirmation); err != nil {
return nil, err
}
if confirmation.RetCode != 0 {
return nil, fmt.Errorf("code:%d, info:%v message:%s", confirmation.RetCode, retCode[confirmation.RetCode], confirmation.RetMsg)
}
inResp := <-ch // Blocking read is acceptable; channel has a built in timeout already
if inResp.Err != nil {
return nil, inResp.Err
}
if len(inResp.Responses) != 1 {
return nil, fmt.Errorf("expected 1 response, received %d", len(inResp.Responses))
}
var ret WebsocketOrderResponse
if err := json.Unmarshal(inResp.Responses[0], &ret); err != nil {
return nil, err
}
if len(ret.Data) != 1 {
return nil, fmt.Errorf("expected 1 response, received %d", len(ret.Data))
}
if ret.Data[0].RejectReason != "EC_NoError" {
return nil, fmt.Errorf("order rejected: %s", ret.Data[0].RejectReason)
}
return &ret.Data[0], nil
}
var retCode = map[int64]string{
10404: "either op type is not found or category is not correct/supported",
10429: "request exceeds rate limit",
20006: "reqId is duplicated",
10016: "internal server error",
10019: "ws trade service is restarting, please reconnect",
20003: "too frequent requests under the same session",
10403: "exceed IP rate limit. 3000 requests per second per IP",
}