Skip to content

Commit

Permalink
support v5 ws trade (#183)
Browse files Browse the repository at this point in the history
* support v5 ws trade
Simple implementation, does not care whether the ws request is submitted successfully, the actual situation is based on the message received by order ws.

* change module name
  • Loading branch information
drinkthere authored Sep 25, 2024
1 parent 91aae7b commit e3a25f8
Show file tree
Hide file tree
Showing 5 changed files with 320 additions and 1 deletion.
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
module github.com/hirokisan/bybit/v2
module github.com/drinkthere/bybit

go 1.21

Expand Down
47 changes: 47 additions & 0 deletions v5_account_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ type V5AccountServiceI interface {
GetCollateralInfo(V5GetCollateralInfoParam) (*V5GetCollateralInfoResponse, error)
GetAccountInfo() (*V5GetAccountInfoResponse, error)
GetTransactionLog(V5GetTransactionLogParam) (*V5GetTransactionLogResponse, error)
GetFeeRate(V5GetFeeRateParam) (*V5GetFeeRateResponse, error)
}

// V5AccountService :
Expand Down Expand Up @@ -277,3 +278,49 @@ func (s *V5AccountService) GetTransactionLog(param V5GetTransactionLogParam) (*V

return &res, nil
}

// V5GetFeeRateParam :
type V5GetFeeRateParam struct {
Category CategoryV5 `json:"category"`
Symbol SymbolV5 `json:"symbol"`
BaseCoin *Coin `url:"baseCoin,omitempty"`
}

// V5GetFeeRateResponse :
type V5GetFeeRateResponse struct {
CommonV5Response `json:",inline"`
Result V5GetFeeRateResult `json:"result"`
}

// V5GetFeeRateResult :
type V5GetFeeRateResult struct {
Category CategoryV5 `json:"category,omitempty"`
List V5GetFeeRateList `json:"list"`
}

// V5GetFeeRateList :
type V5GetFeeRateList []V5GetFeeRateItem

// V5GetFeeRateItem :
type V5GetFeeRateItem struct {
Symbol SymbolV5 `json:"symbol"`
BaseCoin *Coin `url:"baseCoin,omitempty"`
TakerFeeRate string `json:"takerFeeRate"`
MakerFeeRate string `json:"makerFeeRate"`
}

// GetFeeRate :
func (s *V5AccountService) GetFeeRate(param V5GetFeeRateParam) (*V5GetFeeRateResponse, error) {
var res V5GetFeeRateResponse

queryString, err := query.Values(param)
if err != nil {
return nil, err
}

if err := s.client.getV5Privately("/v5/account/fee-rate", queryString, &res); err != nil {
return nil, err
}

return &res, nil
}
14 changes: 14 additions & 0 deletions v5_client_web_socket_service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
type V5WebsocketServiceI interface {
Public(CategoryV5) (V5WebsocketPublicService, error)
Private() (V5WebsocketPrivateService, error)
Trade() (V5WebsocketTradeService, error)
}

// V5WebsocketService :
Expand Down Expand Up @@ -51,6 +52,19 @@ func (s *V5WebsocketService) Private() (V5WebsocketPrivateServiceI, error) {
}, nil
}

// Trade :
func (s *V5WebsocketService) Trade() (V5WebsocketTradeServiceI, error) {
url := s.client.baseURL + V5WebsocketTradePath
c, _, err := websocket.DefaultDialer.Dial(url, nil)
if err != nil {
return nil, err
}
return &V5WebsocketTradeService{
client: s.client,
connection: c,
}, nil
}

// V5 :
func (c *WebSocketClient) V5() *V5WebsocketService {
return &V5WebsocketService{c}
Expand Down
189 changes: 189 additions & 0 deletions v5_ws_trade.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
package bybit

import (
"context"
"encoding/json"
"errors"
"fmt"
"os"
"os/signal"
"sync"
"time"

"github.com/gorilla/websocket"
)

// V5WebsocketTradeServiceI :
type V5WebsocketTradeServiceI interface {
Start(context.Context, ErrHandler) error
Login() error
Run() error
Ping() error
Close() error

CreateOrder(orders []*V5CreateOrderParam) error
CancelOrder(orders []*V5CancelOrderParam) error
}

// V5WebsocketTradeService :
type V5WebsocketTradeService struct {
client *WebSocketClient
connection *websocket.Conn

mu sync.Mutex
}

const (
// V5WebsocketTradePath :
V5WebsocketTradePath = "/v5/trade"
)

// V5WebsocketTradeTopic :
type V5WebsocketTradeTopic string

const (
// V5WebsocketTradeTopicPong :
V5WebsocketTradeTopicPong V5WebsocketTradeTopic = "pong"
)

// judgeTopic :
func (s *V5WebsocketTradeService) judgeTopic(respBody []byte) (V5WebsocketTradeTopic, error) {
parsedData := map[string]interface{}{}
if err := json.Unmarshal(respBody, &parsedData); err != nil {
return "", err
}
if retMsg, ok := parsedData["op"].(string); ok && retMsg == "pong" {
return V5WebsocketTradeTopicPong, nil
}

if authStatus, ok := parsedData["success"].(bool); ok {
if !authStatus {
return "", errors.New("auth failed: " + parsedData["ret_msg"].(string))
}
}
return "", nil
}

// parseResponse :
func (s *V5WebsocketTradeService) parseResponse(respBody []byte, response interface{}) error {
if err := json.Unmarshal(respBody, &response); err != nil {
return err
}
return nil
}

// Login : Apply for authentication when establishing a connection.
func (s *V5WebsocketTradeService) Login() error {
param, err := s.client.buildAuthParam()
if err != nil {
return err
}
if err := s.writeMessage(websocket.TextMessage, param); err != nil {
return err
}
return nil
}

// Start :
func (s *V5WebsocketTradeService) Start(ctx context.Context, errHandler ErrHandler) error {
done := make(chan struct{})

go func() {
defer close(done)
defer s.connection.Close()
_ = s.connection.SetReadDeadline(time.Now().Add(60 * time.Second))
s.connection.SetPongHandler(func(string) error {
_ = s.connection.SetReadDeadline(time.Now().Add(60 * time.Second))
return nil
})

for {
if err := s.Run(); err != nil {
if errHandler == nil {
return
}
errHandler(IsErrWebsocketClosed(err), err)
return
}
}
}()

ticker := time.NewTicker(20 * time.Second)
defer ticker.Stop()

ctx, stop := signal.NotifyContext(ctx, os.Interrupt)
defer stop()

for {
select {
case <-done:
return nil
case <-ticker.C:
if err := s.Ping(); err != nil {
return err
}
case <-ctx.Done():
s.client.debugf("caught websocket trade service interrupt signal")

if err := s.Close(); err != nil {
return err
}
select {
case <-done:
case <-time.After(time.Second):
}
return nil
}
}
}

// Run :
func (s *V5WebsocketTradeService) Run() error {
_, message, err := s.connection.ReadMessage()
if err != nil {
return err
}

topic, err := s.judgeTopic(message)
if err != nil {
return err
}
switch topic {
case V5WebsocketTradeTopicPong:
if err := s.connection.PongHandler()("pong"); err != nil {
return fmt.Errorf("pong: %w", err)
}
}
return nil
}

// Ping :
func (s *V5WebsocketTradeService) Ping() error {
// NOTE: It appears that two messages need to be sent.
// REF: https://github.com/hirokisan/bybit/pull/127#issuecomment-1537479346
if err := s.writeMessage(websocket.PingMessage, nil); err != nil {
return err
}
if err := s.writeMessage(websocket.TextMessage, []byte(`{"op":"ping"}`)); err != nil {
return err
}
return nil
}

// Close :
func (s *V5WebsocketTradeService) Close() error {
if err := s.writeMessage(websocket.CloseMessage, websocket.FormatCloseMessage(websocket.CloseNormalClosure, "")); err != nil && !errors.Is(err, websocket.ErrCloseSent) {
return err
}
return nil
}

func (s *V5WebsocketTradeService) writeMessage(messageType int, body []byte) error {
s.mu.Lock()
defer s.mu.Unlock()

if err := s.connection.WriteMessage(messageType, body); err != nil {
return err
}
return nil
}
69 changes: 69 additions & 0 deletions v5_ws_trade_order.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package bybit

import (
"encoding/json"
"fmt"
"github.com/google/uuid"
"github.com/gorilla/websocket"
"strconv"
"time"
)

// CreateOrder :
func (s *V5WebsocketTradeService) CreateOrder(orders []*V5CreateOrderParam) error {
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
headers := make(map[string]string)
headers["X-BAPI-TIMESTAMP"] = timestamp
headers["X-BAPI-RECV-WINDOW"] = "8000"

param := struct {
ReqId string `json:"reqId"`
Headers map[string]string `json:"header"`
Op string `json:"op"`
Args []*V5CreateOrderParam `json:"args"`
}{
ReqId: uuid.New().String(),
Headers: headers,
Op: "order.create",
Args: orders,
}
buf, err := json.Marshal(param)
if err != nil {
fmt.Printf("error is %+v", err)
return err
}

if err := s.writeMessage(websocket.TextMessage, buf); err != nil {
return err
}
return nil
}

func (s *V5WebsocketTradeService) CancelOrder(orders []*V5CancelOrderParam) error {
timestamp := strconv.FormatInt(time.Now().UnixMilli(), 10)
headers := make(map[string]string)
headers["X-BAPI-TIMESTAMP"] = timestamp
headers["X-BAPI-RECV-WINDOW"] = "8000"

param := struct {
ReqId string `json:"reqId"`
Headers map[string]string `json:"header"`
Op string `json:"op"`
Args []*V5CancelOrderParam `json:"args"`
}{
ReqId: uuid.New().String(),
Headers: headers,
Op: "order.cancel",
Args: orders,
}
buf, err := json.Marshal(param)
if err != nil {
fmt.Printf("error is %+v", err)
return err
}

if err := s.writeMessage(websocket.TextMessage, buf); err != nil {
return err
}
return nil
}

0 comments on commit e3a25f8

Please sign in to comment.