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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`). |
Expand Down Expand Up @@ -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 里的密钥名。生产环境推荐使用。 |
Expand Down
34 changes: 32 additions & 2 deletions main.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,33 @@ 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
# ---------------------------------------------------------------------------
RUNTIME_SETTINGS = load_platform_runtime_settings(project_id_resolver=get_project_id)
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
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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,
Expand Down
18 changes: 15 additions & 3 deletions tests/test_event_loop.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,28 @@ 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)

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):
Expand Down