diff --git a/README.md b/README.md index c3bffb6..c20aa5d 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# IBKR Global ETF Rotation +# InteractiveBrokersPlatform ![Python](https://img.shields.io/badge/Python-3.9%2B-blue) ![Platform](https://img.shields.io/badge/Broker-Interactive%20Brokers-red) -![Strategy](https://img.shields.io/badge/Strategy-Global%20ETF%20Rotation-green) +![Strategy](https://img.shields.io/badge/Strategy-US%20Equity%20Profiles-green) ![GCP](https://img.shields.io/badge/GCP-Cloud%20Run%20%2B%20GCE-4285F4) [English](#english) | [中文](#中文) @@ -12,18 +12,38 @@ ## English -Quarterly momentum rotation across 22 global ETFs (international markets, commodities, US sectors, US broad market, tech, and semiconductors) with daily canary emergency check. Designed to stay more stable than high-beta tech strategies while still allowing major tech leadership to enter the rotation. Deployed on GCP Cloud Run, connecting to IB Gateway on GCE. +IBKR runtime for shared `us_equity` strategy profiles from `UsEquityStrategies`. Today it supports: -This runtime now supports multiple `us_equity` profiles sourced from `UsEquityStrategies`. +- `global_etf_rotation` (`Global ETF Rotation Defense`): quarterly ETF momentum rotation with daily canary defense +- `russell_1000_multi_factor_defensive` (`Russell 1000 Multi-Factor Defensive`): monthly stock-selection strategy that consumes a precomputed feature snapshot +- `cash_buffer_branch_default` (`Tech Pullback Cash Buffer`, alias `tech_pullback_cash_buffer`): monthly tech-heavy stock-selection branch with explicit BOXX cash buffer -Current runtime-facing profiles: -- `global_etf_rotation` — quarterly ETF rotation (current rollback line) -- `cash_buffer_branch_default` — monthly stock-selection branch with an explicit `80%` stock cap and `BOXX` parking +Current strategy implementations are sourced from `UsEquityStrategies`. -Full strategy documentation lives in `UsEquityStrategies`; the sections here stay focused on execution and deployment. +Full strategy documentation now lives in [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies). The strategy section below is kept as an execution-side summary. +This runtime matrix is the authoritative enablement source for IBKR. `UsEquityStrategies` only describes strategy-layer compatibility and human-readable metadata. ### Strategy +**Supported `STRATEGY_PROFILE` values** + +- `global_etf_rotation` +- `russell_1000_multi_factor_defensive` +- `cash_buffer_branch_default` + +Human-readable alias is available for config review and future migration: + +- `tech_pullback_cash_buffer` -> `cash_buffer_branch_default` + + +**IBKR profile matrix** + +| Canonical profile | Display name | Alias | Enabled | Default | Rollback | Domain | Runtime note | +| --- | --- | --- | --- | --- | --- | --- | --- | +| `global_etf_rotation` | Global ETF Rotation Defense | `global_macro_etf_rotation` | Yes | Yes | Yes | `us_equity` | current rollback line | +| `russell_1000_multi_factor_defensive` | Russell 1000 Multi-Factor Defensive | `r1000_multifactor_defensive` | Yes | No | No | `us_equity` | defensive stock baseline | +| `cash_buffer_branch_default` | Tech Pullback Cash Buffer | `tech_pullback_cash_buffer` | Yes | No | No | `us_equity` | current IBKR paper dry-run candidate | + **Pool (22 ETFs + 1 safe haven):** | Category | Tickers | @@ -55,6 +75,31 @@ Full strategy documentation lives in `UsEquityStrategies`; the sections here sta - 2023+ CAGR: 29.2% | Max Drawdown: 20.9% - Legacy non-tech baseline and prior `QQQ` default remain available in the research script for comparison +### Snapshot-based stock profile (`russell_1000_multi_factor_defensive`) + +- Signal source: precomputed feature snapshot file +- Default benchmark: `SPY` +- Default safe haven: `BOXX` +- Runtime expectation: + - the feature snapshot is produced upstream + - Cloud Run / local runtime reads the latest file from `IBKR_FEATURE_SNAPSHOT_PATH` +- Required snapshot columns: + - `symbol`, `sector`, `mom_6_1`, `mom_12_1`, `sma200_gap`, `vol_63`, `maxdd_126` + - optional passthrough columns such as `as_of`, `close`, `volume`, `adv20_usd`, `history_days`, `eligible` + +Recommended upstream task: + +```bash +PYTHONPATH=src:. python3 scripts/run_russell_1000_snapshot_task.py +``` + +Then point this runtime at the generated file: + +```bash +STRATEGY_PROFILE=russell_1000_multi_factor_defensive +IBKR_FEATURE_SNAPSHOT_PATH=/var/data/r1000_feature_snapshot.csv +``` + ### Architecture ``` @@ -113,18 +158,14 @@ 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. | -| `STRATEGY_PROFILE` | Yes | Strategy profile selector. Supported `us_equity` values: `global_etf_rotation`, `cash_buffer_branch_default` | +| `STRATEGY_PROFILE` | Yes | Strategy profile selector. Supported `us_equity` values: `global_etf_rotation`, `russell_1000_multi_factor_defensive`, `cash_buffer_branch_default` (alias: `tech_pullback_cash_buffer`) | | `ACCOUNT_GROUP` | Yes | Account-group selector. No default fallback. | +| `IBKR_FEATURE_SNAPSHOT_PATH` | Conditionally required | Required when `STRATEGY_PROFILE=russell_1000_multi_factor_defensive`. 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. | | `GLOBAL_TELEGRAM_CHAT_ID` | Yes | Telegram chat ID used by this service. | | `NOTIFY_LANG` | No | `en` (default) or `zh` | -| `IBKR_FEATURE_SNAPSHOT_PATH` | Required for snapshot-based profiles | Latest feature snapshot file path. Required for `cash_buffer_branch_default`. | -| `IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH` | Required for `cash_buffer_branch_default` | Sidecar manifest path used by runtime freshness / contract checks. | -| `IBKR_STRATEGY_CONFIG_PATH` | Required for `cash_buffer_branch_default` | Canonical runtime config path used for manifest/config matching. | -| `IBKR_RECONCILIATION_OUTPUT_PATH` | No | Optional structured reconciliation output path for dry-run / paper execution logs. | -| `IBKR_DRY_RUN_ONLY` | No | `true` to block order submission and only emit planned actions; `false` for real paper orders. | The selected account-group entry must provide at least: @@ -144,27 +185,34 @@ If you use instance-name resolution with `ib_gateway_zone`, the Cloud Run runtim For the current first rollout, keep GitHub / Cloud Run focused on service-level values: ```bash -# rollback / legacy ETF line STRATEGY_PROFILE=global_etf_rotation ACCOUNT_GROUP=default IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME=ibkr-account-groups GLOBAL_TELEGRAM_CHAT_ID= NOTIFY_LANG=zh -# optional transition fallback only: +# Optional transition fallback only: IB_GATEWAY_ZONE=us-central1-c IB_GATEWAY_IP_MODE=internal ``` +For the snapshot-based stock profiles: + +```bash +STRATEGY_PROFILE=russell_1000_multi_factor_defensive +ACCOUNT_GROUP=default +IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME=ibkr-account-groups +IBKR_FEATURE_SNAPSHOT_PATH=/var/data/r1000_feature_snapshot.csv +GLOBAL_TELEGRAM_CHAT_ID= +NOTIFY_LANG=zh +``` + ```bash -# snapshot-based stock paper branch STRATEGY_PROFILE=cash_buffer_branch_default ACCOUNT_GROUP=default IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME=ibkr-account-groups IBKR_FEATURE_SNAPSHOT_PATH=/var/data/cash_buffer_branch_feature_snapshot_latest.csv IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH=/var/data/cash_buffer_branch_feature_snapshot_latest.csv.manifest.json -IBKR_STRATEGY_CONFIG_PATH=/app/research/configs/growth_pullback_cash_buffer_branch_default.json -IBKR_RECONCILIATION_OUTPUT_PATH=/var/log/ibkr_cash_buffer_branch_reconciliation.json IBKR_DRY_RUN_ONLY=true GLOBAL_TELEGRAM_CHAT_ID= NOTIFY_LANG=zh @@ -219,7 +267,7 @@ Recommended setup: - `CLOUD_RUN_REGION` - `CLOUD_RUN_SERVICE` - `TELEGRAM_TOKEN_SECRET_NAME` (recommended when Cloud Run already uses Secret Manager for `TELEGRAM_TOKEN`) - - `STRATEGY_PROFILE` (recommended: `global_etf_rotation` for rollback, or `cash_buffer_branch_default` for the snapshot-based paper branch) + - `STRATEGY_PROFILE` (recommended: `global_etf_rotation`) - `ACCOUNT_GROUP` (recommended: `default`) - `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` - `GLOBAL_TELEGRAM_CHAT_ID` @@ -232,7 +280,7 @@ Recommended setup: On every push to `main`, the workflow updates the existing Cloud Run service with the values above and removes legacy env vars that should now live in the account-group config (`IB_CLIENT_ID`, `IB_GATEWAY_INSTANCE_NAME`, `IB_GATEWAY_MODE`) plus the older transport vars (`IB_GATEWAY_HOST`, `IB_GATEWAY_PORT`, `TELEGRAM_CHAT_ID`). If `IB_GATEWAY_ZONE` or `IB_GATEWAY_IP_MODE` are blank in GitHub, the workflow also removes them from Cloud Run to avoid drift. -`STRATEGY_PROFILE` now supports both `global_etf_rotation` and `cash_buffer_branch_default` under the shared `us_equity` domain. `ACCOUNT_GROUP` selects one account-group config entry, and the service still fails fast if that runtime identity is incomplete. +For now, `STRATEGY_PROFILE` still only supports one strategy profile. The current strategy domain is `us_equity`, and the repo now keeps a thin strategy registry so future expansion can grow by domain + profile instead of mixing strategy and platform in one layer. `ACCOUNT_GROUP` now selects one account-group config entry, and the service fails fast if that runtime identity is incomplete. Important: @@ -289,13 +337,9 @@ gcloud run services update ibkr-quant \ 基于 IBKR 的全球 ETF 季度轮动策略(国际市场、商品、美股行业、美股宽基、科技和半导体),含每日金丝雀应急机制。定位上比 `TQQQ`、`SOXL` 这类高弹性科技策略更稳健,但不再把科技完全排除在外。部署在 GCP Cloud Run,连接 GCE 上的 IB Gateway。 -当前这个 runtime 已经可以承载多个来自 `UsEquityStrategies` 的 `us_equity` profile。 - -当前运行侧最相关的两个 profile: -- `global_etf_rotation`:季度 ETF 轮动,也是当前 rollback 线 -- `cash_buffer_branch_default`:月频个股分支,`risk_on` 明确只上 `80%` 股票,其余停在 `BOXX` +当前 `global_etf_rotation`、`russell_1000_multi_factor_defensive` 和 `cash_buffer_branch_default` 的策略实现都来自 `UsEquityStrategies`。 -完整策略说明放在 `UsEquityStrategies`;这里主要保留执行和部署侧摘要。 +完整策略说明现在放在 [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies#global_etf_rotation)。下面的策略章节主要保留执行侧摘要。 ### 策略 @@ -352,18 +396,13 @@ IBKR 账户 |------|------|------| | `IB_GATEWAY_ZONE` | 可选过渡项 | GCE zone(如 `us-central1-a`)。推荐直接放进选中的账号组配置里;这里只保留过渡 fallback。 | | `IB_GATEWAY_IP_MODE` | 可选过渡项 | `internal`(默认)或 `external`。推荐直接放进选中的账号组配置里;这里只保留过渡 fallback。 | -| `STRATEGY_PROFILE` | 是 | 策略档位选择。当前支持的 `us_equity` 值:`global_etf_rotation`、`cash_buffer_branch_default` | +| `STRATEGY_PROFILE` | 是 | 策略档位选择。当前可用的 `us_equity` 值:`global_etf_rotation`、`russell_1000_multi_factor_defensive`、`cash_buffer_branch_default`(alias:`tech_pullback_cash_buffer`) | | `ACCOUNT_GROUP` | 是 | 账号组选择器,不再提供默认回退。 | | `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Cloud Run 建议必填 | 账号组配置 JSON 在 Secret Manager 里的密钥名。生产环境推荐使用。 | | `IB_ACCOUNT_GROUP_CONFIG_JSON` | 否 | 本地开发用的账号组配置 JSON fallback。不建议在生产 Cloud Run 直接使用。 | | `TELEGRAM_TOKEN` | 是 | Telegram 机器人 Token。Cloud Run 上更推荐走 Secret Manager 引用,不要直接写成明文 env。 | | `GLOBAL_TELEGRAM_CHAT_ID` | 是 | 这个服务使用的 Telegram Chat ID。 | | `NOTIFY_LANG` | 否 | `en`(默认)或 `zh` | -| `IBKR_FEATURE_SNAPSHOT_PATH` | snapshot 型策略必填 | 最新 feature snapshot 文件路径。`cash_buffer_branch_default` 必填。 | -| `IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH` | `cash_buffer_branch_default` 必填 | snapshot sidecar manifest 路径,用于 freshness / contract 检查。 | -| `IBKR_STRATEGY_CONFIG_PATH` | `cash_buffer_branch_default` 必填 | runtime 侧 canonical config 路径,用于 manifest/config 匹配。 | -| `IBKR_RECONCILIATION_OUTPUT_PATH` | 否 | dry-run / paper 执行后的结构化对账输出路径。 | -| `IBKR_DRY_RUN_ONLY` | 否 | `true` 时只输出计划动作不下单;`false` 时允许真正的 paper 下单。 | 选中的账号组配置里,至少要有: @@ -443,7 +482,7 @@ IB_GATEWAY_IP_MODE=internal - `CLOUD_RUN_REGION` - `CLOUD_RUN_SERVICE` - `TELEGRAM_TOKEN_SECRET_NAME`(如果 Cloud Run 上的 `TELEGRAM_TOKEN` 已经改成 Secret Manager,建议配置) - - `STRATEGY_PROFILE`(rollback 建议用 `global_etf_rotation`,snapshot 个股 paper 分支用 `cash_buffer_branch_default`) + - `STRATEGY_PROFILE`(建议设为 `global_etf_rotation`) - `ACCOUNT_GROUP`(建议设为 `default`) - `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` - `GLOBAL_TELEGRAM_CHAT_ID` @@ -456,7 +495,7 @@ IB_GATEWAY_IP_MODE=internal 每次 push 到 `main` 时,这个 workflow 会把上面这些值同步到现有 Cloud Run 服务里,并清掉已经转移到账号组配置里的旧 env(`IB_CLIENT_ID`、`IB_GATEWAY_INSTANCE_NAME`、`IB_GATEWAY_MODE`)以及更早的传输层 env(`IB_GATEWAY_HOST`、`IB_GATEWAY_PORT`、`TELEGRAM_CHAT_ID`)。如果 GitHub 里没有配置 `IB_GATEWAY_ZONE` 或 `IB_GATEWAY_IP_MODE`,workflow 也会把 Cloud Run 上这两个旧值一起删除,避免双配置源漂移。 -`STRATEGY_PROFILE` 现在支持 `global_etf_rotation` 和 `cash_buffer_branch_default` 两个 `us_equity` profile。`ACCOUNT_GROUP` 仍然是严格必填项,并会选中一份账号组配置;运行身份不完整时,服务会直接失败,不再静默回退。 +`STRATEGY_PROFILE` 当前只有一个可用值;当前策略域是 `us_equity`,本地策略注册表只用于域和 profile 校验。`ACCOUNT_GROUP` 是严格必填项,并会选中一份账号组配置。运行身份不完整时,服务会直接失败,不再静默回退。 注意: diff --git a/requirements.txt b/requirements.txt index f190f4d..08bbd42 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ flask gunicorn quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@v0.6.0 -us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@5c40d49927b7a86e27feee23b82da81338320404 +us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@17fa78c22463070b88ba4a884c84e7a761835ccb pandas numpy requests diff --git a/strategy_registry.py b/strategy_registry.py index 64fc203..40bf2a8 100644 --- a/strategy_registry.py +++ b/strategy_registry.py @@ -1,33 +1,48 @@ from __future__ import annotations -from us_equity_strategies import get_strategy_definitions as get_us_equity_strategy_definitions - -from quant_platform_kit.common.strategies import ( - StrategyDefinition, - US_EQUITY_DOMAIN, - get_supported_profiles_for_platform as qpk_get_supported_profiles_for_platform, - resolve_strategy_definition as qpk_resolve_strategy_definition, +from us_equity_strategies.platform_registry_support import ( + build_platform_profile_matrix, + get_enabled_profiles_for_platform, + resolve_platform_strategy_definition, ) -IBKR_PLATFORM = "ibkr" +from quant_platform_kit.common.strategies import StrategyDefinition, US_EQUITY_DOMAIN +IBKR_PLATFORM = "ibkr" DEFAULT_STRATEGY_PROFILE = "global_etf_rotation" - -STRATEGY_DEFINITIONS = get_us_equity_strategy_definitions() +ROLLBACK_STRATEGY_PROFILE = DEFAULT_STRATEGY_PROFILE + +# 平台启用状态从策略定义层拆出来;当前只让 IBKR 明确启用这三条。 +IBKR_ENABLED_PROFILES = frozenset( + { + "cash_buffer_branch_default", + "global_etf_rotation", + "russell_1000_multi_factor_defensive", + } +) PLATFORM_SUPPORTED_DOMAINS: dict[str, frozenset[str]] = { IBKR_PLATFORM: frozenset({US_EQUITY_DOMAIN}), } -SUPPORTED_STRATEGY_PROFILES = frozenset(STRATEGY_DEFINITIONS) +SUPPORTED_STRATEGY_PROFILES = IBKR_ENABLED_PROFILES def get_supported_profiles_for_platform(platform_id: str) -> frozenset[str]: - return qpk_get_supported_profiles_for_platform( - STRATEGY_DEFINITIONS, - PLATFORM_SUPPORTED_DOMAINS, - platform_id=platform_id, + return get_enabled_profiles_for_platform( + platform_id, + expected_platform_id=IBKR_PLATFORM, + enabled_profiles=IBKR_ENABLED_PROFILES, + ) + + +def get_platform_profile_matrix() -> list[dict[str, object]]: + return build_platform_profile_matrix( + platform_id=IBKR_PLATFORM, + enabled_profiles=IBKR_ENABLED_PROFILES, + default_profile=DEFAULT_STRATEGY_PROFILE, + rollback_profile=ROLLBACK_STRATEGY_PROFILE, ) @@ -36,10 +51,11 @@ def resolve_strategy_definition( *, platform_id: str, ) -> StrategyDefinition: - return qpk_resolve_strategy_definition( + return resolve_platform_strategy_definition( raw_value, platform_id=platform_id, - strategy_definitions=STRATEGY_DEFINITIONS, + expected_platform_id=IBKR_PLATFORM, + enabled_profiles=IBKR_ENABLED_PROFILES, platform_supported_domains=PLATFORM_SUPPORTED_DOMAINS, require_explicit=True, ) diff --git a/tests/test_runtime_config_support.py b/tests/test_runtime_config_support.py index 7a888ad..5abf14e 100644 --- a/tests/test_runtime_config_support.py +++ b/tests/test_runtime_config_support.py @@ -5,7 +5,7 @@ load_platform_runtime_settings, parse_account_group_configs, ) -from strategy_registry import IBKR_PLATFORM, US_EQUITY_DOMAIN, get_supported_profiles_for_platform +from strategy_registry import IBKR_PLATFORM, US_EQUITY_DOMAIN, get_platform_profile_matrix, get_supported_profiles_for_platform MINIMAL_GROUP_JSON = ( @@ -137,6 +137,26 @@ def test_platform_supported_profiles_are_filtered_by_registry(): ) +def test_load_platform_runtime_settings_accepts_cash_buffer_alias(monkeypatch): + monkeypatch.setenv("STRATEGY_PROFILE", "tech_pullback_cash_buffer") + monkeypatch.setenv("ACCOUNT_GROUP", "default") + monkeypatch.setenv("IB_ACCOUNT_GROUP_CONFIG_JSON", MINIMAL_GROUP_JSON) + monkeypatch.setenv("IBKR_FEATURE_SNAPSHOT_PATH", "/tmp/cash-buffer.csv") + + settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1") + + assert settings.strategy_profile == "cash_buffer_branch_default" + + +def test_platform_profile_matrix_marks_default_and_rollback(): + rows = get_platform_profile_matrix() + by_profile = {row["canonical_profile"]: row for row in rows} + assert by_profile["global_etf_rotation"]["is_default"] is True + assert by_profile["global_etf_rotation"]["is_rollback"] is True + assert by_profile["cash_buffer_branch_default"]["display_name"] == "Tech Pullback Cash Buffer" + + + def test_load_platform_runtime_settings_reads_feature_snapshot_path(monkeypatch): monkeypatch.setenv("STRATEGY_PROFILE", "russell_1000_multi_factor_defensive") monkeypatch.setenv("ACCOUNT_GROUP", "default") diff --git a/tests/test_strategy_loader.py b/tests/test_strategy_loader.py index bc36170..5213392 100644 --- a/tests/test_strategy_loader.py +++ b/tests/test_strategy_loader.py @@ -43,3 +43,20 @@ def test_load_signal_logic_module_resolves_cash_buffer_branch_default(monkeypatc assert module.__name__ == "us_equity_strategies.strategies.cash_buffer_branch_default" assert module.SIGNAL_SOURCE == "feature_snapshot" + + +def test_load_signal_logic_module_resolves_cash_buffer_alias(monkeypatch): + try: + import pandas # noqa: F401 + except ModuleNotFoundError: + return + + market_calendars_module = types.ModuleType("pandas_market_calendars") + market_calendars_module.get_calendar = lambda name: None + monkeypatch.setitem(sys.modules, "pandas_market_calendars", market_calendars_module) + sys.modules.pop("us_equity_strategies.strategies.cash_buffer_branch_default", None) + + module = load_signal_logic_module("tech_pullback_cash_buffer") + + assert module.__name__ == "us_equity_strategies.strategies.cash_buffer_branch_default" + assert module.SIGNAL_SOURCE == "feature_snapshot"