Skip to content

Commit

Permalink
bybit/websocket: allow a shared ID between outbound payloads for inbo…
Browse files Browse the repository at this point in the history
…und matching
  • Loading branch information
Ryan O'Hara-Reid committed Dec 11, 2024
1 parent c316397 commit 8d0b749
Show file tree
Hide file tree
Showing 3 changed files with 53 additions and 14 deletions.
24 changes: 24 additions & 0 deletions exchanges/bybit/bybit_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,14 @@ func (p *PlaceOrderParams) Validate() error {
return nil
}

// LoadID loads the order link ID into the parameter, only if it is not already set
func (p *PlaceOrderParams) LoadID(id string) string {
if p.OrderLinkID == "" {
p.OrderLinkID = id
}
return p.OrderLinkID
}

// OrderResponse holds newly placed order information.
type OrderResponse struct {
OrderID string `json:"orderId"`
Expand Down Expand Up @@ -465,6 +473,14 @@ func (p *AmendOrderParams) Validate() error {
return nil
}

// LoadID loads the order link ID into the parameter, only if it is not already set
func (p *AmendOrderParams) LoadID(id string) string {
if p.OrderLinkID == "" {
p.OrderLinkID = id
}
return p.OrderLinkID
}

// AllZero checks if all the arguments are a zero value
func AllZero(args ...any) bool {
for _, v := range args {
Expand Down Expand Up @@ -521,6 +537,14 @@ func (p *CancelOrderParams) Validate() error {
return nil
}

// LoadID loads the order link ID into the parameter, only if it is not already set
func (p *CancelOrderParams) LoadID(id string) string {
if p.OrderLinkID == "" {
p.OrderLinkID = id
}
return p.OrderLinkID
}

// TradeOrders represents category and list of trade orders of the category.
type TradeOrders struct {
List []TradeOrder `json:"list"`
Expand Down
4 changes: 2 additions & 2 deletions exchanges/bybit/bybit_websocket.go
Original file line number Diff line number Diff line change
Expand Up @@ -339,8 +339,8 @@ func (by *Bybit) wsHandleAuthenticated(_ context.Context, respRaw []byte) error
return by.wsProcessExecution(&result)
case chanOrder:
// Below provides a way of matching an order change to a websocket request. There is no batch support for this
// so the first element will be used to match the order ID.
if id, err := jsonparser.GetString(respRaw, "data", "[0]", "orderId"); err == nil {
// so the first element will be used to match the order link ID.
if id, err := jsonparser.GetString(respRaw, "data", "[0]", "orderLinkId"); err == nil {
if by.Websocket.Match.IncomingWithData(id, respRaw) {
return nil // If the data has been routed, return
}
Expand Down
39 changes: 27 additions & 12 deletions exchanges/bybit/bybit_websocket_requests.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,14 @@ type WebsocketGeneralPayload struct {
Arguments []any `json:"args"`
}

// IDLoader is an interface to load an ID that is generated from the request. If the ID is loaded by a client then it
// will be returned and that ID will be used.
type IDLoader interface {
LoadID(generated string) (stored string)
}

// SendWebsocketRequest sends a request to the exchange through the websocket connection
func (by *Bybit) SendWebsocketRequest(ctx context.Context, op string, argument any) (*WebsocketOrderDetails, error) {
func (by *Bybit) SendWebsocketRequest(ctx context.Context, op string, argument IDLoader) (*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 := by.Websocket.GetConnection(OutboundTradeConnection)
Expand All @@ -141,14 +147,28 @@ func (by *Bybit) SendWebsocketRequest(ctx context.Context, op string, argument a
return nil, err
}

out := WebsocketGeneralPayload{
RequestID: strconv.FormatInt(outbound.GenerateMessageID(false), 10),
Header: map[string]string{"X-BAPI-TIMESTAMP": strconv.FormatInt(time.Now().UnixMilli(), 10)},
Operation: op,
Arguments: []any{argument},
tn := time.Now()
id := strconv.FormatInt(outbound.GenerateMessageID(false), 10)
nanoTs := strconv.FormatInt(tn.UnixNano(), 10) // UnixNano is used to ensure the ID is unique.

Check failure on line 152 in exchanges/bybit/bybit_websocket_requests.go

View workflow job for this annotation

GitHub Actions / lint

ST1003: var nanoTs should be nanoTS (stylecheck)

// Sets OrderLinkID to the outbound payload so that the response can be matched to the request in the inbound connection.
argumentID := argument.LoadID(nanoTs + id)

// 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.
wait, err := inbound.MatchReturnResponses(ctx, argumentID, 1)
if err != nil {
return nil, err
}

outResp, err := outbound.SendMessageReturnResponse(ctx, request.Unset, out.RequestID, out)
// TODO: Create new function to return a channel so that we can select on multiple connections, this is a slight potential optimisation.
outResp, err := outbound.SendMessageReturnResponse(ctx, request.Unset, id, WebsocketGeneralPayload{
RequestID: id,
Header: map[string]string{"X-BAPI-TIMESTAMP": strconv.FormatInt(tn.UnixMilli(), 10)},
Operation: op,
Arguments: []any{argument},
})
if err != nil {
return nil, err
}
Expand All @@ -162,11 +182,6 @@ func (by *Bybit) SendWebsocketRequest(ctx context.Context, op string, argument a
return nil, fmt.Errorf("code:%d, info:%v message:%s", confirmation.RetCode, retCode[confirmation.RetCode], confirmation.RetMsg)
}

wait, err := inbound.MatchReturnResponses(ctx, confirmation.RequestAcknowledgement.OrderID, 1)
if err != nil {
return nil, err
}

// Wait for the response to come back from the inbound connection.
inResp := <-wait
if inResp.Err != nil {
Expand Down

0 comments on commit 8d0b749

Please sign in to comment.