Skip to content

Commit ca6cae8

Browse files
committed
Adopt canonical US equity profile keys
1 parent 95afeee commit ca6cae8

10 files changed

Lines changed: 51 additions & 41 deletions

.github/workflows/sync-cloud-run-env.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,11 +57,11 @@ jobs:
5757
NOTIFY_LANG
5858
)
5959
60-
if [ "${STRATEGY_PROFILE:-}" = "russell_1000_multi_factor_defensive" ] || [ "${STRATEGY_PROFILE:-}" = "tech_pullback_cash_buffer" ]; then
60+
if [ "${STRATEGY_PROFILE:-}" = "russell_1000_multi_factor_defensive" ] || [ "${STRATEGY_PROFILE:-}" = "qqq_tech_enhancement" ]; then
6161
required_vars+=(IBKR_FEATURE_SNAPSHOT_PATH)
6262
fi
6363
64-
if [ "${STRATEGY_PROFILE:-}" = "tech_pullback_cash_buffer" ]; then
64+
if [ "${STRATEGY_PROFILE:-}" = "qqq_tech_enhancement" ]; then
6565
required_vars+=(IBKR_FEATURE_SNAPSHOT_MANIFEST_PATH IBKR_STRATEGY_CONFIG_PATH IBKR_RECONCILIATION_OUTPUT_PATH)
6666
fi
6767

README.md

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ IBKR runtime for shared `us_equity` strategy profiles from `UsEquityStrategies`.
1616

1717
- `global_etf_rotation` (`Global ETF Rotation`): quarterly ETF momentum rotation with daily canary defense
1818
- `russell_1000_multi_factor_defensive` (`Russell 1000 Multi-Factor`): monthly stock-selection strategy that consumes a precomputed feature snapshot
19-
- `tech_pullback_cash_buffer` (`QQQ Tech Enhancement`): monthly tech-heavy stock-selection branch with explicit BOXX cash buffer
19+
- `qqq_tech_enhancement` (`QQQ Tech Enhancement`, alias: `tech_pullback_cash_buffer`): monthly tech-heavy stock-selection branch with explicit BOXX cash buffer
2020

2121
Current strategy implementations are sourced from `UsEquityStrategies`.
2222

@@ -40,8 +40,8 @@ The mainline runtime now follows one path only:
4040

4141
- `global_etf_rotation`
4242
- `russell_1000_multi_factor_defensive`
43-
- `semiconductor_rotation_income`
44-
- `tech_pullback_cash_buffer`
43+
- `soxl_soxx_trend_income` (alias: `semiconductor_rotation_income`)
44+
- `qqq_tech_enhancement` (alias: `tech_pullback_cash_buffer`)
4545

4646

4747
**IBKR profile status**
@@ -50,8 +50,8 @@ The mainline runtime now follows one path only:
5050
| --- | --- | --- | --- | --- | --- | --- | --- |
5151
| `global_etf_rotation` | Global ETF Rotation | Yes | Yes | Yes | Yes | `us_equity` | current rollback line |
5252
| `russell_1000_multi_factor_defensive` | Russell 1000 Multi-Factor | Yes | Yes | No | No | `us_equity` | defensive stock baseline |
53-
| `semiconductor_rotation_income` | SOXL/SOXX Semiconductor Trend Income | Yes | Yes | No | No | `us_equity` | current IBKR dry-run line |
54-
| `tech_pullback_cash_buffer` | QQQ Tech Enhancement | Yes | Yes | No | No | `us_equity` | enabled feature-snapshot alternative |
53+
| `soxl_soxx_trend_income` | SOXL/SOXX Semiconductor Trend Income | Yes | Yes | No | No | `us_equity` | current IBKR dry-run line |
54+
| `qqq_tech_enhancement` | QQQ Tech Enhancement | Yes | Yes | No | No | `us_equity` | enabled feature-snapshot alternative |
5555

5656
Check the current matrix locally:
5757

@@ -173,7 +173,7 @@ The selected `ACCOUNT_GROUP` is now the runtime identity. Keep broker-specific i
173173
|----------|----------|-------------|
174174
| `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. |
175175
| `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. |
176-
| `STRATEGY_PROFILE` | Yes | Strategy profile selector. Supported `us_equity` values: `global_etf_rotation`, `russell_1000_multi_factor_defensive`, `semiconductor_rotation_income`, `tech_pullback_cash_buffer` |
176+
| `STRATEGY_PROFILE` | Yes | Strategy profile selector. Supported `us_equity` values: `global_etf_rotation`, `russell_1000_multi_factor_defensive`, `soxl_soxx_trend_income`, `qqq_tech_enhancement` |
177177
| `ACCOUNT_GROUP` | Yes | Account-group selector. No default fallback. |
178178
| `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`). |
179179
| `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Yes for Cloud Run | Secret Manager secret name for account-group config JSON. Recommended production source. |
@@ -223,7 +223,7 @@ NOTIFY_LANG=zh
223223
```
224224

225225
```bash
226-
STRATEGY_PROFILE=tech_pullback_cash_buffer
226+
STRATEGY_PROFILE=qqq_tech_enhancement
227227
ACCOUNT_GROUP=default
228228
IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME=ibkr-account-groups
229229
IBKR_FEATURE_SNAPSHOT_PATH=/var/data/tech_pullback_cash_buffer_feature_snapshot_latest.csv
@@ -352,7 +352,7 @@ gcloud run services update ibkr-quant \
352352

353353
基于 IBKR 的全球 ETF 季度轮动策略(国际市场、商品、美股行业、美股宽基、科技和半导体),含每日金丝雀应急机制。定位上比 `TQQQ``SOXL` 这类高弹性科技策略更稳健,但不再把科技完全排除在外。部署在 GCP Cloud Run,连接 GCE 上的 IB Gateway。
354354

355-
当前 `global_etf_rotation``russell_1000_multi_factor_defensive``semiconductor_rotation_income``tech_pullback_cash_buffer` 的策略实现都来自 `UsEquityStrategies`
355+
当前 `global_etf_rotation``russell_1000_multi_factor_defensive``soxl_soxx_trend_income``qqq_tech_enhancement` 的策略实现都来自 `UsEquityStrategies`
356356

357357
完整策略说明现在放在 [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies#global_etf_rotation)。下面的策略章节主要保留执行侧摘要。
358358

@@ -422,7 +422,7 @@ IBKR 账户
422422
|------|------|------|
423423
| `IB_GATEWAY_ZONE` | 可选过渡项 | GCE zone(如 `us-central1-a`)。推荐直接放进选中的账号组配置里;这里只保留过渡 fallback。 |
424424
| `IB_GATEWAY_IP_MODE` | 可选过渡项 | `internal`(默认)或 `external`。推荐直接放进选中的账号组配置里;这里只保留过渡 fallback。 |
425-
| `STRATEGY_PROFILE` || 策略档位选择。当前可用的 `us_equity` 值:`global_etf_rotation``russell_1000_multi_factor_defensive``semiconductor_rotation_income``tech_pullback_cash_buffer` |
425+
| `STRATEGY_PROFILE` || 策略档位选择。当前可用的 `us_equity` 值:`global_etf_rotation``russell_1000_multi_factor_defensive``soxl_soxx_trend_income``qqq_tech_enhancement` |
426426
| `ACCOUNT_GROUP` || 账号组选择器,不再提供默认回退。 |
427427
| `IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME` | Cloud Run 建议必填 | 账号组配置 JSON 在 Secret Manager 里的密钥名。生产环境推荐使用。 |
428428
| `IB_ACCOUNT_GROUP_CONFIG_JSON` || 本地开发用的账号组配置 JSON fallback。不建议在生产 Cloud Run 直接使用。 |
@@ -448,7 +448,7 @@ IBKR 账户
448448
当前第一步,建议让 GitHub / Cloud Run 只维护服务级变量:
449449

450450
```bash
451-
STRATEGY_PROFILE=semiconductor_rotation_income
451+
STRATEGY_PROFILE=soxl_soxx_trend_income
452452
ACCOUNT_GROUP=default
453453
IB_ACCOUNT_GROUP_CONFIG_SECRET_NAME=ibkr-account-groups
454454
GLOBAL_TELEGRAM_CHAT_ID=<telegram-chat-id>

decision_mapper.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
from collections.abc import Mapping
44
from typing import Any
55

6+
from us_equity_strategies.catalog import resolve_canonical_profile
7+
68
from quant_platform_kit.strategy_contracts import (
79
StrategyDecision,
810
build_allocation_intent,
@@ -16,7 +18,8 @@
1618

1719

1820
def _resolve_allocation_order(strategy_profile: str) -> str:
19-
if strategy_profile == "semiconductor_rotation_income":
21+
canonical_profile = resolve_canonical_profile(strategy_profile)
22+
if canonical_profile == "soxl_soxx_trend_income":
2023
return "risk_income_safe"
2124
return "risk_safe_income"
2225

@@ -116,6 +119,7 @@ def map_strategy_decision(
116119
runtime_metadata: Mapping[str, Any] | None = None,
117120
) -> tuple[dict[str, float] | None, str, bool, str, dict[str, Any]]:
118121
runtime_metadata = dict(runtime_metadata or {})
122+
canonical_profile = resolve_canonical_profile(strategy_profile)
119123
diagnostics = dict(decision.diagnostics)
120124
risk_flags = tuple(str(flag) for flag in decision.risk_flags)
121125
no_execute = bool(_NO_EXECUTE_FLAGS & set(risk_flags))
@@ -126,16 +130,16 @@ def map_strategy_decision(
126130
allocation_payload = build_allocation_payload(
127131
build_allocation_intent(
128132
normalized_decision,
129-
strategy_profile=strategy_profile,
130-
strategy_symbols_order=_resolve_allocation_order(strategy_profile),
133+
strategy_profile=canonical_profile,
134+
strategy_symbols_order=_resolve_allocation_order(canonical_profile),
131135
)
132136
)
133137
signal_desc = _derive_signal_description(decision, runtime_metadata)
134138
status_desc = _derive_status_description(decision, runtime_metadata)
135139
is_emergency = bool(_EMERGENCY_FLAGS & set(risk_flags))
136140

137141
metadata: dict[str, Any] = {**runtime_metadata, **diagnostics}
138-
metadata.setdefault("strategy_profile", strategy_profile)
142+
metadata.setdefault("strategy_profile", canonical_profile)
139143
metadata.setdefault("status_icon", "🐤")
140144
metadata.setdefault(
141145
"managed_symbols",

notifications/telegram.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,8 +62,11 @@
6262
"strategy_name_global_etf_rotation": "全球 ETF 轮动",
6363
"strategy_name_russell_1000_multi_factor_defensive": "罗素1000多因子",
6464
"strategy_name_tech_pullback_cash_buffer": "QQQ 科技增强",
65+
"strategy_name_qqq_tech_enhancement": "QQQ 科技增强",
6566
"strategy_name_hybrid_growth_income": "TQQQ 增长收益",
67+
"strategy_name_tqqq_growth_income": "TQQQ 增长收益",
6668
"strategy_name_semiconductor_rotation_income": "SOXL/SOXX 半导体趋势收益",
69+
"strategy_name_soxl_soxx_trend_income": "SOXL/SOXX 半导体趋势收益",
6770
},
6871
"en": {
6972
"rebalance_title": "🔔 【Trade Execution Report】",
@@ -123,8 +126,11 @@
123126
"strategy_name_global_etf_rotation": "Global ETF Rotation",
124127
"strategy_name_russell_1000_multi_factor_defensive": "Russell 1000 Multi-Factor",
125128
"strategy_name_tech_pullback_cash_buffer": "QQQ Tech Enhancement",
129+
"strategy_name_qqq_tech_enhancement": "QQQ Tech Enhancement",
126130
"strategy_name_hybrid_growth_income": "TQQQ Growth Income",
131+
"strategy_name_tqqq_growth_income": "TQQQ Growth Income",
127132
"strategy_name_semiconductor_rotation_income": "SOXL/SOXX Semiconductor Trend Income",
133+
"strategy_name_soxl_soxx_trend_income": "SOXL/SOXX Semiconductor Trend Income",
128134
},
129135
}
130136

requirements.txt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
3-
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@v0.7.9
4-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@v0.7.12
3+
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@v0.7.10
4+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@v0.7.13
55
pandas
66
numpy
77
requests

strategy_registry.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,10 @@
2323

2424
IBKR_ROLLOUT_ALLOWLIST = frozenset(
2525
{
26-
"tech_pullback_cash_buffer",
26+
"qqq_tech_enhancement",
2727
"global_etf_rotation",
2828
"russell_1000_multi_factor_defensive",
29-
"semiconductor_rotation_income",
29+
"soxl_soxx_trend_income",
3030
}
3131
)
3232

tests/test_decision_mapper.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ def test_map_strategy_decision_maps_weight_positions_and_safe_haven():
1717

1818
target_weights, signal_desc, is_emergency, status_desc, metadata = map_strategy_decision(
1919
decision,
20-
strategy_profile="tech_pullback_cash_buffer",
20+
strategy_profile="qqq_tech_enhancement",
2121
runtime_metadata={"status_icon": "🧲", "dry_run_only": True},
2222
)
2323

@@ -46,7 +46,7 @@ def test_map_strategy_decision_returns_noop_when_flagged_no_execute():
4646

4747
target_weights, signal_desc, is_emergency, status_desc, metadata = map_strategy_decision(
4848
decision,
49-
strategy_profile="tech_pullback_cash_buffer",
49+
strategy_profile="qqq_tech_enhancement",
5050
runtime_metadata={
5151
"status_icon": "🛑",
5252
"snapshot_guard_decision": "fail_closed",
@@ -80,7 +80,7 @@ def test_map_strategy_decision_translates_value_targets_for_semiconductor_strate
8080

8181
target_weights, signal_desc, is_emergency, status_desc, metadata = map_strategy_decision(
8282
decision,
83-
strategy_profile="semiconductor_rotation_income",
83+
strategy_profile="soxl_soxx_trend_income",
8484
runtime_metadata={
8585
"portfolio_total_equity": 50000.0,
8686
"status_icon": "🚀",

tests/test_runtime_config_support.py

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -149,8 +149,8 @@ def test_load_platform_runtime_settings_rejects_unknown_strategy_profile(monkeyp
149149
def test_platform_supported_profiles_are_filtered_by_registry():
150150
assert get_supported_profiles_for_platform(IBKR_PLATFORM) == frozenset(
151151
{
152-
"semiconductor_rotation_income",
153-
"tech_pullback_cash_buffer",
152+
"soxl_soxx_trend_income",
153+
"qqq_tech_enhancement",
154154
"global_etf_rotation",
155155
"russell_1000_multi_factor_defensive",
156156
}
@@ -160,8 +160,8 @@ def test_platform_supported_profiles_are_filtered_by_registry():
160160
def test_platform_eligible_profiles_are_exposed_by_capability_matrix():
161161
assert get_eligible_profiles_for_platform(IBKR_PLATFORM) == frozenset(
162162
{
163-
"semiconductor_rotation_income",
164-
"tech_pullback_cash_buffer",
163+
"soxl_soxx_trend_income",
164+
"qqq_tech_enhancement",
165165
"global_etf_rotation",
166166
"russell_1000_multi_factor_defensive",
167167
}
@@ -176,7 +176,7 @@ def test_load_platform_runtime_settings_accepts_tech_pullback_cash_buffer(monkey
176176

177177
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
178178

179-
assert settings.strategy_profile == "tech_pullback_cash_buffer"
179+
assert settings.strategy_profile == "qqq_tech_enhancement"
180180
assert settings.strategy_display_name == "QQQ Tech Enhancement"
181181
assert settings.strategy_target_mode == "weight"
182182

@@ -186,7 +186,7 @@ def test_platform_profile_matrix_marks_default_and_rollback():
186186
by_profile = {row["canonical_profile"]: row for row in rows}
187187
assert by_profile["global_etf_rotation"]["is_default"] is True
188188
assert by_profile["global_etf_rotation"]["is_rollback"] is True
189-
assert by_profile["tech_pullback_cash_buffer"]["display_name"] == "QQQ Tech Enhancement"
189+
assert by_profile["qqq_tech_enhancement"]["display_name"] == "QQQ Tech Enhancement"
190190

191191

192192
def test_platform_profile_status_matrix_matches_current_ibkr_rollout():
@@ -196,8 +196,8 @@ def test_platform_profile_status_matrix_matches_current_ibkr_rollout():
196196
assert set(by_profile) == {
197197
"global_etf_rotation",
198198
"russell_1000_multi_factor_defensive",
199-
"semiconductor_rotation_income",
200-
"tech_pullback_cash_buffer",
199+
"soxl_soxx_trend_income",
200+
"qqq_tech_enhancement",
201201
}
202202
assert by_profile["global_etf_rotation"] == {
203203
"canonical_profile": "global_etf_rotation",
@@ -209,9 +209,9 @@ def test_platform_profile_status_matrix_matches_current_ibkr_rollout():
209209
"is_rollback": True,
210210
"platform": "ibkr",
211211
}
212-
assert by_profile["semiconductor_rotation_income"]["display_name"] == "SOXL/SOXX Semiconductor Trend Income"
213-
assert by_profile["semiconductor_rotation_income"]["eligible"] is True
214-
assert by_profile["semiconductor_rotation_income"]["enabled"] is True
212+
assert by_profile["soxl_soxx_trend_income"]["display_name"] == "SOXL/SOXX Semiconductor Trend Income"
213+
assert by_profile["soxl_soxx_trend_income"]["eligible"] is True
214+
assert by_profile["soxl_soxx_trend_income"]["enabled"] is True
215215

216216

217217
def test_print_strategy_profile_status_json_matches_registry():
@@ -300,17 +300,17 @@ def test_load_platform_runtime_settings_derives_artifact_paths_from_root(monkeyp
300300
settings = load_platform_runtime_settings(project_id_resolver=lambda: "project-1")
301301

302302
assert settings.strategy_artifact_root == str(tmp_path)
303-
assert settings.strategy_artifact_dir == str(tmp_path / "tech_pullback_cash_buffer")
303+
assert settings.strategy_artifact_dir == str(tmp_path / "qqq_tech_enhancement")
304304
assert settings.feature_snapshot_path == str(
305-
tmp_path / "tech_pullback_cash_buffer" / "tech_pullback_cash_buffer_feature_snapshot_latest.csv"
305+
tmp_path / "qqq_tech_enhancement" / "qqq_tech_enhancement_feature_snapshot_latest.csv"
306306
)
307307
assert settings.feature_snapshot_manifest_path == str(
308308
tmp_path
309-
/ "tech_pullback_cash_buffer"
310-
/ "tech_pullback_cash_buffer_feature_snapshot_latest.csv.manifest.json"
309+
/ "qqq_tech_enhancement"
310+
/ "qqq_tech_enhancement_feature_snapshot_latest.csv.manifest.json"
311311
)
312312
assert settings.reconciliation_output_path == str(
313-
tmp_path / "tech_pullback_cash_buffer" / "reconciliation"
313+
tmp_path / "qqq_tech_enhancement" / "reconciliation"
314314
)
315315
assert settings.strategy_config_path is not None
316316
assert settings.strategy_config_path.endswith("growth_pullback_tech_pullback_cash_buffer.json")

tests/test_snapshot_strategy_runtime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ def test_compute_signals_loads_tech_pullback_cash_buffer_runtime(strategy_module
147147
result = module.compute_signals(None, {"AAPL"})
148148

149149
assert result[0]["BOXX"] == pytest.approx(0.2)
150-
assert result[4]["strategy_profile"] == "tech_pullback_cash_buffer"
150+
assert result[4]["strategy_profile"] == "qqq_tech_enhancement"
151151
assert result[4]["strategy_config_source"] in {"env", "external_config"}
152152
assert result[4]["realized_stock_weight"] == pytest.approx(0.8)
153153
assert result[4]["snapshot_guard_decision"] == "proceed"

tests/test_strategy_loader.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ def test_load_strategy_entrypoint_for_profile_resolves_tech_pullback_cash_buffer
3232

3333
entrypoint = load_strategy_entrypoint_for_profile("tech_pullback_cash_buffer")
3434

35-
assert entrypoint.manifest.profile == "tech_pullback_cash_buffer"
35+
assert entrypoint.manifest.profile == "qqq_tech_enhancement"
3636
assert entrypoint.manifest.default_config["safe_haven"] == "BOXX"
3737

3838

0 commit comments

Comments
 (0)