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
22 changes: 22 additions & 0 deletions src/project_x_py/orderbook/realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,10 @@ async def _on_market_depth_update(self, data: dict[str, Any]) -> None:
depth entry contains DomType information for proper processing.
"""
try:
if not isinstance(data, dict):
self.logger.debug("Ignoring malformed market depth update")
return

self.logger.debug(f"Market depth callback received: {list(data.keys())}")
# The data comes structured as {"contract_id": ..., "data": ...}
contract_id = data.get("contract_id", "")
Expand Down Expand Up @@ -234,6 +238,10 @@ async def _on_quote_update(self, data: dict[str, Any]) -> None:
trigger quote-specific callbacks for client applications.
"""
try:
if not isinstance(data, dict):
self.logger.debug("Ignoring malformed quote update")
return

# The data comes structured as {"contract_id": ..., "data": ...}
contract_id = data.get("contract_id", "")
if not self._is_relevant_contract(contract_id):
Expand Down Expand Up @@ -287,6 +295,9 @@ def _is_relevant_contract(self, contract_id: str) -> bool:
>>> handler._is_relevant_contract("CON.F.US.NQ.H25") # ES orderbook
False
"""
if not isinstance(contract_id, str) or not contract_id:
return False

if contract_id == self.orderbook.instrument:
return True

Expand Down Expand Up @@ -328,7 +339,14 @@ async def _process_market_depth(self, data: dict[str, Any]) -> None:
This method acquires the orderbook lock and processes all updates
atomically to ensure data consistency.
"""
if not isinstance(data, dict):
self.logger.debug("Ignoring malformed market depth payload")
return

market_data = data.get("data", [])
if not isinstance(market_data, list):
self.logger.debug("Ignoring market depth payload with non-list data")
return
if not market_data:
return

Expand Down Expand Up @@ -396,6 +414,10 @@ async def _process_single_depth_entry(
This method should only be called from within _process_market_depth
while the orderbook lock is already held.
"""
if not isinstance(entry, dict):
self.logger.debug("Ignoring malformed market depth entry")
return

try:
trade_type = entry.get("type", 0)
price = float(entry.get("price", 0))
Expand Down
46 changes: 27 additions & 19 deletions tests/orderbook/test_realtime.py
Original file line number Diff line number Diff line change
Expand Up @@ -246,10 +246,7 @@ def test_is_relevant_contract_edge_cases(self, realtime_handler):
# Empty contract IDs
assert realtime_handler._is_relevant_contract("") is False

# BUG DISCOVERED: None contract ID causes AttributeError
# Should handle None gracefully but currently crashes
with pytest.raises(AttributeError):
realtime_handler._is_relevant_contract(None)
assert realtime_handler._is_relevant_contract(None) is False

# Fixed: Partial matches should not qualify - using exact match instead of startswith
assert realtime_handler._is_relevant_contract("MNQH25") is False
Expand Down Expand Up @@ -522,21 +519,32 @@ async def test_handle_missing_required_fields(self, realtime_handler, mock_order
@pytest.mark.asyncio
async def test_handle_none_data(self, realtime_handler, mock_orderbook_base):
"""Test handling of None data."""
# BUG DISCOVERED: The code doesn't handle None data properly
# _process_market_depth crashes with AttributeError: 'NoneType' object has no attribute 'get'
# _is_relevant_contract crashes with AttributeError: 'NoneType' object has no attribute 'replace'
# These should be fixed to handle None gracefully

# For now, we expect these to raise exceptions (documenting the bugs)
with pytest.raises(AttributeError):
await realtime_handler._process_market_depth(None)

# Quote update might handle None better - let's test
try:
await realtime_handler._on_quote_update(None)
except (AttributeError, TypeError):
# Expected due to None handling bug
pass
await realtime_handler._process_market_depth(None)
await realtime_handler._on_market_depth_update(None)
await realtime_handler._on_quote_update(None)
assert realtime_handler._is_relevant_contract(None) is False

@pytest.mark.asyncio
async def test_handle_none_depth_entries(self, realtime_handler, mock_orderbook_base):
"""Test handling of None entries inside market depth data."""
depth_data = {
"contract_id": "CON.F.US.MNQ.U25",
"data": [
None,
{
"contractId": "CON.F.US.MNQ.U25",
"type": DomType.BID.value,
"price": 21000.0,
"size": 10,
"side": "Bid",
"timestamp": datetime.now(UTC).isoformat(),
},
],
}

await realtime_handler._on_market_depth_update(depth_data)

assert mock_orderbook_base._trigger_callbacks.called


class TestThreadSafety:
Expand Down