Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions src/project_x_py/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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."""
Expand Down
4 changes: 2 additions & 2 deletions src/project_x_py/order_manager/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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}")

Expand Down
2 changes: 1 addition & 1 deletion src/project_x_py/order_manager/tracking.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
61 changes: 61 additions & 0 deletions tests/order_manager/test_order_core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down