读取策略配置
\n正在读取登录状态、账号配置和当前状态。
\n \ndiff --git a/examples/targets/qmt/README.zh-CN.md b/examples/targets/qmt/README.zh-CN.md new file mode 100644 index 0000000..51b8905 --- /dev/null +++ b/examples/targets/qmt/README.zh-CN.md @@ -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,属预期 diff --git a/examples/targets/qmt/dry_run.example.json b/examples/targets/qmt/dry_run.example.json index 6d36565..c97a2aa 100644 --- a/examples/targets/qmt/dry_run.example.json +++ b/examples/targets/qmt/dry_run.example.json @@ -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"], diff --git a/examples/targets/qmt/industry_etf_dry_run.example.json b/examples/targets/qmt/industry_etf_dry_run.example.json new file mode 100644 index 0000000..3990a9f --- /dev/null +++ b/examples/targets/qmt/industry_etf_dry_run.example.json @@ -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" + } +} diff --git a/internal_dependency_matrix.json b/internal_dependency_matrix.json index 090b5eb..f7b2231 100644 --- a/internal_dependency_matrix.json +++ b/internal_dependency_matrix.json @@ -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", @@ -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", @@ -153,7 +153,7 @@ "path": "pyproject.toml", "package": "quant-platform-kit", "source_repo": "QuantPlatformKit", - "ref": "bc5351d3567b2bb60416dc969062c37eedbbf65e" + "ref": "7b6e3ce33e6563db4794fa7b865db9ec428dc478" }, { "consumer_repo": "QmtPlatform", diff --git a/scripts/build_runtime_switch.py b/scripts/build_runtime_switch.py index 48c7353..579fe49 100644 --- a/scripts/build_runtime_switch.py +++ b/scripts/build_runtime_switch.py @@ -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 = { @@ -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) diff --git a/tests/strategy_switch_worker_validation.mjs b/tests/strategy_switch_worker_validation.mjs index 4c8fe51..095fed7 100644 --- a/tests/strategy_switch_worker_validation.mjs +++ b/tests/strategy_switch_worker_validation.mjs @@ -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"')); @@ -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')); @@ -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", diff --git a/tests/test_runtime_settings.py b/tests/test_runtime_settings.py index 518322e..c47c400 100644 --- a/tests/test_runtime_settings.py +++ b/tests/test_runtime_settings.py @@ -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", ] @@ -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"], { diff --git a/web/strategy-switch-console/README.zh-CN.md b/web/strategy-switch-console/README.zh-CN.md index 2341395..6194e71 100644 --- a/web/strategy-switch-console/README.zh-CN.md +++ b/web/strategy-switch-console/README.zh-CN.md @@ -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 和构建脚本会拒绝这些直接覆盖项。 @@ -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`,清掉旧策略留下的插件配置。 diff --git a/web/strategy-switch-console/account-options.example.json b/web/strategy-switch-console/account-options.example.json index 5f8444f..d8e570f 100644 --- a/web/strategy-switch-console/account-options.example.json +++ b/web/strategy-switch-console/account-options.example.json @@ -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"] + } ] } diff --git a/web/strategy-switch-console/index.html b/web/strategy-switch-console/index.html index b866fd4..ee894dd 100644 --- a/web/strategy-switch-console/index.html +++ b/web/strategy-switch-console/index.html @@ -22,6 +22,7 @@ --ib: #2f5f9f; --sw: #9b4153; --ft: #94651f; + --qmt: #b45309; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; @@ -313,6 +314,48 @@ --platform-color: var(--ft); } + .platform-button[data-platform="qmt"] { + --platform-color: var(--qmt); + } + + .execution-cash-policy-section { + grid-template-columns: 1fr; + } + + .execution-cash-policy-heading { + grid-column: 1 / -1; + margin: 0; + font-size: 13px; + font-weight: 780; + color: var(--ink); + } + + .execution-cash-policy-note { + grid-column: 1 / -1; + margin: 0; + color: var(--muted); + font-size: 12px; + line-height: 1.45; + } + + .execution-cash-policy-grid { + grid-column: 1 / -1; + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 14px; + align-items: start; + } + + .policy-block-muted .selection-meta { + color: var(--danger); + } + + @media (max-width: 720px) { + .execution-cash-policy-grid { + grid-template-columns: 1fr; + } + } + .mark { width: 44px; height: 44px; @@ -1299,6 +1342,7 @@
允许融资与预留现金覆盖不能同时生效。
+A 股 QMT 不使用 margin / 平台预留现金;现金约束在策略参数 execution_cash_reserve_ratio 内配置。
+选平台、目标账号和策略,一次执行完成切换。
\n正在读取登录状态、账号配置和当前状态。
\n \n登录后查看账号配置。
\n\n登录后才可执行切换。
\n \n选平台、目标账号和策略,一次执行完成切换。
\n正在读取登录状态、账号配置和当前状态。
\n \n登录后查看账号配置。
\n\n允许融资与预留现金覆盖不能同时生效。
\nA 股 QMT 不使用 margin / 平台预留现金;现金约束在策略参数 execution_cash_reserve_ratio 内配置。
\n登录后才可执行切换。
\n \n