From 4810038bd2375e86d658c47b3c90e028539ad2e7 Mon Sep 17 00:00:00 2001 From: Pablo Date: Thu, 11 Dec 2025 01:02:23 +0100 Subject: [PATCH] feat: add support for websocket algo orders --- binance/async_client.py | 41 +++++++++++-- binance/client.py | 41 +++++++++++-- .../test_async_client_ws_futures_requests.py | 60 +++++++++++++++++++ tests/test_client_ws_futures_requests.py | 55 +++++++++++++++++ 4 files changed, 189 insertions(+), 8 deletions(-) diff --git a/binance/async_client.py b/binance/async_client.py index 1791d860..98f26330 100644 --- a/binance/async_client.py +++ b/binance/async_client.py @@ -4024,9 +4024,33 @@ async def ws_futures_create_order(self, **params): Send in a new order https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api """ - if "newClientOrderId" not in params: - params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22() - return await self._ws_futures_api_request("order.place", True, params) + # Check if this is a conditional order type that needs to use algo endpoint + order_type = params.get("type", "").upper() + conditional_types = [ + "STOP", + "STOP_MARKET", + "TAKE_PROFIT", + "TAKE_PROFIT_MARKET", + "TRAILING_STOP_MARKET", + ] + + if order_type in conditional_types: + # Route to algo order endpoint + if "clientAlgoId" not in params: + params["clientAlgoId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22() + # Remove newClientOrderId if it was added by default + params.pop("newClientOrderId", None) + if "algoType" not in params: + params["algoType"] = "CONDITIONAL" + # Convert stopPrice to triggerPrice for algo orders + if "stopPrice" in params and "triggerPrice" not in params: + params["triggerPrice"] = params.pop("stopPrice") + return await self._ws_futures_api_request("algoOrder.place", True, params) + else: + # Use regular order endpoint + if "newClientOrderId" not in params: + params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22() + return await self._ws_futures_api_request("order.place", True, params) async def ws_futures_edit_order(self, **params): """ @@ -4040,12 +4064,21 @@ async def ws_futures_cancel_order(self, **params): cancel an order https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Cancel-Order """ - return await self._ws_futures_api_request("order.cancel", True, params) + is_conditional = False + if "algoId" in params or "clientAlgoId" in params: + is_conditional = True + + if is_conditional: + return await self._ws_futures_api_request("algoOrder.cancel", True, params) + else: + return await self._ws_futures_api_request("order.cancel", True, params) async def ws_futures_get_order(self, **params): """ Get an order https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Query-Order + + Note: Algo/conditional orders cannot be queried via websocket API """ return await self._ws_futures_api_request("order.status", True, params) diff --git a/binance/client.py b/binance/client.py index 6e9acea2..313d67a0 100755 --- a/binance/client.py +++ b/binance/client.py @@ -13862,9 +13862,33 @@ def ws_futures_create_order(self, **params): Send in a new order https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api """ - if "newClientOrderId" not in params: - params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22() - return self._ws_futures_api_request_sync("order.place", True, params) + # Check if this is a conditional order type that needs to use algo endpoint + order_type = params.get("type", "").upper() + conditional_types = [ + "STOP", + "STOP_MARKET", + "TAKE_PROFIT", + "TAKE_PROFIT_MARKET", + "TRAILING_STOP_MARKET", + ] + + if order_type in conditional_types: + # Route to algo order endpoint + if "clientAlgoId" not in params: + params["clientAlgoId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22() + # Remove newClientOrderId if it was added by default + params.pop("newClientOrderId", None) + if "algoType" not in params: + params["algoType"] = "CONDITIONAL" + # Convert stopPrice to triggerPrice for algo orders + if "stopPrice" in params and "triggerPrice" not in params: + params["triggerPrice"] = params.pop("stopPrice") + return self._ws_futures_api_request_sync("algoOrder.place", True, params) + else: + # Use regular order endpoint + if "newClientOrderId" not in params: + params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22() + return self._ws_futures_api_request_sync("order.place", True, params) def ws_futures_edit_order(self, **params): """ @@ -13878,12 +13902,21 @@ def ws_futures_cancel_order(self, **params): cancel an order https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Cancel-Order """ - return self._ws_futures_api_request_sync("order.cancel", True, params) + is_conditional = False + if "algoId" in params or "clientAlgoId" in params: + is_conditional = True + + if is_conditional: + return self._ws_futures_api_request_sync("algoOrder.cancel", True, params) + else: + return self._ws_futures_api_request_sync("order.cancel", True, params) def ws_futures_get_order(self, **params): """ Get an order https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Query-Order + + Note: Algo/conditional orders cannot be queried via websocket API """ return self._ws_futures_api_request_sync("order.status", True, params) diff --git a/tests/test_async_client_ws_futures_requests.py b/tests/test_async_client_ws_futures_requests.py index 5f1034c4..3f347f23 100644 --- a/tests/test_async_client_ws_futures_requests.py +++ b/tests/test_async_client_ws_futures_requests.py @@ -203,3 +203,63 @@ async def test_ws_futures_create_cancel_algo_order(futuresClientAsync): ) assert cancel_result["algoId"] == order["algoId"] + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+") +@pytest.mark.asyncio() +async def test_ws_futures_create_conditional_order_auto_routing(futuresClientAsync): + """Test that conditional order types are automatically routed to algo endpoint""" + ticker = await futuresClientAsync.ws_futures_get_order_book_ticker(symbol="LTCUSDT") + positions = await futuresClientAsync.ws_futures_v2_account_position(symbol="LTCUSDT") + + # Create a STOP_MARKET order using ws_futures_create_order + # It should automatically route to the algo endpoint + # Use a price above current market price for BUY STOP + trigger_price = float(ticker["askPrice"]) * 1.5 + order = await futuresClientAsync.ws_futures_create_order( + symbol=ticker["symbol"], + side="BUY", + positionSide=positions[0]["positionSide"], + type="STOP_MARKET", + quantity=1, + triggerPrice=trigger_price, + ) + + assert order["symbol"] == ticker["symbol"] + assert "algoId" in order + assert order["algoType"] == "CONDITIONAL" + + # Cancel the order using algoId + cancel_result = await futuresClientAsync.ws_futures_cancel_order( + symbol=ticker["symbol"], algoId=order["algoId"] + ) + assert cancel_result["algoId"] == order["algoId"] + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+") +@pytest.mark.asyncio() +async def test_ws_futures_conditional_order_with_stop_price(futuresClientAsync): + """Test that stopPrice is converted to triggerPrice for conditional orders""" + ticker = await futuresClientAsync.ws_futures_get_order_book_ticker(symbol="LTCUSDT") + positions = await futuresClientAsync.ws_futures_v2_account_position(symbol="LTCUSDT") + + # Create a TAKE_PROFIT_MARKET order with stopPrice (should be converted to triggerPrice) + # Use a price above current market price for SELL TAKE_PROFIT + trigger_price = float(ticker["askPrice"]) * 1.5 + order = await futuresClientAsync.ws_futures_create_order( + symbol=ticker["symbol"], + side="SELL", + positionSide=positions[0]["positionSide"], + type="TAKE_PROFIT_MARKET", + quantity=1, + stopPrice=trigger_price, # This should be converted to triggerPrice + ) + + assert order["symbol"] == ticker["symbol"] + assert "algoId" in order + assert order["algoType"] == "CONDITIONAL" + + # Cancel the order + await futuresClientAsync.ws_futures_cancel_order( + symbol=ticker["symbol"], algoId=order["algoId"] + ) diff --git a/tests/test_client_ws_futures_requests.py b/tests/test_client_ws_futures_requests.py index 9c3732e6..5c16f925 100644 --- a/tests/test_client_ws_futures_requests.py +++ b/tests/test_client_ws_futures_requests.py @@ -114,3 +114,58 @@ def test_ws_futures_create_cancel_algo_order(futuresClient): ) assert cancel_result["algoId"] == order["algoId"] + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+") +def test_ws_futures_create_conditional_order_auto_routing(futuresClient): + """Test that conditional order types are automatically routed to algo endpoint""" + ticker = futuresClient.ws_futures_get_order_book_ticker(symbol="LTCUSDT") + positions = futuresClient.ws_futures_v2_account_position(symbol="LTCUSDT") + + trigger_price = float(ticker["askPrice"]) * 1.5 + order = futuresClient.ws_futures_create_order( + symbol=ticker["symbol"], + side="BUY", + positionSide=positions[0]["positionSide"], + type="STOP_MARKET", + quantity=1, + triggerPrice=trigger_price, + ) + + assert order["symbol"] == ticker["symbol"] + assert "algoId" in order + assert order["algoType"] == "CONDITIONAL" + + # Cancel using algoId parameter + cancel_result = futuresClient.ws_futures_cancel_order( + symbol=ticker["symbol"], algoId=order["algoId"] + ) + assert cancel_result["algoId"] == order["algoId"] + + +@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+") +def test_ws_futures_conditional_order_with_stop_price(futuresClient): + """Test that stopPrice is converted to triggerPrice for conditional orders""" + ticker = futuresClient.ws_futures_get_order_book_ticker(symbol="LTCUSDT") + positions = futuresClient.ws_futures_v2_account_position(symbol="LTCUSDT") + + # Create a TAKE_PROFIT_MARKET order with stopPrice (should be converted to triggerPrice) + # Use a price above current market price for SELL TAKE_PROFIT + trigger_price = float(ticker["askPrice"]) * 1.5 + order = futuresClient.ws_futures_create_order( + symbol=ticker["symbol"], + side="SELL", + positionSide=positions[0]["positionSide"], + type="TAKE_PROFIT_MARKET", + quantity=1, + stopPrice=trigger_price, # This should be converted to triggerPrice + ) + + assert order["symbol"] == ticker["symbol"] + assert "algoId" in order + assert order["algoType"] == "CONDITIONAL" + + # Cancel the order + futuresClient.ws_futures_cancel_order( + symbol=ticker["symbol"], algoId=order["algoId"] + )