From 31c80e944ed0037be324c96dc2ddd6ec5b3b4941 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Mon, 29 Jun 2026 19:52:03 -0700 Subject: [PATCH] fix: tolerate extra order API fields --- src/project_x_py/models.py | 11 +++- src/project_x_py/order_manager/core.py | 4 +- src/project_x_py/order_manager/tracking.py | 2 +- tests/order_manager/test_order_core.py | 61 ++++++++++++++++++++++ 4 files changed, 73 insertions(+), 5 deletions(-) diff --git a/src/project_x_py/models.py b/src/project_x_py/models.py index 05c9897..bbca64b 100644 --- a/src/project_x_py/models.py +++ b/src/project_x_py/models.py @@ -112,8 +112,9 @@ - `types`: Type definitions and protocols """ -from dataclasses import dataclass -from typing import Union +from collections.abc import Mapping +from dataclasses import dataclass, fields +from typing import Any, Union __all__ = [ "Account", @@ -246,6 +247,12 @@ def is_cancelled(self) -> bool: """Check if order was cancelled.""" return self.status == 3 # OrderStatus.CANCELLED + @classmethod + def from_api(cls, data: Mapping[str, Any]) -> "Order": + """Create an Order from API data, ignoring additive unknown fields.""" + field_names = {field.name for field in fields(cls)} + return cls(**{name: data[name] for name in field_names if name in data}) + @property def is_rejected(self) -> bool: """Check if order was rejected.""" diff --git a/src/project_x_py/order_manager/core.py b/src/project_x_py/order_manager/core.py index bfe12a8..96bf4be 100644 --- a/src/project_x_py/order_manager/core.py +++ b/src/project_x_py/order_manager/core.py @@ -715,7 +715,7 @@ async def search_open_orders( open_orders = [] for order_data in orders: try: - order = Order(**order_data) + order = Order.from_api(order_data) open_orders.append(order) # Update our cache @@ -912,7 +912,7 @@ async def get_order_by_id(self, order_id: int) -> Order | None: order_data = await self.get_tracked_order_status(order_id_str) if order_data: try: - return Order(**order_data) + return Order.from_api(order_data) except Exception as e: self.logger.debug(f"Failed to parse cached order data: {e}") diff --git a/src/project_x_py/order_manager/tracking.py b/src/project_x_py/order_manager/tracking.py index 4f7e221..a86bf37 100644 --- a/src/project_x_py/order_manager/tracking.py +++ b/src/project_x_py/order_manager/tracking.py @@ -717,7 +717,7 @@ async def _on_order_update(self, order_data: dict[str, Any] | list[Any]) -> None from project_x_py.models import Order try: - order_obj = Order(**actual_order_data) + order_obj = Order.from_api(actual_order_data) event_payload = { "order": order_obj, "order_id": order_id, # Add order_id for compatibility diff --git a/tests/order_manager/test_order_core.py b/tests/order_manager/test_order_core.py index 2d46bc2..337b23c 100644 --- a/tests/order_manager/test_order_core.py +++ b/tests/order_manager/test_order_core.py @@ -65,6 +65,67 @@ async def test_search_open_orders_populates_cache( assert order_manager.tracked_orders[str(resp_order["id"])] == resp_order assert order_manager.order_status_cache[str(resp_order["id"])] == 1 + @pytest.mark.asyncio + async def test_search_open_orders_ignores_unknown_api_fields(self, order_manager): + """search_open_orders tolerates additive API fields like fills.""" + resp_order = { + "id": 101, + "accountId": 12345, + "contractId": "MGC", + "creationTimestamp": "2024-01-01T01:00:00Z", + "updateTimestamp": None, + "status": 1, + "type": 1, + "side": 0, + "size": 2, + "fills": [{"price": 2100.5, "size": 1}], + } + order_manager.project_x.account_info.id = 12345 + order_manager.project_x._make_request = AsyncMock( + return_value={"success": True, "orders": [resp_order]} + ) + + orders = await order_manager.search_open_orders() + + assert orders == [ + Order( + id=101, + accountId=12345, + contractId="MGC", + creationTimestamp="2024-01-01T01:00:00Z", + updateTimestamp=None, + status=1, + type=1, + side=0, + size=2, + ) + ] + assert not hasattr(orders[0], "fills") + assert order_manager.tracked_orders["101"] == resp_order + + @pytest.mark.asyncio + async def test_get_order_by_id_ignores_unknown_cached_fields(self, order_manager): + """get_order_by_id tolerates additive fields in tracked order data.""" + order_manager._realtime_enabled = True + order_manager.tracked_orders["101"] = { + "id": 101, + "accountId": 12345, + "contractId": "MGC", + "creationTimestamp": "2024-01-01T01:00:00Z", + "updateTimestamp": None, + "status": 1, + "type": 1, + "side": 0, + "size": 2, + "fills": [{"price": 2100.5, "size": 1}], + } + + order = await order_manager.get_order_by_id(101) + + assert isinstance(order, Order) + assert order.id == 101 + assert not hasattr(order, "fills") + @pytest.mark.asyncio async def test_is_order_filled_cache_hit(self, order_manager): """is_order_filled returns True from cache and does not call _make_request if cached."""