From cc431b6951785203ac73d4a5863904250f0aba96 Mon Sep 17 00:00:00 2001 From: Alex Kwiatkowski Date: Sun, 28 Jun 2026 21:27:28 -0700 Subject: [PATCH] ignore malformed orderbook realtime payloads --- src/project_x_py/orderbook/realtime.py | 22 ++++++++++++ tests/orderbook/test_realtime.py | 46 +++++++++++++++----------- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/project_x_py/orderbook/realtime.py b/src/project_x_py/orderbook/realtime.py index c9e67fa..6445dc4 100644 --- a/src/project_x_py/orderbook/realtime.py +++ b/src/project_x_py/orderbook/realtime.py @@ -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", "") @@ -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): @@ -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 @@ -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 @@ -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)) diff --git a/tests/orderbook/test_realtime.py b/tests/orderbook/test_realtime.py index cac377c..2989b1d 100644 --- a/tests/orderbook/test_realtime.py +++ b/tests/orderbook/test_realtime.py @@ -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 @@ -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: