Skip to content
Merged
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
20 changes: 17 additions & 3 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,9 +882,20 @@ def require_gateway_execution_backend():
)


def connect_ib(*, validate_trading_permissions: bool = True):
def connect_ib(
*,
read_only: bool = False,
validate_trading_permissions: bool = True,
):
"""Connect to the Gateway with an optional read-only session boundary."""
require_gateway_execution_backend()
return build_broker_adapters().connect_ib(
# A probe must not merely skip the local what-if permission check. Its
# Gateway session itself must be read-only, so a future probe change cannot
# accidentally acquire broker write authority.
adapter_dry_run_override = True if read_only else None
return build_broker_adapters(
dry_run_only_override=adapter_dry_run_override,
).connect_ib(
validate_trading_permissions=validate_trading_permissions,
)

Expand Down Expand Up @@ -1735,7 +1746,10 @@ def _handle_probe(*, response_body: str = "Probe OK"):
http_method=request.method,
execution_window="probe",
)
ib = connect_ib(validate_trading_permissions=False)
ib = connect_ib(
read_only=True,
validate_trading_permissions=False,
)
snapshot = build_portfolio_snapshot(ib)
positions = tuple(getattr(snapshot, "positions", ()) or ())
buying_power = float(getattr(snapshot, "buying_power", 0.0) or 0.0)
Expand Down
16 changes: 16 additions & 0 deletions tests/test_event_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,22 @@ def fake_ibkr_connect(host, port, client_id, **kwargs):
assert observed["args"] == ("127.0.0.1", 4001, 1, {"timeout": 60, "readonly": False})


def test_live_runtime_can_force_a_read_only_gateway_session(strategy_module_factory, monkeypatch):
module = strategy_module_factory(IBKR_DRY_RUN_ONLY="false")
observed = {}

def fake_ibkr_connect(host, port, client_id, **kwargs):
observed["args"] = (host, port, client_id, kwargs)
return object()

monkeypatch.setattr(module, "ibkr_connect_ib", fake_ibkr_connect)

adapters = module.build_broker_adapters(dry_run_only_override=True)
adapters.connect_ib_fn("127.0.0.1", 4001, 1, timeout=60)

assert observed["args"] == ("127.0.0.1", 4001, 1, {"timeout": 60, "readonly": True})


def test_connect_ib_retries_with_offset_client_ids(strategy_module_factory, monkeypatch):
module = strategy_module_factory(
IBKR_CONNECT_ATTEMPTS="3",
Expand Down
13 changes: 10 additions & 3 deletions tests/test_request_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,13 @@ def disconnect(self):
"attach_strategy_plugin_report",
lambda *args, **kwargs: (_ for _ in ()).throw(AssertionError("health probe should not attach strategy plugin reports")),
)
def fake_connect_ib(*, validate_trading_permissions=True):
observed["connection_options"].append(validate_trading_permissions)
def fake_connect_ib(*, read_only=False, validate_trading_permissions=True):
observed["connection_options"].append(
{
"read_only": read_only,
"validate_trading_permissions": validate_trading_permissions,
}
)
return FakeIB()

monkeypatch.setattr(strategy_module, "connect_ib", fake_connect_ib)
Expand All @@ -589,7 +594,9 @@ def fake_connect_ib(*, validate_trading_permissions=True):
assert observed["report"]["summary"]["positions_count"] == 1
assert observed["disconnects"] == 1
assert observed["notifications"] == []
assert observed["connection_options"] == [False]
assert observed["connection_options"] == [
{"read_only": True, "validate_trading_permissions": False}
]


def test_handle_probe_connect_timeout_sends_concise_connection_notification(strategy_module, monkeypatch):
Expand Down