Skip to content

Commit 6bc822a

Browse files
Pigbibicursoragent
andcommitted
Add QMT dry-run console architecture and cash/margin mutual exclusion.
Wire QMT as the sole cn_equity platform with dry-run-only targets, industry ETF as the primary profile, and UI guards that block live execution until broker accounts exist. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 8e9d593 commit 6bc822a

13 files changed

Lines changed: 422 additions & 78 deletions
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
# QMT(A 股)Runtime Target 架构
2+
3+
当前阶段:**仅 dry-run,无 live 券商账号**。中信 / 国金等券商差异不在 `platform_id` 层建模,将来 live 时在 `account-options` 增加 target 项并在对应 miniQMT 部署侧区分即可。
4+
5+
## 分层
6+
7+
|| 仓库 / ID | 说明 |
8+
| --- | --- | --- |
9+
| 策略 | `CnEquityStrategies` / `cn_*` profile |`cn_equity` domain |
10+
| 平台 | `QmtPlatform` / `platform_id=qmt` | 统一 miniQMT 执行层,一个 Platform 仓 |
11+
| 运行配置 | `QuantRuntimeSettings` | target 示例、切换控制台、`manual-strategy-switch` |
12+
| 变量作用域 | `variable_scope=repository` | QMT 使用仓库级 GitHub Variables |
13+
14+
## Runtime-enabled 策略(切换页可选)
15+
16+
| Profile | 输入 | 示例 target |
17+
| --- | --- | --- |
18+
| `cn_industry_etf_rotation` | `market_history` | `industry_etf_dry_run.example.json`**主轨**|
19+
| `cn_dividend_quality_snapshot` | `feature_snapshot` | `dividend_quality_dry_run.example.json` |
20+
21+
`cn_index_etf_tactical_rotation` 在策略 catalog 中为 **research_backtest_only**,不要放进 `strategy-profiles.example.json` 的 runtime 列表。
22+
23+
## 账号路由(`account-options`
24+
25+
每个 **dry-run target** 对应控制台一条 QMT 账号项,字段对齐示例 JSON:
26+
27+
- `target_name`:与 `examples/targets/qmt/*.example.json` 的 target 名一致
28+
- `variable_scope``repository`
29+
- `deployment_selector` / `account_selector``qmt`
30+
- `account_scope``CN`
31+
- `service_name``qmt-quant-service`
32+
- `cash_currency``CNY`
33+
- `supported_domains``["cn_equity"]`
34+
- `default_strategy_profile`:与该 target 默认策略一致
35+
36+
**不要**在账号配置里放 miniQMT 密码、券商 token 或本机路径;fixture 路径走 QmtPlatform 仓库变量(如 `QMT_MARKET_HISTORY_PATH`)。
37+
38+
## 执行模式
39+
40+
- 控制台 QMT tab **锁定 paper(dry-run)**,Worker 拒绝 `execution_mode=live`
41+
- `QMT_DRY_RUN_ONLY=true` 为默认;live 上线前需单独评审并扩展 workflow / sync。
42+
43+
## 与美股平台的差异
44+
45+
- 美股:一个券商一个 Platform 仓(IBKR、Schwab…),同一策略可跨平台。
46+
- A 股:仅 **QMT 一个 Platform 仓**;多策略、多 dry-run target,但无「中信 Platform / 国金 Platform」拆分。
47+
48+
## 部署 checklist(当前)
49+
50+
1. `strategy-profiles.example.json` 登记 runtime-enabled 的 `cn_*` profile
51+
2. `account-options.example.json` 配置 QMT dry-run 账号项
52+
3. 同步 KV:`strategy_profiles``account_options`
53+
4. 触发 `manual-strategy-switch`(platform=`qmt`)— Cloud Run env sync 仍会 skip,属预期

examples/targets/qmt/dry_run.example.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
{
22
"$schema": "../../../schemas/runtime-target.schema.json",
33
"target_id": "qmt/dry_run",
4-
"description": "Example QMT repository-scoped dry-run deployment target for A-share strategies.",
4+
"description": "Legacy alias target id; prefer qmt/industry_etf_dry_run for new deployments.",
55
"github": {
66
"repository": "QuantStrategyLab/QmtPlatform",
77
"variable_scope": "repository"
88
},
99
"runtime_target": {
1010
"platform_id": "qmt",
11-
"strategy_profile": "cn_index_etf_tactical_rotation",
11+
"strategy_profile": "cn_industry_etf_rotation",
1212
"dry_run_only": true,
1313
"deployment_selector": "qmt",
1414
"account_selector": ["qmt"],
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{
2+
"$schema": "../../../schemas/runtime-target.schema.json",
3+
"target_id": "qmt/industry_etf_dry_run",
4+
"description": "Primary QMT dry-run target for cn_industry_etf_rotation (A-share industry ETF rotation).",
5+
"github": {
6+
"repository": "QuantStrategyLab/QmtPlatform",
7+
"variable_scope": "repository"
8+
},
9+
"runtime_target": {
10+
"platform_id": "qmt",
11+
"strategy_profile": "cn_industry_etf_rotation",
12+
"dry_run_only": true,
13+
"deployment_selector": "qmt",
14+
"account_selector": ["qmt"],
15+
"account_scope": "CN",
16+
"service_name": "qmt-quant-service",
17+
"execution_mode": "dry_run",
18+
"scheduler": {
19+
"timezone": "Asia/Shanghai",
20+
"main_time": "45 15 * * *",
21+
"probe_time": "35 9,15 * * *",
22+
"precheck_time": "45 9 * * *"
23+
}
24+
},
25+
"plugin_mounts_variable": "QMT_STRATEGY_PLUGIN_MOUNTS_JSON",
26+
"plugin_mounts": [],
27+
"extra_variables": {
28+
"QMT_DRY_RUN_ONLY": "true",
29+
"QMT_MARKET_HISTORY_PATH": "data/fixtures/market_history.sample.csv"
30+
}
31+
}

scripts/build_runtime_switch.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@
8484
"russell_top50_leader_rotation": US_SNAPSHOT_SCHEDULER,
8585
"hk_low_vol_dividend_quality_snapshot": HK_SNAPSHOT_SCHEDULER,
8686
"cn_index_etf_tactical_rotation": CN_DAILY_SCHEDULER,
87+
"cn_industry_etf_rotation": CN_DAILY_SCHEDULER,
8788
"cn_dividend_quality_snapshot": CN_SNAPSHOT_SCHEDULER,
8889
}
8990
PLATFORM_DRY_RUN_VARIABLES = {
@@ -782,7 +783,12 @@ def _scheduler_plan_for_strategy(
782783
return dict(US_DAILY_SCHEDULER)
783784
scheduler = STRATEGY_SCHEDULER_PROFILES.get(profile)
784785
if scheduler is None:
785-
scheduler = HK_DAILY_SCHEDULER if profile.startswith("hk_") else US_DAILY_SCHEDULER
786+
if profile.startswith("cn_"):
787+
scheduler = CN_DAILY_SCHEDULER
788+
elif profile.startswith("hk_"):
789+
scheduler = HK_DAILY_SCHEDULER
790+
else:
791+
scheduler = US_DAILY_SCHEDULER
786792
return dict(scheduler)
787793

788794

tests/strategy_switch_worker_validation.mjs

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,12 @@ assert.ok(indexHtml.includes('optionOverlayMode: "期权层状态"'));
3939
assert.ok(indexHtml.includes('optionOverlayMode: "Option layer"'));
4040
assert.ok(indexHtml.includes("optionOverlayDefaultsFromProfileItem"));
4141
assert.ok(indexHtml.includes('id="cash-only-execution-mode-select"'));
42-
assert.ok(indexHtml.includes('class="form-section cash-only-section"'));
42+
assert.ok(indexHtml.includes('class="form-section execution-cash-policy-section"'));
43+
assert.ok(indexHtml.includes('function reconcileExecutionCashPolicy('));
44+
assert.ok(indexHtml.includes('qmt: { label: "QMT"'));
45+
assert.ok(indexHtml.includes('cn_industry_etf_rotation'));
46+
assert.ok(indexHtml.includes('platformDryRunOnly'));
47+
assert.ok(indexHtml.includes('qmtDryRunOnlyNote'));
4348
assert.ok(indexHtml.includes('cashOnlyExecutionMode: "允许融资"'));
4449
assert.ok(indexHtml.includes('cashOnlyExecutionValueYes: "允许融资:是"'));
4550
assert.ok(indexHtml.includes('cashOnlyExecutionMode: "Allow margin"'));
@@ -63,7 +68,7 @@ assert.ok(indexHtml.includes('el("dca-base-investment-usd-input").addEventListen
6368
assert.ok(indexHtml.includes('label_zh: "纳指100 / 标普500 定投"'));
6469
assert.ok(indexHtml.includes('class="form-section income-layer-section"'));
6570
assert.ok(indexHtml.includes('class="form-section dca-section"'));
66-
assert.ok(indexHtml.includes('class="control-block reserve-policy-block section-wide"'));
71+
assert.ok(indexHtml.includes('class="control-block reserve-policy-block policy-block"'));
6772
assert.ok(indexHtml.includes('profile: "ibit_smart_dca"'));
6873
assert.ok(indexHtml.includes('IBIT 比特币定投'));
6974
assert.ok(indexHtml.includes('localStrategyLabels'));
@@ -554,6 +559,22 @@ assert.throws(
554559
}),
555560
/IBIT Z-Score exit settings/,
556561
);
562+
assert.throws(
563+
() => __test.normalizeSwitchInputs({
564+
platform: "qmt",
565+
target_name: "industry_etf_dry_run",
566+
strategy_profile: "cn_industry_etf_rotation",
567+
execution_mode: "live",
568+
}),
569+
/does not support live execution yet/,
570+
);
571+
const normalizedQmtDryRunInputs = __test.normalizeSwitchInputs({
572+
platform: "qmt",
573+
target_name: "industry_etf_dry_run",
574+
strategy_profile: "cn_industry_etf_rotation",
575+
execution_mode: "paper",
576+
});
577+
assert.equal(normalizedQmtDryRunInputs.execution_mode, "paper");
557578
assert.throws(
558579
() => __test.normalizeSwitchInputs({
559580
platform: "ibkr",

tests/test_runtime_settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -506,9 +506,9 @@ def test_build_switch_target_defaults_qmt_repository_scope(self):
506506
"--platform",
507507
"qmt",
508508
"--target-name",
509-
"dry-run",
509+
"industry-etf-dry-run",
510510
"--strategy-profile",
511-
"cn_index_etf_tactical_rotation",
511+
"cn_industry_etf_rotation",
512512
"--execution-mode",
513513
"dry_run",
514514
]
@@ -526,7 +526,7 @@ def test_build_switch_target_defaults_qmt_repository_scope(self):
526526
self.assertEqual(target["runtime_target"]["service_name"], "qmt-quant-service")
527527
self.assertEqual(target["runtime_target"]["dry_run_only"], True)
528528
self.assertEqual(assignments["QMT_DRY_RUN_ONLY"], "true")
529-
self.assertEqual(assignments["STRATEGY_PROFILE"], "cn_index_etf_tactical_rotation")
529+
self.assertEqual(assignments["STRATEGY_PROFILE"], "cn_industry_etf_rotation")
530530
self.assertEqual(
531531
target["runtime_target"]["scheduler"],
532532
{

web/strategy-switch-console/README.zh-CN.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,9 @@ Worker 会校验 dispatch 参数必须匹配这里的某个账号项,也会校
125125

126126
登录用户访问 `/api/config` 时,Worker 还会读取目标平台仓库的当前 GitHub Variables。读取优先级是账号匹配的 `CLOUD_RUN_SERVICE_TARGETS_JSON`、匹配的 `RUNTIME_TARGET_JSON.strategy_profile``STRATEGY_PROFILE`;都读不到时,页面才回退到 `default_strategy_profile`
127127

128-
切换表单也支持可选的预留现金覆盖项:所选账号币种下的最小预留现金和预留现金比例。如果账号现金币种固定,可以在账号配置里把 `cash_currency` 设为 `USD``HKD`;否则页面会按所选策略推断,港股策略显示 HKD,美股策略显示 USD。沿用当前策略会保留平台现有变量;如果平台没有显式配置预留现金变量,源码默认是不额外预留(账号币种 `0`、比例 `0%`)。填写后,Worker 会把它们传给 `manual-strategy-switch.yml`,由 workflow 写入平台对应变量,例如 `IBKR_MIN_RESERVED_CASH_USD``IBKR_RESERVED_CASH_RATIO`
128+
切换表单也支持可选的预留现金覆盖项:所选账号币种下的最小预留现金和预留现金比例。如果账号现金币种固定,可以在账号配置里把 `cash_currency` 设为 `USD``HKD``CNY`;否则页面会按所选策略推断,A 股策略显示 CNY,港股策略显示 HKD,美股策略显示 USD。沿用当前策略会保留平台现有变量;如果平台没有显式配置预留现金变量,源码默认是不额外预留(账号币种 `0`、比例 `0%`)。填写后,Worker 会把它们传给 `manual-strategy-switch.yml`,由 workflow 写入平台对应变量,例如 `IBKR_MIN_RESERVED_CASH_USD``IBKR_RESERVED_CASH_RATIO`
129+
130+
「允许融资」与「预留现金覆盖」在网页上互斥:选「允许融资:是」会禁用预留现金覆盖;设置比例/金额类预留覆盖会禁用「允许融资:是」,并自动切到「否」。QMT(A 股)平台不展示这两项,现金约束在 CnEquityStrategies 策略参数 `execution_cash_reserve_ratio` 内配置。
129131

130132
收入层控件来自 `strategy-profiles.example.json` 里的 live 验证策略元数据。切换页可以沿用当前配置、按 profile 默认起始金额和最高比例开启收入层,或关闭收入层。期权层也来自同一份策略 profile 元数据,但网页只暴露三态策略:沿用当前、启用 profile 默认 recipe 和预算、或关闭并清理期权层变量。手工切换请求仍不能通过 `extra_variables_json` 覆盖直接期权 overlay / LEAPS 字段;Worker 和构建脚本会拒绝这些直接覆盖项。
131133

@@ -139,9 +141,11 @@ Worker 会校验 dispatch 参数必须匹配这里的某个账号项,也会校
139141

140142
-`strategy-profiles.example.json` 增加 runtime-enabled profile id 和显示名称。
141143
- 运行 `python3 scripts/sync_strategy_switch_page_asset.py` 重新生成 `strategy_profiles_asset.js`
142-
- 给每个策略 profile 设置 `domain`。当前支持 `us_equity``hk_equity`
144+
- 给每个策略 profile 设置 `domain`。当前支持 `us_equity``hk_equity``cn_equity`
143145
-`account-options.example.json` 和已部署的 KV 账号配置里更新对应账号的 `default_strategy_profile``supported_domains`
144146
- LongBridge 和 IBKR 账号默认写 `["us_equity", "hk_equity"]`,除非你明确要把某个账号限制成单市场。
147+
- QMT 账号写 `supported_domains: ["cn_equity"]``cash_currency: "CNY"`,并指向 `QuantStrategyLab/QmtPlatform` 仓库里的 target(见 `examples/targets/qmt/`)。当前阶段 **仅 dry-run**,无 live 券商账号;控制台会锁定 paper 模式,Worker 拒绝 QMT live 切换。Worker 后端已支持 `qmt`;平台 Cloud Run sync 目前仍会在 workflow 里 skip,切换策略本身可正常触发。
148+
- 架构说明见 `examples/targets/qmt/README.zh-CN.md`。Runtime-enabled 的 A 股策略为 `cn_industry_etf_rotation`(主轨)与 `cn_dividend_quality_snapshot``cn_index_etf_tactical_rotation` 为 research-only,不要放进切换页策略目录。
145149
- main 分支部署 workflow 会在 Worker 部署后,用 `strategy-profiles.example.json` 自动更新已部署 KV 的 `strategy_profiles` key。手动部署时,可用 Worker 同步 token 调用 `/api/internal/sync-strategy-profiles`
146150
- 确认平台仓库当前的 `RUNTIME_TARGET_JSON.strategy_profile` 或账号级 `CLOUD_RUN_SERVICE_TARGETS_JSON` 使用同一个 id。
147151
-`manual-strategy-switch.yml` 统一管理平台 plugin mounts。策略不需要插件时,它会写入空的 `*_STRATEGY_PLUGIN_MOUNTS_JSON`,清掉旧策略留下的插件配置。

web/strategy-switch-console/account-options.example.json

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,5 +83,33 @@
8383
"default_strategy_profile": "russell_top50_leader_rotation",
8484
"supported_domains": ["us_equity"]
8585
}
86+
],
87+
"qmt": [
88+
{
89+
"key": "industry_etf_dry_run",
90+
"label": "industry_etf_dry_run",
91+
"target_name": "industry_etf_dry_run",
92+
"variable_scope": "repository",
93+
"deployment_selector": "qmt",
94+
"account_selector": "qmt",
95+
"account_scope": "CN",
96+
"service_name": "qmt-quant-service",
97+
"cash_currency": "CNY",
98+
"default_strategy_profile": "cn_industry_etf_rotation",
99+
"supported_domains": ["cn_equity"]
100+
},
101+
{
102+
"key": "dividend_quality_dry_run",
103+
"label": "dividend_quality_dry_run",
104+
"target_name": "dividend_quality_dry_run",
105+
"variable_scope": "repository",
106+
"deployment_selector": "qmt",
107+
"account_selector": "qmt",
108+
"account_scope": "CN",
109+
"service_name": "qmt-quant-service",
110+
"cash_currency": "CNY",
111+
"default_strategy_profile": "cn_dividend_quality_snapshot",
112+
"supported_domains": ["cn_equity"]
113+
}
86114
]
87115
}

0 commit comments

Comments
 (0)