diff --git a/go.mod b/go.mod index e732abe..67a3fa5 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module github.com/hirokisan/bybit/v2 +module github.com/drinkthere/bybit go 1.21 diff --git a/v5_account_service.go b/v5_account_service.go index fb5231a..21f03cc 100644 --- a/v5_account_service.go +++ b/v5_account_service.go @@ -16,6 +16,7 @@ type V5AccountServiceI interface { GetCollateralInfo(V5GetCollateralInfoParam) (*V5GetCollateralInfoResponse, error) GetAccountInfo() (*V5GetAccountInfoResponse, error) GetTransactionLog(V5GetTransactionLogParam) (*V5GetTransactionLogResponse, error) + GetFeeRate(V5GetFeeRateParam) (*V5GetFeeRateResponse, error) } // V5AccountService : @@ -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 +} diff --git a/v5_client_web_socket_service.go b/v5_client_web_socket_service.go index ce09add..d408c77 100644 --- a/v5_client_web_socket_service.go +++ b/v5_client_web_socket_service.go @@ -8,6 +8,7 @@ import ( type V5WebsocketServiceI interface { Public(CategoryV5) (V5WebsocketPublicService, error) Private() (V5WebsocketPrivateService, error) + Trade() (V5WebsocketTradeService, error) } // V5WebsocketService : @@ -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} diff --git a/v5_ws_trade.go b/v5_ws_trade.go new file mode 100644 index 0000000..24747d4 --- /dev/null +++ b/v5_ws_trade.go @@ -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 +} diff --git a/v5_ws_trade_order.go b/v5_ws_trade_order.go new file mode 100644 index 0000000..da3e96c --- /dev/null +++ b/v5_ws_trade_order.go @@ -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 +}