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
53 changes: 53 additions & 0 deletions examples/targets/qmt/README.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# QMT(A 股)Runtime Target 架构

当前阶段:**仅 dry-run,无 live 券商账号**。中信 / 国金等券商差异不在 `platform_id` 层建模,将来 live 时在 `account-options` 增加 target 项并在对应 miniQMT 部署侧区分即可。

## 分层

| 层 | 仓库 / ID | 说明 |
| --- | --- | --- |
| 策略 | `CnEquityStrategies` / `cn_*` profile | 仅 `cn_equity` domain |
| 平台 | `QmtPlatform` / `platform_id=qmt` | 统一 miniQMT 执行层,一个 Platform 仓 |
| 运行配置 | `QuantRuntimeSettings` | target 示例、切换控制台、`manual-strategy-switch` |
| 变量作用域 | `variable_scope=repository` | QMT 使用仓库级 GitHub Variables |

## Runtime-enabled 策略(切换页可选)

| Profile | 输入 | 示例 target |
| --- | --- | --- |
| `cn_industry_etf_rotation` | `market_history` | `industry_etf_dry_run.example.json`(**主轨**) |
| `cn_dividend_quality_snapshot` | `feature_snapshot` | `dividend_quality_dry_run.example.json` |

`cn_index_etf_tactical_rotation` 在策略 catalog 中为 **research_backtest_only**,不要放进 `strategy-profiles.example.json` 的 runtime 列表。

## 账号路由(`account-options`)

每个 **dry-run target** 对应控制台一条 QMT 账号项,字段对齐示例 JSON:

- `target_name`:与 `examples/targets/qmt/*.example.json` 的 target 名一致
- `variable_scope`:`repository`
- `deployment_selector` / `account_selector`:`qmt`
- `account_scope`:`CN`
- `service_name`:`qmt-quant-service`
- `cash_currency`:`CNY`
- `supported_domains`:`["cn_equity"]`
- `default_strategy_profile`:与该 target 默认策略一致

**不要**在账号配置里放 miniQMT 密码、券商 token 或本机路径;fixture 路径走 QmtPlatform 仓库变量(如 `QMT_MARKET_HISTORY_PATH`)。

## 执行模式

- 控制台 QMT tab **锁定 paper(dry-run)**,Worker 拒绝 `execution_mode=live`。
- `QMT_DRY_RUN_ONLY=true` 为默认;live 上线前需单独评审并扩展 workflow / sync。

## 与美股平台的差异

- 美股:一个券商一个 Platform 仓(IBKR、Schwab…),同一策略可跨平台。
- A 股:仅 **QMT 一个 Platform 仓**;多策略、多 dry-run target,但无「中信 Platform / 国金 Platform」拆分。

## 部署 checklist(当前)

1. `strategy-profiles.example.json` 登记 runtime-enabled 的 `cn_*` profile
2. `account-options.example.json` 配置 QMT dry-run 账号项
3. 同步 KV:`strategy_profiles`、`account_options`
4. 触发 `manual-strategy-switch`(platform=`qmt`)— Cloud Run env sync 仍会 skip,属预期
4 changes: 2 additions & 2 deletions examples/targets/qmt/dry_run.example.json
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
{
"$schema": "../../../schemas/runtime-target.schema.json",
"target_id": "qmt/dry_run",
"description": "Example QMT repository-scoped dry-run deployment target for A-share strategies.",
"description": "Legacy alias target id; prefer qmt/industry_etf_dry_run for new deployments.",
"github": {
"repository": "QuantStrategyLab/QmtPlatform",
"variable_scope": "repository"
},
"runtime_target": {
"platform_id": "qmt",
"strategy_profile": "cn_index_etf_tactical_rotation",
"strategy_profile": "cn_industry_etf_rotation",
"dry_run_only": true,
"deployment_selector": "qmt",
"account_selector": ["qmt"],
Expand Down
31 changes: 31 additions & 0 deletions examples/targets/qmt/industry_etf_dry_run.example.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"$schema": "../../../schemas/runtime-target.schema.json",
"target_id": "qmt/industry_etf_dry_run",
"description": "Primary QMT dry-run target for cn_industry_etf_rotation (A-share industry ETF rotation).",
"github": {
"repository": "QuantStrategyLab/QmtPlatform",
"variable_scope": "repository"
},
"runtime_target": {
"platform_id": "qmt",
"strategy_profile": "cn_industry_etf_rotation",
"dry_run_only": true,
"deployment_selector": "qmt",
"account_selector": ["qmt"],
"account_scope": "CN",
"service_name": "qmt-quant-service",
"execution_mode": "dry_run",
"scheduler": {
"timezone": "Asia/Shanghai",
"main_time": "45 15 * * *",
"probe_time": "35 9,15 * * *",
"precheck_time": "45 9 * * *"
}
},
"plugin_mounts_variable": "QMT_STRATEGY_PLUGIN_MOUNTS_JSON",
"plugin_mounts": [],
"extra_variables": {
"QMT_DRY_RUN_ONLY": "true",
"QMT_MARKET_HISTORY_PATH": "data/fixtures/market_history.sample.csv"
}
}
24 changes: 12 additions & 12 deletions internal_dependency_matrix.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,14 +34,14 @@
"path": "requirements.txt",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "bc5351d3567b2bb60416dc969062c37eedbbf65e"
"ref": "7b6e3ce33e6563db4794fa7b865db9ec428dc478"
},
{
"consumer_repo": "CharlesSchwabPlatform",
"path": "requirements.txt",
"package": "us-equity-strategies",
"source_repo": "UsEquityStrategies",
"ref": "5aa949df2962b70d6fdb11c33f7d150492795f16"
"ref": "608f491f4ef083c752ec29ea2669665d5de4a219"
},
{
"consumer_repo": "CryptoStrategies",
Expand Down Expand Up @@ -69,63 +69,63 @@
"path": "requirements.txt",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "bc5351d3567b2bb60416dc969062c37eedbbf65e"
"ref": "7b6e3ce33e6563db4794fa7b865db9ec428dc478"
},
{
"consumer_repo": "FirstradePlatform",
"path": "requirements.txt",
"package": "us-equity-strategies",
"source_repo": "UsEquityStrategies",
"ref": "5aa949df2962b70d6fdb11c33f7d150492795f16"
"ref": "608f491f4ef083c752ec29ea2669665d5de4a219"
},
{
"consumer_repo": "HkEquityStrategies",
"path": "pyproject.toml",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "bc5351d3567b2bb60416dc969062c37eedbbf65e"
"ref": "7b6e3ce33e6563db4794fa7b865db9ec428dc478"
},
{
"consumer_repo": "InteractiveBrokersPlatform",
"path": "requirements.txt",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "bc5351d3567b2bb60416dc969062c37eedbbf65e"
"ref": "7b6e3ce33e6563db4794fa7b865db9ec428dc478"
},
{
"consumer_repo": "InteractiveBrokersPlatform",
"path": "requirements.txt",
"package": "us-equity-strategies",
"source_repo": "UsEquityStrategies",
"ref": "5aa949df2962b70d6fdb11c33f7d150492795f16"
"ref": "608f491f4ef083c752ec29ea2669665d5de4a219"
},
{
"consumer_repo": "InteractiveBrokersPlatform",
"path": "requirements.txt",
"package": "hk-equity-strategies",
"source_repo": "HkEquityStrategies",
"ref": "f4a7d9252e4c82750045872ff852cb474b172e91"
"ref": "9775fea22a0f397422d27b6a3340934b1ea7f064"
},
{
"consumer_repo": "LongBridgePlatform",
"path": "requirements.txt",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "bc5351d3567b2bb60416dc969062c37eedbbf65e"
"ref": "7b6e3ce33e6563db4794fa7b865db9ec428dc478"
},
{
"consumer_repo": "LongBridgePlatform",
"path": "requirements.txt",
"package": "us-equity-strategies",
"source_repo": "UsEquityStrategies",
"ref": "5aa949df2962b70d6fdb11c33f7d150492795f16"
"ref": "608f491f4ef083c752ec29ea2669665d5de4a219"
},
{
"consumer_repo": "LongBridgePlatform",
"path": "requirements.txt",
"package": "hk-equity-strategies",
"source_repo": "HkEquityStrategies",
"ref": "f4a7d9252e4c82750045872ff852cb474b172e91"
"ref": "9775fea22a0f397422d27b6a3340934b1ea7f064"
},
{
"consumer_repo": "UsEquitySnapshotPipelines",
Expand Down Expand Up @@ -153,7 +153,7 @@
"path": "pyproject.toml",
"package": "quant-platform-kit",
"source_repo": "QuantPlatformKit",
"ref": "bc5351d3567b2bb60416dc969062c37eedbbf65e"
"ref": "7b6e3ce33e6563db4794fa7b865db9ec428dc478"
},
{
"consumer_repo": "QmtPlatform",
Expand Down
8 changes: 7 additions & 1 deletion scripts/build_runtime_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@
"russell_top50_leader_rotation": US_SNAPSHOT_SCHEDULER,
"hk_low_vol_dividend_quality_snapshot": HK_SNAPSHOT_SCHEDULER,
"cn_index_etf_tactical_rotation": CN_DAILY_SCHEDULER,
"cn_industry_etf_rotation": CN_DAILY_SCHEDULER,
"cn_dividend_quality_snapshot": CN_SNAPSHOT_SCHEDULER,
}
PLATFORM_DRY_RUN_VARIABLES = {
Expand Down Expand Up @@ -782,7 +783,12 @@ def _scheduler_plan_for_strategy(
return dict(US_DAILY_SCHEDULER)
scheduler = STRATEGY_SCHEDULER_PROFILES.get(profile)
if scheduler is None:
scheduler = HK_DAILY_SCHEDULER if profile.startswith("hk_") else US_DAILY_SCHEDULER
if profile.startswith("cn_"):
scheduler = CN_DAILY_SCHEDULER
elif profile.startswith("hk_"):
scheduler = HK_DAILY_SCHEDULER
else:
scheduler = US_DAILY_SCHEDULER
return dict(scheduler)


Expand Down
25 changes: 23 additions & 2 deletions tests/strategy_switch_worker_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,12 @@ assert.ok(indexHtml.includes('optionOverlayMode: "期权层状态"'));
assert.ok(indexHtml.includes('optionOverlayMode: "Option layer"'));
assert.ok(indexHtml.includes("optionOverlayDefaultsFromProfileItem"));
assert.ok(indexHtml.includes('id="cash-only-execution-mode-select"'));
assert.ok(indexHtml.includes('class="form-section cash-only-section"'));
assert.ok(indexHtml.includes('class="form-section execution-cash-policy-section"'));
assert.ok(indexHtml.includes('function reconcileExecutionCashPolicy('));
assert.ok(indexHtml.includes('qmt: { label: "QMT"'));
assert.ok(indexHtml.includes('cn_industry_etf_rotation'));
assert.ok(indexHtml.includes('platformDryRunOnly'));
assert.ok(indexHtml.includes('qmtDryRunOnlyNote'));
assert.ok(indexHtml.includes('cashOnlyExecutionMode: "允许融资"'));
assert.ok(indexHtml.includes('cashOnlyExecutionValueYes: "允许融资:是"'));
assert.ok(indexHtml.includes('cashOnlyExecutionMode: "Allow margin"'));
Expand All @@ -63,7 +68,7 @@ assert.ok(indexHtml.includes('el("dca-base-investment-usd-input").addEventListen
assert.ok(indexHtml.includes('label_zh: "纳指100 / 标普500 定投"'));
assert.ok(indexHtml.includes('class="form-section income-layer-section"'));
assert.ok(indexHtml.includes('class="form-section dca-section"'));
assert.ok(indexHtml.includes('class="control-block reserve-policy-block section-wide"'));
assert.ok(indexHtml.includes('class="control-block reserve-policy-block policy-block"'));
assert.ok(indexHtml.includes('profile: "ibit_smart_dca"'));
assert.ok(indexHtml.includes('IBIT 比特币定投'));
assert.ok(indexHtml.includes('localStrategyLabels'));
Expand Down Expand Up @@ -554,6 +559,22 @@ assert.throws(
}),
/IBIT Z-Score exit settings/,
);
assert.throws(
() => __test.normalizeSwitchInputs({
platform: "qmt",
target_name: "industry_etf_dry_run",
strategy_profile: "cn_industry_etf_rotation",
execution_mode: "live",
}),
/does not support live execution yet/,
);
const normalizedQmtDryRunInputs = __test.normalizeSwitchInputs({
platform: "qmt",
target_name: "industry_etf_dry_run",
strategy_profile: "cn_industry_etf_rotation",
execution_mode: "paper",
});
assert.equal(normalizedQmtDryRunInputs.execution_mode, "paper");
assert.throws(
() => __test.normalizeSwitchInputs({
platform: "ibkr",
Expand Down
6 changes: 3 additions & 3 deletions tests/test_runtime_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -506,9 +506,9 @@ def test_build_switch_target_defaults_qmt_repository_scope(self):
"--platform",
"qmt",
"--target-name",
"dry-run",
"industry-etf-dry-run",
"--strategy-profile",
"cn_index_etf_tactical_rotation",
"cn_industry_etf_rotation",
"--execution-mode",
"dry_run",
]
Expand All @@ -526,7 +526,7 @@ def test_build_switch_target_defaults_qmt_repository_scope(self):
self.assertEqual(target["runtime_target"]["service_name"], "qmt-quant-service")
self.assertEqual(target["runtime_target"]["dry_run_only"], True)
self.assertEqual(assignments["QMT_DRY_RUN_ONLY"], "true")
self.assertEqual(assignments["STRATEGY_PROFILE"], "cn_index_etf_tactical_rotation")
self.assertEqual(assignments["STRATEGY_PROFILE"], "cn_industry_etf_rotation")
self.assertEqual(
target["runtime_target"]["scheduler"],
{
Expand Down
8 changes: 6 additions & 2 deletions web/strategy-switch-console/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,9 @@ Worker 会校验 dispatch 参数必须匹配这里的某个账号项,也会校

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

切换表单也支持可选的预留现金覆盖项:所选账号币种下的最小预留现金和预留现金比例。如果账号现金币种固定,可以在账号配置里把 `cash_currency` 设为 `USD` 或 `HKD`;否则页面会按所选策略推断,港股策略显示 HKD,美股策略显示 USD。沿用当前策略会保留平台现有变量;如果平台没有显式配置预留现金变量,源码默认是不额外预留(账号币种 `0`、比例 `0%`)。填写后,Worker 会把它们传给 `manual-strategy-switch.yml`,由 workflow 写入平台对应变量,例如 `IBKR_MIN_RESERVED_CASH_USD` 和 `IBKR_RESERVED_CASH_RATIO`。
切换表单也支持可选的预留现金覆盖项:所选账号币种下的最小预留现金和预留现金比例。如果账号现金币种固定,可以在账号配置里把 `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`。

「允许融资」与「预留现金覆盖」在网页上互斥:选「允许融资:是」会禁用预留现金覆盖;设置比例/金额类预留覆盖会禁用「允许融资:是」,并自动切到「否」。QMT(A 股)平台不展示这两项,现金约束在 CnEquityStrategies 策略参数 `execution_cash_reserve_ratio` 内配置。

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

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

- 在 `strategy-profiles.example.json` 增加 runtime-enabled profile id 和显示名称。
- 运行 `python3 scripts/sync_strategy_switch_page_asset.py` 重新生成 `strategy_profiles_asset.js`。
- 给每个策略 profile 设置 `domain`。当前支持 `us_equity` 和 `hk_equity`。
- 给每个策略 profile 设置 `domain`。当前支持 `us_equity`、`hk_equity` 和 `cn_equity`。
- 在 `account-options.example.json` 和已部署的 KV 账号配置里更新对应账号的 `default_strategy_profile` 和 `supported_domains`。
- LongBridge 和 IBKR 账号默认写 `["us_equity", "hk_equity"]`,除非你明确要把某个账号限制成单市场。
- 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,切换策略本身可正常触发。
- 架构说明见 `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,不要放进切换页策略目录。
- main 分支部署 workflow 会在 Worker 部署后,用 `strategy-profiles.example.json` 自动更新已部署 KV 的 `strategy_profiles` key。手动部署时,可用 Worker 同步 token 调用 `/api/internal/sync-strategy-profiles`。
- 确认平台仓库当前的 `RUNTIME_TARGET_JSON.strategy_profile` 或账号级 `CLOUD_RUN_SERVICE_TARGETS_JSON` 使用同一个 id。
- 让 `manual-strategy-switch.yml` 统一管理平台 plugin mounts。策略不需要插件时,它会写入空的 `*_STRATEGY_PLUGIN_MOUNTS_JSON`,清掉旧策略留下的插件配置。
Expand Down
28 changes: 28 additions & 0 deletions web/strategy-switch-console/account-options.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -83,5 +83,33 @@
"default_strategy_profile": "russell_top50_leader_rotation",
"supported_domains": ["us_equity"]
}
],
"qmt": [
{
"key": "industry_etf_dry_run",
"label": "industry_etf_dry_run",
"target_name": "industry_etf_dry_run",
"variable_scope": "repository",
"deployment_selector": "qmt",
"account_selector": "qmt",
"account_scope": "CN",
"service_name": "qmt-quant-service",
"cash_currency": "CNY",
"default_strategy_profile": "cn_industry_etf_rotation",
"supported_domains": ["cn_equity"]
},
{
"key": "dividend_quality_dry_run",
"label": "dividend_quality_dry_run",
"target_name": "dividend_quality_dry_run",
"variable_scope": "repository",
"deployment_selector": "qmt",
"account_selector": "qmt",
"account_scope": "CN",
"service_name": "qmt-quant-service",
"cash_currency": "CNY",
"default_strategy_profile": "cn_dividend_quality_snapshot",
"supported_domains": ["cn_equity"]
}
]
}
Loading