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
109 changes: 74 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -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) | [中文](#中文)
Expand All @@ -12,18 +12,38 @@
<a id="english"></a>
## 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 |
Expand Down Expand Up @@ -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

```
Expand Down Expand Up @@ -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:

Expand All @@ -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=<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=<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=<telegram-chat-id>
NOTIFY_LANG=zh
Expand Down Expand Up @@ -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`
Expand All @@ -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:

Expand Down Expand Up @@ -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)。下面的策略章节主要保留执行侧摘要

### 策略

Expand Down Expand Up @@ -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 下单。 |

选中的账号组配置里,至少要有:

Expand Down Expand Up @@ -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`
Expand All @@ -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` 是严格必填项,并会选中一份账号组配置运行身份不完整时,服务会直接失败,不再静默回退。

注意:

Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -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
Expand Down
50 changes: 33 additions & 17 deletions strategy_registry.py
Original file line number Diff line number Diff line change
@@ -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,
)


Expand All @@ -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,
)
22 changes: 21 additions & 1 deletion tests/test_runtime_config_support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down Expand Up @@ -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")
Expand Down
Loading