Skip to content

Commit 424cd91

Browse files
Pigbibicodex
andauthored
fix: keep IBKR reconciliation probes read-only (#417)
Co-authored-by: Codex <noreply@openai.com>
1 parent e1df3fd commit 424cd91

4 files changed

Lines changed: 41 additions & 7 deletions

File tree

application/runtime_broker_adapters.py

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,14 @@ def capture_api_error(_request_id, error_code, error_message, _contract):
156156
ib.RaiseRequestErrors = original_raise_request_errors
157157
ib.RequestTimeout = original_request_timeout
158158

159-
def connect_ib(self):
159+
def connect_ib(self, *, validate_trading_permissions: bool = True):
160+
"""Connect to the Gateway, optionally without an order-write permission probe.
161+
162+
Account validation is safe and remains mandatory for every connection.
163+
The live trading permission check uses IBKR's ``whatIfOrder`` API, which
164+
is non-transmitting but still exercises an order-validation endpoint.
165+
Health and reconciliation probes must not invoke that endpoint.
166+
"""
160167
self.ensure_event_loop_fn()
161168
host = self.host_resolver()
162169
last_error = None
@@ -179,7 +186,8 @@ def connect_ib(self):
179186
)
180187
try:
181188
self.validate_configured_accounts(ib)
182-
self.validate_trading_permissions(ib)
189+
if validate_trading_permissions:
190+
self.validate_trading_permissions(ib)
183191
except Exception:
184192
disconnect_fn = getattr(ib, "disconnect", None)
185193
if callable(disconnect_fn):

main.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -882,9 +882,11 @@ def require_gateway_execution_backend():
882882
)
883883

884884

885-
def connect_ib():
885+
def connect_ib(*, validate_trading_permissions: bool = True):
886886
require_gateway_execution_backend()
887-
return build_broker_adapters().connect_ib()
887+
return build_broker_adapters().connect_ib(
888+
validate_trading_permissions=validate_trading_permissions,
889+
)
888890

889891

890892
def _build_health_probe_connection_error_message(exc: Exception) -> str:
@@ -1733,7 +1735,7 @@ def _handle_probe(*, response_body: str = "Probe OK"):
17331735
http_method=request.method,
17341736
execution_window="probe",
17351737
)
1736-
ib = connect_ib()
1738+
ib = connect_ib(validate_trading_permissions=False)
17371739
snapshot = build_portfolio_snapshot(ib)
17381740
positions = tuple(getattr(snapshot, "positions", ()) or ())
17391741
buying_power = float(getattr(snapshot, "buying_power", 0.0) or 0.0)

tests/test_request_handling.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -528,7 +528,7 @@ def fake_dry_run(*, recycle_on_timeout, timeout_state):
528528

529529

530530
def test_handle_probe_checks_account_snapshot_without_success_notification(strategy_module, monkeypatch):
531-
observed = {"events": [], "disconnects": 0, "notifications": []}
531+
observed = {"events": [], "disconnects": 0, "notifications": [], "connection_options": []}
532532

533533
class FakeIB:
534534
def disconnect(self):
@@ -562,7 +562,11 @@ def disconnect(self):
562562
"attach_strategy_plugin_report",
563563
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("health probe should not attach strategy plugin reports")),
564564
)
565-
monkeypatch.setattr(strategy_module, "connect_ib", lambda: FakeIB())
565+
def fake_connect_ib(*, validate_trading_permissions=True):
566+
observed["connection_options"].append(validate_trading_permissions)
567+
return FakeIB()
568+
569+
monkeypatch.setattr(strategy_module, "connect_ib", fake_connect_ib)
566570
monkeypatch.setattr(strategy_module, "build_portfolio_snapshot", lambda ib: snapshot)
567571
monkeypatch.setattr(
568572
strategy_module,
@@ -585,6 +589,7 @@ def disconnect(self):
585589
assert observed["report"]["summary"]["positions_count"] == 1
586590
assert observed["disconnects"] == 1
587591
assert observed["notifications"] == []
592+
assert observed["connection_options"] == [False]
588593

589594

590595
def test_handle_probe_connect_timeout_sends_concise_connection_notification(strategy_module, monkeypatch):

tests/test_runtime_broker_adapters.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -352,3 +352,22 @@ def whatIfOrder(self, _contract, _order):
352352
)
353353

354354
assert adapters.connect_ib().managedAccounts() == ["U1234567"]
355+
356+
357+
def test_connect_ib_skips_trading_permission_probe_for_explicit_read_only_connection():
358+
class FakeIB:
359+
def managedAccounts(self):
360+
return ["U1234567"]
361+
362+
def whatIfOrder(self, _contract, _order):
363+
pytest.fail("read-only connection must not probe trading permissions")
364+
365+
adapters = _build_adapters(account_ids=("U1234567",), execution_mode="live")
366+
adapters = adapters.__class__(
367+
**{
368+
**adapters.__dict__,
369+
"connect_ib_fn": lambda *_args, **_kwargs: FakeIB(),
370+
}
371+
)
372+
373+
assert adapters.connect_ib(validate_trading_permissions=False).managedAccounts() == ["U1234567"]

0 commit comments

Comments
 (0)