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
37 changes: 24 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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**
Expand All @@ -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:

Expand Down Expand Up @@ -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. |
Expand Down Expand Up @@ -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=<telegram-chat-id>
NOTIFY_LANG=zh
```
Expand Down Expand Up @@ -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 \
Expand All @@ -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 \
Expand All @@ -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 启用状态、部署和凭据说明。

Expand Down Expand Up @@ -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 直接使用。 |
Expand Down Expand Up @@ -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 \
Expand All @@ -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 \
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand Down
10 changes: 9 additions & 1 deletion strategy_registry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}),
Expand Down
75 changes: 20 additions & 55 deletions tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -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"] == "<required>"
assert plan["set_env"]["IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH"] == "<required>"
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():
Expand Down
Loading