diff --git a/README.md b/README.md index 4591214..e7f40c6 100644 --- a/README.md +++ b/README.md @@ -143,6 +143,7 @@ The following API endpoints have been implemented - [`/v5/order/amend` Amend Order](https://bybit-exchange.github.io/docs/v5/order/amend-order) - [`/v5/order/cancel` Cancel Order](https://bybit-exchange.github.io/docs/v5/order/cancel-order) - [`/v5/order/realtime` Get Open Orders](https://bybit-exchange.github.io/docs/v5/order/open-order) +- [`/v5/order/cancel-all` Cancel All Orders](https://bybit-exchange.github.io/docs/v5/order/cancel-all) #### Account diff --git a/integrationtest/v5/order/order_test.go b/integrationtest/v5/order/order_test.go index b04a0ed..fb4b70b 100644 --- a/integrationtest/v5/order/order_test.go +++ b/integrationtest/v5/order/order_test.go @@ -150,3 +150,31 @@ func TestGetOpenOrders(t *testing.T) { require.NoError(t, err) } } + +func TestCancelAllOrders(t *testing.T) { + client := bybit.NewTestClient().WithAuthFromEnv() + category := bybit.CategoryV5Spot + symbol := bybit.SymbolV5BTCUSDT + { + price := "10000.0" + _, err := client.V5().Order().CreateOrder(bybit.V5CreateOrderParam{ + Category: category, + Symbol: symbol, + Side: bybit.SideBuy, + OrderType: bybit.OrderTypeLimit, + Qty: "0.01", + Price: &price, + }) + require.NoError(t, err) + } + + res, err := client.V5().Order().CancelAllOrders(bybit.V5CancelAllOrdersParam{ + Category: category, + }) + require.NoError(t, err) + { + goldenFilename := "./testdata/v5-cancel-all-orders.json" + testhelper.Compare(t, goldenFilename, testhelper.ConvertToJSON(res.Result)) + testhelper.UpdateFile(t, goldenFilename, testhelper.ConvertToJSON(res.Result)) + } +} diff --git a/integrationtest/v5/order/testdata/v5-cancel-all-orders.json b/integrationtest/v5/order/testdata/v5-cancel-all-orders.json new file mode 100644 index 0000000..4a24f7d --- /dev/null +++ b/integrationtest/v5/order/testdata/v5-cancel-all-orders.json @@ -0,0 +1,11 @@ +{ + "LinearInverseOption": { + "list": { + "orderId": "", + "orderLinkId": "" + } + }, + "Spot": { + "success": "1" + } +} \ No newline at end of file diff --git a/v5_order_service.go b/v5_order_service.go index d541f83..2b2e9e6 100644 --- a/v5_order_service.go +++ b/v5_order_service.go @@ -13,6 +13,7 @@ type V5OrderServiceI interface { AmendOrder(V5AmendOrderParam) (*V5AmendOrderResponse, error) CancelOrder(V5CancelOrderParam) (*V5CancelOrderResponse, error) GetOpenOrders(V5GetOpenOrdersParam) (*V5GetOpenOrdersResponse, error) + CancelAllOrders(V5CancelAllOrdersParam) (*V5CancelAllOrdersResponse, error) } // V5OrderService : @@ -260,3 +261,82 @@ func (s *V5OrderService) GetOpenOrders(param V5GetOpenOrdersParam) (*V5GetOpenOr return &res, nil } + +// V5CancelAllOrdersParam : +// If you pass multiple of these params, the system will process one of param, which priority is symbol > baseCoin > settleCoin. +type V5CancelAllOrdersParam struct { + Category CategoryV5 `json:"category"` + + Symbol *SymbolV5 `json:"symbol,omitempty"` + BaseCoin *Coin `json:"baseCoin,omitempty"` + SettleCoin *Coin `json:"settleCoin,omitempty"` + OrderFilter *OrderFilter `json:"orderFilter,omitempty"` // If not passed, Order by default +} + +func (p V5CancelAllOrdersParam) validate() error { + if p.Category == CategoryV5Linear || p.Category == CategoryV5Inverse { + if p.Symbol == nil && p.BaseCoin == nil && p.SettleCoin == nil { + return fmt.Errorf("symbol or baseCoin or settleCoin is needed for linear and inverse") + } + } + if p.Category != CategoryV5Spot && p.OrderFilter != nil { + return fmt.Errorf("orderFilter is for spot only") + } + return nil +} + +// V5CancelAllOrdersResponse : +type V5CancelAllOrdersResponse struct { + CommonV5Response `json:",inline"` + Result V5CancelAllOrdersResult `json:"result"` +} + +// V5CancelAllOrdersResult : +type V5CancelAllOrdersResult struct { + LinearInverseOption *V5CancelAllOrdersLinearInverseOptionResult + Spot *V5CancelAllOrdersSpotResult +} + +// UnmarshalJSON : +func (r *V5CancelAllOrdersResult) UnmarshalJSON(data []byte) error { + if err := json.Unmarshal(data, &r.LinearInverseOption); err != nil { + return err + } + if err := json.Unmarshal(data, &r.Spot); err != nil { + return err + } + return nil +} + +// V5CancelAllOrdersLinearInverseOptionResult : +type V5CancelAllOrdersLinearInverseOptionResult struct { + List []struct { + OrderID string `json:"orderId"` + OrderLinkID string `json:"orderLinkId"` + } `json:"list"` +} + +// V5CancelAllOrdersSpotResult : +type V5CancelAllOrdersSpotResult struct { + Success string `json:"success"` // 1: success, 0: fail +} + +// CancelAllOrders : +func (s *V5OrderService) CancelAllOrders(param V5CancelAllOrdersParam) (*V5CancelAllOrdersResponse, error) { + var res V5CancelAllOrdersResponse + + if err := param.validate(); err != nil { + return nil, fmt.Errorf("validate param: %w", err) + } + + body, err := json.Marshal(param) + if err != nil { + return &res, fmt.Errorf("json marshal: %w", err) + } + + if err := s.client.postV5JSON("/v5/order/cancel-all", body, &res); err != nil { + return &res, err + } + + return &res, nil +} diff --git a/v5_order_service_test.go b/v5_order_service_test.go index 6647bdf..1958e1e 100644 --- a/v5_order_service_test.go +++ b/v5_order_service_test.go @@ -369,3 +369,106 @@ func TestV5Order_GetOpenOrders(t *testing.T) { assert.Error(t, err) }) } + +func TestV5Order_CancelAllOrders(t *testing.T) { + t.Run("success", func(t *testing.T) { + t.Run("linear", func(t *testing.T) { + symbol := SymbolV5BTCUSDT + param := V5CancelAllOrdersParam{ + Category: CategoryV5Linear, + Symbol: &symbol, + } + + path := "/v5/order/cancel-all" + method := http.MethodPost + status := http.StatusOK + respBody := map[string]interface{}{ + "result": map[string]interface{}{ + "list": []map[string]interface{}{ + { + "orderId": "f6a73e1f-39b5-4dee-af21-1460b2e3b27c", + "orderLinkId": "a001", + }, + }, + }, + } + bytesBody, err := json.Marshal(respBody) + require.NoError(t, err) + + server, teardown := testhelper.NewServer( + testhelper.WithHandlerOption(path, method, status, bytesBody), + ) + defer teardown() + + client := NewTestClient(). + WithBaseURL(server.URL). + WithAuth("test", "test") + + resp, err := client.V5().Order().CancelAllOrders(param) + require.NoError(t, err) + + require.NotNil(t, resp) + testhelper.Compare(t, respBody["result"], resp.Result.LinearInverseOption) + }) + t.Run("spot", func(t *testing.T) { + param := V5CancelAllOrdersParam{ + Category: CategoryV5Spot, + } + + path := "/v5/order/cancel-all" + method := http.MethodPost + status := http.StatusOK + respBody := map[string]interface{}{ + "result": map[string]interface{}{ + "success": "1", + }, + } + bytesBody, err := json.Marshal(respBody) + require.NoError(t, err) + + server, teardown := testhelper.NewServer( + testhelper.WithHandlerOption(path, method, status, bytesBody), + ) + defer teardown() + + client := NewTestClient(). + WithBaseURL(server.URL). + WithAuth("test", "test") + + resp, err := client.V5().Order().CancelAllOrders(param) + require.NoError(t, err) + + require.NotNil(t, resp) + testhelper.Compare(t, respBody["result"], resp.Result.Spot) + }) + }) + t.Run("authentication required", func(t *testing.T) { + symbol := SymbolV5BTCUSDT + param := V5CancelAllOrdersParam{ + Category: CategoryV5Spot, + Symbol: &symbol, + } + + path := "/v5/order/cancel-all" + method := http.MethodPost + status := http.StatusOK + respBody := map[string]interface{}{ + "result": map[string]interface{}{ + "success": "1", + }, + } + bytesBody, err := json.Marshal(respBody) + require.NoError(t, err) + + server, teardown := testhelper.NewServer( + testhelper.WithHandlerOption(path, method, status, bytesBody), + ) + defer teardown() + + client := NewTestClient(). + WithBaseURL(server.URL) + + _, err = client.V5().Order().CancelAllOrders(param) + assert.Error(t, err) + }) +}