Skip to content

Commit 19760e5

Browse files
committed
refactor(orders): Improve order tracking and chaining
1 parent 3edd369 commit 19760e5

File tree

2 files changed

+80
-29
lines changed

2 files changed

+80
-29
lines changed

src/project_x_py/order_manager/tracking.py

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -182,15 +182,23 @@ async def _on_order_update(self, order_data: dict[str, Any] | list[Any]) -> None
182182
}
183183

184184
if new_status in status_events:
185-
await self._trigger_callbacks(
186-
status_events[new_status],
187-
{
188-
"order_id": order_id,
189-
"order_data": actual_order_data,
185+
from project_x_py.models import Order
186+
187+
try:
188+
order_obj = Order(**actual_order_data)
189+
event_payload = {
190+
"order": order_obj,
190191
"old_status": old_status,
191192
"new_status": new_status,
192-
},
193-
)
193+
}
194+
await self._trigger_callbacks(
195+
status_events[new_status], event_payload
196+
)
197+
except Exception as e:
198+
logger.error(
199+
f"Failed to create Order object from data: {e}",
200+
extra={"order_data": actual_order_data},
201+
)
194202

195203
# OCO Logic: If a linked order is filled, cancel the other.
196204
if new_status == 2: # Filled

src/project_x_py/order_tracker.py

Lines changed: 65 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -54,9 +54,12 @@
5454

5555
import asyncio
5656
import logging
57+
import warnings
5758
from types import TracebackType
5859
from typing import TYPE_CHECKING, Any, Union
5960

61+
from typing_extensions import deprecated
62+
6063
from project_x_py.event_bus import EventType
6164
from project_x_py.models import BracketOrderResponse, Order, OrderPlaceResponse
6265

@@ -127,15 +130,17 @@ async def _setup_event_handlers(self) -> None:
127130

128131
# Handler for order fills
129132
async def on_fill(data: dict[str, Any]) -> None:
130-
if data.get("order_id") == self.order_id:
131-
self._filled_order = data.get("order_data")
133+
order = data.get("order")
134+
if order and order.id == self.order_id:
135+
self._filled_order = order
132136
self._current_status = 2 # FILLED
133137
self._fill_event.set()
134138

135139
# Handler for status changes
136140
async def on_status_change(data: dict[str, Any]) -> None:
137-
if data.get("order_id") == self.order_id:
138-
new_status = data.get("new_status")
141+
order = data.get("order")
142+
if order and order.id == self.order_id:
143+
new_status = order.status
139144
self._current_status = new_status
140145

141146
# Set status-specific events
@@ -252,32 +257,38 @@ async def wait_for_status(self, status: int, timeout: float = 30.0) -> Order:
252257
if status not in self._status_events:
253258
self._status_events[status] = asyncio.Event()
254259

255-
# Check if already at target status
260+
# Check current status before waiting
256261
if self._current_status == status:
257262
order = await self.order_manager.get_order_by_id(self.order_id)
258-
if order:
263+
if order and order.status == status:
259264
return order
260265

266+
# Wait for the event
261267
try:
262268
await asyncio.wait_for(self._status_events[status].wait(), timeout=timeout)
263-
264-
if self._error and status != self._current_status:
265-
raise self._error
266-
267-
# Fetch latest order data
269+
except TimeoutError:
270+
# After timeout, check the status one last time via API
268271
order = await self.order_manager.get_order_by_id(self.order_id)
269272
if order and order.status == status:
270273
return order
271-
else:
272-
raise OrderLifecycleError(
273-
f"Status event received but order not in expected state {status}"
274-
)
275-
276-
except TimeoutError:
277274
raise TimeoutError(
278275
f"Order {self.order_id} did not reach status {status} within {timeout} seconds"
279276
) from None
280277

278+
# After event is received
279+
if self._error and status != self._current_status:
280+
raise self._error
281+
282+
order = await self.order_manager.get_order_by_id(self.order_id)
283+
if order and order.status == status:
284+
return order
285+
else:
286+
# This can happen if event fires but API state is not yet consistent,
287+
# or if another status update arrived quickly.
288+
raise OrderLifecycleError(
289+
f"Status event received but order not in expected state {status}. Current state: {order.status if order else 'not found'}"
290+
)
291+
281292
async def modify_or_cancel(
282293
self, new_price: float | None = None, new_size: int | None = None
283294
) -> bool:
@@ -429,10 +440,9 @@ def with_take_profit(
429440
self,
430441
offset: float | None = None,
431442
price: float | None = None,
432-
limit: bool = True,
433443
) -> "OrderChainBuilder":
434444
"""Add a take profit to the order chain."""
435-
self.take_profit = {"offset": offset, "price": price, "limit": limit}
445+
self.take_profit = {"offset": offset, "price": price}
436446
return self
437447

438448
def with_trail_stop(
@@ -519,9 +529,33 @@ async def execute(self) -> BracketOrderResponse:
519529
)
520530

521531
# Add trailing stop if configured
522-
if self.trail_stop and result.success and result.entry_order_id:
523-
# TODO: Implement trailing stop order
524-
logger.warning("Trailing stop orders not yet implemented")
532+
if self.trail_stop and result.success and result.stop_order_id:
533+
logger.info(
534+
f"Replacing stop order {result.stop_order_id} with trailing stop."
535+
)
536+
try:
537+
await self.order_manager.cancel_order(result.stop_order_id)
538+
trail_offset = self.trail_stop["offset"]
539+
stop_side = 1 if self.side == 0 else 0 # Opposite of entry
540+
541+
trail_response = await self.order_manager.place_trailing_stop_order(
542+
contract_id=contract_id,
543+
side=stop_side,
544+
size=self.size,
545+
trail_price=trail_offset,
546+
)
547+
if trail_response.success:
548+
logger.info(
549+
f"Trailing stop order placed: {trail_response.orderId}"
550+
)
551+
# Note: The BracketOrderResponse does not have a field for the trailing stop ID.
552+
# The original stop_order_id will remain in the response.
553+
else:
554+
logger.error(
555+
f"Failed to place trailing stop: {trail_response.errorMessage}"
556+
)
557+
except Exception as e:
558+
logger.error(f"Error replacing stop with trailing stop: {e}")
525559

526560
return result
527561

@@ -571,6 +605,9 @@ class OrderLifecycleError(Exception):
571605

572606

573607
# Convenience function for creating order trackers
608+
@deprecated(
609+
"Use TradingSuite.track_order() instead. This function will be removed in v4.0.0."
610+
)
574611
def track_order(
575612
trading_suite: "TradingSuite",
576613
order: Union[Order, OrderPlaceResponse, int] | None = None,
@@ -593,6 +630,12 @@ def track_order(
593630
filled = await tracker.wait_for_fill()
594631
```
595632
"""
633+
warnings.warn(
634+
"track_order() is deprecated, use TradingSuite.track_order() instead. "
635+
"This function will be removed in v4.0.0",
636+
DeprecationWarning,
637+
stacklevel=2,
638+
)
596639
tracker = OrderTracker(trading_suite)
597640
if order:
598641
if isinstance(order, Order | OrderPlaceResponse):

0 commit comments

Comments
 (0)