diff --git a/src/cerbero_mcp/exchanges/ibkr/client.py b/src/cerbero_mcp/exchanges/ibkr/client.py index 47f81b0..304fda6 100644 --- a/src/cerbero_mcp/exchanges/ibkr/client.py +++ b/src/cerbero_mcp/exchanges/ibkr/client.py @@ -278,9 +278,9 @@ class IBKRClient: # ── Order writes ──────────────────────────────────────────── - _AUTO_CONFIRM_WHITELIST = ( - "outside RTH", "no market data", "you are submitting", "the contract", - ) + # Auto-confirm policy: any IBKR warning that is NOT in _CRITICAL_WARNINGS + # is auto-confirmed up to _AUTO_CONFIRM_MAX_CYCLES times. Hardening to a + # strict whitelist (allow-list) is deferred — V1 trades safety for UX. _CRITICAL_WARNINGS = ( "margin", "suitability", "credit", "rejected", "insufficient", ) diff --git a/tests/unit/exchanges/ibkr/test_client.py b/tests/unit/exchanges/ibkr/test_client.py index 05063aa..bfd17de 100644 --- a/tests/unit/exchanges/ibkr/test_client.py +++ b/tests/unit/exchanges/ibkr/test_client.py @@ -205,3 +205,28 @@ async def test_cancel_order(httpx_mock: HTTPXMock, client): ) res = await client.cancel_order("OID42") assert res["canceled"] is True + + +@pytest.mark.asyncio +async def test_place_order_too_many_confirmations(httpx_mock: HTTPXMock, client): + httpx_mock.add_response(url=re.compile(r".*/tickle"), json={}) + httpx_mock.add_response( + url=re.compile(r".*/trsrv/secdef/search"), + json=[{"conid": 265598, "symbol": "AAPL", "secType": "STK"}], + ) + # Initial place + 3 reply cycles all return new warnings — should fail at MAX_CYCLES + httpx_mock.add_response( + url=re.compile(r".*/iserver/account/DU1234/orders$"), + method="POST", + json=[{"id": "msg1", "message": ["outside RTH"]}], + ) + for n in range(2, 5): + httpx_mock.add_response( + url=re.compile(rf".*/iserver/reply/msg{n-1}$"), + method="POST", + json=[{"id": f"msg{n}", "message": ["outside RTH"]}], + ) + with pytest.raises(IBKRError, match="IBKR_ORDER_TOO_MANY_CONFIRMATIONS"): + await client.place_order( + symbol="AAPL", side="buy", qty=1, order_type="market", + )