Skip to content

Commit 0e14989

Browse files
committed
Enable mega cap dynamic top20 profile
1 parent bfa503e commit 0e14989

7 files changed

Lines changed: 90 additions & 7 deletions

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

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -77,10 +77,14 @@ jobs:
7777
missing_vars+=("LONGPORT_APP_SECRET_SECRET_NAME")
7878
fi
7979
80-
if { [ "${STRATEGY_PROFILE:-}" = "tech_communication_pullback_enhancement" ] || [ "${STRATEGY_PROFILE:-}" = "qqq_tech_enhancement" ]; } && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_PATH:-}" ]; then
80+
if { [ "${STRATEGY_PROFILE:-}" = "tech_communication_pullback_enhancement" ] || [ "${STRATEGY_PROFILE:-}" = "qqq_tech_enhancement" ] || [ "${STRATEGY_PROFILE:-}" = "mega_cap_leader_rotation_dynamic_top20" ]; } && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_PATH:-}" ]; then
8181
missing_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_PATH")
8282
fi
8383
84+
if [ "${STRATEGY_PROFILE:-}" = "mega_cap_leader_rotation_dynamic_top20" ] && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH:-}" ]; then
85+
missing_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH")
86+
fi
87+
8488
if [ "${#missing_vars[@]}" -gt 0 ]; then
8589
echo "enabled=false" >> "$GITHUB_OUTPUT"
8690
echo "HK Cloud Run env sync is enabled, but these values are missing:" >&2
@@ -279,10 +283,14 @@ jobs:
279283
missing_vars+=("LONGPORT_APP_SECRET_SECRET_NAME")
280284
fi
281285
282-
if { [ "${STRATEGY_PROFILE:-}" = "tech_communication_pullback_enhancement" ] || [ "${STRATEGY_PROFILE:-}" = "qqq_tech_enhancement" ]; } && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_PATH:-}" ]; then
286+
if { [ "${STRATEGY_PROFILE:-}" = "tech_communication_pullback_enhancement" ] || [ "${STRATEGY_PROFILE:-}" = "qqq_tech_enhancement" ] || [ "${STRATEGY_PROFILE:-}" = "mega_cap_leader_rotation_dynamic_top20" ]; } && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_PATH:-}" ]; then
283287
missing_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_PATH")
284288
fi
285289
290+
if [ "${STRATEGY_PROFILE:-}" = "mega_cap_leader_rotation_dynamic_top20" ] && [ -z "${LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH:-}" ]; then
291+
missing_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH")
292+
fi
293+
286294
if [ "${#missing_vars[@]}" -gt 0 ]; then
287295
echo "enabled=false" >> "$GITHUB_OUTPUT"
288296
echo "SG Cloud Run env sync is enabled, but these values are missing:" >&2

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
Quant system on LongPort OpenAPI and Google Cloud Run.
1111

1212
This repository uses `QuantPlatformKit` for LongPort token handling, context bootstrap, account snapshot access, market data, and order submission. Cloud Run deploys this repository directly.
13-
The LongBridge runtime can execute all five live `us_equity` profiles from `UsEquityStrategies`; `LongBridgePlatform` keeps the LongPort runtime, token refresh, execution, and notification flow.
13+
The LongBridge runtime can execute all six live `us_equity` profiles from `UsEquityStrategies`; `LongBridgePlatform` keeps the LongPort runtime, token refresh, execution, and notification flow.
1414

1515
Full strategy documentation now lives in [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies). The sections below focus on LongBridge runtime behavior, profile enablement, deployment, and credentials.
1616
This runtime matrix is the authoritative enablement source for LongBridge. `UsEquityStrategies` carries strategy-layer logic, cadence, compatibility, and metadata.
@@ -33,6 +33,7 @@ Platform execution no longer depends on `strategy/allocation.py` or hard-coded s
3333
| --- | --- | --- | --- | --- | --- | --- | --- |
3434
| `global_etf_rotation` | Global ETF Rotation | Yes | Yes | No | No | `us_equity` | enabled weight-mode rotation line |
3535
| `russell_1000_multi_factor_defensive` | Russell 1000 Multi-Factor | Yes | Yes | No | No | `us_equity` | enabled feature-snapshot stock baseline |
36+
| `mega_cap_leader_rotation_dynamic_top20` | Mega Cap Leader Rotation Dynamic Top20 | Yes | Yes | No | No | `us_equity` | selectable monthly feature-snapshot leader rotation |
3637
| `soxl_soxx_trend_income` | SOXL/SOXX Semiconductor Trend Income | Yes | Yes | Yes | Yes | `us_equity` | current LongBridge default |
3738
| `tqqq_growth_income` | TQQQ Growth Income | Yes | Yes | No | No | `us_equity` | current SG dry-run line |
3839
| `tech_communication_pullback_enhancement` | Tech/Communication Pullback Enhancement | Yes | Yes | No | No | `us_equity` | current HK feature-snapshot line |
@@ -61,7 +62,7 @@ Telegram notifications include structured execution and heartbeat messages, with
6162
| `LONGPORT_APP_SECRET` | Yes | LongPort OpenAPI app secret (for token refresh); recommended to inject from the region-specific Secret Manager secret for this deployment, such as `longport-app-secret-hk` / `longport-app-secret-sg` |
6263
| `LONGPORT_SECRET_NAME` | No | Secret Manager secret name for LongPort token (default: `longport_token_hk`) |
6364
| `ACCOUNT_PREFIX` | No | Alert/log prefix for account/environment (default: `DEFAULT`) |
64-
| `STRATEGY_PROFILE` | No | Strategy profile selector (default: `soxl_soxx_trend_income`; enabled values include `global_etf_rotation`, `russell_1000_multi_factor_defensive`, `tech_communication_pullback_enhancement` on HK, and `tqqq_growth_income` on SG) |
65+
| `STRATEGY_PROFILE` | No | Strategy profile selector (default: `soxl_soxx_trend_income`; enabled values include `global_etf_rotation`, `mega_cap_leader_rotation_dynamic_top20`, `russell_1000_multi_factor_defensive`, `soxl_soxx_trend_income`, `tech_communication_pullback_enhancement`, and `tqqq_growth_income`) |
6566
| `ACCOUNT_REGION` | No | Account region marker for platform-style deployment (e.g. `HK`, `SG`; defaults to `ACCOUNT_PREFIX` / `DEFAULT`) |
6667
| `LONGBRIDGE_DRY_RUN_ONLY` | No | Set to `true` to keep the selected deployment in dry-run mode. |
6768
| `NOTIFY_LANG` | No | Notification language: `en` (English, default) or `zh` (Chinese) |
@@ -153,7 +154,7 @@ IAM: the Cloud Run service account needs **Secret Manager Admin** (or Secret Acc
153154
基于 LongPort OpenAPI 和 Google Cloud Run 的量化交易系统。
154155

155156
这个仓库通过 `QuantPlatformKit` 复用 LongPort token 处理、上下文初始化、账户快照、行情读取和下单逻辑。Cloud Run 直接部署这个仓库。
156-
`LongBridgePlatform` 现在可直接执行 `UsEquityStrategies` 里的全部 5 条 live `us_equity` 策略:`global_etf_rotation``russell_1000_multi_factor_defensive``soxl_soxx_trend_income``tqqq_growth_income``tech_communication_pullback_enhancement`;仓库本身继续保留 LongPort 运行时、token 刷新、执行和通知流程。
157+
`LongBridgePlatform` 现在可直接执行 `UsEquityStrategies` 里的全部 6 条 live `us_equity` 策略:`global_etf_rotation``mega_cap_leader_rotation_dynamic_top20``russell_1000_multi_factor_defensive``soxl_soxx_trend_income``tqqq_growth_income``tech_communication_pullback_enhancement`;仓库本身继续保留 LongPort 运行时、token 刷新、执行和通知流程。
157158

158159
完整策略说明现在放在 [`UsEquityStrategies`](https://github.com/QuantStrategyLab/UsEquityStrategies)。下面这些章节只保留 LongBridge 运行时、profile 启用状态、部署和凭据说明。
159160

@@ -174,6 +175,7 @@ IAM: the Cloud Run service account needs **Secret Manager Admin** (or Secret Acc
174175
| --- | --- | --- | --- | --- | --- | --- | --- |
175176
| `global_etf_rotation` | Global ETF Rotation | Yes | Yes | No | No | `us_equity` | 已启用的 weight-mode 轮动线 |
176177
| `russell_1000_multi_factor_defensive` | Russell 1000 Multi-Factor | Yes | Yes | No | No | `us_equity` | 已启用的 feature-snapshot 个股基线 |
178+
| `mega_cap_leader_rotation_dynamic_top20` | Mega Cap Leader Rotation Dynamic Top20 | Yes | Yes | No | No | `us_equity` | 可选的月度 feature-snapshot 龙头轮动线 |
177179
| `soxl_soxx_trend_income` | SOXL/SOXX 半导体趋势收益 | Yes | Yes | Yes | Yes | `us_equity` | 当前 LongBridge 默认回退线 |
178180
| `tqqq_growth_income` | TQQQ 增长收益 | Yes | Yes | No | No | `us_equity` | 当前 SG dry-run 线路 |
179181
| `tech_communication_pullback_enhancement` | 科技通信回调增强 | Yes | Yes | No | No | `us_equity` | 当前 HK feature-snapshot 线路 |
@@ -202,7 +204,7 @@ Telegram 通知包含结构化的调仓和心跳消息,支持中英文切换
202204
| `LONGPORT_APP_SECRET` || LongPort OpenAPI 应用密钥(用于刷新 Token);建议从当前部署对应区域的 Secret Manager 密钥注入,例如 `longport-app-secret-hk` / `longport-app-secret-sg` |
203205
| `LONGPORT_SECRET_NAME` || Secret Manager 中的密钥名称(默认: `longport_token_hk`|
204206
| `ACCOUNT_PREFIX` || 通知/日志前缀,区分账户环境(默认: `DEFAULT`|
205-
| `STRATEGY_PROFILE` || 策略档位选择(默认: `soxl_soxx_trend_income`;已启用值还包括 `global_etf_rotation``russell_1000_multi_factor_defensive`,当前线上 HK 用 `tech_communication_pullback_enhancement`,SG 用 `tqqq_growth_income`|
207+
| `STRATEGY_PROFILE` || 策略档位选择(默认: `soxl_soxx_trend_income`;已启用值还包括 `global_etf_rotation``mega_cap_leader_rotation_dynamic_top20``russell_1000_multi_factor_defensive``soxl_soxx_trend_income``tech_communication_pullback_enhancement` `tqqq_growth_income`|
206208
| `ACCOUNT_REGION` || 平台化部署时的账户区域标记(如 `HK``SG`;默认按 `ACCOUNT_PREFIX` / `DEFAULT` 推断) |
207209
| `LONGBRIDGE_DRY_RUN_ONLY` || 设为 `true` 时,该部署保持 dry-run。 |
208210
| `NOTIFY_LANG` || 通知语言: `en`(英文,默认)或 `zh`(中文) |

requirements.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
flask
22
gunicorn
33
quant-platform-kit @ git+https://github.com/QuantStrategyLab/QuantPlatformKit.git@v0.7.15
4-
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@v0.7.21
4+
us-equity-strategies @ git+https://github.com/QuantStrategyLab/UsEquityStrategies.git@v0.7.22
55
pandas
66
requests
77
pytz

strategy_registry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
LONGBRIDGE_ROLLOUT_ALLOWLIST = frozenset(
2626
{
2727
"global_etf_rotation",
28+
"mega_cap_leader_rotation_dynamic_top20",
2829
"russell_1000_multi_factor_defensive",
2930
"soxl_soxx_trend_income",
3031
"tqqq_growth_income",

tests/test_runtime_config_support.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def test_platform_supported_profiles_are_filtered_by_registry(self):
5858
frozenset(
5959
{
6060
"global_etf_rotation",
61+
"mega_cap_leader_rotation_dynamic_top20",
6162
"russell_1000_multi_factor_defensive",
6263
"tqqq_growth_income",
6364
"soxl_soxx_trend_income",
@@ -72,6 +73,7 @@ def test_platform_eligible_profiles_are_exposed_by_capability_matrix(self):
7273
frozenset(
7374
{
7475
"global_etf_rotation",
76+
"mega_cap_leader_rotation_dynamic_top20",
7577
"russell_1000_multi_factor_defensive",
7678
"tqqq_growth_income",
7779
"soxl_soxx_trend_income",
@@ -131,6 +133,7 @@ def test_platform_profile_status_matrix_matches_current_longbridge_rollout(self)
131133
set(by_profile),
132134
{
133135
"global_etf_rotation",
136+
"mega_cap_leader_rotation_dynamic_top20",
134137
"russell_1000_multi_factor_defensive",
135138
"tqqq_growth_income",
136139
"soxl_soxx_trend_income",
@@ -163,6 +166,9 @@ def test_platform_profile_status_matrix_matches_current_longbridge_rollout(self)
163166
self.assertTrue(by_profile["tech_communication_pullback_enhancement"]["eligible"])
164167
self.assertTrue(by_profile["tech_communication_pullback_enhancement"]["enabled"])
165168
self.assertEqual(by_profile["tech_communication_pullback_enhancement"]["display_name"], "Tech/Communication Pullback Enhancement")
169+
self.assertTrue(by_profile["mega_cap_leader_rotation_dynamic_top20"]["eligible"])
170+
self.assertTrue(by_profile["mega_cap_leader_rotation_dynamic_top20"]["enabled"])
171+
self.assertEqual(by_profile["mega_cap_leader_rotation_dynamic_top20"]["display_name"], "Mega Cap Leader Rotation Dynamic Top20")
166172

167173
def test_loads_feature_snapshot_env_for_tech_profile(self):
168174
with patch.dict(
@@ -220,6 +226,10 @@ def test_print_strategy_profile_status_json_matches_registry(self):
220226
self.assertEqual(by_profile["tech_communication_pullback_enhancement"]["input_mode"], "feature_snapshot")
221227
self.assertTrue(by_profile["tech_communication_pullback_enhancement"]["requires_snapshot_artifacts"])
222228
self.assertTrue(by_profile["tech_communication_pullback_enhancement"]["requires_strategy_config_path"])
229+
self.assertEqual(by_profile["mega_cap_leader_rotation_dynamic_top20"]["profile_group"], "snapshot_backed")
230+
self.assertEqual(by_profile["mega_cap_leader_rotation_dynamic_top20"]["input_mode"], "feature_snapshot")
231+
self.assertTrue(by_profile["mega_cap_leader_rotation_dynamic_top20"]["requires_snapshot_artifacts"])
232+
self.assertFalse(by_profile["mega_cap_leader_rotation_dynamic_top20"]["requires_strategy_config_path"])
223233
self.assertFalse(
224234
by_profile["russell_1000_multi_factor_defensive"]["requires_strategy_config_path"]
225235
)
@@ -239,6 +249,7 @@ def test_print_strategy_profile_status_table_contains_expected_headers(self):
239249
self.assertIn("requires_snapshot_artifacts", result.stdout)
240250
self.assertIn("soxl_soxx_trend_income", result.stdout)
241251
self.assertIn("global_etf_rotation", result.stdout)
252+
self.assertIn("mega_cap_leader_rotation_dynamic_top20", result.stdout)
242253
self.assertIn("russell_1000_multi_factor_defensive", result.stdout)
243254
self.assertIn("Global ETF Rotation", result.stdout)
244255
self.assertIn("Russell 1000 Multi-Factor", result.stdout)
@@ -303,5 +314,38 @@ def test_print_strategy_switch_env_plan_for_russell(self):
303314
self.assertIn("LONGBRIDGE_STRATEGY_CONFIG_PATH", plan["remove_if_present"])
304315

305316

317+
def test_print_strategy_switch_env_plan_for_mega_cap_dynamic_top20(self):
318+
result = subprocess.run(
319+
[
320+
sys.executable,
321+
str(SWITCH_PLAN_SCRIPT_PATH),
322+
"--profile",
323+
"mega_cap_leader_rotation_dynamic_top20",
324+
"--account-region",
325+
"hk",
326+
"--json",
327+
],
328+
check=True,
329+
capture_output=True,
330+
text=True,
331+
)
332+
333+
plan = json.loads(result.stdout)
334+
self.assertEqual(plan["canonical_profile"], "mega_cap_leader_rotation_dynamic_top20")
335+
self.assertEqual(plan["set_env"]["ACCOUNT_REGION"], "HK")
336+
self.assertEqual(plan["set_env"]["ACCOUNT_PREFIX"], "HK")
337+
self.assertEqual(plan["profile_group"], "snapshot_backed")
338+
self.assertEqual(plan["input_mode"], "feature_snapshot")
339+
self.assertTrue(plan["requires_snapshot_artifacts"])
340+
self.assertFalse(plan["requires_strategy_config_path"])
341+
self.assertEqual(plan["set_env"]["LONGBRIDGE_FEATURE_SNAPSHOT_PATH"], "<required>")
342+
self.assertEqual(plan["set_env"]["LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH"], "<required>")
343+
self.assertIn("LONGBRIDGE_STRATEGY_CONFIG_PATH", plan["remove_if_present"])
344+
self.assertEqual(
345+
plan["hints"]["feature_snapshot_filename"],
346+
"mega_cap_leader_rotation_dynamic_top20_feature_snapshot_latest.csv",
347+
)
348+
349+
306350
if __name__ == "__main__":
307351
unittest.main()

tests/test_strategy_loader.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,19 @@ def test_load_strategy_entrypoint_resolves_soxl_soxx_trend_income(self):
4444
("SOXL", "SOXX", "BOXX", "QQQI", "SPYI"),
4545
)
4646

47+
def test_load_strategy_entrypoint_resolves_mega_cap_dynamic_top20(self):
48+
try:
49+
from strategy_loader import load_strategy_entrypoint_for_profile
50+
51+
entrypoint = load_strategy_entrypoint_for_profile("mega_cap_leader_rotation_dynamic_top20")
52+
except ModuleNotFoundError as exc:
53+
if exc.name in {"numpy", "pandas"}:
54+
self.skipTest(f"{exc.name} is not installed")
55+
raise
56+
57+
self.assertEqual(entrypoint.manifest.profile, "mega_cap_leader_rotation_dynamic_top20")
58+
self.assertEqual(entrypoint.manifest.required_inputs, frozenset({"feature_snapshot"}))
59+
4760
def test_load_strategy_entrypoint_rejects_legacy_soxl_alias(self):
4861
try:
4962
from strategy_loader import load_strategy_entrypoint_for_profile
@@ -89,6 +102,19 @@ def test_load_strategy_runtime_adapter_declares_russell_inputs(self):
89102
self.assertEqual(adapter.portfolio_input_name, "portfolio_snapshot")
90103
self.assertEqual(adapter.status_icon, "📏")
91104

105+
def test_load_strategy_runtime_adapter_declares_mega_cap_inputs(self):
106+
from strategy_loader import load_strategy_runtime_adapter_for_profile
107+
108+
adapter = load_strategy_runtime_adapter_for_profile("mega_cap_leader_rotation_dynamic_top20")
109+
110+
self.assertEqual(
111+
adapter.available_inputs,
112+
frozenset({"feature_snapshot", "portfolio_snapshot"}),
113+
)
114+
self.assertEqual(adapter.portfolio_input_name, "portfolio_snapshot")
115+
self.assertTrue(adapter.require_snapshot_manifest)
116+
self.assertEqual(adapter.status_icon, "👑")
117+
92118
def test_load_strategy_runtime_adapter_declares_hybrid_inputs(self):
93119
from strategy_loader import load_strategy_runtime_adapter_for_profile
94120

tests/test_sync_cloud_run_env_workflow.sh

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ grep -Fq 'missing_vars+=("TELEGRAM_TOKEN_SECRET_NAME or TELEGRAM_TOKEN")' "$work
4444
grep -Fq 'missing_vars+=("LONGPORT_APP_KEY_SECRET_NAME")' "$workflow_file"
4545
grep -Fq 'missing_vars+=("LONGPORT_APP_SECRET_SECRET_NAME")' "$workflow_file"
4646
grep -Fq 'missing_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_PATH")' "$workflow_file"
47+
grep -Fq 'missing_vars+=("LONGBRIDGE_FEATURE_SNAPSHOT_MANIFEST_PATH")' "$workflow_file"
48+
grep -Fq '[ "${STRATEGY_PROFILE:-}" = "mega_cap_leader_rotation_dynamic_top20" ]' "$workflow_file"
4749
grep -Fq 'secret_pairs+=("TELEGRAM_TOKEN=${TELEGRAM_TOKEN_SECRET_NAME}:latest")' "$workflow_file"
4850
grep -Fq 'secret_pairs+=("LONGPORT_APP_KEY=${LONGPORT_APP_KEY_SECRET_NAME}:latest")' "$workflow_file"
4951
grep -Fq 'secret_pairs+=("LONGPORT_APP_SECRET=${LONGPORT_APP_SECRET_SECRET_NAME}:latest")' "$workflow_file"

0 commit comments

Comments
 (0)