From 28a2a3f88039736c78e4fe61876e8c9b3657c22f Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 17 Apr 2026 05:33:26 +0800 Subject: [PATCH] Increase IBKR API handshake timeout --- README.md | 2 ++ main.py | 34 ++++++++++++++++++++++++++++++++-- tests/test_event_loop.py | 18 +++++++++++++++--- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index ead685d..7e938e2 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ The selected `ACCOUNT_GROUP` is now the runtime identity. Keep broker-specific i |----------|----------|-------------| | `IB_GATEWAY_ZONE` | Optional fallback | GCE zone (for example `us-central1-a`). Recommended to keep in the selected account-group entry; this env var is only a transition fallback. | | `IB_GATEWAY_IP_MODE` | Optional fallback | `internal` (default) or `external`. Recommended to keep in the selected account-group entry; this env var is only a transition fallback. | +| `IBKR_CONNECT_TIMEOUT_SECONDS` | No | IB API handshake timeout in seconds. Defaults to `60`; raise only if Gateway remote API startup is consistently slow. | | `STRATEGY_PROFILE` | Yes | Strategy profile selector. Supported `us_equity` values: `global_etf_rotation`, `russell_1000_multi_factor_defensive`, `tqqq_growth_income`, `soxl_soxx_trend_income`, `tech_communication_pullback_enhancement`, `mega_cap_leader_rotation_aggressive`, `mega_cap_leader_rotation_dynamic_top20`, `dynamic_mega_leveraged_pullback` | | `ACCOUNT_GROUP` | Yes | Account-group selector. Set explicitly for each deployment. | | `IBKR_FEATURE_SNAPSHOT_PATH` | Conditionally required | Required for snapshot-backed profiles such as `russell_1000_multi_factor_defensive`, `tech_communication_pullback_enhancement`, and `mega_cap_leader_rotation_dynamic_top20`. Path to the latest feature snapshot file (`.csv`, `.json`, `.jsonl`, `.parquet`). | @@ -331,6 +332,7 @@ IBKR 账户 |------|------|------| | `IB_GATEWAY_ZONE` | 可选过渡项 | GCE zone(如 `us-central1-a`)。推荐直接放进选中的账号组配置里;这里只保留过渡 fallback。 | | `IB_GATEWAY_IP_MODE` | 可选过渡项 | `internal`(默认)或 `external`。推荐直接放进选中的账号组配置里;这里只保留过渡 fallback。 | +| `IBKR_CONNECT_TIMEOUT_SECONDS` | 否 | IB API 握手超时时间,单位秒。默认 `60`;只有 Gateway 远程 API 启动持续偏慢时才需要调高。 | | `STRATEGY_PROFILE` | 是 | 策略档位选择。当前可用的 `us_equity` 值:`global_etf_rotation`、`russell_1000_multi_factor_defensive`、`tqqq_growth_income`、`soxl_soxx_trend_income`、`tech_communication_pullback_enhancement`、`mega_cap_leader_rotation_aggressive`、`mega_cap_leader_rotation_dynamic_top20`、`dynamic_mega_leveraged_pullback` | | `ACCOUNT_GROUP` | 是 | 账号组选择器,每个部署都要显式设置。 | | `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Cloud Run 建议必填 | 账号组配置 JSON 在 Secret Manager 里的密钥名。生产环境推荐使用。 | diff --git a/main.py b/main.py index 64da826..97a1d75 100644 --- a/main.py +++ b/main.py @@ -139,6 +139,25 @@ def get_ib_port(): return 4002 if mode == "paper" else 4001 +def get_ib_connect_timeout_seconds(): + raw_value = os.getenv("IBKR_CONNECT_TIMEOUT_SECONDS", "60") + try: + timeout_seconds = int(raw_value) + except (TypeError, ValueError): + print( + f"Invalid IBKR_CONNECT_TIMEOUT_SECONDS={raw_value!r}; using 60", + flush=True, + ) + return 60 + if timeout_seconds <= 0: + print( + f"Invalid IBKR_CONNECT_TIMEOUT_SECONDS={raw_value!r}; using 60", + flush=True, + ) + return 60 + return timeout_seconds + + # --------------------------------------------------------------------------- # Config # --------------------------------------------------------------------------- @@ -146,6 +165,7 @@ def get_ib_port(): IB_HOST = None IB_PORT = get_ib_port() IB_CLIENT_ID = RUNTIME_SETTINGS.ib_client_id +IB_CONNECT_TIMEOUT_SECONDS = get_ib_connect_timeout_seconds() STRATEGY_PROFILE = RUNTIME_SETTINGS.strategy_profile STRATEGY_DISPLAY_NAME = RUNTIME_SETTINGS.strategy_display_name ACCOUNT_GROUP = RUNTIME_SETTINGS.account_group @@ -245,10 +265,19 @@ def send_tg_message(message): def connect_ib(): host = get_ib_host() print( - f"Connecting to IB gateway {host}:{IB_PORT} (mode={RUNTIME_SETTINGS.ib_gateway_mode}, client_id={IB_CLIENT_ID})", + "Connecting to IB gateway " + f"{host}:{IB_PORT} " + f"(mode={RUNTIME_SETTINGS.ib_gateway_mode}, " + f"client_id={IB_CLIENT_ID}, " + f"timeout={IB_CONNECT_TIMEOUT_SECONDS}s)", flush=True, ) - return ibkr_connect_ib(host, IB_PORT, IB_CLIENT_ID) + return ibkr_connect_ib( + host, + IB_PORT, + IB_CLIENT_ID, + timeout=IB_CONNECT_TIMEOUT_SECONDS, + ) def log_runtime_event(log_context, event, **fields): @@ -297,6 +326,7 @@ def build_execution_report(log_context): "ib_gateway_mode": RUNTIME_SETTINGS.ib_gateway_mode, "ib_gateway_ip_mode": RUNTIME_SETTINGS.ib_gateway_ip_mode, "ib_client_id": IB_CLIENT_ID, + "ib_connect_timeout_seconds": IB_CONNECT_TIMEOUT_SECONDS, }, artifacts={ "feature_snapshot_path": FEATURE_SNAPSHOT_PATH, diff --git a/tests/test_event_loop.py b/tests/test_event_loop.py index 3672a1f..a88919e 100644 --- a/tests/test_event_loop.py +++ b/tests/test_event_loop.py @@ -24,8 +24,8 @@ def worker(): def test_connect_ib_prepares_event_loop_before_connect(strategy_module, monkeypatch): observed = {} - def fake_ibkr_connect(host, port, client_id): - observed["args"] = (host, port, client_id) + def fake_ibkr_connect(host, port, client_id, **kwargs): + observed["args"] = (host, port, client_id, kwargs) return object() monkeypatch.setattr(strategy_module, "ibkr_connect_ib", fake_ibkr_connect) @@ -33,7 +33,19 @@ def fake_ibkr_connect(host, port, client_id): with ThreadPoolExecutor(max_workers=1) as executor: executor.submit(strategy_module.connect_ib).result() - assert observed["args"] == ("127.0.0.1", 4001, 1) + assert observed["args"] == ("127.0.0.1", 4001, 1, {"timeout": 60}) + + +def test_ib_connect_timeout_can_be_overridden(strategy_module_factory): + module = strategy_module_factory(IBKR_CONNECT_TIMEOUT_SECONDS="75") + + assert module.IB_CONNECT_TIMEOUT_SECONDS == 75 + + +def test_ib_connect_timeout_falls_back_when_invalid(strategy_module_factory): + module = strategy_module_factory(IBKR_CONNECT_TIMEOUT_SECONDS="bad") + + assert module.IB_CONNECT_TIMEOUT_SECONDS == 60 def test_instance_name_alias_is_used_as_host(strategy_module):