From 6bc822ae366f7ae79ae101506285b6760c081b1c Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:43:52 +0800 Subject: [PATCH 1/2] 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 --- examples/targets/qmt/README.zh-CN.md | 53 +++ examples/targets/qmt/dry_run.example.json | 4 +- .../qmt/industry_etf_dry_run.example.json | 31 ++ scripts/build_runtime_switch.py | 8 +- tests/strategy_switch_worker_validation.mjs | 25 +- tests/test_runtime_settings.py | 6 +- web/strategy-switch-console/README.zh-CN.md | 8 +- .../account-options.example.json | 28 ++ web/strategy-switch-console/index.html | 313 ++++++++++++++---- web/strategy-switch-console/page_asset.js | 2 +- .../strategy-profiles.example.json | 16 + .../strategy_profiles_asset.js | 2 +- web/strategy-switch-console/worker.js | 4 + 13 files changed, 422 insertions(+), 78 deletions(-) create mode 100644 examples/targets/qmt/README.zh-CN.md create mode 100644 examples/targets/qmt/industry_etf_dry_run.example.json diff --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/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 @@

LongBridge

+
@@ -1349,12 +1393,35 @@

LongBridge

-
- +
+

现金与融资

+

允许融资与预留现金覆盖不能同时生效。

+ +
+ + + + + + + +
@@ -1371,25 +1438,7 @@

LongBridge

-
- - - - - -
+
@@ -1418,6 +1467,7 @@

切换摘要

ibkr: "QuantStrategyLab/InteractiveBrokersPlatform", schwab: "QuantStrategyLab/CharlesSchwabPlatform", firstrade: "QuantStrategyLab/FirstradePlatform", + qmt: "QuantStrategyLab/QmtPlatform", }; const platformMeta = { @@ -1425,6 +1475,7 @@

切换摘要

ibkr: { label: "IBKR", code: "IB", accent: "var(--ib)" }, schwab: { label: "Schwab", code: "SW", accent: "var(--sw)" }, firstrade: { label: "Firstrade", code: "FT", accent: "var(--ft)" }, + qmt: { label: "QMT", code: "QM", accent: "var(--qmt)" }, }; const reservePolicyModes = ["current", "none", "ratio", "floor", "max"]; @@ -1465,6 +1516,8 @@

切换摘要

{ profile: "russell_top50_leader_rotation", label: "Russell Top50 Leader Rotation", label_en: "Russell Top50 Leader Rotation", label_zh: "罗素 Top50 领涨轮动", domain: "us_equity", runtime_enabled: true, income_layer_enabled: true, income_layer_start_usd: "300000", income_layer_max_ratio: "0.25", income_layer_allocations: { SCHD: 0.45, DGRO: 0.30, SGOV: 0.25 } }, { profile: "hk_global_etf_tactical_rotation", label: "HK Global ETF Tactical Rotation", label_en: "HK Global ETF Tactical Rotation", label_zh: "港股全球 ETF 战术轮动", domain: "hk_equity", runtime_enabled: true }, { profile: "hk_low_vol_dividend_quality_snapshot", label: "HK Low-Vol Dividend Quality Snapshot", label_en: "HK Low-Vol Dividend Quality Snapshot", label_zh: "港股低波红利质量快照", domain: "hk_equity", runtime_enabled: true }, + { profile: "cn_industry_etf_rotation", label: "CN Industry ETF Rotation", label_en: "CN Industry ETF Rotation", label_zh: "A股行业 ETF 轮动", domain: "cn_equity", runtime_enabled: true }, + { profile: "cn_dividend_quality_snapshot", label: "CN Dividend Quality Snapshot", label_en: "CN Dividend Quality Snapshot", label_zh: "A股红利质量快照", domain: "cn_equity", runtime_enabled: true }, ]; const localStrategyLabels = { @@ -1476,6 +1529,8 @@

切换摘要

russell_top50_leader_rotation: { zh: "罗素 Top50 领涨轮动", en: "Russell Top50 Leader Rotation" }, hk_global_etf_tactical_rotation: { zh: "港股全球 ETF 战术轮动", en: "HK Global ETF Tactical Rotation" }, hk_low_vol_dividend_quality_snapshot: { zh: "港股低波红利质量快照", en: "HK Low-Vol Dividend Quality Snapshot" }, + cn_industry_etf_rotation: { zh: "A股行业 ETF 轮动", en: "CN Industry ETF Rotation" }, + cn_dividend_quality_snapshot: { zh: "A股红利质量快照", en: "CN Dividend Quality Snapshot" }, }; const fallbackIncomeLayerDefaults = { @@ -1533,7 +1588,7 @@

切换摘要

}; let optionOverlayDefaults = {}; - const strategyDomains = ["us_equity", "hk_equity"]; + const strategyDomains = ["us_equity", "hk_equity", "cn_equity"]; let strategyOptions = []; let strategyLabels = {}; let strategyCatalog = {}; @@ -1551,6 +1606,9 @@

切换摘要

firstrade: [ { key: "preview", label: "Firstrade", target_name: "preview", supported_domains: ["us_equity"] }, ], + qmt: [ + { key: "preview", label: "QMT dry-run", target_name: "industry_etf_dry_run", cash_currency: "CNY", default_strategy_profile: "cn_industry_etf_rotation", supported_domains: ["cn_equity"] }, + ], }; const copy = { @@ -1619,6 +1677,13 @@

切换摘要

cashOnlyExecutionValueNo: "允许融资:否", currentCashOnlyExecution: "当前允许融资", pendingCashOnlyExecution: "待提交允许融资", + executionCashPolicyTitle: "现金与融资", + executionCashPolicyNote: "允许融资与预留现金覆盖不能同时生效;选「是」会清空预留覆盖,设预留覆盖会强制「否」。", + executionCashMarginBlocksReserve: "已选允许融资,预留现金覆盖已禁用。", + executionCashReserveBlocksMargin: "已设预留现金覆盖,不能选允许融资。", + qmtPlatformCashNote: "A 股 QMT 不使用 margin / 平台预留现金;现金约束在策略参数 execution_cash_reserve_ratio 内配置。", + qmtDryRunOnlyNote: "QMT 当前仅支持 dry-run(模拟),尚无 live 券商账号。", + invalidExecutionCashPolicyNote: "允许融资与预留现金覆盖冲突,请只保留一种约束。", dcaMode: "定投模式", dcaModeFixed: "定额定投", dcaModeSmart: "智能定投", @@ -1694,6 +1759,7 @@

切换摘要

strategyMeta: "该账号仅显示 {domains} 策略", usEquity: "美股", hkEquity: "港股", + cnEquity: "A股", currentStrategy: "当前策略", nextStrategy: "切换策略", notRead: "未读取", @@ -1770,6 +1836,13 @@

切换摘要

cashOnlyExecutionValueNo: "Allow margin: No", currentCashOnlyExecution: "Current allow margin", pendingCashOnlyExecution: "Pending allow margin", + executionCashPolicyTitle: "Cash and margin", + executionCashPolicyNote: "Allow margin and reserve-cash overrides cannot both apply. Yes clears reserve overrides; reserve overrides force No.", + executionCashMarginBlocksReserve: "Allow margin is selected; reserve-cash overrides are disabled.", + executionCashReserveBlocksMargin: "Reserve-cash override is active; allow margin Yes is disabled.", + qmtPlatformCashNote: "QMT A-share does not use margin or platform reserve cash; cash constraints live in strategy execution_cash_reserve_ratio.", + qmtDryRunOnlyNote: "QMT is dry-run only for now; no live broker accounts are configured.", + invalidExecutionCashPolicyNote: "Allow margin and reserve-cash overrides conflict. Keep only one constraint.", dcaMode: "DCA mode", dcaModeFixed: "Fixed DCA", dcaModeSmart: "Smart DCA", @@ -1845,6 +1918,7 @@

切换摘要

strategyMeta: "This account only shows {domains} strategies", usEquity: "US equity", hkEquity: "HK equity", + cnEquity: "CN A-share", currentStrategy: "Current strategy", nextStrategy: "Switch strategy", notRead: "Not read", @@ -1900,6 +1974,7 @@

切换摘要

ibkr: { accountKey: "preview", strategy: "", executionMode: "live", pluginMode: "auto", ...defaultReserveForm() }, schwab: { accountKey: "preview", strategy: "", executionMode: "live", pluginMode: "auto", ...defaultReserveForm() }, firstrade: { accountKey: "preview", strategy: "", executionMode: "live", pluginMode: "auto", ...defaultReserveForm() }, + qmt: { accountKey: "preview", strategy: "", executionMode: "paper", pluginMode: "auto", ...defaultReserveForm() }, }, }; @@ -1950,20 +2025,61 @@

切换摘要

function domainLabel(domain) { if (domain === "hk_equity") return t("hkEquity"); + if (domain === "cn_equity") return t("cnEquity"); return t("usEquity"); } + function platformSupportsMarginPolicy(platform = state.selected) { + return platform !== "qmt"; + } + + function platformSupportsReservedCashPolicy(platform = state.selected) { + return platform !== "qmt"; + } + + function platformDryRunOnly(platform = state.selected) { + return platform === "qmt"; + } + + function allowMarginExplicitlySelected(form) { + return normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode) === "disabled"; + } + + function reserveCashOverrideActive(form) { + const mode = normalizeReservePolicyMode(form?.reservePolicyMode); + return mode === "ratio" || mode === "floor" || mode === "max"; + } + + function executionCashPolicyConflict(form) { + return allowMarginExplicitlySelected(form) && reserveCashOverrideActive(form); + } + + function reconcileExecutionCashPolicy(form, changed) { + if (!form || !executionCashPolicyConflict(form)) return; + if (changed === "margin") { + form.reservePolicyMode = "none"; + form.reservedCashTouched = true; + form.minReservedCashUsd = ""; + form.reservedCashRatio = ""; + } else if (changed === "reserve") { + form.cashOnlyExecutionMode = "enabled"; + form.cashOnlyExecutionTouched = true; + } + } + function strategyDomain(profile) { return strategyCatalog[profile]?.domain || ""; } function selectedCashCurrency(platform = state.selected, account = selectedAccount(platform)) { const configured = String(account?.cash_currency || "").trim().toUpperCase(); - if (configured === "USD" || configured === "HKD") return configured; + if (configured === "USD" || configured === "HKD" || configured === "CNY") return configured; const domain = strategyDomain(state.forms[platform]?.strategy); if (domain === "hk_equity") return "HKD"; + if (domain === "cn_equity") return "CNY"; const supported = supportedDomainsForAccount(platform, account); if (supported.length === 1 && supported[0] === "hk_equity") return "HKD"; + if (supported.length === 1 && supported[0] === "cn_equity") return "CNY"; return "USD"; } @@ -2115,6 +2231,7 @@

切换摘要

function inferSupportedDomains(platform, account) { void account; + if (platform === "qmt") return ["cn_equity"]; if (platform === "longbridge" || platform === "ibkr") return ["us_equity", "hk_equity"]; return ["us_equity"]; } @@ -2474,6 +2591,7 @@

切换摘要

} function defaultExecutionModeForAccount(platform, account, fallback = "live") { + if (platformDryRunOnly(platform)) return "paper"; const currentMode = normalizeExecutionMode( currentEntryForAccount(platform, account)?.execution_mode, currentEntryForAccount(platform, account)?.dry_run_only, @@ -2487,7 +2605,7 @@

切换摘要

account?.account_scope, account?.service_name, ].join(" ").toLowerCase(); - if (hint.split(/\s+/).includes("paper") || hint.includes("-paper") || hint.includes("_paper")) { + if (hint.split(/\s+/).includes("paper") || hint.includes("-paper") || hint.includes("_paper") || hint.includes("dry_run") || hint.includes("dry-run")) { return "paper"; } return fallback; @@ -2506,9 +2624,11 @@

切换摘要

account?.account_scope, account?.service_name, ].join(" ").toLowerCase(); - const hinted = hint.includes("smart-dca") || hint.includes("smart_dca") + const hinted = hint.includes("dividend") + ? "cn_dividend_quality_snapshot" + : (hint.includes("industry") ? "cn_industry_etf_rotation" : (hint.includes("smart-dca") || hint.includes("smart_dca") ? "nasdaq_sp500_smart_dca" - : (hint.includes("soxl") ? "soxl_soxx_trend_income" : (hint.includes("tqqq") ? "tqqq_growth_income" : "")); + : (hint.includes("soxl") ? "soxl_soxx_trend_income" : (hint.includes("tqqq") ? "tqqq_growth_income" : "")))); if (hinted && strategyAllowedForAccount(platform, account, hinted)) return hinted; if (fallback && strategyAllowedForAccount(platform, account, fallback)) return fallback; return strategyChoicesForAccount(platform, account)[0] || ""; @@ -2626,6 +2746,7 @@

切换摘要

if (platform === "ibkr") return `interactive-brokers-${targetName.toLowerCase()}-service`; if (platform === "schwab") return "charles-schwab-quant-service"; if (platform === "firstrade") return "firstrade-quant-service"; + if (platform === "qmt") return "qmt-quant-service"; return ""; } @@ -2646,12 +2767,19 @@

切换摘要

} function hasValidReservePolicy(platform = state.selected) { + if (!platformSupportsReservedCashPolicy(platform)) return true; const form = state.forms[platform]; const mode = normalizeReservePolicyMode(form?.reservePolicyMode); if (mode === "current" || mode === "none") return true; return Boolean(reservePolicyOverrideForForm(form, platform)); } + function hasValidExecutionCashPolicy(platform = state.selected) { + if (!platformSupportsMarginPolicy(platform) && !platformSupportsReservedCashPolicy(platform)) return true; + const form = state.forms[platform]; + return !executionCashPolicyConflict(form) && hasValidReservePolicy(platform); + } + function hasValidIncomeLayerPolicy(platform = state.selected) { const form = state.forms[platform]; if (!incomeLayerSupported(form?.strategy)) return true; @@ -2684,7 +2812,7 @@

切换摘要

function hasValidStrategySelection(platform = state.selected) { return hasRunnableStrategySelection(platform) && - hasValidReservePolicy(platform) && + hasValidExecutionCashPolicy(platform) && hasValidIncomeLayerPolicy(platform) && hasValidOptionOverlayPolicy(platform) && hasValidDcaPolicy(platform) && @@ -2696,6 +2824,7 @@

切换摘要

} function reservePolicyOverrideForForm(form, platform = state.selected) { + if (!platformSupportsReservedCashPolicy(platform)) return null; const mode = normalizeReservePolicyMode(form?.reservePolicyMode); const floor = cleanDisplayNumber(form?.minReservedCashUsd); const ratio = cleanDisplayRatio(form?.reservedCashRatio); @@ -2760,7 +2889,8 @@

切换摘要

return { inputs: { option_overlay_mode: mode } }; } - function cashOnlyExecutionOverrideForForm(form) { + function cashOnlyExecutionOverrideForForm(form, platform = state.selected) { + if (!platformSupportsMarginPolicy(platform)) return null; const mode = normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode); if (mode === "current") return null; return { inputs: { cash_only_execution_mode: mode } }; @@ -2815,12 +2945,15 @@

切换摘要

]) { if (account[field]) inputs[field] = account[field]; } - const reserveOverride = reservePolicyOverrideForForm(form, platform); - const reserveMode = normalizeReservePolicyMode(form.reservePolicyMode); - inputs.reserved_cash_policy_mode = reserveMode; - if (reserveOverride) { - Object.assign(inputs, reserveOverride.inputs); - mergeExtraVariables(inputs, reserveOverride.extraVariables); + const reserveOverride = platformSupportsReservedCashPolicy(platform) + ? reservePolicyOverrideForForm(form, platform) + : null; + if (platformSupportsReservedCashPolicy(platform)) { + inputs.reserved_cash_policy_mode = normalizeReservePolicyMode(form.reservePolicyMode); + if (reserveOverride) { + Object.assign(inputs, reserveOverride.inputs); + mergeExtraVariables(inputs, reserveOverride.extraVariables); + } } const runtimeTargetOverride = runtimeTargetOverrideForForm(form); inputs.runtime_target_enabled_mode = normalizeRuntimeTargetMode(form.runtimeTargetMode); @@ -2839,10 +2972,12 @@

切换摘要

if (optionOverlayOverride) { Object.assign(inputs, optionOverlayOverride.inputs); } - const cashOnlyOverride = cashOnlyExecutionOverrideForForm(form); - inputs.cash_only_execution_mode = normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode); - if (cashOnlyOverride) { - Object.assign(inputs, cashOnlyOverride.inputs); + const cashOnlyOverride = cashOnlyExecutionOverrideForForm(form, platform); + if (platformSupportsMarginPolicy(platform)) { + inputs.cash_only_execution_mode = normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode); + if (cashOnlyOverride) { + Object.assign(inputs, cashOnlyOverride.inputs); + } } const dcaOverride = dcaOverrideForForm(form); if (dcaOverride) { @@ -3473,6 +3608,8 @@

切换摘要

el("ibit-zscore-exit-mode-meta").textContent = ""; } const incomeDefaults = incomeLayerDefaultForStrategy(form.strategy); + el("income-layer-section").hidden = platform === "qmt"; + el("option-overlay-section").hidden = platform === "qmt"; incomeLayerModeSelect.replaceChildren(); if (incomeDefaults) { incomeLayerModeSelect.disabled = false; @@ -3492,19 +3629,44 @@

切换摘要

el("income-layer-start-meta").textContent = t("incomeLayerStartMeta"); el("income-layer-ratio-meta").textContent = t("incomeLayerRatioMeta"); } - reservePolicyModeSelect.replaceChildren(); - for (const mode of reservePolicyModes) { - reservePolicyModeSelect.append(new Option(t(`reservePolicy${mode[0].toUpperCase()}${mode.slice(1)}`), mode, false, mode === normalizeReservePolicyMode(form.reservePolicyMode))); + const supportsMargin = platformSupportsMarginPolicy(platform); + const supportsReserve = platformSupportsReservedCashPolicy(platform); + const executionCashPolicyGrid = el("execution-cash-policy-grid"); + const qmtPlatformCashNote = el("qmt-platform-cash-note"); + const executionCashPolicyNote = el("execution-cash-policy-note"); + executionCashPolicyGrid.hidden = !supportsMargin && !supportsReserve; + qmtPlatformCashNote.hidden = supportsMargin || supportsReserve; + executionCashPolicyNote.hidden = !supportsMargin || !supportsReserve; + + const marginBlocksReserve = supportsMargin && supportsReserve && allowMarginExplicitlySelected(form); + const reserveBlocksMargin = supportsMargin && supportsReserve && reserveCashOverrideActive(form); + + if (supportsReserve) { + reservePolicyModeSelect.replaceChildren(); + for (const mode of reservePolicyModes) { + reservePolicyModeSelect.append(new Option(t(`reservePolicy${mode[0].toUpperCase()}${mode.slice(1)}`), mode, false, mode === normalizeReservePolicyMode(form.reservePolicyMode))); + } + const reserveMode = normalizeReservePolicyMode(form.reservePolicyMode); + el("min-reserved-cash-label").textContent = t("minReservedCash").replace( + "{currency}", + selectedCashCurrency(platform, account), + ); + reservePolicyModeSelect.disabled = marginBlocksReserve; + minReservedCashInput.disabled = marginBlocksReserve || reserveMode === "current" || reserveMode === "none" || reserveMode === "ratio"; + reservedCashRatioInput.disabled = marginBlocksReserve || reserveMode === "current" || reserveMode === "none" || reserveMode === "floor"; + minReservedCashInput.value = reserveMode === "ratio" || reserveMode === "none" ? "" : form.minReservedCashUsd; + reservedCashRatioInput.value = reserveMode === "floor" || reserveMode === "none" ? "" : form.reservedCashRatio; + el("reserve-policy-block").classList.toggle("policy-block-muted", marginBlocksReserve); + el("min-reserve-block").classList.toggle("policy-block-muted", marginBlocksReserve); + el("reserve-ratio-block").classList.toggle("policy-block-muted", marginBlocksReserve); + el("reserve-policy-mode-meta").textContent = marginBlocksReserve + ? t("executionCashMarginBlocksReserve") + : t("reservedCashModeMeta"); + } else { + reservePolicyModeSelect.replaceChildren(); + minReservedCashInput.value = ""; + reservedCashRatioInput.value = ""; } - const reserveMode = normalizeReservePolicyMode(form.reservePolicyMode); - el("min-reserved-cash-label").textContent = t("minReservedCash").replace( - "{currency}", - selectedCashCurrency(platform, account), - ); - minReservedCashInput.disabled = reserveMode === "current" || reserveMode === "none" || reserveMode === "ratio"; - reservedCashRatioInput.disabled = reserveMode === "current" || reserveMode === "none" || reserveMode === "floor"; - minReservedCashInput.value = reserveMode === "ratio" || reserveMode === "none" ? "" : form.minReservedCashUsd; - reservedCashRatioInput.value = reserveMode === "floor" || reserveMode === "none" ? "" : form.reservedCashRatio; const incomeMode = normalizeIncomeLayerMode(form.incomeLayerMode); const incomeLayerInputsDisabled = !incomeDefaults || incomeMode === "disabled"; incomeLayerStartUsdInput.disabled = incomeLayerInputsDisabled; @@ -3541,19 +3703,27 @@

切换摘要

el("option-overlay-mode-meta").textContent = t("optionOverlayModeMeta"); } - syncCashOnlyExecutionForAccount(platform); - cashOnlyExecutionModeSelect.replaceChildren(); - for (const mode of cashOnlyExecutionModes) { - cashOnlyExecutionModeSelect.append( - new Option( + if (supportsMargin) { + syncCashOnlyExecutionForAccount(platform); + cashOnlyExecutionModeSelect.replaceChildren(); + for (const mode of cashOnlyExecutionModes) { + const option = new Option( cashOnlyExecutionModeLabel(mode), mode, false, mode === normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode), - ), - ); + ); + if (mode === "disabled" && reserveBlocksMargin) option.disabled = true; + cashOnlyExecutionModeSelect.append(option); + } + el("cash-only-policy-block").classList.toggle("policy-block-muted", reserveBlocksMargin); + el("cash-only-execution-mode-meta").textContent = reserveBlocksMargin + ? t("executionCashReserveBlocksMargin") + : t("cashOnlyExecutionModeMeta"); + } else { + cashOnlyExecutionModeSelect.replaceChildren(); + el("cash-only-execution-mode-meta").textContent = ""; } - el("cash-only-execution-mode-meta").textContent = t("cashOnlyExecutionModeMeta"); const dcaDefaults = dcaConfigForStrategy(form.strategy); dcaModeSelect.replaceChildren(); @@ -3580,9 +3750,15 @@

切换摘要

el("dca-base-meta").textContent = t("dcaModeMeta"); } - document.querySelectorAll("[data-mode]").forEach((button) => { + if (platformDryRunOnly(platform)) { + form.executionMode = "paper"; + } + document.querySelectorAll("#mode-control [data-mode]").forEach((button) => { + const dryRunOnly = platformDryRunOnly(platform); + button.disabled = dryRunOnly && button.dataset.mode === "live"; button.classList.toggle("active", button.dataset.mode === form.executionMode); }); + el("mode-meta").textContent = platformDryRunOnly(platform) ? t("qmtDryRunOnlyNote") : ""; } function renderSummary() { @@ -3640,7 +3816,7 @@

切换摘要

const hasPrivateAccounts = state.configSource === "private"; const loadingConfig = state.configSource === "loading"; const hasRunnableStrategy = hasRunnableStrategySelection(); - const hasValidReserve = hasValidReservePolicy(); + const hasValidReserve = hasValidExecutionCashPolicy(); const hasValidIncomeLayer = hasValidIncomeLayerPolicy(); const hasValidOptionOverlay = hasValidOptionOverlayPolicy(); const hasValidDca = hasValidDcaPolicy(); @@ -3672,7 +3848,9 @@

切换摘要

: t("invalidDcaNote")) : t("invalidOptionOverlayNote")) : t("invalidIncomeLayerNote")) - : t("invalidReservePolicyNote")) + : (executionCashPolicyConflict(state.forms[state.selected]) + ? t("invalidExecutionCashPolicyNote") + : t("invalidReservePolicyNote"))) : t("invalidStrategyNote")) : t("missingConfigNote"))) : t("readonlyNote"); @@ -3942,7 +4120,8 @@

切换摘要

el("mode-control").addEventListener("click", (event) => { const button = event.target.closest("[data-mode]"); - if (!button) return; + if (!button || button.disabled) return; + if (platformDryRunOnly(state.selected) && button.dataset.mode === "live") return; state.forms[state.selected].executionMode = button.dataset.mode; render(); }); @@ -4007,6 +4186,7 @@

切换摘要

const form = state.forms[state.selected]; form.cashOnlyExecutionMode = normalizeCashOnlyExecutionMode(el("cash-only-execution-mode-select").value); form.cashOnlyExecutionTouched = form.cashOnlyExecutionMode !== "current"; + reconcileExecutionCashPolicy(form, "margin"); render(); }); @@ -4029,6 +4209,7 @@

切换摘要

form.reservePolicyMode = normalizeReservePolicyMode(el("reserve-policy-mode-select").value); form.reservedCashTouched = form.reservePolicyMode !== "current"; if (form.reservePolicyMode === "current") syncReservePolicyForAccount(state.selected); + reconcileExecutionCashPolicy(form, "reserve"); render(); }); diff --git a/web/strategy-switch-console/page_asset.js b/web/strategy-switch-console/page_asset.js index e3acc65..46251e1 100644 --- a/web/strategy-switch-console/page_asset.js +++ b/web/strategy-switch-console/page_asset.js @@ -1,2 +1,2 @@ // Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand. -export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch\n \n \n\n\n
\n
\n

策略切换

\n

选平台、目标账号和策略,一次执行完成切换。

\n
\n
\n \n \n \n \n
\n
\n\n
\n
\n 初始化控制台\n

读取策略配置

\n

正在读取登录状态、账号配置和当前状态。

\n
\n
\n
\n\n
\n \n\n
\n
\n
\n 当前平台\n

LongBridge

\n
\n\n \n\n \n\n
\n \n\n \n\n
\n 模式\n
\n \n \n
\n
\n\n
\n \n\n \n\n \n
\n\n
\n \n\n \n\n \n
\n\n
\n \n
\n\n
\n \n
\n\n
\n \n\n \n
\n\n
\n \n\n \n\n \n
\n
\n\n
\n \n

登录后才可执行切换。

\n

\n
\n
\n\n \n
\n
\n\n \n\n\n"; +export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch\n \n \n\n\n
\n
\n

策略切换

\n

选平台、目标账号和策略,一次执行完成切换。

\n
\n
\n \n \n \n \n
\n
\n\n
\n
\n 初始化控制台\n

读取策略配置

\n

正在读取登录状态、账号配置和当前状态。

\n
\n
\n
\n\n
\n \n\n
\n
\n
\n 当前平台\n

LongBridge

\n
\n\n \n\n \n\n
\n \n\n \n\n
\n 模式\n
\n \n \n
\n \n
\n\n
\n \n\n \n\n \n
\n\n
\n \n\n \n\n \n
\n\n
\n \n
\n\n
\n

现金与融资

\n

允许融资与预留现金覆盖不能同时生效。

\n \n
\n \n\n \n\n \n\n \n
\n
\n\n
\n \n\n \n
\n\n
\n
\n\n
\n \n

登录后才可执行切换。

\n

\n
\n
\n\n \n
\n
\n\n \n\n\n"; diff --git a/web/strategy-switch-console/strategy-profiles.example.json b/web/strategy-switch-console/strategy-profiles.example.json index 8d0f9b4..d37d499 100644 --- a/web/strategy-switch-console/strategy-profiles.example.json +++ b/web/strategy-switch-console/strategy-profiles.example.json @@ -137,5 +137,21 @@ "label_zh": "港股低波红利质量快照", "domain": "hk_equity", "runtime_enabled": true + }, + { + "profile": "cn_industry_etf_rotation", + "label": "CN Industry ETF Rotation", + "label_en": "CN Industry ETF Rotation", + "label_zh": "A股行业 ETF 轮动", + "domain": "cn_equity", + "runtime_enabled": true + }, + { + "profile": "cn_dividend_quality_snapshot", + "label": "CN Dividend Quality Snapshot", + "label_en": "CN Dividend Quality Snapshot", + "label_zh": "A股红利质量快照", + "domain": "cn_equity", + "runtime_enabled": true } ] diff --git a/web/strategy-switch-console/strategy_profiles_asset.js b/web/strategy-switch-console/strategy_profiles_asset.js index 18a4a20..60c892c 100644 --- a/web/strategy-switch-console/strategy_profiles_asset.js +++ b/web/strategy-switch-console/strategy_profiles_asset.js @@ -1,2 +1,2 @@ // Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand. -export const DEFAULT_STRATEGY_PROFILES = [{"profile": "tqqq_growth_income", "label": "TQQQ Growth Income", "label_en": "TQQQ Growth Income", "label_zh": "TQQQ 增长收益", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "250000", "income_layer_max_ratio": "0.55", "income_layer_allocations": {"SCHD": 0.3, "DGRO": 0.2, "SGOV": 0.4, "SPYI": 0.08, "QQQI": 0.02}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "tqqq_leaps_growth_v1", "option_growth_overlay_start_usd": "250000", "option_growth_overlay_nav_budget_ratio": "0.03", "option_income_overlay_enabled": false}, {"profile": "soxl_soxx_trend_income", "label": "SOXL/SOXX Semiconductor Trend Income", "label_en": "SOXL/SOXX Semiconductor Trend Income", "label_zh": "SOXL/SOXX 半导体趋势收益", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "150000", "income_layer_max_ratio": "0.95", "income_layer_allocations": {"SCHD": 0.15, "DGRO": 0.1, "SGOV": 0.7, "SPYI": 0.04, "QQQI": 0.01}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": false, "option_income_overlay_enabled": true, "option_income_overlay_recipe": "soxx_put_credit_spread_income_v1", "option_income_overlay_start_usd": "150000", "option_income_overlay_nav_risk_ratio": "0.01"}, {"profile": "nasdaq_sp500_smart_dca", "label": "Nasdaq 100 / S&P 500 DCA", "label_en": "Nasdaq 100 / S&P 500 DCA", "label_zh": "纳指100 / 标普500 定投", "domain": "us_equity", "runtime_enabled": true, "dca_enabled": true, "dca_default_mode": "fixed", "dca_default_base_investment_usd": "1000"}, {"profile": "ibit_smart_dca", "label": "IBIT Bitcoin ETF DCA", "label_en": "IBIT Bitcoin ETF DCA", "label_zh": "IBIT 比特币定投", "domain": "us_equity", "runtime_enabled": true, "dca_enabled": true, "dca_default_mode": "fixed", "dca_default_base_investment_usd": "1000"}, {"profile": "global_etf_rotation", "label": "Global ETF Rotation", "label_en": "Global ETF Rotation", "label_zh": "全球 ETF 轮动", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "500000", "income_layer_max_ratio": "0.15", "income_layer_allocations": {"SCHD": 0.4, "DGRO": 0.25, "SGOV": 0.3, "SPYI": 0.05}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "500000", "option_growth_overlay_nav_budget_ratio": "0.015", "option_income_overlay_enabled": false}, {"profile": "russell_top50_leader_rotation", "label": "Russell Top50 Leader Rotation", "label_en": "Russell Top50 Leader Rotation", "label_zh": "罗素 Top50 领涨轮动", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "300000", "income_layer_max_ratio": "0.25", "income_layer_allocations": {"SCHD": 0.45, "DGRO": 0.3, "SGOV": 0.25}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "300000", "option_growth_overlay_nav_budget_ratio": "0.015", "option_income_overlay_enabled": false}, {"profile": "hk_global_etf_tactical_rotation", "label": "HK Global ETF Tactical Rotation", "label_en": "HK Global ETF Tactical Rotation", "label_zh": "港股全球 ETF 战术轮动", "domain": "hk_equity", "runtime_enabled": true}, {"profile": "hk_low_vol_dividend_quality_snapshot", "label": "HK Low-Vol Dividend Quality Snapshot", "label_en": "HK Low-Vol Dividend Quality Snapshot", "label_zh": "港股低波红利质量快照", "domain": "hk_equity", "runtime_enabled": true}]; +export const DEFAULT_STRATEGY_PROFILES = [{"profile": "tqqq_growth_income", "label": "TQQQ Growth Income", "label_en": "TQQQ Growth Income", "label_zh": "TQQQ 增长收益", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "250000", "income_layer_max_ratio": "0.55", "income_layer_allocations": {"SCHD": 0.3, "DGRO": 0.2, "SGOV": 0.4, "SPYI": 0.08, "QQQI": 0.02}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "tqqq_leaps_growth_v1", "option_growth_overlay_start_usd": "250000", "option_growth_overlay_nav_budget_ratio": "0.03", "option_income_overlay_enabled": false}, {"profile": "soxl_soxx_trend_income", "label": "SOXL/SOXX Semiconductor Trend Income", "label_en": "SOXL/SOXX Semiconductor Trend Income", "label_zh": "SOXL/SOXX 半导体趋势收益", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "150000", "income_layer_max_ratio": "0.95", "income_layer_allocations": {"SCHD": 0.15, "DGRO": 0.1, "SGOV": 0.7, "SPYI": 0.04, "QQQI": 0.01}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": false, "option_income_overlay_enabled": true, "option_income_overlay_recipe": "soxx_put_credit_spread_income_v1", "option_income_overlay_start_usd": "150000", "option_income_overlay_nav_risk_ratio": "0.01"}, {"profile": "nasdaq_sp500_smart_dca", "label": "Nasdaq 100 / S&P 500 DCA", "label_en": "Nasdaq 100 / S&P 500 DCA", "label_zh": "纳指100 / 标普500 定投", "domain": "us_equity", "runtime_enabled": true, "dca_enabled": true, "dca_default_mode": "fixed", "dca_default_base_investment_usd": "1000"}, {"profile": "ibit_smart_dca", "label": "IBIT Bitcoin ETF DCA", "label_en": "IBIT Bitcoin ETF DCA", "label_zh": "IBIT 比特币定投", "domain": "us_equity", "runtime_enabled": true, "dca_enabled": true, "dca_default_mode": "fixed", "dca_default_base_investment_usd": "1000"}, {"profile": "global_etf_rotation", "label": "Global ETF Rotation", "label_en": "Global ETF Rotation", "label_zh": "全球 ETF 轮动", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "500000", "income_layer_max_ratio": "0.15", "income_layer_allocations": {"SCHD": 0.4, "DGRO": 0.25, "SGOV": 0.3, "SPYI": 0.05}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "500000", "option_growth_overlay_nav_budget_ratio": "0.015", "option_income_overlay_enabled": false}, {"profile": "russell_top50_leader_rotation", "label": "Russell Top50 Leader Rotation", "label_en": "Russell Top50 Leader Rotation", "label_zh": "罗素 Top50 领涨轮动", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "300000", "income_layer_max_ratio": "0.25", "income_layer_allocations": {"SCHD": 0.45, "DGRO": 0.3, "SGOV": 0.25}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "300000", "option_growth_overlay_nav_budget_ratio": "0.015", "option_income_overlay_enabled": false}, {"profile": "hk_global_etf_tactical_rotation", "label": "HK Global ETF Tactical Rotation", "label_en": "HK Global ETF Tactical Rotation", "label_zh": "港股全球 ETF 战术轮动", "domain": "hk_equity", "runtime_enabled": true}, {"profile": "hk_low_vol_dividend_quality_snapshot", "label": "HK Low-Vol Dividend Quality Snapshot", "label_en": "HK Low-Vol Dividend Quality Snapshot", "label_zh": "港股低波红利质量快照", "domain": "hk_equity", "runtime_enabled": true}, {"profile": "cn_industry_etf_rotation", "label": "CN Industry ETF Rotation", "label_en": "CN Industry ETF Rotation", "label_zh": "A股行业 ETF 轮动", "domain": "cn_equity", "runtime_enabled": true}, {"profile": "cn_dividend_quality_snapshot", "label": "CN Dividend Quality Snapshot", "label_en": "CN Dividend Quality Snapshot", "label_zh": "A股红利质量快照", "domain": "cn_equity", "runtime_enabled": true}]; diff --git a/web/strategy-switch-console/worker.js b/web/strategy-switch-console/worker.js index 84aa3af..614666a 100644 --- a/web/strategy-switch-console/worker.js +++ b/web/strategy-switch-console/worker.js @@ -1055,6 +1055,9 @@ function normalizeSwitchInputs(raw) { const targetName = cleanSlug(raw.target_name, "target_name"); const strategyProfile = cleanSlug(raw.strategy_profile, "strategy_profile").toLowerCase(); const executionMode = cleanChoice(raw.execution_mode || "live", ["live", "paper"], "execution_mode"); + if (platform === "qmt" && executionMode === "live") { + throw new Error("QMT platform does not support live execution yet; use paper/dry_run mode"); + } const pluginMode = cleanChoice(raw.plugin_mode || "auto", ["auto", "none", "custom"], "plugin_mode"); const optionOverlayMode = cleanChoice(raw.option_overlay_mode || "current", OPTION_OVERLAY_MODES, "option_overlay_mode"); const cashOnlyExecutionMode = cleanChoice( @@ -1541,6 +1544,7 @@ function supportedDomainsForAccount(platform, option) { function inferAccountSupportedDomains(platform, option) { void option; + if (platform === "qmt") return ["cn_equity"]; if (platform === "longbridge" || platform === "ibkr") return ["us_equity", "hk_equity"]; return ["us_equity"]; } From fde1b4abe4f12677f15c773c07991a8abf7f7d41 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Sun, 28 Jun 2026 00:45:15 +0800 Subject: [PATCH 2/2] Sync internal dependency matrix with current platform consumer pins. Align QPK, UES, and HKE refs checked by CI with sibling repo requirements. Co-authored-by: Cursor --- internal_dependency_matrix.json | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) 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",