diff --git a/README.md b/README.md index 2a25ab2..90196fd 100644 --- a/README.md +++ b/README.md @@ -343,6 +343,7 @@ The following API endpoints have been implemented - [Kline](https://bybit-exchange.github.io/docs/v5/websocket/public/kline) - [Ticker](https://bybit-exchange.github.io/docs/v5/websocket/public/ticker) - [Trade](https://bybit-exchange.github.io/docs/v5/websocket/public/trade) +- [Liquidation](https://bybit-exchange.github.io/docs/v5/websocket/public/liquidation) #### Private Topics V5 diff --git a/derivative_common.go b/derivative_common.go index e1048d8..004bdd3 100644 --- a/derivative_common.go +++ b/derivative_common.go @@ -158,8 +158,8 @@ type DerivativesKlineParam struct { Symbol SymbolDerivative `url:"symbol"` Category CategoryDerivative `url:"category"` Interval Interval `url:"interval"` - Start int `url:"start"` // timestamp point for result, in milliseconds - End int `url:"end"` // timestamp point for result, in milliseconds + Start int64 `url:"start"` // timestamp point for result, in milliseconds + End int64 `url:"end"` // timestamp point for result, in milliseconds Limit *int `url:"limit,omitempty"` } @@ -479,8 +479,8 @@ type DerivativesMarkPriceKlineParam struct { Category CategoryDerivative `url:"category"` Symbol SymbolDerivative `url:"symbol"` Interval Interval `url:"interval"` - Start int `url:"start"` // timestamp point for result, in milliseconds - End int `url:"end"` // timestamp point for result, in milliseconds + Start int64 `url:"start"` // timestamp point for result, in milliseconds + End int64 `url:"end"` // timestamp point for result, in milliseconds Limit *int `url:"limit,omitempty"` } @@ -547,8 +547,8 @@ type DerivativesIndexPriceKlineParam struct { Category CategoryDerivative `url:"category"` Symbol SymbolDerivative `url:"symbol"` Interval Interval `url:"interval"` - Start int `url:"start"` // timestamp point for result, in milliseconds - End int `url:"end"` // timestamp point for result, in milliseconds + Start int64 `url:"start"` // timestamp point for result, in milliseconds + End int64 `url:"end"` // timestamp point for result, in milliseconds Limit *int `url:"limit,omitempty"` } diff --git a/enum.go b/enum.go index 5953578..f78908e 100644 --- a/enum.go +++ b/enum.go @@ -94,6 +94,22 @@ const ( // Interval : type Interval string +var Intervals = []Interval{ + Interval1, + Interval3, + Interval5, + Interval15, + Interval30, + Interval60, + Interval120, + Interval240, + Interval360, + Interval720, + IntervalD, + IntervalW, + IntervalM, +} + const ( // Interval1 : Interval1 = Interval("1") diff --git a/response.go b/response.go index e2fa6a2..47fc461 100644 --- a/response.go +++ b/response.go @@ -57,7 +57,7 @@ func checkV5ResponseBody(body []byte) error { switch { case commonResponse.RetCode == 10006, commonResponse.RetCode == 10018: - rateLimitError := &RateLimitError{} + rateLimitError := &RateLimitV5Error{} if err := json.Unmarshal(body, rateLimitError); err != nil { return err } @@ -121,6 +121,14 @@ func (r *RateLimitError) Error() string { return fmt.Sprintf("%s, %s", r.RetMsg, time.Until(time.Unix(int64(r.RateLimitResetMs/1000), 0))) } +type RateLimitV5Error struct { + *CommonV5Response `json:",inline"` +} + +func (r *RateLimitV5Error) Error() string { + return fmt.Sprintf(r.RetMsg) +} + var ( // ErrPathNotFound : Request path not found ErrPathNotFound = errors.New("path not found") diff --git a/v5_client_web_socket_service.go b/v5_client_web_socket_service.go index 70c8691..665e157 100644 --- a/v5_client_web_socket_service.go +++ b/v5_client_web_socket_service.go @@ -23,12 +23,13 @@ func (s *V5WebsocketService) Public(category CategoryV5) (V5WebsocketPublicServi return nil, err } return &V5WebsocketPublicService{ - client: s.client, - connection: c, - paramOrderBookMap: make(map[V5WebsocketPublicOrderBookParamKey]func(V5WebsocketPublicOrderBookResponse) error), - paramKlineMap: make(map[V5WebsocketPublicKlineParamKey]func(V5WebsocketPublicKlineResponse) error), - paramTickerMap: make(map[V5WebsocketPublicTickerParamKey]func(V5WebsocketPublicTickerResponse) error), - paramTradeMap: make(map[V5WebsocketPublicTradeParamKey]func(V5WebsocketPublicTradeResponse) error), + client: s.client, + connection: c, + paramOrderBookMap: make(map[V5WebsocketPublicOrderBookParamKey]func(V5WebsocketPublicOrderBookResponse) error), + paramKlineMap: make(map[V5WebsocketPublicKlineParamKey]func(V5WebsocketPublicKlineResponse) error), + paramTickerMap: make(map[V5WebsocketPublicTickerParamKey]func(V5WebsocketPublicTickerResponse) error), + paramTradeMap: make(map[V5WebsocketPublicTradeParamKey]func(V5WebsocketPublicTradeResponse) error), + paramLiquidationMap: make(map[V5WebsocketPublicLiquidationParamKey]func(V5WebsocketPublicLiquidationResponse) error), }, nil } diff --git a/v5_market_service.go b/v5_market_service.go index 10b4c1c..08df825 100644 --- a/v5_market_service.go +++ b/v5_market_service.go @@ -36,8 +36,8 @@ type V5GetKlineParam struct { Category CategoryV5 `url:"category"` Symbol SymbolV5 `url:"symbol"` Interval Interval `url:"interval"` - Start *int `url:"start,omitempty"` // timestamp point for result, in milliseconds - End *int `url:"end,omitempty"` // timestamp point for result, in milliseconds + Start *int64 `url:"start,omitempty"` // timestamp point for result, in milliseconds + End *int64 `url:"end,omitempty"` // timestamp point for result, in milliseconds Limit *int `url:"limit,omitempty"` // Limit for data size per page. [1, 200]. Default: 200 } @@ -113,8 +113,8 @@ type V5GetMarkPriceKlineParam struct { Category CategoryV5 `url:"category"` Symbol SymbolV5 `url:"symbol"` Interval Interval `url:"interval"` - Start *int `url:"start,omitempty"` // timestamp point for result, in milliseconds - End *int `url:"end,omitempty"` // timestamp point for result, in milliseconds + Start *int64 `url:"start,omitempty"` // timestamp point for result, in milliseconds + End *int64 `url:"end,omitempty"` // timestamp point for result, in milliseconds Limit *int `url:"limit,omitempty"` // Limit for data size per page. [1, 200]. Default: 200 } @@ -189,8 +189,8 @@ type V5GetIndexPriceKlineParam struct { Category CategoryV5 `url:"category"` Symbol SymbolV5 `url:"symbol"` Interval Interval `url:"interval"` - Start *int `url:"start,omitempty"` // timestamp point for result, in milliseconds - End *int `url:"end,omitempty"` // timestamp point for result, in milliseconds + Start *int64 `url:"start,omitempty"` // timestamp point for result, in milliseconds + End *int64 `url:"end,omitempty"` // timestamp point for result, in milliseconds Limit *int `url:"limit,omitempty"` // Limit for data size per page. [1, 200]. Default: 200 } @@ -265,8 +265,8 @@ type V5GetPremiumIndexPriceKlineParam struct { Category CategoryV5 `url:"category"` Symbol SymbolV5 `url:"symbol"` Interval Interval `url:"interval"` - Start *int `url:"start,omitempty"` // timestamp point for result, in milliseconds - End *int `url:"end,omitempty"` // timestamp point for result, in milliseconds + Start *int64 `url:"start,omitempty"` // timestamp point for result, in milliseconds + End *int64 `url:"end,omitempty"` // timestamp point for result, in milliseconds Limit *int `url:"limit,omitempty"` // Limit for data size per page. [1, 200]. Default: 200 } diff --git a/v5_ws_public.go b/v5_ws_public.go index 88a1bec..931fd4d 100644 --- a/v5_ws_public.go +++ b/v5_ws_public.go @@ -40,6 +40,11 @@ type V5WebsocketPublicServiceI interface { V5WebsocketPublicTradeParamKey, func(V5WebsocketPublicTradeResponse) error, ) (func() error, error) + + SubscribeLiquidation( + V5WebsocketPublicLiquidationParamKey, + func(V5WebsocketPublicLiquidationResponse) error, + ) (func() error, error) } // V5WebsocketPublicService : @@ -49,10 +54,11 @@ type V5WebsocketPublicService struct { mu sync.Mutex - paramOrderBookMap map[V5WebsocketPublicOrderBookParamKey]func(V5WebsocketPublicOrderBookResponse) error - paramKlineMap map[V5WebsocketPublicKlineParamKey]func(V5WebsocketPublicKlineResponse) error - paramTickerMap map[V5WebsocketPublicTickerParamKey]func(V5WebsocketPublicTickerResponse) error - paramTradeMap map[V5WebsocketPublicTradeParamKey]func(V5WebsocketPublicTradeResponse) error + paramOrderBookMap map[V5WebsocketPublicOrderBookParamKey]func(V5WebsocketPublicOrderBookResponse) error + paramKlineMap map[V5WebsocketPublicKlineParamKey]func(V5WebsocketPublicKlineResponse) error + paramTickerMap map[V5WebsocketPublicTickerParamKey]func(V5WebsocketPublicTickerResponse) error + paramTradeMap map[V5WebsocketPublicTradeParamKey]func(V5WebsocketPublicTradeResponse) error + paramLiquidationMap map[V5WebsocketPublicLiquidationParamKey]func(V5WebsocketPublicLiquidationResponse) error } const ( @@ -80,6 +86,9 @@ const ( // V5WebsocketPublicTopicTrade : V5WebsocketPublicTopicTrade = V5WebsocketPublicTopic("publicTrade") + + // V5WebsocketPublicTopicLiquidation : + V5WebsocketPublicTopicLiquidation = V5WebsocketPublicTopic("liquidation") ) func (t V5WebsocketPublicTopic) String() string { @@ -102,6 +111,8 @@ func (s *V5WebsocketPublicService) judgeTopic(respBody []byte) (V5WebsocketPubli return V5WebsocketPublicTopicTicker, nil case strings.Contains(topic, V5WebsocketPublicTopicTrade.String()): return V5WebsocketPublicTopicTrade, nil + case strings.Contains(topic, V5WebsocketPublicTopicLiquidation.String()): + return V5WebsocketPublicTopicLiquidation, nil } } return "", nil @@ -251,6 +262,20 @@ func (s *V5WebsocketPublicService) Run() error { return err } + if err := f(resp); err != nil { + return err + } + case V5WebsocketPublicTopicLiquidation: + var resp V5WebsocketPublicLiquidationResponse + if err := s.parseResponse(message, &resp); err != nil { + return err + } + + f, err := s.retrieveLiquidationFunc(resp.Key()) + if err != nil { + return err + } + if err := f(resp); err != nil { return err } diff --git a/v5_ws_public_kline.go b/v5_ws_public_kline.go index c9f6af2..fc5c709 100644 --- a/v5_ws_public_kline.go +++ b/v5_ws_public_kline.go @@ -72,8 +72,8 @@ type V5WebsocketPublicKlineResponse struct { // V5WebsocketPublicKlineData : type V5WebsocketPublicKlineData struct { - Start int `json:"start"` - End int `json:"end"` + Start int64 `json:"start"` + End int64 `json:"end"` Interval Interval `json:"interval"` Open string `json:"open"` Close string `json:"close"` @@ -82,7 +82,7 @@ type V5WebsocketPublicKlineData struct { Volume string `json:"volume"` Turnover string `json:"turnover"` Confirm bool `json:"confirm"` - Timestamp int `json:"timestamp"` + Timestamp int64 `json:"timestamp"` } // Key : diff --git a/v5_ws_public_liquidation.go b/v5_ws_public_liquidation.go new file mode 100644 index 0000000..41bc084 --- /dev/null +++ b/v5_ws_public_liquidation.go @@ -0,0 +1,115 @@ +package bybit + +import ( + "encoding/json" + "errors" + "fmt" + "strings" + + "github.com/gorilla/websocket" +) + +// SubscribeLiquidation : +func (s *V5WebsocketPublicService) SubscribeLiquidation( + key V5WebsocketPublicLiquidationParamKey, + f func(V5WebsocketPublicLiquidationResponse) error, +) (func() error, error) { + if err := s.addParamLiquidationFunc(key, f); err != nil { + return nil, err + } + param := struct { + Op string `json:"op"` + Args []interface{} `json:"args"` + }{ + Op: "subscribe", + Args: []interface{}{key.Topic()}, + } + buf, err := json.Marshal(param) + if err != nil { + return nil, err + } + if err := s.writeMessage(websocket.TextMessage, buf); err != nil { + return nil, err + } + return func() error { + param := struct { + Op string `json:"op"` + Args []interface{} `json:"args"` + }{ + Op: "unsubscribe", + Args: []interface{}{key.Topic()}, + } + buf, err := json.Marshal(param) + if err != nil { + return err + } + if err := s.writeMessage(websocket.TextMessage, []byte(buf)); err != nil { + return err + } + s.removeParamLiquidationFunc(key) + return nil + }, nil +} + +// V5WebsocketPublicLiquidationParamKey : +type V5WebsocketPublicLiquidationParamKey struct { + Symbol SymbolV5 +} + +// Topic : +func (k *V5WebsocketPublicLiquidationParamKey) Topic() string { + return fmt.Sprintf("%s.%s", V5WebsocketPublicTopicLiquidation, k.Symbol) +} + +// V5WebsocketPublicLiquidationResponse : +type V5WebsocketPublicLiquidationResponse struct { + Topic string `json:"topic"` + Type string `json:"type"` + TimeStamp int64 `json:"ts"` + Data []V5WebsocketPublicLiquidationData `json:"data"` +} + +// V5WebsocketPublicLiquidationData : +type V5WebsocketPublicLiquidationData struct { + UpdatedTime uint64 `json:"updatedTime"` // The updated timestamp (ms) + Symbol SymbolV5 `json:"symbol"` // Symbol name + Side Side `json:"side"` // Position side. Buy,Sell. When you receive a Buy update, this means that a long position has been liquidated + Size string `json:"size"` // Executed size + Price string `json:"price"` // Bankruptcy price +} + +// Key : +func (r *V5WebsocketPublicLiquidationResponse) Key() V5WebsocketPublicLiquidationParamKey { + topic := r.Topic + arr := strings.Split(topic, ".") + if arr[0] != V5WebsocketPublicTopicLiquidation.String() || len(arr) != 2 { + return V5WebsocketPublicLiquidationParamKey{} + } + + return V5WebsocketPublicLiquidationParamKey{ + Symbol: SymbolV5(arr[1]), + } +} + +// addParamLiquidationFunc : +func (s *V5WebsocketPublicService) addParamLiquidationFunc(key V5WebsocketPublicLiquidationParamKey, f func(V5WebsocketPublicLiquidationResponse) error) error { + if _, exist := s.paramLiquidationMap[key]; exist { + return errors.New("already registered for this key") + } + s.paramLiquidationMap[key] = f + return nil +} + +// removeParamLiquidationFunc : +func (s *V5WebsocketPublicService) removeParamLiquidationFunc(key V5WebsocketPublicLiquidationParamKey) { + delete(s.paramLiquidationMap, key) +} + +// retrievePositionFunc : +func (s *V5WebsocketPublicService) retrieveLiquidationFunc(key V5WebsocketPublicLiquidationParamKey) (func(V5WebsocketPublicLiquidationResponse) error, error) { + f, exist := s.paramLiquidationMap[key] + if !exist { + return nil, errors.New("func not found") + } + return f, nil +} diff --git a/v5_ws_public_liquidation_test.go b/v5_ws_public_liquidation_test.go new file mode 100644 index 0000000..b85a532 --- /dev/null +++ b/v5_ws_public_liquidation_test.go @@ -0,0 +1,59 @@ +package bybit + +import ( + "encoding/json" + "testing" + + "github.com/hirokisan/bybit/v2/testhelper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestWebsocketV5Public_Liquidation(t *testing.T) { + respBody := map[string]interface{}{ + "topic": "liquidation.BTCUSDT", + "type": "snapshot", + "ts": 1673251091822, + "data": []map[string]interface{}{ + { + "price": "25844.48", + "side": "Buy", + "size": "2.8", + "symbol": "BTCUSDT", + "updatedTime": 1673251091822, + }, + }, + } + bytesBody, err := json.Marshal(respBody) + require.NoError(t, err) + + category := CategoryV5Linear + + server, teardown := testhelper.NewWebsocketServer( + testhelper.WithWebsocketHandlerOption(V5WebsocketPublicPathFor(category), bytesBody), + ) + defer teardown() + + wsClient := NewTestWebsocketClient(). + WithBaseURL(server.URL) + + svc, err := wsClient.V5().Public(category) + require.NoError(t, err) + + { + _, err := svc.SubscribeLiquidation( + V5WebsocketPublicLiquidationParamKey{ + Symbol: SymbolV5BTCUSDT, + }, + func(response V5WebsocketPublicLiquidationResponse) error { + assert.Equal(t, respBody["topic"], response.Topic) + return nil + }, + ) + require.NoError(t, err) + } + + assert.NoError(t, svc.Run()) + assert.NoError(t, svc.Ping()) + assert.NoError(t, svc.Close()) +}