Skip to content

Commit 8eee50e

Browse files
committed
feat: add support for websocket algo orders (sammchardy#1646)
1 parent b422e29 commit 8eee50e

File tree

4 files changed

+189
-8
lines changed

4 files changed

+189
-8
lines changed

binance/async_client.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4484,9 +4484,33 @@ async def ws_futures_create_order(self, **params):
44844484
Send in a new order
44854485
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api
44864486
"""
4487-
if "newClientOrderId" not in params:
4488-
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
4489-
return await self._ws_futures_api_request("order.place", True, params)
4487+
# Check if this is a conditional order type that needs to use algo endpoint
4488+
order_type = params.get("type", "").upper()
4489+
conditional_types = [
4490+
"STOP",
4491+
"STOP_MARKET",
4492+
"TAKE_PROFIT",
4493+
"TAKE_PROFIT_MARKET",
4494+
"TRAILING_STOP_MARKET",
4495+
]
4496+
4497+
if order_type in conditional_types:
4498+
# Route to algo order endpoint
4499+
if "clientAlgoId" not in params:
4500+
params["clientAlgoId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
4501+
# Remove newClientOrderId if it was added by default
4502+
params.pop("newClientOrderId", None)
4503+
if "algoType" not in params:
4504+
params["algoType"] = "CONDITIONAL"
4505+
# Convert stopPrice to triggerPrice for algo orders
4506+
if "stopPrice" in params and "triggerPrice" not in params:
4507+
params["triggerPrice"] = params.pop("stopPrice")
4508+
return await self._ws_futures_api_request("algoOrder.place", True, params)
4509+
else:
4510+
# Use regular order endpoint
4511+
if "newClientOrderId" not in params:
4512+
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
4513+
return await self._ws_futures_api_request("order.place", True, params)
44904514

44914515
async def ws_futures_edit_order(self, **params):
44924516
"""
@@ -4500,12 +4524,21 @@ async def ws_futures_cancel_order(self, **params):
45004524
cancel an order
45014525
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Cancel-Order
45024526
"""
4503-
return await self._ws_futures_api_request("order.cancel", True, params)
4527+
is_conditional = False
4528+
if "algoId" in params or "clientAlgoId" in params:
4529+
is_conditional = True
4530+
4531+
if is_conditional:
4532+
return await self._ws_futures_api_request("algoOrder.cancel", True, params)
4533+
else:
4534+
return await self._ws_futures_api_request("order.cancel", True, params)
45044535

45054536
async def ws_futures_get_order(self, **params):
45064537
"""
45074538
Get an order
45084539
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Query-Order
4540+
4541+
Note: Algo/conditional orders cannot be queried via websocket API
45094542
"""
45104543
return await self._ws_futures_api_request("order.status", True, params)
45114544

binance/client.py

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13862,9 +13862,33 @@ def ws_futures_create_order(self, **params):
1386213862
Send in a new order
1386313863
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api
1386413864
"""
13865-
if "newClientOrderId" not in params:
13866-
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
13867-
return self._ws_futures_api_request_sync("order.place", True, params)
13865+
# Check if this is a conditional order type that needs to use algo endpoint
13866+
order_type = params.get("type", "").upper()
13867+
conditional_types = [
13868+
"STOP",
13869+
"STOP_MARKET",
13870+
"TAKE_PROFIT",
13871+
"TAKE_PROFIT_MARKET",
13872+
"TRAILING_STOP_MARKET",
13873+
]
13874+
13875+
if order_type in conditional_types:
13876+
# Route to algo order endpoint
13877+
if "clientAlgoId" not in params:
13878+
params["clientAlgoId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
13879+
# Remove newClientOrderId if it was added by default
13880+
params.pop("newClientOrderId", None)
13881+
if "algoType" not in params:
13882+
params["algoType"] = "CONDITIONAL"
13883+
# Convert stopPrice to triggerPrice for algo orders
13884+
if "stopPrice" in params and "triggerPrice" not in params:
13885+
params["triggerPrice"] = params.pop("stopPrice")
13886+
return self._ws_futures_api_request_sync("algoOrder.place", True, params)
13887+
else:
13888+
# Use regular order endpoint
13889+
if "newClientOrderId" not in params:
13890+
params["newClientOrderId"] = self.CONTRACT_ORDER_PREFIX + self.uuid22()
13891+
return self._ws_futures_api_request_sync("order.place", True, params)
1386813892

1386913893
def ws_futures_edit_order(self, **params):
1387013894
"""
@@ -13878,12 +13902,21 @@ def ws_futures_cancel_order(self, **params):
1387813902
cancel an order
1387913903
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Cancel-Order
1388013904
"""
13881-
return self._ws_futures_api_request_sync("order.cancel", True, params)
13905+
is_conditional = False
13906+
if "algoId" in params or "clientAlgoId" in params:
13907+
is_conditional = True
13908+
13909+
if is_conditional:
13910+
return self._ws_futures_api_request_sync("algoOrder.cancel", True, params)
13911+
else:
13912+
return self._ws_futures_api_request_sync("order.cancel", True, params)
1388213913

1388313914
def ws_futures_get_order(self, **params):
1388413915
"""
1388513916
Get an order
1388613917
https://developers.binance.com/docs/derivatives/usds-margined-futures/trade/websocket-api/Query-Order
13918+
13919+
Note: Algo/conditional orders cannot be queried via websocket API
1388713920
"""
1388813921
return self._ws_futures_api_request_sync("order.status", True, params)
1388913922

tests/test_async_client_ws_futures_requests.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -203,3 +203,63 @@ async def test_ws_futures_create_cancel_algo_order(futuresClientAsync):
203203
)
204204

205205
assert cancel_result["algoId"] == order["algoId"]
206+
207+
208+
@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
209+
@pytest.mark.asyncio()
210+
async def test_ws_futures_create_conditional_order_auto_routing(futuresClientAsync):
211+
"""Test that conditional order types are automatically routed to algo endpoint"""
212+
ticker = await futuresClientAsync.ws_futures_get_order_book_ticker(symbol="LTCUSDT")
213+
positions = await futuresClientAsync.ws_futures_v2_account_position(symbol="LTCUSDT")
214+
215+
# Create a STOP_MARKET order using ws_futures_create_order
216+
# It should automatically route to the algo endpoint
217+
# Use a price above current market price for BUY STOP
218+
trigger_price = float(ticker["askPrice"]) * 1.5
219+
order = await futuresClientAsync.ws_futures_create_order(
220+
symbol=ticker["symbol"],
221+
side="BUY",
222+
positionSide=positions[0]["positionSide"],
223+
type="STOP_MARKET",
224+
quantity=1,
225+
triggerPrice=trigger_price,
226+
)
227+
228+
assert order["symbol"] == ticker["symbol"]
229+
assert "algoId" in order
230+
assert order["algoType"] == "CONDITIONAL"
231+
232+
# Cancel the order using algoId
233+
cancel_result = await futuresClientAsync.ws_futures_cancel_order(
234+
symbol=ticker["symbol"], algoId=order["algoId"]
235+
)
236+
assert cancel_result["algoId"] == order["algoId"]
237+
238+
239+
@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
240+
@pytest.mark.asyncio()
241+
async def test_ws_futures_conditional_order_with_stop_price(futuresClientAsync):
242+
"""Test that stopPrice is converted to triggerPrice for conditional orders"""
243+
ticker = await futuresClientAsync.ws_futures_get_order_book_ticker(symbol="LTCUSDT")
244+
positions = await futuresClientAsync.ws_futures_v2_account_position(symbol="LTCUSDT")
245+
246+
# Create a TAKE_PROFIT_MARKET order with stopPrice (should be converted to triggerPrice)
247+
# Use a price above current market price for SELL TAKE_PROFIT
248+
trigger_price = float(ticker["askPrice"]) * 1.5
249+
order = await futuresClientAsync.ws_futures_create_order(
250+
symbol=ticker["symbol"],
251+
side="SELL",
252+
positionSide=positions[0]["positionSide"],
253+
type="TAKE_PROFIT_MARKET",
254+
quantity=1,
255+
stopPrice=trigger_price, # This should be converted to triggerPrice
256+
)
257+
258+
assert order["symbol"] == ticker["symbol"]
259+
assert "algoId" in order
260+
assert order["algoType"] == "CONDITIONAL"
261+
262+
# Cancel the order
263+
await futuresClientAsync.ws_futures_cancel_order(
264+
symbol=ticker["symbol"], algoId=order["algoId"]
265+
)

tests/test_client_ws_futures_requests.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,58 @@ def test_ws_futures_create_cancel_algo_order(futuresClient):
114114
)
115115

116116
assert cancel_result["algoId"] == order["algoId"]
117+
118+
119+
@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
120+
def test_ws_futures_create_conditional_order_auto_routing(futuresClient):
121+
"""Test that conditional order types are automatically routed to algo endpoint"""
122+
ticker = futuresClient.ws_futures_get_order_book_ticker(symbol="LTCUSDT")
123+
positions = futuresClient.ws_futures_v2_account_position(symbol="LTCUSDT")
124+
125+
trigger_price = float(ticker["askPrice"]) * 1.5
126+
order = futuresClient.ws_futures_create_order(
127+
symbol=ticker["symbol"],
128+
side="BUY",
129+
positionSide=positions[0]["positionSide"],
130+
type="STOP_MARKET",
131+
quantity=1,
132+
triggerPrice=trigger_price,
133+
)
134+
135+
assert order["symbol"] == ticker["symbol"]
136+
assert "algoId" in order
137+
assert order["algoType"] == "CONDITIONAL"
138+
139+
# Cancel using algoId parameter
140+
cancel_result = futuresClient.ws_futures_cancel_order(
141+
symbol=ticker["symbol"], algoId=order["algoId"]
142+
)
143+
assert cancel_result["algoId"] == order["algoId"]
144+
145+
146+
@pytest.mark.skipif(sys.version_info < (3, 8), reason="websockets_proxy Python 3.8+")
147+
def test_ws_futures_conditional_order_with_stop_price(futuresClient):
148+
"""Test that stopPrice is converted to triggerPrice for conditional orders"""
149+
ticker = futuresClient.ws_futures_get_order_book_ticker(symbol="LTCUSDT")
150+
positions = futuresClient.ws_futures_v2_account_position(symbol="LTCUSDT")
151+
152+
# Create a TAKE_PROFIT_MARKET order with stopPrice (should be converted to triggerPrice)
153+
# Use a price above current market price for SELL TAKE_PROFIT
154+
trigger_price = float(ticker["askPrice"]) * 1.5
155+
order = futuresClient.ws_futures_create_order(
156+
symbol=ticker["symbol"],
157+
side="SELL",
158+
positionSide=positions[0]["positionSide"],
159+
type="TAKE_PROFIT_MARKET",
160+
quantity=1,
161+
stopPrice=trigger_price, # This should be converted to triggerPrice
162+
)
163+
164+
assert order["symbol"] == ticker["symbol"]
165+
assert "algoId" in order
166+
assert order["algoType"] == "CONDITIONAL"
167+
168+
# Cancel the order
169+
futuresClient.ws_futures_cancel_order(
170+
symbol=ticker["symbol"], algoId=order["algoId"]
171+
)

0 commit comments

Comments
 (0)