diff --git a/README.md b/README.md index 034985a..f17469d 100644 --- a/README.md +++ b/README.md @@ -39,10 +39,7 @@ The mainline runtime now follows one path only: - `tqqq_growth_income` - `soxl_soxx_trend_income` - `tech_communication_pullback_enhancement` -- `mega_cap_leader_rotation_aggressive` -- `mega_cap_leader_rotation_dynamic_top20` - `mega_cap_leader_rotation_top50_balanced` -- `dynamic_mega_leveraged_pullback` **IBKR profile status** @@ -54,10 +51,10 @@ The mainline runtime now follows one path only: | `tqqq_growth_income` | TQQQ Growth Income | Yes | Yes | `us_equity` | enabled value-mode alternative | | `soxl_soxx_trend_income` | SOXL/SOXX Semiconductor Trend Income | Yes | Yes | `us_equity` | current IBKR live line | | `tech_communication_pullback_enhancement` | Tech/Communication Pullback Enhancement | Yes | Yes | `us_equity` | enabled feature-snapshot alternative | -| `mega_cap_leader_rotation_aggressive` | Mega Cap Leader Rotation Aggressive | Yes | Yes | `us_equity` | enabled aggressive leader rotation | -| `mega_cap_leader_rotation_dynamic_top20` | Mega Cap Leader Rotation Dynamic Top20 | Yes | Yes | `us_equity` | enabled concentrated leader rotation | | `mega_cap_leader_rotation_top50_balanced` | Mega Cap Leader Rotation Top50 Balanced | Yes | Yes | `us_equity` | enabled balanced Top50 leader rotation | -| `dynamic_mega_leveraged_pullback` | Dynamic Mega Leveraged Pullback | Yes | Yes | `us_equity` | enabled 2x mega-cap pullback line | +| `mega_cap_leader_rotation_aggressive` | Mega Cap Leader Rotation Aggressive | Yes | No | `us_equity` | research-only archive | +| `mega_cap_leader_rotation_dynamic_top20` | Mega Cap Leader Rotation Dynamic Top20 | Yes | No | `us_equity` | research-only archive | +| `dynamic_mega_leveraged_pullback` | Dynamic Mega Leveraged Pullback | Yes | No | `us_equity` | research-only archive | Check the current matrix locally: @@ -103,9 +100,12 @@ 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`, `mega_cap_leader_rotation_top50_balanced`, `dynamic_mega_leveraged_pullback` | +| `IBKR_CONNECT_ATTEMPTS` | No | Number of IBKR connection attempts before failing the cycle. Defaults to `3`. | +| `IBKR_CONNECT_RETRY_DELAY_SECONDS` | No | Delay between failed IBKR connection attempts. Defaults to `5`. | +| `IBKR_CLIENT_ID_RETRY_OFFSET` | No | Offset added to the configured `ib_client_id` on each retry, so a timed-out API handshake can retry with a fresh client id. Defaults to `100`. | +| `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_top50_balanced` | | `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`, `mega_cap_leader_rotation_dynamic_top20`, and `mega_cap_leader_rotation_top50_balanced`. Path to the latest feature snapshot file (`.csv`, `.json`, `.jsonl`, `.parquet`). | +| `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_top50_balanced`. Path to the latest feature snapshot file (`.csv`, `.json`, `.jsonl`, `.parquet`). | | `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Yes for Cloud Run | Secret Manager secret name for account-group config JSON. Recommended production source. | | `IB_ACCOUNT_GROUP_CONFIG_JSON` | No | Local/dev JSON fallback for account-group config. Not recommended for production Cloud Run. | | `TELEGRAM_TOKEN` | Yes | Telegram bot token. For Cloud Run, prefer a Secret Manager reference instead of a literal env var. | @@ -165,11 +165,11 @@ NOTIFY_LANG=zh ``` ```bash -STRATEGY_PROFILE=mega_cap_leader_rotation_dynamic_top20 +STRATEGY_PROFILE=mega_cap_leader_rotation_top50_balanced ACCOUNT_GROUP=default IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME=ibkr-account-groups -IBKR_FEATURE_SNAPSHOT_PATH=/var/data/mega_cap_leader_rotation_dynamic_top20_feature_snapshot_latest.csv -IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH=/var/manifests/mega_cap_leader_rotation_dynamic_top20_feature_snapshot_latest.csv.manifest.json +IBKR_FEATURE_SNAPSHOT_PATH=/var/data/mega_cap_leader_rotation_top50_balanced_feature_snapshot_latest.csv +IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH=/var/manifests/mega_cap_leader_rotation_top50_balanced_feature_snapshot_latest.csv.manifest.json GLOBAL_TELEGRAM_CHAT_ID= NOTIFY_LANG=zh ``` @@ -269,6 +269,8 @@ gcloud run deploy interactive-brokers-quant-service \ --source . \ --region us-central1 \ --service-account ibkr-platform-runtime@PROJECT_ID.iam.gserviceaccount.com \ + --concurrency 1 \ + --max-instances 1 \ --network default \ --subnet cloudrun-direct-egress \ --vpc-egress private-ranges-only \ @@ -280,6 +282,8 @@ If the service already exists and your CI only updates source/image, you can pat ```bash gcloud run services update ibkr-quant \ --region us-central1 \ + --concurrency 1 \ + --max-instances 1 \ --network default \ --subnet cloudrun-direct-egress \ --vpc-egress private-ranges-only \ @@ -293,7 +297,7 @@ gcloud run services update ibkr-quant \ IBKR runtime 负责把共享的 `us_equity` 策略档位部署到 GCP Cloud Run,并连接 GCE 上的 IB Gateway 执行。策略逻辑、策略频率、标的池、参数和研究/回测说明都放在 `UsEquityStrategies`;这个仓库只维护 IBKR 运行时、账号组、Gateway 连接、下单和通知。 -当前 `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`、`mega_cap_leader_rotation_top50_balanced` 和 `dynamic_mega_leveraged_pullback` 的策略实现都来自 `UsEquityStrategies`。 +当前可运行的 `global_etf_rotation`、`russell_1000_multi_factor_defensive`、`tqqq_growth_income`、`soxl_soxx_trend_income`、`tech_communication_pullback_enhancement` 和 `mega_cap_leader_rotation_top50_balanced` 的策略实现都来自 `UsEquityStrategies`;`mega_cap_leader_rotation_aggressive`、`mega_cap_leader_rotation_dynamic_top20`、`dynamic_mega_leveraged_pullback` 保留为 research-only 存档,不进入 IBKR rollout。 完整策略说明现在放在 [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies)。这个 README 只保留 IBKR 运行时、profile 启用状态、部署和凭据说明。 @@ -335,7 +339,10 @@ 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`、`mega_cap_leader_rotation_top50_balanced`、`dynamic_mega_leveraged_pullback` | +| `IBKR_CONNECT_ATTEMPTS` | 否 | IBKR 连接失败前最多尝试次数。默认 `3`。 | +| `IBKR_CONNECT_RETRY_DELAY_SECONDS` | 否 | IBKR 连接重试间隔,单位秒。默认 `5`。 | +| `IBKR_CLIENT_ID_RETRY_OFFSET` | 否 | 每次重试时加到 `ib_client_id` 上的偏移量,用新的 client id 避开超时握手留下的卡住会话。默认 `100`。 | +| `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_top50_balanced` | | `ACCOUNT_GROUP` | 是 | 账号组选择器,每个部署都要显式设置。 | | `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Cloud Run 建议必填 | 账号组配置 JSON 在 Secret Manager 里的密钥名。生产环境推荐使用。 | | `IB_ACCOUNT_GROUP_CONFIG_JSON` | 否 | 本地开发用的账号组配置 JSON fallback。不建议在生产 Cloud Run 直接使用。 | @@ -467,6 +474,8 @@ gcloud run deploy interactive-brokers-quant-service \ --source . \ --region us-central1 \ --service-account ibkr-platform-runtime@PROJECT_ID.iam.gserviceaccount.com \ + --concurrency 1 \ + --max-instances 1 \ --network default \ --subnet cloudrun-direct-egress \ --vpc-egress private-ranges-only \ @@ -478,6 +487,8 @@ gcloud run deploy interactive-brokers-quant-service \ ```bash gcloud run services update ibkr-quant \ --region us-central1 \ + --concurrency 1 \ + --max-instances 1 \ --network default \ --subnet cloudrun-direct-egress \ --vpc-egress private-ranges-only \ diff --git a/configs/paper_mega_cap_leader_rotation_dynamic_top20.env.example b/configs/paper_mega_cap_leader_rotation_dynamic_top20.env.example index 2cdf3c0..66a7b91 100644 --- a/configs/paper_mega_cap_leader_rotation_dynamic_top20.env.example +++ b/configs/paper_mega_cap_leader_rotation_dynamic_top20.env.example @@ -1,4 +1,6 @@ -# IBKR paper runtime switch: mega_cap_leader_rotation_dynamic_top20 +# Archived research-only reference. +# mega_cap_leader_rotation_dynamic_top20 is no longer accepted by IBKR rollout +# unless UsEquityStrategies promotes it back to runtime_enabled. STRATEGY_PROFILE=mega_cap_leader_rotation_dynamic_top20 ACCOUNT_GROUP=default IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME=ibkr-account-groups diff --git a/strategy_registry.py b/strategy_registry.py index aecd955..4ebaf66 100644 --- a/strategy_registry.py +++ b/strategy_registry.py @@ -22,7 +22,15 @@ IBKR_PLATFORM = "ibkr" -IBKR_ROLLOUT_ALLOWLIST = get_runtime_enabled_profiles() +RESEARCH_ONLY_ARCHIVED_PROFILES = frozenset( + { + "dynamic_mega_leveraged_pullback", + "mega_cap_leader_rotation_aggressive", + "mega_cap_leader_rotation_dynamic_top20", + } +) + +IBKR_ROLLOUT_ALLOWLIST = get_runtime_enabled_profiles() - RESEARCH_ONLY_ARCHIVED_PROFILES PLATFORM_SUPPORTED_DOMAINS: dict[str, frozenset[str]] = { IBKR_PLATFORM: frozenset({US_EQUITY_DOMAIN}), diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index 10a87a6..fbc12ce 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -154,9 +154,6 @@ def test_platform_supported_profiles_are_filtered_by_registry(): "tqqq_growth_income", "tech_communication_pullback_enhancement", "global_etf_rotation", - "dynamic_mega_leveraged_pullback", - "mega_cap_leader_rotation_aggressive", - "mega_cap_leader_rotation_dynamic_top20", "mega_cap_leader_rotation_top50_balanced", "russell_1000_multi_factor_defensive", } @@ -192,38 +189,23 @@ def test_load_platform_runtime_settings_accepts_tech_communication_pullback_enha assert settings.strategy_target_mode == "weight" -def test_load_platform_runtime_settings_accepts_mega_cap_leader_rotation_dynamic_top20(monkeypatch): - monkeypatch.setenv("STRATEGY_PROFILE", "mega_cap_leader_rotation_dynamic_top20") - monkeypatch.setenv("ACCOUNT_GROUP", "default") - monkeypatch.setenv("IB_ACCOUNT_GROUP_CONFIG_JSON", MINIMAL_GROUP_JSON) - monkeypatch.setenv("IBKR_FEATURE_SNAPSHOT_PATH", "/tmp/mega.csv") - monkeypatch.setenv("IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH", "/tmp/mega.csv.manifest.json") - - settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") - - assert settings.strategy_profile == "mega_cap_leader_rotation_dynamic_top20" - assert settings.strategy_display_name == "Mega Cap Leader Rotation Dynamic Top20" - assert settings.strategy_target_mode == "weight" - assert settings.feature_snapshot_path == "/tmp/mega.csv" - assert settings.feature_snapshot_manifest_path == "/tmp/mega.csv.manifest.json" - assert settings.strategy_config_path is None - - -def test_load_platform_runtime_settings_accepts_dynamic_mega_leveraged_pullback(monkeypatch): - monkeypatch.setenv("STRATEGY_PROFILE", "dynamic_mega_leveraged_pullback") +@pytest.mark.parametrize( + "archived_profile", + ( + "mega_cap_leader_rotation_dynamic_top20", + "mega_cap_leader_rotation_aggressive", + "dynamic_mega_leveraged_pullback", + ), +) +def test_load_platform_runtime_settings_rejects_research_only_archived_profiles(monkeypatch, archived_profile): + monkeypatch.setenv("STRATEGY_PROFILE", archived_profile) monkeypatch.setenv("ACCOUNT_GROUP", "default") monkeypatch.setenv("IB_ACCOUNT_GROUP_CONFIG_JSON", MINIMAL_GROUP_JSON) - monkeypatch.setenv("IBKR_FEATURE_SNAPSHOT_PATH", "/tmp/dynamic-mega.csv") - monkeypatch.setenv("IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH", "/tmp/dynamic-mega.csv.manifest.json") + monkeypatch.setenv("IBKR_FEATURE_SNAPSHOT_PATH", "/tmp/archive.csv") + monkeypatch.setenv("IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH", "/tmp/archive.csv.manifest.json") - settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") - - assert settings.strategy_profile == "dynamic_mega_leveraged_pullback" - assert settings.strategy_display_name == "Dynamic Mega Leveraged Pullback" - assert settings.strategy_target_mode == "weight" - assert settings.feature_snapshot_path == "/tmp/dynamic-mega.csv" - assert settings.feature_snapshot_manifest_path == "/tmp/dynamic-mega.csv.manifest.json" - assert settings.strategy_config_path is None + with pytest.raises(ValueError, match="Unsupported STRATEGY_PROFILE"): + load_platform_runtime_settings(project_id_resolver=lambda: "project-1") def test_load_platform_runtime_settings_accepts_tqqq_growth_income(monkeypatch): @@ -319,21 +301,13 @@ def test_print_strategy_profile_status_json_matches_registry(): assert by_profile["tech_communication_pullback_enhancement"]["input_mode"] == "feature_snapshot" assert by_profile["tech_communication_pullback_enhancement"]["requires_snapshot_artifacts"] is True assert by_profile["tech_communication_pullback_enhancement"]["requires_strategy_config_path"] is True - assert by_profile["mega_cap_leader_rotation_dynamic_top20"]["profile_group"] == "snapshot_backed" - assert by_profile["mega_cap_leader_rotation_dynamic_top20"]["input_mode"] == "feature_snapshot" - assert by_profile["mega_cap_leader_rotation_dynamic_top20"]["requires_snapshot_artifacts"] is True - assert by_profile["mega_cap_leader_rotation_dynamic_top20"]["requires_strategy_config_path"] is False assert by_profile["mega_cap_leader_rotation_top50_balanced"]["profile_group"] == "snapshot_backed" assert by_profile["mega_cap_leader_rotation_top50_balanced"]["input_mode"] == "feature_snapshot" assert by_profile["mega_cap_leader_rotation_top50_balanced"]["requires_snapshot_artifacts"] is True assert by_profile["mega_cap_leader_rotation_top50_balanced"]["requires_strategy_config_path"] is False - assert by_profile["dynamic_mega_leveraged_pullback"]["profile_group"] == "snapshot_backed" - assert ( - by_profile["dynamic_mega_leveraged_pullback"]["input_mode"] - == "feature_snapshot+market_history+benchmark_history+portfolio_snapshot" - ) - assert by_profile["dynamic_mega_leveraged_pullback"]["requires_snapshot_artifacts"] is True - assert by_profile["dynamic_mega_leveraged_pullback"]["requires_strategy_config_path"] is False + assert by_profile["mega_cap_leader_rotation_dynamic_top20"]["enabled"] is False + assert by_profile["mega_cap_leader_rotation_aggressive"]["enabled"] is False + assert by_profile["dynamic_mega_leveraged_pullback"]["enabled"] is False assert by_profile["russell_1000_multi_factor_defensive"]["requires_strategy_config_path"] is False @@ -377,24 +351,15 @@ def test_print_strategy_switch_env_plan_for_tqqq_growth_income(): assert "IBKR_FEATURE_SNAPSHOT_PATH" in plan["remove_if_present"] -def test_print_strategy_switch_env_plan_for_mega_cap_feature_snapshot_profile(): +def test_print_strategy_switch_env_plan_rejects_research_only_archived_profile(): result = subprocess.run( [sys.executable, str(SWITCH_PLAN_SCRIPT_PATH), "--profile", "mega_cap_leader_rotation_dynamic_top20", "--json"], - check=True, capture_output=True, text=True, ) - plan = json.loads(result.stdout) - assert plan["canonical_profile"] == "mega_cap_leader_rotation_dynamic_top20" - assert plan["profile_group"] == "snapshot_backed" - assert plan["input_mode"] == "feature_snapshot" - assert plan["requires_snapshot_artifacts"] is True - assert plan["requires_strategy_config_path"] is False - assert plan["set_env"]["IBKR_FEATURE_SNAPSHOT_PATH"] == "" - assert plan["set_env"]["IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH"] == "" - assert "IBKR_STRATEGY_CONFIG_PATH" in plan["remove_if_present"] - assert "IBKR_RECONCILIATION_OUTPUT_PATH" in plan["remove_if_present"] + assert result.returncode != 0 + assert "Unsupported STRATEGY_PROFILE" in result.stderr def test_print_strategy_switch_env_plan_for_mega_cap_top50_balanced_profile(): diff --git a/tests/test_strategy_loader.py b/tests/test_strategy_loader.py index 32b7c93..0c7993a 100644 --- a/tests/test_strategy_loader.py +++ b/tests/test_strategy_loader.py @@ -55,7 +55,7 @@ def test_load_strategy_entrypoint_for_profile_resolves_tqqq_growth_income(monkey assert entrypoint.manifest.default_config["attack_allocation_mode"] == "fixed_qqq_tqqq_pullback" -def test_load_strategy_entrypoint_for_profile_resolves_mega_cap_dynamic_top20(monkeypatch): +def test_load_strategy_entrypoint_for_profile_rejects_archived_mega_cap_dynamic_top20(monkeypatch): try: import pandas # noqa: F401 except ModuleNotFoundError: @@ -65,11 +65,8 @@ def test_load_strategy_entrypoint_for_profile_resolves_mega_cap_dynamic_top20(mo market_calendars_module.get_calendar = lambda name: None monkeypatch.setitem(sys.modules, "pandas_market_calendars", market_calendars_module) - entrypoint = load_strategy_entrypoint_for_profile("mega_cap_leader_rotation_dynamic_top20") - - assert entrypoint.manifest.profile == "mega_cap_leader_rotation_dynamic_top20" - assert entrypoint.manifest.required_inputs == frozenset({"feature_snapshot"}) - assert entrypoint.manifest.default_config["holdings_count"] == 4 + with pytest.raises(ValueError, match="Unsupported STRATEGY_PROFILE"): + load_strategy_entrypoint_for_profile("mega_cap_leader_rotation_dynamic_top20") def test_load_strategy_entrypoint_for_profile_rejects_legacy_cash_buffer_profile(monkeypatch): @@ -104,7 +101,7 @@ def test_load_strategy_runtime_adapter_for_profile_resolves_tech_communication_p assert adapter.snapshot_contract_version == "tech_communication_pullback_enhancement.feature_snapshot.v1" -def test_load_strategy_runtime_adapter_for_profile_resolves_mega_cap_dynamic_top20(monkeypatch): +def test_load_strategy_runtime_adapter_for_profile_rejects_archived_mega_cap_dynamic_top20(monkeypatch): try: import pandas # noqa: F401 except ModuleNotFoundError: @@ -114,12 +111,8 @@ def test_load_strategy_runtime_adapter_for_profile_resolves_mega_cap_dynamic_top market_calendars_module.get_calendar = lambda name: None monkeypatch.setitem(sys.modules, "pandas_market_calendars", market_calendars_module) - adapter = load_strategy_runtime_adapter_for_profile("mega_cap_leader_rotation_dynamic_top20") - - assert adapter.status_icon == "👑" - assert adapter.available_inputs == frozenset({"feature_snapshot"}) - assert adapter.require_snapshot_manifest is True - assert adapter.snapshot_contract_version == "mega_cap_leader_rotation_dynamic_top20.feature_snapshot.v1" + with pytest.raises(ValueError, match="Unsupported STRATEGY_PROFILE"): + load_strategy_runtime_adapter_for_profile("mega_cap_leader_rotation_dynamic_top20") def test_load_strategy_runtime_adapter_for_profile_resolves_global_etf_rotation_inputs(monkeypatch):