From 5def960ff0d655e6c587c32b0eec574fa66f0a12 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Wed, 8 Jul 2026 05:10:31 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20remove=20default=5Fstrategy=5Fprofi?= =?UTF-8?q?le=20=E2=80=94=20single=20source=20of=20truth=20is=20GitHub=20v?= =?UTF-8?q?ariables?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: default_strategy_profile in platform-config.json created a false impression that changing it would change the running strategy. In reality, the runtime reads RUNTIME_TARGET_JSON from GitHub variables, and the two sources could diverge silently (LongBridge TQQQ bug). Changes: - platform-config.json: delete default_strategy_profile from all 6 platforms - build_config.py: delete report_default_strategy_profile_drift() and gate - build_platform_config.py / inject_platform_config.py: stop generating it - app.js: simplify defaultStrategyForAccount → reads only GitHub variables; remove hardcoded fallback; remove account.default_strategy_profile reads - worker.js: remove default_strategy_profile from KV sync/clean logic - account-options.example.json: remove all default_strategy_profile fields - tests: delete 6 test methods; update assertions and test data - docs: update README.md / README.zh-CN.md / qmt README - Regenerated: config.js, index.html, page_asset.js, app_js.js Architecture after: one platform = one strategy source = GitHub variable RUNTIME_TARGET_JSON. platform-config.json owns capabilities and deployment config only. No more silent divergence between config and runtime. Co-Authored-By: Claude --- examples/targets/qmt/README.zh-CN.md | 2 +- platform-config.json | 14 +-- python/scripts/build_config.py | 56 ----------- python/scripts/build_platform_config.py | 1 - python/scripts/inject_platform_config.py | 1 - python/tests/test_runtime_settings.py | 92 ------------------- tests/strategy_switch_worker_validation.mjs | 10 +- tests/test_cash_financing.js | 11 +-- web/strategy-switch-console/README.md | 7 +- web/strategy-switch-console/README.zh-CN.md | 7 +- .../account-options.example.json | 11 --- web/strategy-switch-console/app.js | 54 ++--------- web/strategy-switch-console/app_js.js | 2 +- web/strategy-switch-console/config.js | 6 -- web/strategy-switch-console/index.html | 2 +- web/strategy-switch-console/page_asset.js | 2 +- web/strategy-switch-console/worker.js | 5 - 17 files changed, 28 insertions(+), 255 deletions(-) diff --git a/examples/targets/qmt/README.zh-CN.md b/examples/targets/qmt/README.zh-CN.md index a19cff6..fec6273 100644 --- a/examples/targets/qmt/README.zh-CN.md +++ b/examples/targets/qmt/README.zh-CN.md @@ -34,7 +34,7 @@ - `service_name`:`qmt-quant-service` - `cash_currency`:`CNY` - `supported_domains`:`["cn_equity"]` -- `default_strategy_profile`:与该 target 默认策略一致 +- 策略 profile 通过 GitHub 变量 `RUNTIME_TARGET_JSON.strategy_profile` 管理,不存储在账号配置中 **不要**在账号配置里放 miniQMT 密码、券商 token 或本机路径;fixture 路径走 QmtPlatform 仓库变量(如 `QMT_MARKET_HISTORY_PATH`)。 diff --git a/platform-config.json b/platform-config.json index 8d4ec86..8917500 100644 --- a/platform-config.json +++ b/platform-config.json @@ -51,8 +51,7 @@ "hk_equity" ], "cash_currency": "USD", - "default_execution_mode": "live", - "default_strategy_profile": "soxl_soxx_trend_income" + "default_execution_mode": "live" }, "deployment": { "default_execution_mode": "live", @@ -91,8 +90,7 @@ "hk_equity" ], "cash_currency": "USD", - "default_execution_mode": "live", - "default_strategy_profile": "soxl_soxx_trend_income" + "default_execution_mode": "live" }, "deployment": { "default_execution_mode": "live", @@ -129,8 +127,7 @@ "us_equity" ], "cash_currency": "USD", - "default_execution_mode": "live", - "default_strategy_profile": "soxl_soxx_trend_income" + "default_execution_mode": "live" }, "deployment": { "default_execution_mode": "live", @@ -167,8 +164,7 @@ "us_equity" ], "cash_currency": "USD", - "default_execution_mode": "live", - "default_strategy_profile": "ibit_smart_dca" + "default_execution_mode": "live" }, "deployment": { "default_execution_mode": "live", @@ -202,7 +198,6 @@ "label": "QMT", "target_name": "default", "cash_currency": "CNY", - "default_strategy_profile": "cn_industry_etf_rotation", "supported_domains": [ "cn_equity" ] @@ -239,7 +234,6 @@ "label": "Binance", "target_name": "default", "cash_currency": "USD", - "default_strategy_profile": "crypto_live_pool_rotation", "supported_domains": [ "crypto" ] diff --git a/python/scripts/build_config.py b/python/scripts/build_config.py index 41c355b..79c69c0 100644 --- a/python/scripts/build_config.py +++ b/python/scripts/build_config.py @@ -52,23 +52,6 @@ def validate(config: dict) -> list[str]: errors.append(f"platform {pid}: missing default_account") if "supported_domains" not in pdata: errors.append(f"platform {pid}: missing supported_domains") - default_profile = pdata.get("default_account", {}).get("default_strategy_profile") - if default_profile: - strategy = config.get("strategies", {}).get(default_profile) - if not isinstance(strategy, dict): - errors.append(f"platform {pid}: default_strategy_profile {default_profile} is unknown") - else: - if strategy.get("domain") not in pdata.get("supported_domains", []): - errors.append(f"platform {pid}: default_strategy_profile {default_profile} domain is unsupported") - if strategy.get("runtime_enabled") is not True: - errors.append(f"platform {pid}: default_strategy_profile {default_profile} is not runtime_enabled") - if strategy.get("can_switch_live") is not True: - errors.append(f"platform {pid}: default_strategy_profile {default_profile} cannot switch live") - lifecycle_stage = str(strategy.get("lifecycle_stage") or "").strip() - if lifecycle_stage != "runtime_enabled": - errors.append( - f"platform {pid}: default_strategy_profile {default_profile} lifecycle_stage is not runtime_enabled" - ) for sid, sdata in config.get("strategies", {}).items(): if "domain" not in sdata: errors.append(f"strategy {sid}: missing domain") @@ -100,36 +83,6 @@ def _strategy_catalog_by_profile(strategy_catalog: object | None) -> dict[str, d return catalog -def report_default_strategy_profile_drift(config: dict, strategy_catalog: object | None = None) -> list[str]: - """Report whether platform default_strategy_profile entries drift from the strategy catalog.""" - errors: list[str] = [] - catalog = _strategy_catalog_by_profile(strategy_catalog) - - for pid, pdata in config.get("platforms", {}).items(): - default_profile = pdata.get("default_account", {}).get("default_strategy_profile") - if not default_profile: - continue - - strategy = catalog.get(default_profile) - if not isinstance(strategy, dict): - errors.append( - f"platform {pid}: default_strategy_profile {default_profile} missing from strategy_profiles" - ) - continue - - if strategy.get("runtime_enabled") is not True: - errors.append(f"platform {pid}: default_strategy_profile {default_profile} is not runtime_enabled") - if strategy.get("can_switch_live") is not True: - errors.append(f"platform {pid}: default_strategy_profile {default_profile} cannot switch live") - lifecycle_stage = str(strategy.get("lifecycle_stage") or "").strip() - if lifecycle_stage != "runtime_enabled": - errors.append( - f"platform {pid}: default_strategy_profile {default_profile} lifecycle_stage is not runtime_enabled" - ) - - return errors - - def build_live_candidate_queue(strategy_catalog: object | None = None) -> list[dict[str, object]]: """Build a control-plane queue of profiles that may need live-promotion review.""" catalog = _strategy_catalog_by_profile(strategy_catalog) @@ -282,7 +235,6 @@ def build_platform_health_report( config = config if config is not None else load_config() catalog = _strategy_catalog_by_profile(strategy_catalog) config_errors = validate(config) - default_profile_errors = report_default_strategy_profile_drift(config, catalog) derivation_errors = report_strategy_profile_derivation_drift(config, catalog) live_candidate_queue = build_live_candidate_queue(catalog) automation_registry = build_strategy_automation_registry(config) @@ -298,12 +250,6 @@ def build_platform_health_report( "severity": "critical", "messages": config_errors, }, - { - "name": "default_strategy_profile_gate", - "status": "fail" if default_profile_errors else "pass", - "severity": "critical", - "messages": default_profile_errors, - }, { "name": "strategy_profile_derivation", "status": "fail" if derivation_errors else "pass", @@ -595,8 +541,6 @@ def main() -> int: return 0 errors = validate(config) - if args.check: - errors.extend(report_default_strategy_profile_drift(config)) if errors: print("Validation ERRORS:") for e in errors: diff --git a/python/scripts/build_platform_config.py b/python/scripts/build_platform_config.py index 8caf3fc..c87f24d 100644 --- a/python/scripts/build_platform_config.py +++ b/python/scripts/build_platform_config.py @@ -62,7 +62,6 @@ def build_config_module(config: dict) -> str: "cash_currency": acct.get("cash_currency", "USD"), } for fld in ( - "default_strategy_profile", "service_name", "account_scope", "deployment_selector", diff --git a/python/scripts/inject_platform_config.py b/python/scripts/inject_platform_config.py index 03ba7eb..7ab874e 100644 --- a/python/scripts/inject_platform_config.py +++ b/python/scripts/inject_platform_config.py @@ -42,7 +42,6 @@ def main() -> int: cash_currency=acct.get("cash_currency", "USD"), ) for fld in ( - "default_strategy_profile", "service_name", "account_scope", "deployment_selector", diff --git a/python/tests/test_runtime_settings.py b/python/tests/test_runtime_settings.py index f20c3ce..6b77231 100644 --- a/python/tests/test_runtime_settings.py +++ b/python/tests/test_runtime_settings.py @@ -92,80 +92,6 @@ def test_manual_switch_platform_choices_cover_supported_platforms(self): self.assertEqual(set(platform_choices), set(runtime_settings.SUPPORTED_PLATFORMS)) - def test_platform_config_default_strategy_profiles_exist(self): - config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8")) - profiles = set(config["strategies"]) - for platform, data in config["platforms"].items(): - default_profile = data.get("default_account", {}).get("default_strategy_profile") - if default_profile: - with self.subTest(platform=platform): - self.assertIn(default_profile, profiles) - - def test_platform_config_default_strategy_profiles_are_live_switchable(self): - config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8")) - for platform, data in config["platforms"].items(): - default_profile = data.get("default_account", {}).get("default_strategy_profile") - if not default_profile: - continue - strategy = config["strategies"][default_profile] - with self.subTest(platform=platform): - self.assertTrue(strategy["runtime_enabled"]) - self.assertTrue(strategy["can_switch_live"]) - self.assertEqual(strategy["lifecycle_stage"], "runtime_enabled") - - def test_default_strategy_profile_catalog_matches_platform_gate_fields(self): - config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8")) - catalog = json.loads( - (ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json").read_text(encoding="utf-8") - ) - - self.assertEqual(build_config.report_default_strategy_profile_drift(config, catalog), []) - - def test_default_strategy_profile_catalog_reports_gate_drift(self): - config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8")) - catalog = { - item["profile"]: item - for item in json.loads( - (ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json").read_text( - encoding="utf-8" - ) - ) - } - profile = config["platforms"]["longbridge"]["default_account"]["default_strategy_profile"] - catalog[profile] = { - **catalog[profile], - "runtime_enabled": False, - "can_switch_live": False, - "lifecycle_stage": "research_backtest_only", - } - - errors = build_config.report_default_strategy_profile_drift(config, catalog) - - self.assertIn( - "platform longbridge: default_strategy_profile soxl_soxx_trend_income is not runtime_enabled", - errors, - ) - self.assertIn( - "platform longbridge: default_strategy_profile soxl_soxx_trend_income cannot switch live", - errors, - ) - self.assertIn( - "platform longbridge: default_strategy_profile soxl_soxx_trend_income lifecycle_stage is not runtime_enabled", - errors, - ) - - def test_build_config_check_includes_default_strategy_profile_drift(self): - with ( - patch.object(sys, "argv", ["build_config.py", "--check"]), - patch.object(build_config, "load_config", return_value={"platforms": {}, "strategies": {}}), - patch.object(build_config, "validate", return_value=[]), - patch.object(build_config, "report_default_strategy_profile_drift", return_value=["drift"]) as drift, - patch("builtins.print"), - ): - self.assertEqual(build_config.main(), 1) - - drift.assert_called_once_with({"platforms": {}, "strategies": {}}) - def test_live_candidate_queue_lists_profiles_needing_promotion_review(self): catalog = [ { @@ -206,7 +132,6 @@ def test_live_candidate_queue_cli_outputs_json_only(self): patch.object(sys, "argv", ["build_config.py", "--live-candidate-queue"]), patch.object(build_config, "load_config", return_value={"platforms": {}, "strategies": {}}), patch.object(build_config, "validate", return_value=[]), - patch.object(build_config, "report_default_strategy_profile_drift", return_value=[]), patch.object(build_config, "build_live_candidate_queue", return_value=[{"profile": "candidate"}]), patch("builtins.print") as printed, ): @@ -281,23 +206,6 @@ def test_platform_health_report_summarizes_current_config(self): self.assertIn("automation_lane_counts", report["summary"]) self.assertIn("python3 python/scripts/build_config.py --check", report["codex_repair_context"]["suggested_commands"]) - def test_platform_health_report_fails_on_default_profile_drift(self): - config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8")) - catalog = json.loads( - (ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json").read_text(encoding="utf-8") - ) - profile = config["platforms"]["longbridge"]["default_account"]["default_strategy_profile"] - for item in catalog: - if item["profile"] == profile: - item["can_switch_live"] = False - break - - report = build_config.build_platform_health_report(config, catalog) - - self.assertEqual(report["status"], "unhealthy") - self.assertEqual(report["recommended_action"], "attempt_codex_fix") - self.assertTrue(report["codex_repair_context"]["safe_to_attempt"]) - def test_platform_health_report_cli_outputs_json(self): report = { "schema_version": "platform_health_report.v1", diff --git a/tests/strategy_switch_worker_validation.mjs b/tests/strategy_switch_worker_validation.mjs index 4dc2951..e37e324 100644 --- a/tests/strategy_switch_worker_validation.mjs +++ b/tests/strategy_switch_worker_validation.mjs @@ -426,7 +426,6 @@ const accountOptions = __test.normalizeAccountOptionsPayload( label: "hk", target_name: "hk", account_selector: "HK", - default_strategy_profile: "hk_low_vol_dividend_quality_snapshot", cash_currency: "HKD", }, { @@ -434,7 +433,6 @@ const accountOptions = __test.normalizeAccountOptionsPayload( label: "sg", target_name: "sg", account_selector: "SG", - default_strategy_profile: "tqqq_growth_income", }, ], ibkr: [ @@ -848,9 +846,7 @@ const updatedAccountOptions = __test.updateAccountOptionsDefaultStrategy( plugin_mode: "auto", }, ); -assert.equal(updatedAccountOptions.changed, true); -assert.equal(updatedAccountOptions.options.longbridge[1].default_strategy_profile, "soxl_soxx_trend_income"); - +// default_strategy_profile update removed — only other fields may change assert.equal( __test.accountOptionMatchesInputs( { target_name: "sg", variable_scope: "default" }, @@ -923,7 +919,6 @@ const updatedIbitZscoreModeOptions = __test.updateAccountOptionsDefaultStrategy( { key: "ibit-primary", target_name: "ibit-primary", - default_strategy_profile: "ibit_smart_dca", supported_domains: ["us_equity"], ibit_zscore_exit_mode: "live", }, @@ -962,8 +957,7 @@ const syncResult = await __test.syncDefaultStrategyForAccount( { login: "pigbibi" }, ); assert.equal(syncResult.synced, true); -assert.equal(syncResult.changed, true); -assert.equal(JSON.parse(kvWrites.get("account_options")).longbridge[1].default_strategy_profile, "soxl_soxx_trend_income"); +// default_strategy_profile is no longer persisted to KV after switch const originalFetch = globalThis.fetch; globalThis.fetch = async (url) => { diff --git a/tests/test_cash_financing.js b/tests/test_cash_financing.js index aeac0e5..1b9a9f7 100644 --- a/tests/test_cash_financing.js +++ b/tests/test_cash_financing.js @@ -186,7 +186,7 @@ function currentEntryForAccount(state, platform, account) { const entry = byPlatform[key]; if (entry && typeof entry === "object") { if (!entry.strategy_profile) { - return { ...entry, strategy_profile: account?.default_strategy_profile || "", source: "worker+account_defaults" }; + return entry; } return entry; } @@ -198,11 +198,7 @@ function currentEntryForAccount(state, platform, account) { const hasMatch = rawCandidates.some((candidate) => candidates.has(candidate)); if (hasMatch) { if (!entry.strategy_profile) { - return { - ...entry, - strategy_profile: account?.default_strategy_profile || "", - source: "worker+account_defaults", - }; + return entry; } return entry; } @@ -210,7 +206,7 @@ function currentEntryForAccount(state, platform, account) { return { runtime_target_enabled: true, - strategy_profile: account?.default_strategy_profile || account?.strategy_profile || "", + strategy_profile: "", source: "account_defaults", }; } @@ -1133,7 +1129,6 @@ console.log("\n=== 11. currentEntryForAccount 映射健壮性 ===\n"); key: "sg", target_name: "sg", label: "sg", - default_strategy_profile: "tqqq_growth_income", }; const entry = currentEntryForAccount(state, "longbridge", account); assert(Boolean(entry), "11d: missing entry should return synthesized defaults"); diff --git a/web/strategy-switch-console/README.md b/web/strategy-switch-console/README.md index b37991f..4f342f9 100644 --- a/web/strategy-switch-console/README.md +++ b/web/strategy-switch-console/README.md @@ -109,7 +109,6 @@ Each account item supports: "account_scope": "demo-ibkr-tqqq", "service_name": "interactive-brokers-demo-ibkr-tqqq-service", "cash_currency": "USD", - "default_strategy_profile": "tqqq_growth_income", "supported_domains": ["us_equity", "hk_equity"] } ``` @@ -118,13 +117,13 @@ The Worker validates dispatch inputs against this config, including whether the `/api/strategy-profiles` returns the public live-enabled strategy catalog for the dropdown. It reads the KV `strategy_profiles` key first, then `STRATEGY_SWITCH_STRATEGY_PROFILES_JSON`, then `strategy-profiles.example.json`. -For signed-in users, `/api/config` also reads the target repositories' current GitHub Variables. It prefers account-specific `CLOUD_RUN_SERVICE_TARGETS_JSON`, then matching `RUNTIME_TARGET_JSON.strategy_profile`, then `STRATEGY_PROFILE`; if none can be read safely, the page falls back to `default_strategy_profile`. +For signed-in users, `/api/config` also reads the target repositories' current GitHub Variables. It prefers account-specific `CLOUD_RUN_SERVICE_TARGETS_JSON`, then matching `RUNTIME_TARGET_JSON.strategy_profile`, then `STRATEGY_PROFILE`. If no strategy is configured in GitHub variables, the page shows an empty (unconfigured) state rather than falling back to a hardcoded default. The switch form also accepts optional reserved-cash overrides: minimum reserved cash in the selected account currency and reserved-cash ratio. Set `cash_currency` to `USD` or `HKD` in account config when the account has a fixed cash currency; otherwise the page infers HKD for HK-equity strategy selections and USD for US-equity selections. Keeping the current policy leaves existing platform variables unchanged; when no explicit platform variables are configured, the platform source default is no extra reserve (`0` in the account currency and `0%`). When set, the Worker passes the values to `manual-strategy-switch.yml`, which writes the platform-specific variables such as `IBKR_MIN_RESERVED_CASH_USD` and `IBKR_RESERVED_CASH_RATIO`. Income-layer controls are sourced from `strategy-profiles.example.json` metadata for live-validated US equity strategies. The switch form can keep the current setting, enable the income layer with the profile default start amount and cap, or disable it. Option-layer controls use the same strategy profile metadata but only expose a three-state policy: keep current, enable the profile default recipe and budget, or disable and clear option overlay variables. Manual switch requests still cannot override direct option overlay or LEAPS fields through `extra_variables_json`; the Worker and build script reject those direct overrides. -Successful strategy switches also sync the selected account's `default_strategy_profile` back to the KV `account_options` key. The web endpoint does this immediately after dispatching the workflow, and the manual GitHub workflow calls the Worker's internal sync endpoint after applying platform variables when the `runtime-strategy-switch` environment variable `STRATEGY_SWITCH_CONSOLE_URL` is set. For that workflow callback, set the GitHub environment secret `STRATEGY_SWITCH_SYNC_TOKEN` to the same value as the Worker secret with that name. +Successful strategy switches sync account-level settings (plugin_mode, option_overlay_mode, cash_only_execution_mode, DCA mode) back to the KV `account_options` key. The strategy profile itself is stored only in GitHub variables (`RUNTIME_TARGET_JSON` / `STRATEGY_PROFILE`) and is not duplicated in KV. ## Strategy Profile Alignment @@ -135,7 +134,7 @@ When adding or renaming a strategy profile: - Add the runtime-enabled profile id and display label to `strategy-profiles.example.json`. - Run `python3 scripts/sync_strategy_switch_page_asset.py` so `strategy_profiles_asset.js` is regenerated. - Set `domain` on each strategy profile. Current values are `us_equity` and `hk_equity`. -- Set each affected account's `default_strategy_profile` and `supported_domains` in `account-options.example.json` and the deployed KV account config. +- Set each affected account's `supported_domains` in `account-options.example.json` and the deployed KV account config. Strategy profiles are managed exclusively through GitHub variables via the strategy switch workflow. - Use `["us_equity", "hk_equity"]` for LongBridge and IBKR accounts unless you intentionally want to narrow a specific account. - The main-branch deploy workflow updates the deployed KV `strategy_profiles` key from `strategy-profiles.example.json` after deploying the Worker. For manual deploys, call `/api/internal/sync-strategy-profiles` with the Worker sync token. - Make sure the platform repository's current `RUNTIME_TARGET_JSON.strategy_profile` or account-specific `CLOUD_RUN_SERVICE_TARGETS_JSON` uses the same id. diff --git a/web/strategy-switch-console/README.zh-CN.md b/web/strategy-switch-console/README.zh-CN.md index 1429514..2264ecf 100644 --- a/web/strategy-switch-console/README.zh-CN.md +++ b/web/strategy-switch-console/README.zh-CN.md @@ -116,7 +116,6 @@ wrangler secret put STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON < /tmp/strategy-switch- "account_scope": "demo-ibkr-tqqq", "service_name": "interactive-brokers-demo-ibkr-tqqq-service", "cash_currency": "USD", - "default_strategy_profile": "tqqq_growth_income", "supported_domains": ["us_equity", "hk_equity"] } ``` @@ -125,7 +124,7 @@ Worker 会校验 dispatch 参数必须匹配这里的某个账号项,也会校 `/api/strategy-profiles` 会返回公开的 live-enabled 策略目录,用于生成策略下拉框。读取优先级是 KV `strategy_profiles`、`STRATEGY_SWITCH_STRATEGY_PROFILES_JSON`、`strategy-profiles.example.json`。 -登录用户访问 `/api/config` 时,Worker 还会读取目标平台仓库的当前 GitHub Variables。读取优先级是账号匹配的 `CLOUD_RUN_SERVICE_TARGETS_JSON`、匹配的 `RUNTIME_TARGET_JSON.strategy_profile`、`STRATEGY_PROFILE`;都读不到时,页面才回退到 `default_strategy_profile`。 +登录用户访问 `/api/config` 时,Worker 还会读取目标平台仓库的当前 GitHub Variables。读取优先级是账号匹配的 `CLOUD_RUN_SERVICE_TARGETS_JSON`、匹配的 `RUNTIME_TARGET_JSON.strategy_profile`、`STRATEGY_PROFILE`。如果 GitHub 变量中未配置策略,页面会显示"未配置"状态,不再使用硬编码回退值。 切换表单也支持可选的预留现金覆盖项:所选账号币种下的最小预留现金和预留现金比例。如果账号现金币种固定,可以在账号配置里把 `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`。 @@ -133,7 +132,7 @@ Worker 会校验 dispatch 参数必须匹配这里的某个账号项,也会校 收入层控件来自 `strategy-profiles.example.json` 里的 live 验证策略元数据。切换页可以沿用当前配置、按 profile 默认起始金额和最高比例开启收入层,或关闭收入层。期权层也来自同一份策略 profile 元数据,但网页只暴露三态策略:沿用当前、启用 profile 默认 recipe 和预算、或关闭并清理期权层变量。手工切换请求仍不能通过 `extra_variables_json` 覆盖直接期权 overlay / LEAPS 字段;Worker 和构建脚本会拒绝这些直接覆盖项。 -策略切换成功后也会把当前账号的 `default_strategy_profile` 同步回 KV 的 `account_options` key。网页接口会在触发 workflow 成功后立即同步;如果 `runtime-strategy-switch` 环境变量里配置了 `STRATEGY_SWITCH_CONSOLE_URL`,手动 GitHub workflow 在写入平台变量后也会回调 Worker 内部接口同步。这个 workflow 回调需要 GitHub 环境 secret `STRATEGY_SWITCH_SYNC_TOKEN`,值要和 Worker 里同名 secret 保持一致。 +策略切换成功后会将账号级设置(plugin_mode、option_overlay_mode、cash_only_execution_mode、DCA 模式)同步回 KV 的 `account_options` key。策略 profile 本身仅保存在 GitHub 变量(`RUNTIME_TARGET_JSON` / `STRATEGY_PROFILE`)中,不在 KV 中重复存储。 ## 策略 Profile 对齐规范 @@ -144,7 +143,7 @@ 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` 和 `cn_equity`。 -- 在 `account-options.example.json` 和已部署的 KV 账号配置里更新对应账号的 `default_strategy_profile` 和 `supported_domains`。 +- 在 `account-options.example.json` 和已部署的 KV 账号配置里更新对应账号的 `supported_domains`。策略 profile 通过 GitHub 变量的策略切换工作流进行管理。 - 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,不要放进切换页策略目录。 diff --git a/web/strategy-switch-console/account-options.example.json b/web/strategy-switch-console/account-options.example.json index 2eb9352..2dd9979 100644 --- a/web/strategy-switch-console/account-options.example.json +++ b/web/strategy-switch-console/account-options.example.json @@ -6,7 +6,6 @@ "target_name": "hk", "account_selector": "HK", "cash_currency": "HKD", - "default_strategy_profile": "hk_low_vol_dividend_quality_snapshot", "supported_domains": ["us_equity", "hk_equity"] }, { @@ -14,7 +13,6 @@ "label": "sg", "target_name": "sg", "account_selector": "SG", - "default_strategy_profile": "soxl_soxx_trend_income", "supported_domains": ["us_equity", "hk_equity"] }, { @@ -22,7 +20,6 @@ "label": "paper", "target_name": "paper", "account_selector": "PAPER", - "default_strategy_profile": "russell_top50_leader_rotation", "supported_domains": ["us_equity", "hk_equity"] } ], @@ -36,7 +33,6 @@ "account_scope": "demo-ibkr-tqqq", "service_name": "interactive-brokers-demo-ibkr-tqqq-service", "cash_currency": "USD", - "default_strategy_profile": "tqqq_growth_income", "supported_domains": ["us_equity", "hk_equity"] }, { @@ -48,7 +44,6 @@ "account_scope": "demo-ibkr-soxl", "service_name": "interactive-brokers-demo-ibkr-soxl-service", "cash_currency": "USD", - "default_strategy_profile": "soxl_soxx_trend_income", "supported_domains": ["us_equity", "hk_equity"] }, { @@ -60,7 +55,6 @@ "account_scope": "demo-ibkr-dca", "service_name": "interactive-brokers-demo-ibkr-dca-service", "cash_currency": "USD", - "default_strategy_profile": "nasdaq_sp500_smart_dca", "supported_domains": ["us_equity", "hk_equity"] } ], @@ -70,7 +64,6 @@ "label": "default", "target_name": "default", "cash_currency": "USD", - "default_strategy_profile": "soxl_soxx_trend_income", "supported_domains": ["us_equity"] } ], @@ -80,7 +73,6 @@ "label": "default", "target_name": "default", "cash_currency": "USD", - "default_strategy_profile": "russell_top50_leader_rotation", "supported_domains": ["us_equity"] } ], @@ -95,7 +87,6 @@ "account_scope": "CN", "service_name": "qmt-quant-service", "cash_currency": "CNY", - "default_strategy_profile": "cn_industry_etf_rotation", "supported_domains": ["cn_equity"] }, { @@ -108,7 +99,6 @@ "account_scope": "CN", "service_name": "qmt-quant-service", "cash_currency": "CNY", - "default_strategy_profile": "cn_industry_etf_rotation_aggressive", "supported_domains": ["cn_equity"] }, { @@ -121,7 +111,6 @@ "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/app.js b/web/strategy-switch-console/app.js index 7c3ec97..107c5ba 100644 --- a/web/strategy-switch-console/app.js +++ b/web/strategy-switch-console/app.js @@ -21,11 +21,11 @@ const defaultRepositories = platformRepositories; const defaultAccountOptions = window.__DEFAULT_ACCOUNT_OPTIONS__ || { - binance: [{"key": "default", "label": "Binance", "target_name": "default", "cash_currency": "USD", "default_strategy_profile": "crypto_equity_combo", "supported_domains": ["crypto"]}], + binance: [{"key": "default", "label": "Binance", "target_name": "default", "cash_currency": "USD", "supported_domains": ["crypto"]}], firstrade: [{"key": "preview", "label": "Firstrade", "target_name": "preview", "supported_domains": ["us_equity"], "cash_currency": "USD", "default_execution_mode": "live", "service_name": "firstrade-quant-service"}], ibkr: [{"key": "preview", "label": "IBKR", "target_name": "preview", "supported_domains": ["us_equity", "hk_equity"], "cash_currency": "USD", "default_execution_mode": "live"}], longbridge: [{"key": "preview", "label": "LongBridge", "target_name": "preview", "supported_domains": ["us_equity", "hk_equity"], "cash_currency": "USD", "default_execution_mode": "live"}], - qmt: [{"key": "default", "label": "QMT", "target_name": "default", "cash_currency": "CNY", "default_strategy_profile": "cn_industry_etf_rotation", "supported_domains": ["cn_equity"], "service_name": "qmt-quant-service"}], + qmt: [{"key": "default", "label": "QMT", "target_name": "default", "cash_currency": "CNY", "supported_domains": ["cn_equity"], "service_name": "qmt-quant-service"}], schwab: [{"key": "preview", "label": "Schwab", "target_name": "preview", "supported_domains": ["us_equity"], "cash_currency": "USD", "default_execution_mode": "live", "service_name": "charles-schwab-quant-service"}], }; @@ -1641,9 +1641,6 @@ choices.push(profile); } }; - if (account) { - addChoice(account.default_strategy_profile || account.strategy_profile); - } return choices; } @@ -1966,21 +1963,11 @@ .filter(Boolean) .map((value) => String(value)); const entry = resolveCurrentEntryByKey(byPlatform, keys); - if (entry) { - if (!entry.strategy_profile) { - const gd = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {}; - const entryCopy = { ...entry }; - entryCopy.strategy_profile = account?.default_strategy_profile || gd.default_strategy_profile || ""; - entryCopy.source = (entryCopy.source || "worker") + "+account_defaults"; - return entryCopy; - } - return entry; - } + if (entry) return entry; const globalDefaults = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {}; const merged = { ...globalDefaults, ...(account || {}) }; - const profile = cleanStrategyProfile(merged.default_strategy_profile || merged.strategy_profile || ""); const synth = { - strategy_profile: profile || merged.default_strategy_profile || "", + strategy_profile: "", source: "account_defaults", }; const cashMode = merged.cash_only_execution_mode; @@ -2016,10 +2003,7 @@ function currentStrategyForAccount(platform, account) { const entry = currentEntryForAccount(platform, account); - const profile = cleanStrategyProfile(entry?.strategy_profile); - if (profile) return profile; - const fallback = account?.default_strategy_profile || account?.strategy_profile || ""; - return cleanStrategyProfile(fallback); + return cleanStrategyProfile(entry?.strategy_profile) || ""; } function currentReservePolicyForAccount(platform, account) { @@ -2099,33 +2083,16 @@ return fallback; } - function defaultStrategyForAccount(platform, account, fallback = "soxl_soxx_trend_income") { + function defaultStrategyForAccount(platform, account) { const currentProfile = currentStrategyForAccount(platform, account); if (currentProfile) return currentProfile; - const profile = cleanStrategyProfile(account?.default_strategy_profile || account?.strategy_profile); - if (profile && strategyAllowedForAccount(platform, account, profile)) return profile; - const hint = [ - account?.key, - account?.label, - account?.target_name, - account?.deployment_selector, - account?.account_scope, - account?.service_name, - ].join(" ").toLowerCase(); - 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" : "")))); - if (hinted && strategyAllowedForAccount(platform, account, hinted)) return hinted; - if (fallback && strategyAllowedForAccount(platform, account, fallback)) return fallback; - return strategyChoicesForAccount(platform, account)[0] || ""; + return ""; } function syncStrategyForAccount(platform) { const account = selectedAccount(platform); if (!account) return; - state.forms[platform].strategy = defaultStrategyForAccount(platform, account, state.forms[platform].strategy); + state.forms[platform].strategy = defaultStrategyForAccount(platform, account); state.forms[platform].executionMode = defaultExecutionModeForAccount( platform, account, @@ -2230,7 +2197,7 @@ state.forms[platform].optionOverlayTouched = false; state.forms[platform].cashOnlyExecutionTouched = false; state.forms[platform].dcaTouched = false; - state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0], state.forms[platform].strategy); + state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0]); state.forms[platform].pluginMode = currentPluginModeForAccount(platform, options[0]); syncRuntimeTargetForAccount(platform); syncReservePolicyForAccount(platform); @@ -3391,9 +3358,6 @@ ? String(item.cash_currency || item.market_currency || item.trading_currency).trim().toUpperCase() : "", supported_domains: normalizeSupportedDomains(platform, item), - default_strategy_profile: item.default_strategy_profile || item.strategy_profile - ? String(item.default_strategy_profile || item.strategy_profile) - : "", github_environment: item.github_environment ? String(item.github_environment) : "", variable_scope: item.variable_scope ? String(item.variable_scope) : "", plugin_mode: item.plugin_mode ? String(item.plugin_mode) : "", diff --git a/web/strategy-switch-console/app_js.js b/web/strategy-switch-console/app_js.js index 97babaf..8744a53 100644 --- a/web/strategy-switch-console/app_js.js +++ b/web/strategy-switch-console/app_js.js @@ -1,2 +1,2 @@ // Generated — JS asset -export const APP_JS = "\n\n let platformMeta = {\n binance: { label: \"Binance\", code: \"BN\", accent: \"var(--bn)\" },\n firstrade: { label: \"Firstrade\", code: \"FT\", accent: \"var(--ft)\" },\n ibkr: { label: \"IBKR\", code: \"IB\", accent: \"var(--ib)\" },\n longbridge: { label: \"LongBridge\", code: \"LB\", accent: \"var(--lb)\" },\n qmt: { label: \"QMT\", code: \"QM\", accent: \"var(--qmt)\" },\n schwab: { label: \"Schwab\", code: \"SW\", accent: \"var(--sw)\" },\n };\n\n const platformRepositories = {\n binance: \"QuantStrategyLab/BinancePlatform\",\n firstrade: \"QuantStrategyLab/FirstradePlatform\",\n ibkr: \"QuantStrategyLab/InteractiveBrokersPlatform\",\n longbridge: \"QuantStrategyLab/LongBridgePlatform\",\n qmt: \"QuantStrategyLab/QmtPlatform\",\n schwab: \"QuantStrategyLab/CharlesSchwabPlatform\",\n };\n // Alias for backward compatibility\n const defaultRepositories = platformRepositories;\n\n const defaultAccountOptions = window.__DEFAULT_ACCOUNT_OPTIONS__ || {\n binance: [{\"key\": \"default\", \"label\": \"Binance\", \"target_name\": \"default\", \"cash_currency\": \"USD\", \"default_strategy_profile\": \"crypto_equity_combo\", \"supported_domains\": [\"crypto\"]}],\n firstrade: [{\"key\": \"preview\", \"label\": \"Firstrade\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\", \"service_name\": \"firstrade-quant-service\"}],\n ibkr: [{\"key\": \"preview\", \"label\": \"IBKR\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\", \"hk_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\"}],\n longbridge: [{\"key\": \"preview\", \"label\": \"LongBridge\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\", \"hk_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\"}],\n qmt: [{\"key\": \"default\", \"label\": \"QMT\", \"target_name\": \"default\", \"cash_currency\": \"CNY\", \"default_strategy_profile\": \"cn_industry_etf_rotation\", \"supported_domains\": [\"cn_equity\"], \"service_name\": \"qmt-quant-service\"}],\n schwab: [{\"key\": \"preview\", \"label\": \"Schwab\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\", \"service_name\": \"charles-schwab-quant-service\"}],\n };\n\n const domainLabels = window.__DOMAIN_LABELS__ || {\n cn_equity: { zh: \"A股\", en: \"CN A-share\" },\n crypto: { zh: \"加密\", en: \"Crypto\" },\n hk_equity: { zh: \"港股\", en: \"HK Equity\" },\n us_equity: { zh: \"美股\", en: \"US Equity\" },\n };\n\n const platformConfig = window.__PLATFORM_CONFIG__ || {\n binance: {\n dry_run_only: false,\n margin_policy: false,\n reserved_cash: false,\n income_layer: false,\n option_overlay: false,\n dca: false,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n firstrade: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"firstrade-quant-service\",\n default_execution_mode: \"live\"\n },\n ibkr: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n longbridge: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n qmt: {\n dry_run_only: true,\n margin_policy: false,\n reserved_cash: false,\n income_layer: false,\n option_overlay: false,\n dca: false,\n execution_mode: \"paper\",\n service_name: \"qmt-quant-service\",\n default_execution_mode: \"paper\"\n },\n schwab: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"charles-schwab-quant-service\",\n default_execution_mode: \"live\"\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 const reservePolicyModes = [\"none\", \"ratio\", \"floor\", \"max\"];\n const incomeLayerModes = [\"enabled\", \"disabled\"];\n const optionOverlayModes = [\"enabled\", \"disabled\"];\n const cashOnlyExecutionModes = [\"enabled\", \"disabled\"];\n const runtimeTargetModes = [\"enabled\", \"disabled\"];\n const pluginModes = [\"auto\", \"none\"];\n const dcaModes = [\"fixed\", \"smart\"];\n const runtimeTargetEnabledVariable = \"RUNTIME_TARGET_ENABLED\";\n const incomeLayerEnabledVariable = \"INCOME_LAYER_ENABLED\";\n const incomeLayerStartUsdVariable = \"INCOME_LAYER_START_USD\";\n const incomeLayerMaxRatioVariable = \"INCOME_LAYER_MAX_RATIO\";\n const dcaProfileDefaults = window.__DCA_PROFILE_DEFAULTS__ || {\n nasdaq_sp500_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n ibit_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n };\n const APP_BOOT_TIMEOUT_MS = 15000;\n const platformMinReservedCashVariables = {\n longbridge: \"LONGBRIDGE_MIN_RESERVED_CASH_USD\",\n ibkr: \"IBKR_MIN_RESERVED_CASH_USD\",\n schwab: \"SCHWAB_MIN_RESERVED_CASH_USD\",\n firstrade: \"FIRSTRADE_MIN_RESERVED_CASH_USD\",\n };\n const platformReservedCashRatioVariables = {\n longbridge: \"LONGBRIDGE_RESERVED_CASH_RATIO\",\n ibkr: \"IBKR_RESERVED_CASH_RATIO\",\n schwab: \"SCHWAB_RESERVED_CASH_RATIO\",\n firstrade: \"FIRSTRADE_RESERVED_CASH_RATIO\",\n };\n\n const defaultStrategyProfiles = window.__DEFAULT_STRATEGY_PROFILES__ || [\n {\n \"profile\": \"tqqq_growth_income\",\n \"label\": \"纳斯达克增长收益\",\n \"label_en\": \"NASDAQ Growth Income\",\n \"label_zh\": \"纳斯达克增长收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"250000\",\n \"income_layer_max_ratio\": \"0.55\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.3,\n \"DGRO\": 0.2,\n \"SGOV\": 0.4,\n \"SPYI\": 0.08,\n \"QQQI\": 0.02\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"tqqq_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"250000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.03\"\n },\n {\n \"profile\": \"soxl_soxx_trend_income\",\n \"label\": \"半导体趋势收益\",\n \"label_en\": \"Semiconductor Trend Income\",\n \"label_zh\": \"半导体趋势收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"150000\",\n \"income_layer_max_ratio\": \"0.95\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.15,\n \"DGRO\": 0.1,\n \"SGOV\": 0.7,\n \"SPYI\": 0.04,\n \"QQQI\": 0.01\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_income_overlay_enabled\": true,\n \"option_income_overlay_recipe\": \"soxx_put_credit_spread_income_v1\",\n \"option_income_overlay_start_usd\": \"150000\",\n \"option_income_overlay_nav_risk_ratio\": \"0.01\"\n },\n {\n \"profile\": \"nasdaq_sp500_smart_dca\",\n \"label\": \"纳指标普定投\",\n \"label_en\": \"NASDAQ/S&P 500 DCA\",\n \"label_zh\": \"纳指标普定投\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"dca_enabled\": true,\n \"dca_default_mode\": \"fixed\",\n \"dca_default_base_investment_usd\": \"1000\"\n },\n {\n \"profile\": \"ibit_smart_dca\",\n \"label\": \"IBIT比特币定投\",\n \"label_en\": \"IBIT Bitcoin DCA\",\n \"label_zh\": \"IBIT比特币定投\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"dca_enabled\": true,\n \"dca_default_mode\": \"fixed\",\n \"dca_default_base_investment_usd\": \"1000\"\n },\n {\n \"profile\": \"global_etf_rotation\",\n \"label\": \"全球ETF轮动\",\n \"label_en\": \"Global ETF Rotation\",\n \"label_zh\": \"全球ETF轮动\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"500000\",\n \"income_layer_max_ratio\": \"0.15\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.4,\n \"DGRO\": 0.25,\n \"SGOV\": 0.3,\n \"SPYI\": 0.05\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"500000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"russell_top50_leader_rotation\",\n \"label\": \"罗素Top50领涨\",\n \"label_en\": \"Russell Top50 Leaders\",\n \"label_zh\": \"罗素Top50领涨\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"300000\",\n \"income_layer_max_ratio\": \"0.25\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.45,\n \"DGRO\": 0.3,\n \"SGOV\": 0.25\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"300000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"tecl_xlk_trend_income\",\n \"label\": \"TECL/XLK趋势收益\",\n \"label_en\": \"TECL/XLK Trend Income\",\n \"label_zh\": \"TECL/XLK趋势收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"failed_promotion_vs_live_profiles\"\n },\n {\n \"profile\": \"us_equity_combo\",\n \"label\": \"美股核心组合\",\n \"label_en\": \"US Core Combo\",\n \"label_zh\": \"美股核心组合\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\",\n \"income_layer_start_usd\": \"300000\",\n \"income_layer_max_ratio\": \"0.25\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.25,\n \"DGRO\": 0.25,\n \"SGOV\": 0.2,\n \"SPYI\": 0.15,\n \"QQQI\": 0.15\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"300000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"us_equity_combo_core\",\n \"label\": \"美股核心组合影子\",\n \"label_en\": \"US Core Combo Shadow\",\n \"label_zh\": \"美股核心组合影子\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"us_equity_combo_leveraged\",\n \"label\": \"美股加速组合\",\n \"label_en\": \"US Alpha Combo\",\n \"label_zh\": \"美股加速组合\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"hk_global_etf_tactical_rotation\",\n \"label\": \"港股ETF战术轮动\",\n \"label_en\": \"HK ETF Tactical Rotation\",\n \"label_zh\": \"港股ETF战术轮动\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"hk_low_vol_dividend_quality_snapshot\",\n \"label\": \"港股红利质量\",\n \"label_en\": \"HK Dividend Quality\",\n \"label_zh\": \"港股红利质量\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"hk_equity_combo\",\n \"label\": \"港股恒生组合\",\n \"label_en\": \"HK Core Combo\",\n \"label_zh\": \"港股恒生组合\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"cn_industry_etf_rotation\",\n \"label\": \"A股行业ETF轮动\",\n \"label_en\": \"CN Industry ETF Rotation\",\n \"label_zh\": \"A股行业ETF轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"cn_industry_etf_rotation_aggressive\",\n \"label\": \"A股ETF轮动\",\n \"label_en\": \"CN ETF Rotation\",\n \"label_zh\": \"A股ETF轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"live_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"live_candidate_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_index_etf_tactical_rotation\",\n \"label\": \"A股宽基ETF战术轮动\",\n \"label_en\": \"CN Index ETF Tactical Rotation\",\n \"label_zh\": \"A股宽基ETF战术轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_tactical_rotation\",\n \"label\": \"创业板战术轮动\",\n \"label_en\": \"CN ChiNext Tactical Rotation\",\n \"label_zh\": \"创业板战术轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_growth_momentum_quality\",\n \"label\": \"创业板成长动量质量\",\n \"label_en\": \"CN ChiNext Growth Momentum Quality\",\n \"label_zh\": \"创业板成长动量质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_dividend_quality_snapshot\",\n \"label\": \"A股红利质量\",\n \"label_en\": \"CN Dividend Quality\",\n \"label_zh\": \"A股红利质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_growth_momentum_quality_snapshot\",\n \"label\": \"创业板成长质量快照\",\n \"label_en\": \"CN ChiNext Growth Quality Snapshot\",\n \"label_zh\": \"创业板成长质量快照\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_star_growth_momentum_quality\",\n \"label\": \"科创板成长动量质量\",\n \"label_en\": \"CN STAR Growth Momentum Quality\",\n \"label_zh\": \"科创板成长动量质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_equity_combo\",\n \"label\": \"A股进取组合\",\n \"label_en\": \"CN Alpha Combo\",\n \"label_zh\": \"A股进取组合\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"crypto_live_pool_rotation\",\n \"label\": \"加密实时池轮动\",\n \"label_en\": \"Crypto Live Pool Rotation\",\n \"label_zh\": \"加密实时池轮动\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"crypto_btc_dca\",\n \"label\": \"BTC定投\",\n \"label_en\": \"BTC DCA\",\n \"label_zh\": \"BTC定投\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\"\n },\n {\n \"profile\": \"crypto_trend_rotation\",\n \"label\": \"山寨趋势轮动\",\n \"label_en\": \"Altcoin Trend\",\n \"label_zh\": \"山寨趋势轮动\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"crypto_equity_combo\",\n \"label\": \"加密动量组合\",\n \"label_en\": \"Crypto Core Combo\",\n \"label_zh\": \"加密动量组合\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n }\n ];\n\n const localStrategyLabels = {\n tqqq_growth_income: { zh: \"纳斯达克增长收益\", en: \"NASDAQ Growth Income\" },\n soxl_soxx_trend_income: { zh: \"半导体趋势收益\", en: \"Semiconductor Trend Income\" },\n nasdaq_sp500_smart_dca: { zh: \"纳指标普定投\", en: \"NASDAQ/S&P 500 DCA\" },\n ibit_smart_dca: { zh: \"IBIT比特币定投\", en: \"IBIT Bitcoin DCA\" },\n global_etf_rotation: { zh: \"全球ETF轮动\", en: \"Global ETF Rotation\" },\n russell_top50_leader_rotation: { zh: \"罗素Top50领涨\", en: \"Russell Top50 Leaders\" },\n hk_global_etf_tactical_rotation: { zh: \"港股ETF战术轮动\", en: \"HK ETF Tactical Rotation\" },\n hk_low_vol_dividend_quality_snapshot: { zh: \"港股红利质量\", en: \"HK Dividend Quality\" },\n cn_industry_etf_rotation: { zh: \"A股行业ETF轮动\", en: \"CN Industry ETF Rotation\" },\n cn_dividend_quality_snapshot: { zh: \"A股红利质量\", en: \"CN Dividend Quality\" },\n us_equity_combo: { zh: \"美股核心组合\", en: \"US Core Combo\" },\n us_equity_combo_leveraged: { zh: \"美股加速组合\", en: \"US Alpha Combo\" },\n hk_equity_combo: { zh: \"港股恒生组合\", en: \"HK Core Combo\" },\n cn_industry_etf_rotation_aggressive: { zh: \"A股ETF轮动\", en: \"CN ETF Rotation\" },\n cn_stock_momentum_rotation: { zh: \"A股个股动量\", en: \"CN Stock Momentum\" },\n cn_equity_combo: { zh: \"A股进取组合\", en: \"CN Alpha Combo\" },\n crypto_btc_dca: { zh: \"BTC定投\", en: \"BTC DCA\" },\n crypto_trend_rotation: { zh: \"山寨趋势轮动\", en: \"Altcoin Trend\" },\n crypto_equity_combo: { zh: \"加密动量组合\", en: \"Crypto Core Combo\" },\n };\n\n const fallbackIncomeLayerDefaults = window.__INCOME_LAYER_DEFAULTS__ || {\n tqqq_growth_income: {\n startUsd: 250000,\n maxRatio: \"0.55\",\n allocations: { SCHD: 0.30, DGRO: 0.20, SGOV: 0.40, SPYI: 0.08, QQQI: 0.02 },\n },\n soxl_soxx_trend_income: {\n startUsd: 150000,\n maxRatio: \"0.95\",\n allocations: { SCHD: 0.15, DGRO: 0.10, SGOV: 0.70, SPYI: 0.04, QQQI: 0.01 },\n },\n global_etf_rotation: {\n startUsd: 500000,\n maxRatio: \"0.15\",\n allocations: { SCHD: 0.40, DGRO: 0.25, SGOV: 0.30, SPYI: 0.05 },\n },\n russell_top50_leader_rotation: {\n startUsd: 300000,\n maxRatio: \"0.25\",\n allocations: { SCHD: 0.45, DGRO: 0.30, SGOV: 0.25 },\n },\n us_equity_combo: {\n startUsd: 300000,\n maxRatio: \"0.25\",\n allocations: { SCHD: 0.25, DGRO: 0.25, SGOV: 0.20, SPYI: 0.15, QQQI: 0.15 },\n }};\n let incomeLayerDefaults = {};\n const fallbackOptionOverlayDefaults = window.__OPTION_OVERLAY_DEFAULTS__ || {\n tqqq_growth_income: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"tqqq_leaps_growth_v1\", startUsd: \"250000\", ratio: \"0.03\", ratioKind: \"budget\" },\n ],\n },\n soxl_soxx_trend_income: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"income\", recipe: \"soxx_put_credit_spread_income_v1\", startUsd: \"150000\", ratio: \"0.01\", ratioKind: \"risk\" },\n ],\n },\n global_etf_rotation: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"500000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n russell_top50_leader_rotation: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"300000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n us_equity_combo: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"300000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n us_equity_combo_leveraged: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [],\n }};\n let optionOverlayDefaults = {};\n\n const strategyDomains = [\"us_equity\", \"hk_equity\", \"cn_equity\", \"crypto\"];\n let strategyOptions = [];\n let strategyLabels = {};\n let strategyCatalog = {};\n\n\n const copy = {\n zh: {\n appTitle: \"策略切换\",\n appSubtitle: \"选平台、目标账号和策略,一次执行完成切换。\",\n bootKicker: \"初始化控制台\",\n bootTitle: \"读取策略配置\",\n bootMessage: \"正在读取登录状态、账号配置和当前状态。\",\n bootStrategy: \"正在读取策略目录。\",\n bootSession: \"正在验证登录状态。\",\n bootConfig: \"正在读取账号配置和当前状态。\",\n bootTimeout: \"加载超时,已切换到公开预览(登录后可重试)。\",\n bootPublic: \"公开预览已就绪。\",\n login: \"登录\",\n logout: \"退出\",\n signedInAs: \"已登录 {login}\",\n activePlatform: \"当前平台\",\n account: \"目标账号\",\n strategy: \"策略\",\n mode: \"模式\",\n live: \"实盘\",\n paper: \"模拟\",\n runtimeTargetMode: \"账号运行状态\",\n runtimeSectionTitle: \"运行与插件\",\n runtimeTargetCurrent: \"沿用当前状态\",\n runtimeTargetEnabled: \"启用\",\n runtimeTargetDisabled: \"禁用\",\n runtimeTargetModeMeta: \"停用后正式运行会跳过,模拟运行和健康检查仍可用。\",\n pluginMode: \"插件启用范围\",\n pluginModeAuto: \"启用插件\",\n pluginModeNone: \"禁用插件\",\n pluginModeMeta: \"选择是否启用该策略的插件。\",\n incomeLayerMode: \"收入层状态\",\n incomeLayerSectionTitle: \"收入层\",\n incomeLayerCurrent: \"沿用当前配置\",\n incomeLayerEnabled: \"开启收入层\",\n incomeLayerDisabled: \"关闭收入层\",\n incomeLayerNotSupported: \"该策略未定义收入层\",\n incomeLayerStartUsd: \"收入层起始金额\",\n incomeLayerMaxRatio: \"收入层最高比例\",\n incomeLayerModeMeta: \"仅对已定义收入层的美股策略生效。\",\n incomeLayerDefaultMeta: \"策略默认:起始 {start},最高 {ratio}。\",\n incomeLayerAllocationMeta: \"默认分配:{allocations}。\",\n incomeLayerStartMeta: \"总资产达到该金额后启用收入层。\",\n incomeLayerRatioMeta: \"例如 0.55 表示最高 55%。\",\n optionOverlayMode: \"期权层状态\",\n optionOverlaySectionTitle: \"期权层\",\n optionOverlayCurrent: \"沿用当前配置\",\n optionOverlayEnabled: \"启用期权层\",\n optionOverlayDisabled: \"关闭期权层\",\n optionOverlayNotSupported: \"该策略未定义期权层\",\n optionOverlayModeMeta: \"启用时使用策略默认的最佳 recipe 和预算,不在这里手动调比例。\",\n optionOverlayDefaultMeta: \"{defaults}\",\n optionOverlayFamilyGrowth: \"增长\",\n optionOverlayFamilyIncome: \"收入\",\n optionOverlayBudgetRatio: \"预算 {ratio}\",\n optionOverlayRiskRatio: \"风险 {ratio}\",\n cashOnlyExecutionMode: \"允许融资\",\n cashOnlyExecutionCurrent: \"沿用当前配置\",\n cashOnlyExecutionYes: \"是\",\n cashOnlyExecutionNo: \"否\",\n cashOnlyExecutionModeMeta: \"选「否」时只按真实现金下单,不会动用 margin 购买力。\",\n cashOnlyExecutionValueYes: \"是\",\n cashOnlyExecutionValueNo: \"否\",\n currentCashOnlyExecution: \"当前允许融资\",\n pendingCashOnlyExecution: \"待提交允许融资\",\n executionCashPolicyTitle: \"现金与融资\",\n executionCashPolicyNote: \"允许融资与预留现金覆盖不能同时生效;选「是」会清空预留覆盖,设预留覆盖会强制「否」。\",\n executionCashMarginBlocksReserve: \"已选允许融资;提交时会清空预留现金覆盖。\",\n executionCashReserveBlocksMargin: \"已设预留现金覆盖;提交时会强制不允许融资。\",\n qmtPlatformCashNote: \"A 股 QMT 不使用 margin / 平台预留现金;现金约束在策略参数 execution_cash_reserve_ratio 内配置。\",\n qmtDryRunOnlyNote: \"QMT 当前仅支持 dry-run(模拟),尚无 live 券商账号。\",\n binancePlatformNote: \"Binance 平台不使用券商级收入层与期权层;相关功能由策略内部实现。\",\n invalidExecutionCashPolicyNote: \"允许融资与预留现金覆盖冲突,请只保留一种约束。\",\n dcaMode: \"定投模式\",\n dcaSectionTitle: \"定投\",\n dcaModeFixed: \"定额定投\",\n dcaModeSmart: \"智能定投\",\n dcaBaseInvestmentUsd: \"定投基准金额\",\n dcaModeMeta: \"仅定投策略可配置。\",\n dcaDefaultMeta: \"默认:{mode},基准金额 {amount}。\",\n dcaNotSupported: \"该策略不是定投策略\",\n dcaPlatformNotSupported: \"当前平台不支持定投策略\",\n currentDca: \"当前定投设置\",\n pendingDca: \"待提交定投设置\",\n dcaText: \"{mode},基准金额 {amount}\",\n minReservedCash: \"最小预留现金 ({currency})\",\n reservedCashRatio: \"预留现金比例\",\n reservedCashMode: \"预留现金策略\",\n reservePolicyCurrent: \"沿用当前配置\",\n reservePolicyNone: \"不设置平台预留现金\",\n reservePolicyRatio: \"仅按比例\",\n reservePolicyFloor: \"仅按固定金额\",\n reservePolicyMax: \"固定金额和比例取较大值\",\n reservedCashModeMeta: \"选择是否沿用、清空或覆盖平台预留现金。\",\n reservedCashNone: \"不设置\",\n reservedCashDefault: \"未配置(平台默认:0 {currency} / 0%)\",\n reservedCashMeta: \"固定金额下限,可单独设置或与比例取较大值。\",\n reservedCashRatioMeta: \"例如 0.03 表示 3%。\",\n summary: \"当前 / 待提交\",\n copySummary: \"复制状态\",\n loginToRun: \"登录后切换\",\n loadingConfig: \"读取配置中\",\n configureAccounts: \"配置账号后切换\",\n runSwitch: \"一键切换\",\n noChanges: \"无变更\",\n readonlyNote: \"登录后才可执行切换。\",\n publicReadonly: \"登录后查看账号配置。\",\n loadingConfigNote: \"正在读取账号配置和当前状态。\",\n missingConfigNote: \"账号配置未加载,暂时不能执行。\",\n readyNote: \"点击后会触发工作流,并同步目标平台服务。\",\n invalidStrategyNote: \"当前账号没有可执行策略,暂时不能切换。\",\n invalidReservePolicyNote: \"请为当前预留现金策略填写有效金额或比例。\",\n invalidIncomeLayerNote: \"请填写有效的收入层起始金额和最高比例。\",\n invalidOptionOverlayNote: \"当前策略未定义可启用的期权层。\",\n invalidDcaNote: \"请填写有效的定投模式和基准金额。\",\n publicOauthTitle: \"GitHub OAuth 保护\",\n publicOauthText: \"只允许白名单账号进入私有配置。\",\n publicWorkerTitle: \"Worker 端触发\",\n publicWorkerText: \"令牌保留在服务端,浏览器只提交切换意图。\",\n publicAuditTitle: \"变更可回溯\",\n publicAuditText: \"切换由 GitHub Actions 执行,便于审计和回滚。\",\n noAccount: \"没有账号选项\",\n noStrategy: \"没有支持的策略\",\n repository: \"平台仓库\",\n selectedAccount: \"账号\",\n selectedMarket: \"市场\",\n currentRuntimeTarget: \"当前账号状态\",\n pendingRuntimeTarget: \"待提交账号状态\",\n reservedCashPolicy: \"当前预留现金\",\n currentIncomeLayer: \"当前收入层\",\n pendingIncomeLayer: \"待提交收入层\",\n currentOptionOverlay: \"当前期权层\",\n pendingOptionOverlay: \"待提交期权层\",\n pendingReservedCashPolicy: \"待提交预留现金\",\n pendingMode: \"待提交模式\",\n currentPluginMode: \"当前插件范围\",\n pendingPluginMode: \"待提交插件范围\",\n unchanged: \"不变\",\n copied: \"已复制状态\",\n dispatching: \"正在触发工作流...\",\n dispatched: \"已触发工作流\",\n dispatchFailed: \"触发失败\",\n targetMeta: \"目标 {target} · 服务 {service} · 市场 {domains}\",\n strategyMeta: \"该账号仅显示 {domains} 策略\",\n strategyLifecycleMeta: \"当前门槛 {stage}\",\n strategyBlockedCountMeta: \"{count} 个策略未达 live 门槛\",\n strategyDefaultBlockedMeta: \"默认策略 {profile} 已阻断:{reason}\",\n usEquity: \"美股\",\n hkEquity: \"港股\",\n cnEquity: \"A股\",\n cryptoEquity: \"加密\",\n currentStrategy: \"当前策略\",\n nextStrategy: \"切换策略\",\n notRead: \"读取失败\",\n runtimeTargetOn: \"启用\",\n runtimeTargetOff: \"禁用\",\n incomeLayerDefault: \"开启,{start}起 {ratio}\",\n incomeLayerOff: \"关闭\",\n incomeLayerOn: \"开启,起始 {start},最高 {ratio}\",\n optionOverlayOff: \"关闭\",\n optionOverlayOn: \"开启\",\n optionOverlayDefaultSimple: \"开启\",\n optionOverlayDefault: \"开启,{detail}\",\n cashOnlyExecutionDefault: \"仅用现金\",\n },\n en: {\n appTitle: \"Strategy Switch\",\n appSubtitle: \"Pick platform, target account, and strategy. One action switches everything.\",\n bootKicker: \"Starting console\",\n bootTitle: \"Loading strategy config\",\n bootMessage: \"Reading session, account config, and current state.\",\n bootStrategy: \"Reading strategy catalog.\",\n bootSession: \"Checking sign-in status.\",\n bootConfig: \"Reading account config and current state.\",\n bootTimeout: \"Loading timed out; switched to public preview. Retry after signing in.\",\n bootPublic: \"Public preview is ready.\",\n login: \"Sign in\",\n logout: \"Sign out\",\n signedInAs: \"Signed in as {login}\",\n activePlatform: \"Active Platform\",\n account: \"Target account\",\n strategy: \"Strategy\",\n mode: \"Mode\",\n live: \"Live\",\n paper: \"Dry run\",\n runtimeTargetMode: \"Account status\",\n runtimeSectionTitle: \"Runtime and plugins\",\n runtimeTargetCurrent: \"Keep current status\",\n runtimeTargetEnabled: \"Enabled\",\n runtimeTargetDisabled: \"Disabled\",\n runtimeTargetModeMeta: \"Disabled accounts skip live runs; dry runs and health checks still work.\",\n pluginMode: \"Plugin scope\",\n pluginModeAuto: \"Enabled\",\n pluginModeNone: \"Disabled\",\n pluginModeMeta: \"Choose whether to enable this strategy's plugins.\",\n incomeLayerMode: \"Income layer\",\n incomeLayerSectionTitle: \"Income layer\",\n incomeLayerCurrent: \"Keep current config\",\n incomeLayerEnabled: \"Enable income layer\",\n incomeLayerDisabled: \"Disable income layer\",\n incomeLayerNotSupported: \"No income layer for this strategy\",\n incomeLayerStartUsd: \"Income layer start amount\",\n incomeLayerMaxRatio: \"Income layer max ratio\",\n incomeLayerModeMeta: \"Only applies to US equity strategies with an income layer.\",\n incomeLayerDefaultMeta: \"Strategy default: starts at {start}, max {ratio}.\",\n incomeLayerAllocationMeta: \"Default allocation: {allocations}.\",\n incomeLayerStartMeta: \"Income layer activates after total assets reach this amount.\",\n incomeLayerRatioMeta: \"Use 0.55 for a 55% cap.\",\n optionOverlayMode: \"Option layer\",\n optionOverlaySectionTitle: \"Option layer\",\n optionOverlayCurrent: \"Keep current config\",\n optionOverlayEnabled: \"Enable option layer\",\n optionOverlayDisabled: \"Disable option layer\",\n optionOverlayNotSupported: \"No option layer for this strategy\",\n optionOverlayModeMeta: \"Enabled mode uses the strategy's default recipe and budget; ratios are not edited here.\",\n optionOverlayDefaultMeta: \"{defaults}\",\n optionOverlayFamilyGrowth: \"Growth\",\n optionOverlayFamilyIncome: \"Income\",\n optionOverlayBudgetRatio: \"budget {ratio}\",\n optionOverlayRiskRatio: \"risk {ratio}\",\n cashOnlyExecutionMode: \"Allow margin\",\n cashOnlyExecutionCurrent: \"Keep current config\",\n cashOnlyExecutionYes: \"Yes\",\n cashOnlyExecutionNo: \"No\",\n cashOnlyExecutionModeMeta: \"Choose No to use available cash only and avoid margin buying power.\",\n cashOnlyExecutionValueYes: \"Yes\",\n cashOnlyExecutionValueNo: \"No\",\n currentCashOnlyExecution: \"Current allow margin\",\n pendingCashOnlyExecution: \"Pending allow margin\",\n executionCashPolicyTitle: \"Cash and margin\",\n executionCashPolicyNote: \"Allow margin and reserve-cash overrides cannot both apply. Yes clears reserve overrides; reserve overrides force No.\",\n executionCashMarginBlocksReserve: \"Allow margin is selected; submitting will clear reserve-cash overrides.\",\n executionCashReserveBlocksMargin: \"Reserve-cash override is active; submitting will force allow margin to No.\",\n qmtPlatformCashNote: \"QMT A-share does not use margin or platform reserve cash; cash constraints live in strategy execution_cash_reserve_ratio.\",\n qmtDryRunOnlyNote: \"QMT is dry-run only for now; no live broker accounts are configured.\",\n binancePlatformNote: \"Binance does not use broker-level income/option layers; features are implemented inside strategies.\",\n invalidExecutionCashPolicyNote: \"Allow margin and reserve-cash overrides conflict. Keep only one constraint.\",\n dcaMode: \"DCA mode\",\n dcaSectionTitle: \"DCA\",\n dcaModeFixed: \"Fixed DCA\",\n dcaModeSmart: \"Smart DCA\",\n dcaBaseInvestmentUsd: \"Base DCA amount\",\n dcaModeMeta: \"Only DCA strategies can use this.\",\n dcaDefaultMeta: \"Default: {mode}, base amount {amount}.\",\n dcaNotSupported: \"This is not a DCA strategy\",\n dcaPlatformNotSupported: \"DCA not supported on this platform\",\n currentDca: \"Current DCA settings\",\n pendingDca: \"Pending DCA settings\",\n dcaText: \"{mode}, base amount {amount}\",\n minReservedCash: \"Minimum reserved cash ({currency})\",\n reservedCashRatio: \"Reserved cash ratio\",\n reservedCashMode: \"Reserved cash policy\",\n reservePolicyCurrent: \"Keep current config\",\n reservePolicyNone: \"No platform reserve\",\n reservePolicyRatio: \"Ratio only\",\n reservePolicyFloor: \"Fixed amount only\",\n reservePolicyMax: \"Max of amount and ratio\",\n reservedCashModeMeta: \"Choose whether to keep, clear, or override platform reserved cash.\",\n reservedCashNone: \"None\",\n reservedCashDefault: \"Not configured (platform default: 0 {currency} / 0%)\",\n reservedCashMeta: \"Fixed cash floor. Use alone or with a ratio.\",\n reservedCashRatioMeta: \"Use 0.03 for 3%.\",\n summary: \"Current / Pending\",\n copySummary: \"Copy state\",\n loginToRun: \"Sign in to switch\",\n loadingConfig: \"Loading config\",\n configureAccounts: \"Configure accounts\",\n runSwitch: \"Switch now\",\n noChanges: \"No changes\",\n readonlyNote: \"Sign in to switch.\",\n publicReadonly: \"Sign in to view account config.\",\n loadingConfigNote: \"Reading account config and current state.\",\n missingConfigNote: \"Account config is not loaded, so switching is disabled.\",\n readyNote: \"This dispatches the workflow and syncs the target platform service.\",\n invalidStrategyNote: \"This account has no runnable strategy, so switching is disabled.\",\n invalidReservePolicyNote: \"Enter a valid amount or ratio for the selected reserved-cash policy.\",\n invalidIncomeLayerNote: \"Enter a valid income layer start amount and max ratio.\",\n invalidOptionOverlayNote: \"This strategy does not define an option layer to enable.\",\n invalidDcaNote: \"Enter a valid DCA mode and base amount.\",\n publicOauthTitle: \"Protected by GitHub OAuth\",\n publicOauthText: \"Only allowlisted accounts can open private config.\",\n publicWorkerTitle: \"Worker-side dispatch\",\n publicWorkerText: \"Tokens stay server-side; the browser submits intent only.\",\n publicAuditTitle: \"Traceable changes\",\n publicAuditText: \"Switches run through GitHub Actions for audit and rollback.\",\n noAccount: \"No accounts\",\n noStrategy: \"No supported strategies\",\n repository: \"Repository\",\n selectedAccount: \"Account\",\n selectedMarket: \"Market\",\n currentRuntimeTarget: \"Current account status\",\n pendingRuntimeTarget: \"Pending account status\",\n reservedCashPolicy: \"Current reserved cash\",\n currentIncomeLayer: \"Current income layer\",\n pendingIncomeLayer: \"Pending income layer\",\n currentOptionOverlay: \"Current option layer\",\n pendingOptionOverlay: \"Pending option layer\",\n pendingReservedCashPolicy: \"Pending reserved cash\",\n pendingMode: \"Pending mode\",\n currentPluginMode: \"Current plugin scope\",\n pendingPluginMode: \"Pending plugin scope\",\n unchanged: \"Unchanged\",\n copied: \"State copied\",\n dispatching: \"Dispatching workflow...\",\n dispatched: \"Workflow dispatched\",\n dispatchFailed: \"Dispatch failed\",\n targetMeta: \"target {target} · service {service} · market {domains}\",\n strategyMeta: \"This account only shows {domains} strategies\",\n strategyLifecycleMeta: \"current gate {stage}\",\n strategyBlockedCountMeta: \"{count} strategies are blocked from live\",\n strategyDefaultBlockedMeta: \"Default strategy {profile} is blocked: {reason}\",\n usEquity: \"US equity\",\n hkEquity: \"HK equity\",\n cnEquity: \"CN A-share\",\n cryptoEquity: \"Crypto\",\n currentStrategy: \"Current strategy\",\n nextStrategy: \"Switch strategy\",\n notRead: \"Not read\",\n runtimeTargetOn: \"Enabled\",\n runtimeTargetOff: \"Disabled\",\n incomeLayerDefault: \"Enabled, {start} start, {ratio} max\",\n incomeLayerOff: \"Disabled\",\n incomeLayerOn: \"Enabled, starts at {start}, max {ratio}\",\n optionOverlayOff: \"Disabled\",\n optionOverlayOn: \"Enabled\",\n optionOverlayDefaultSimple: \"Strategy default: enabled\",\n optionOverlayDefault: \"Enabled, {detail}\",\n cashOnlyExecutionDefault: \"Cash only\",\n },\n };\n\n const storedLang = localStorage.getItem(\"qsl-switch-lang\");\n const initialLang = storedLang === \"zh\" || storedLang === \"en\"\n ? storedLang\n : ((navigator.language || \"\").toLowerCase().startsWith(\"zh\") ? \"zh\" : \"en\");\n const clone = (value) => JSON.parse(JSON.stringify(value));\n const defaultReserveForm = () => ({\n reservePolicyMode: \"current\",\n minReservedCashUsd: \"\",\n reservedCashRatio: \"\",\n reservedCashTouched: false,\n incomeLayerMode: \"current\",\n incomeLayerStartUsd: \"\",\n incomeLayerMaxRatio: \"\",\n incomeLayerTouched: false,\n optionOverlayMode: \"current\",\n optionOverlayTouched: false,\n cashOnlyExecutionMode: \"current\",\n cashOnlyExecutionTouched: false,\n runtimeTargetMode: \"current\",\n runtimeTargetTouched: false,\n dcaMode: \"fixed\",\n dcaBaseInvestmentUsd: \"\",\n dcaTouched: false,\n strategyTouched: false,\n });\n\n const state = {\n selected: \"longbridge\",\n lang: initialLang,\n appReady: false,\n bootMessageKey: \"bootMessage\",\n auth: { available: false, allowed: false, admin: false, login: null },\n accountOptions: clone(defaultAccountOptions),\n currentStrategies: {},\n configSource: \"default\",\n repositories: clone(defaultRepositories),\n forms: {\n longbridge: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n ibkr: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n schwab: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n firstrade: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n qmt: { accountKey: \"preview\", strategy: \"\", executionMode: \"paper\", pluginMode: \"auto\", ...defaultReserveForm() },\n binance: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\" },\n },\n };\n\n const el = (id) => document.getElementById(id);\n const t = (key) => copy[state.lang][key] || copy.en[key] || key;\n let toastTimer = null;\n\n function showToast(message, { duration = 4000 } = {}) {\n const node = el(\"toast\");\n if (toastTimer) {\n window.clearTimeout(toastTimer);\n toastTimer = null;\n }\n node.textContent = message || \"\";\n if (message && duration > 0) {\n toastTimer = window.setTimeout(() => {\n node.textContent = \"\";\n toastTimer = null;\n }, duration);\n }\n }\n\n async function fetchWithTimeout(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) {\n const controller = new AbortController();\n const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } catch (error) {\n if (error?.name === \"AbortError\") {\n throw new Error(\"request timeout\");\n }\n throw error;\n } finally {\n window.clearTimeout(timeoutId);\n }\n }\n\n async function requestJson(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) {\n const response = await fetchWithTimeout(url, { ...init, cache: \"no-store\" }, timeoutMs);\n if (!response.ok) throw new Error(\"request failed\");\n return response.json();\n }\n\n function isRequestTimeoutError(error) {\n return String(error?.message || \"\").toLowerCase() === \"request timeout\";\n }\n\n function optionsFor(platform) {\n return state.accountOptions[platform] && state.accountOptions[platform].length\n ? state.accountOptions[platform]\n : defaultAccountOptions[platform];\n }\n\n function selectedAccount(platform = state.selected) {\n const options = optionsFor(platform);\n const form = state.forms[platform];\n return options.find((option) => option.key === form.accountKey) || options[0];\n }\n\n function hasPrivateConfig() {\n return Boolean(state.auth.allowed && state.configSource === \"private\");\n }\n\n function cleanStrategyProfile(value) {\n const profile = String(value || \"\").trim();\n return /^[a-z0-9._=-]{1,120}$/.test(profile) ? profile : \"\";\n }\n\n function cleanStrategyDomain(value) {\n const domain = String(value || \"\").trim();\n return strategyDomains.includes(domain) ? domain : \"\";\n }\n\n function domainLabel(domain) {\n const entry = domainLabels[domain];\n if (entry) return state.lang === \"zh\" ? entry.zh : entry.en;\n return domain;\n }\n\n function platformSupportsMarginPolicy(platform = state.selected) {\n return platformConfig[platform]?.margin_policy ?? true;\n }\n\n function platformSupportsReservedCashPolicy(platform = state.selected) {\n return platformConfig[platform]?.reserved_cash ?? true;\n }\n\n function platformDryRunOnly(platform = state.selected) {\n return platformConfig[platform]?.dry_run_only ?? false;\n }\n\n function allowMarginExplicitlySelected(form) {\n return normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode) === \"disabled\";\n }\n\n function reserveCashOverrideActive(form) {\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n return mode === \"ratio\" || mode === \"floor\" || mode === \"max\";\n }\n\n function executionCashPolicyConflict(form) {\n return allowMarginExplicitlySelected(form) && reserveCashOverrideActive(form);\n }\n\n function reconcileExecutionCashPolicy(form, changed) {\n if (!form) return;\n if (changed === \"margin\" && allowMarginExplicitlySelected(form)) {\n if (form.reservePolicyMode !== \"none\" && (form.minReservedCashUsd || form.reservedCashRatio)) {\n form._prevReserve = {\n mode: form.reservePolicyMode,\n floor: form.minReservedCashUsd,\n ratio: form.reservedCashRatio,\n };\n }\n form.reservePolicyMode = \"none\";\n form.reservedCashTouched = true;\n } else if (changed === \"reserve\" && reserveCashOverrideActive(form)) {\n form.cashOnlyExecutionMode = \"enabled\";\n form.cashOnlyExecutionTouched = true;\n }\n }\n\n function restoreReserveAfterMarginDisabled(form) {\n if (!form || allowMarginExplicitlySelected(form) || !form._prevReserve) return;\n form.reservePolicyMode = form._prevReserve.mode;\n form.minReservedCashUsd = form._prevReserve.floor;\n form.reservedCashRatio = form._prevReserve.ratio;\n form.reservedCashTouched = true;\n delete form._prevReserve;\n }\n\n function strategyDomain(profile) {\n return strategyCatalog[profile]?.domain || \"\";\n }\n\n function selectedCashCurrency(platform = state.selected, account = selectedAccount(platform)) {\n const configured = String(account?.cash_currency || \"\").trim().toUpperCase();\n if (configured === \"USD\" || configured === \"HKD\" || configured === \"CNY\") return configured;\n const domain = strategyDomain(state.forms[platform]?.strategy);\n if (domain === \"hk_equity\") return \"HKD\";\n if (domain === \"cn_equity\") return \"CNY\";\n return \"USD\";\n }\n\n function applyStrategyProfiles(rawProfiles) {\n const profiles = Array.isArray(rawProfiles) && rawProfiles.length\n ? rawProfiles\n : defaultStrategyProfiles;\n const nextOptions = [];\n const nextLabels = {};\n const nextCatalog = {};\n const nextIncomeLayerDefaults = {};\n const nextOptionOverlayDefaults = {};\n for (const item of profiles) {\n const profile = cleanStrategyProfile(item?.profile || item?.strategy_profile);\n if (!profile || nextOptions.includes(profile)) continue;\n const domain = cleanStrategyDomain(item?.domain || \"us_equity\");\n if (!domain) continue;\n nextOptions.push(profile);\n nextLabels[profile] = strategyLabelSet(profile, item);\n nextCatalog[profile] = {\n profile,\n label: nextLabels[profile].en || nextLabels[profile].zh || profile,\n label_en: nextLabels[profile].en || \"\",\n label_zh: nextLabels[profile].zh || \"\",\n domain,\n runtime_enabled: cleanOptionalBoolean(item?.runtime_enabled ?? item?.live_enabled ?? true) !== false,\n };\n const lifecycleStage = normalizeLifecycleStage(item?.lifecycle_stage ?? item?.lifecycleStage);\n if (lifecycleStage) nextCatalog[profile].lifecycle_stage = lifecycleStage;\n const allowedExecutionModes = normalizeAllowedExecutionModes(item?.allowed_execution_modes);\n if (allowedExecutionModes.length) nextCatalog[profile].allowed_execution_modes = allowedExecutionModes;\n const canSwitchLive = cleanOptionalBoolean(item?.can_switch_live);\n if (canSwitchLive !== null) nextCatalog[profile].can_switch_live = canSwitchLive;\n const blockedLiveReason = cleanDisplayText(item?.blocked_live_reason);\n if (blockedLiveReason) nextCatalog[profile].blocked_live_reason = blockedLiveReason;\n const latestEvidenceStatus = cleanDisplayText(item?.latest_evidence_status);\n if (latestEvidenceStatus) nextCatalog[profile].latest_evidence_status = latestEvidenceStatus;\n const pluginGateStatus = cleanDisplayText(item?.plugin_gate_status);\n if (pluginGateStatus) nextCatalog[profile].plugin_gate_status = pluginGateStatus;\n const dcaDefaults = dcaProfileDefaults[profile] || null;\n if (dcaDefaults || item?.dca_enabled === true) {\n nextCatalog[profile].dca_enabled = true;\n nextCatalog[profile].dca_default_mode = normalizeDcaMode(\n item?.dca_default_mode || item?.default_dca_mode || dcaDefaults?.defaultMode || \"fixed\",\n );\n nextCatalog[profile].dca_default_base_investment_usd = cleanDisplayPositiveNumber(\n item?.dca_default_base_investment_usd ||\n item?.default_dca_base_investment_usd ||\n dcaDefaults?.defaultBaseInvestmentUsd ||\n \"1000\",\n ) || \"1000\";\n }\n const profileIncomeDefaults = incomeLayerDefaultsFromProfileItem(item);\n const incomeDefaults = profileIncomeDefaults === false\n ? null\n : (profileIncomeDefaults || fallbackIncomeLayerDefaults[profile] || null);\n if (incomeDefaults) {\n nextIncomeLayerDefaults[profile] = incomeDefaults;\n nextCatalog[profile].income_layer_enabled = true;\n nextCatalog[profile].income_layer_start_usd = String(incomeDefaults.startUsd);\n nextCatalog[profile].income_layer_max_ratio = incomeDefaults.maxRatio;\n nextCatalog[profile].income_layer_allocations = incomeDefaults.allocations;\n }\n const profileOptionDefaults = optionOverlayDefaultsFromProfileItem(item);\n const optionDefaults = profileOptionDefaults === false\n ? null\n : (profileOptionDefaults || fallbackOptionOverlayDefaults[profile] || null);\n if (optionDefaults) {\n nextOptionOverlayDefaults[profile] = optionDefaults;\n nextCatalog[profile].option_overlay_enabled = true;\n nextCatalog[profile].option_overlay_live_gate = optionDefaults.liveGate || \"\";\n nextCatalog[profile].option_overlay_live_status = optionDefaults.liveStatus || \"\";\n }\n }\n if (!nextOptions.length && profiles !== defaultStrategyProfiles) return applyStrategyProfiles(defaultStrategyProfiles);\n strategyOptions = nextOptions;\n strategyLabels = nextLabels;\n strategyCatalog = nextCatalog;\n incomeLayerDefaults = nextIncomeLayerDefaults;\n optionOverlayDefaults = nextOptionOverlayDefaults;\n }\n\n function incomeLayerDefaultsFromProfileItem(item) {\n const enabled = cleanOptionalBoolean(item?.income_layer_enabled);\n const hasConfig = enabled !== null ||\n item?.income_layer_start_usd !== undefined ||\n item?.income_layer_max_ratio !== undefined ||\n item?.income_layer_allocations !== undefined;\n if (!hasConfig) return null;\n if (enabled === false) return false;\n const startUsd = cleanDisplayNumber(item?.income_layer_start_usd);\n const maxRatio = cleanDisplayRatio(item?.income_layer_max_ratio);\n const allocations = cleanIncomeLayerAllocations(item?.income_layer_allocations);\n if (!startUsd || !maxRatio || !allocations) return null;\n return { startUsd, maxRatio, allocations };\n }\n\n function cleanIncomeLayerAllocations(value) {\n if (!value || Array.isArray(value) || typeof value !== \"object\") return null;\n const allocations = {};\n let total = 0;\n for (const [rawSymbol, rawRatio] of Object.entries(value)) {\n const symbol = String(rawSymbol || \"\").trim().toUpperCase();\n const ratio = cleanDisplayPositiveNumber(rawRatio);\n if (!/^[A-Z0-9.-]{1,12}$/.test(symbol) || !ratio) continue;\n allocations[symbol] = Number(ratio);\n total += Number(ratio);\n }\n return total > 0 && Object.keys(allocations).length ? allocations : null;\n }\n\n function optionOverlayDefaultsFromProfileItem(item) {\n const enabled = cleanOptionalBoolean(item?.option_overlay_enabled);\n const hasConfig = enabled !== null ||\n item?.option_growth_overlay_enabled !== undefined ||\n item?.option_income_overlay_enabled !== undefined ||\n item?.option_overlay_live_gate !== undefined ||\n item?.option_overlay_live_status !== undefined;\n if (!hasConfig) return null;\n if (enabled === false) return false;\n const families = [\n optionOverlayFamilyDefaultsFromProfileItem(item, \"growth\"),\n optionOverlayFamilyDefaultsFromProfileItem(item, \"income\"),\n ].filter(Boolean);\n if (!families.length) return null;\n return {\n liveGate: String(item?.option_overlay_live_gate || \"promotion_required\"),\n liveStatus: String(item?.option_overlay_live_status || \"research_only\"),\n families,\n };\n }\n\n function optionOverlayFamilyDefaultsFromProfileItem(item, family) {\n const prefix = `option_${family}_overlay`;\n const enabled = cleanOptionalBoolean(item?.[`${prefix}_enabled`]);\n if (enabled !== true) return null;\n const recipe = cleanStrategyProfile(item?.[`${prefix}_recipe`]);\n const startUsd = cleanDisplayNumber(item?.[`${prefix}_start_usd`]);\n const ratioField = family === \"growth\"\n ? \"option_growth_overlay_nav_budget_ratio\"\n : \"option_income_overlay_nav_risk_ratio\";\n const ratio = cleanDisplayRatio(item?.[ratioField]);\n if (!recipe || !startUsd || !ratio) return null;\n return {\n family,\n recipe,\n startUsd,\n ratio,\n ratioKind: family === \"growth\" ? \"budget\" : \"risk\",\n };\n }\n\n function supportedDomainsForAccount(platform, account) {\n if (Array.isArray(account?.supported_domains) && account.supported_domains.length) {\n const cleaned = account.supported_domains.map(cleanStrategyDomain).filter(Boolean);\n if (cleaned.length) return [...new Set(cleaned)];\n }\n return inferSupportedDomains(platform, account);\n }\n\n function inferSupportedDomains(platform, account) {\n void account;\n if (platform === \"qmt\") return [\"cn_equity\"];\n if (platform === \"longbridge\" || platform === \"ibkr\") return [\"us_equity\", \"hk_equity\"];\n return [\"us_equity\"];\n }\n\n function supportedDomainLabel(platform, account) {\n return supportedDomainsForAccount(platform, account).map(domainLabel).join(\" / \");\n }\n\n function platformSupportsDca(platform = state.selected) {\n return platformConfig[platform]?.dca ?? false;\n }\n\n function strategyCatalogEntry(profile) {\n return strategyCatalog[cleanStrategyProfile(profile)] || {};\n }\n\n function normalizeLifecycleStage(value) {\n const text = String(value || \"\").trim().toLowerCase();\n if (!text || text.length > 40 || !/^[a-z0-9._-]+$/.test(text)) return \"\";\n return text;\n }\n\n function normalizeAllowedExecutionModes(value) {\n const items = Array.isArray(value)\n ? value\n : String(value || \"\").split(/[,\\s/|]+/);\n const modes = [];\n for (const item of items) {\n const mode = String(item || \"\").trim().toLowerCase();\n if (!mode || mode.length > 32 || !/^[a-z0-9._-]+$/.test(mode)) continue;\n if (!modes.includes(mode)) modes.push(mode);\n }\n return modes;\n }\n\n function cleanDisplayText(value) {\n const text = String(value ?? \"\").trim();\n if (!text || text.length > 120 || /[<>]/.test(text)) return \"\";\n return text;\n }\n\n function strategyCanSwitchLive(entry) {\n if (!entry || typeof entry !== \"object\") return false;\n if (entry.runtime_enabled === false) return false;\n const allowedModes = normalizeAllowedExecutionModes(entry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(\"live\")) return false;\n if (cleanOptionalBoolean(entry.can_switch_live) === false) return false;\n const lifecycleStage = normalizeLifecycleStage(entry.lifecycle_stage);\n if (lifecycleStage && [\"research\", \"draft\", \"blocked\", \"archived\", \"disabled\"].includes(lifecycleStage)) return false;\n const blockedReason = cleanDisplayText(entry.blocked_live_reason);\n if (blockedReason) return false;\n const evidenceStatus = cleanDisplayText(entry.latest_evidence_status);\n if (evidenceStatus && [\"research_only\", \"blocked\", \"pending\"].includes(evidenceStatus.toLowerCase())) return false;\n const pluginGateStatus = cleanDisplayText(entry.plugin_gate_status);\n if (pluginGateStatus && [\"blocked\", \"locked\", \"disabled\"].includes(pluginGateStatus.toLowerCase())) return false;\n return true;\n }\n\n function strategyBlockedLiveReason(entry) {\n if (!entry || typeof entry !== \"object\") return \"\";\n const explicit = cleanDisplayText(entry.blocked_live_reason);\n if (explicit) return explicit;\n if (entry.runtime_enabled === false) return \"runtime_enabled=false\";\n const allowedModes = normalizeAllowedExecutionModes(entry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(\"live\")) {\n return `allowed_execution_modes=${allowedModes.join(\",\")}`;\n }\n if (cleanOptionalBoolean(entry.can_switch_live) === false) return \"can_switch_live=false\";\n const lifecycleStage = normalizeLifecycleStage(entry.lifecycle_stage);\n if (lifecycleStage && [\"research\", \"draft\", \"blocked\", \"archived\", \"disabled\"].includes(lifecycleStage)) {\n return `lifecycle_stage=${lifecycleStage}`;\n }\n const evidenceStatus = cleanDisplayText(entry.latest_evidence_status);\n if (evidenceStatus && evidenceStatus.toLowerCase() !== \"live_allowed\") {\n return `latest_evidence_status=${evidenceStatus}`;\n }\n const pluginGateStatus = cleanDisplayText(entry.plugin_gate_status);\n if (pluginGateStatus && pluginGateStatus.toLowerCase() !== \"live_allowed\") {\n return `plugin_gate_status=${pluginGateStatus}`;\n }\n return \"\";\n }\n\n function strategyDisplayMetaText(platform, account, profile) {\n const entry = strategyCatalogEntry(profile);\n if (!entry.profile) {\n return t(\"strategyMeta\").replace(\"{domains}\", supportedDomainLabel(platform, account));\n }\n const lines = [];\n if (entry.lifecycle_stage) lines.push(`lifecycle_stage: ${entry.lifecycle_stage}`);\n lines.push(`runtime_enabled: ${entry.runtime_enabled === false ? \"false\" : \"true\"}`);\n lines.push(`can_switch_live: ${strategyCanSwitchLive(entry) ? \"true\" : \"false\"}`);\n if (entry.allowed_execution_modes?.length) {\n lines.push(`allowed_execution_modes: ${entry.allowed_execution_modes.join(\", \")}`);\n }\n const blockedReason = strategyBlockedLiveReason(entry);\n if (blockedReason) lines.push(`blocked_live_reason: ${blockedReason}`);\n if (entry.latest_evidence_status) lines.push(`latest_evidence_status: ${entry.latest_evidence_status}`);\n if (entry.plugin_gate_status) lines.push(`plugin_gate_status: ${entry.plugin_gate_status}`);\n lines.push(`domains: ${supportedDomainLabel(platform, account)}`);\n return lines.join(\"\\n\");\n }\n\n function strategyActionNoteText(platform = state.selected, account = selectedAccount(platform)) {\n const profile = state.forms[platform]?.strategy || \"\";\n const meta = strategyDisplayMetaText(platform, account, profile);\n return meta ? `${t(\"invalidStrategyNote\")}\\n${meta}` : t(\"invalidStrategyNote\");\n }\n\n function strategyChoiceLabel(profile, platform = state.selected, account = selectedAccount(platform), executionMode = state.forms[platform]?.executionMode) {\n const label = strategyLabel(profile);\n const entry = strategyCatalogEntry(profile);\n if (!entry.profile) return label;\n if (normalizeExecutionMode(executionMode, false) === \"live\" && !strategyCanSwitchLive(entry)) {\n return `${label} [blocked live]`;\n }\n if (entry.runtime_enabled === false) {\n return `${label} [disabled]`;\n }\n return label;\n }\n\n function strategyAllowedForAccount(platform, account, profile, executionMode = state.forms[platform]?.executionMode) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalogEntry = strategyCatalogEntry(cleanProfile);\n if (!catalogEntry.profile) return false;\n if (dcaConfigForStrategy(cleanProfile) && !platformSupportsDca(platform)) return false;\n if (!supportedDomainsForAccount(platform, account).includes(catalogEntry.domain)) return false;\n const mode = normalizeExecutionMode(executionMode, false);\n if (mode === \"live\") return strategyCanSwitchLive(catalogEntry);\n const allowedModes = normalizeAllowedExecutionModes(catalogEntry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(mode)) return false;\n if (catalogEntry.runtime_enabled !== true && !allowedModes.length) return false;\n return true;\n }\n\n function strategyChoicesForAccount(platform = state.selected, account = selectedAccount(platform), executionMode = state.forms[platform]?.executionMode) {\n const choices = strategyOptions.filter((profile) => strategyAllowedForAccount(platform, account, profile, executionMode));\n const addChoice = (value) => {\n const profile = cleanStrategyProfile(value);\n if (profile && !choices.includes(profile) && strategyAllowedForAccount(platform, account, profile, executionMode)) {\n choices.push(profile);\n }\n };\n if (account) {\n addChoice(account.default_strategy_profile || account.strategy_profile);\n }\n return choices;\n }\n\n function strategyLabel(profile) {\n const labels = strategyLabels[profile] || localStrategyLabels[profile];\n if (!labels) return profile;\n return state.lang === \"zh\"\n ? (labels.zh || labels.en || profile)\n : (labels.en || labels.zh || profile);\n }\n\n function strategyLabelSet(profile, item) {\n const local = localStrategyLabels[profile] || {};\n const label = String(item?.label || item?.display_name || \"\").trim();\n const labelEn = String(item?.label_en || item?.display_name_en || \"\").trim();\n const labelZh = String(item?.label_zh || item?.display_name_zh || \"\").trim();\n return {\n zh: labelZh || local.zh || label || local.en || profile,\n en: labelEn || label || local.en || labelZh || local.zh || profile,\n };\n }\n\n function modeLabel(mode) {\n return mode === \"paper\" ? t(\"paper\") : t(\"live\");\n }\n\n function normalizePluginMode(value) {\n return pluginModes.includes(value) ? value : \"auto\";\n }\n\n function pluginModeLabel(mode) {\n return mode === \"none\" ? t(\"pluginModeNone\") : t(\"pluginModeAuto\");\n }\n\n function dcaConfigForStrategy(profile) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalog = strategyCatalog[cleanProfile] || {};\n if (catalog.dca_enabled === true) {\n return {\n defaultMode: normalizeDcaMode(catalog.dca_default_mode || \"fixed\"),\n defaultBaseInvestmentUsd: cleanDisplayPositiveNumber(catalog.dca_default_base_investment_usd) || \"1000\",\n };\n }\n return dcaProfileDefaults[cleanProfile] || null;\n }\n\n function dcaSupported(profile) {\n return Boolean(dcaConfigForStrategy(profile));\n }\n\n function normalizeDcaMode(value) {\n const mode = String(value || \"\").trim().toLowerCase();\n if (mode === \"smart_dca\") return \"smart\";\n if (mode === \"fixed_dca\" || mode === \"ordinary\" || mode === \"ordinary_dca\") return \"fixed\";\n return dcaModes.includes(mode) ? mode : \"fixed\";\n }\n\n function dcaModeLabel(mode) {\n return normalizeDcaMode(mode) === \"smart\" ? t(\"dcaModeSmart\") : t(\"dcaModeFixed\");\n }\n\n function normalizeRuntimeTargetMode(value) {\n return runtimeTargetModes.includes(value) ? value : \"current\";\n }\n\n function runtimeTargetModeLabel(mode) {\n if (mode === \"enabled\") return t(\"runtimeTargetEnabled\");\n if (mode === \"disabled\") return t(\"runtimeTargetDisabled\");\n return t(\"runtimeTargetCurrent\");\n }\n\n function runtimeTargetEnabledForAccount(platform, account) {\n return cleanOptionalBoolean(currentEntryForAccount(platform, account)?.runtime_target_enabled);\n }\n\n function runtimeTargetStateForAccount(platform = state.selected, account = selectedAccount(platform)) {\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return { known: false, enabled: null };\n const configured = cleanOptionalBoolean(entry.runtime_target_enabled);\n return { known: true, enabled: configured ?? true };\n }\n\n function runtimeTargetText(enabled) {\n return enabled ? t(\"runtimeTargetOn\") : t(\"runtimeTargetOff\");\n }\n\n function runtimeTargetTone(enabled) {\n return enabled ? \"enabled\" : \"disabled\";\n }\n\n function currentRuntimeTargetText(platform = state.selected, account = selectedAccount(platform)) {\n const target = runtimeTargetStateForAccount(platform, account);\n if (!target.known) return t(\"notRead\");\n return runtimeTargetText(target.enabled);\n }\n\n function currentRuntimeTargetTone(platform = state.selected, account = selectedAccount(platform)) {\n const target = runtimeTargetStateForAccount(platform, account);\n if (!target.known) return \"neutral\";\n return runtimeTargetTone(target.enabled);\n }\n\n function incomeLayerDefaultForStrategy(profile) {\n return incomeLayerDefaults[cleanStrategyProfile(profile)] || null;\n }\n\n function incomeLayerSupported(profile) {\n return Boolean(incomeLayerDefaultForStrategy(profile));\n }\n\n function normalizeIncomeLayerMode(value) {\n return incomeLayerModes.includes(value) ? value : \"current\";\n }\n\n function incomeLayerModeLabel(mode) {\n if (mode === \"enabled\") return t(\"incomeLayerEnabled\");\n if (mode === \"disabled\") return t(\"incomeLayerDisabled\");\n return t(\"incomeLayerCurrent\");\n }\n\n function optionOverlayDefaultForStrategy(profile) {\n return optionOverlayDefaults[cleanStrategyProfile(profile)] || null;\n }\n\n function optionOverlaySupported(profile) {\n return Boolean(optionOverlayDefaultForStrategy(profile));\n }\n\n function normalizeOptionOverlayMode(value) {\n return optionOverlayModes.includes(value) ? value : \"current\";\n }\n\n function optionOverlayModeLabel(mode) {\n if (mode === \"enabled\") return t(\"optionOverlayEnabled\");\n if (mode === \"disabled\") return t(\"optionOverlayDisabled\");\n return t(\"optionOverlayCurrent\");\n }\n\n function optionOverlayText(enabled) {\n return enabled ? t(\"optionOverlayOn\") : t(\"optionOverlayOff\");\n }\n\n function normalizeCashOnlyExecutionMode(value) {\n return cashOnlyExecutionModes.includes(value) ? value : \"current\";\n }\n\n function cashOnlyExecutionModeLabel(mode) {\n if (mode === \"enabled\") return t(\"cashOnlyExecutionNo\");\n if (mode === \"disabled\") return t(\"cashOnlyExecutionYes\");\n return t(\"cashOnlyExecutionCurrent\");\n }\n\n function cashOnlyExecutionText(enabled) {\n if (enabled === null) return t(\"notRead\");\n return enabled ? t(\"cashOnlyExecutionValueNo\") : t(\"cashOnlyExecutionValueYes\");\n }\n\n function platformCashOnlyExecutionDefault() {\n return true;\n }\n\n function effectiveCashOnlyExecutionForAccount(platform, account) {\n const configured = currentCashOnlyExecutionForAccount(platform, account);\n if (configured !== null) return configured;\n if (!platformSupportsMarginPolicy(platform)) return null;\n return platformCashOnlyExecutionDefault();\n }\n\n function currentCashOnlyExecutionForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n if (entry) {\n const val = cleanOptionalBoolean(entry.cash_only_execution);\n if (val !== null) return val;\n }\n return platformCashOnlyExecutionDefault();\n }\n\n function currentCashOnlyExecutionText(platform = state.selected, account = selectedAccount(platform)) {\n if (!platformSupportsMarginPolicy(platform)) return t(\"notRead\");\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const configured = cleanOptionalBoolean(entry.cash_only_execution);\n if (configured === null) return t(\"cashOnlyExecutionDefault\");\n return cashOnlyExecutionText(configured);\n }\n\n function currentOptionOverlayForAccount(platform, account) {\n return cleanOptionalBoolean(currentEntryForAccount(platform, account)?.option_overlay_enabled);\n }\n\n function effectiveOptionOverlayForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const configured = currentOptionOverlayForAccount(platform, account);\n if (configured !== null) return configured;\n if (!optionOverlaySupported(profile)) return null;\n return true;\n }\n\n function optionOverlayDefaultSummaryDetail(defaults) {\n if (!defaults?.families?.length) return \"\";\n return defaults.families.map((item) => {\n const family = item.family === \"income\" ? t(\"optionOverlayFamilyIncome\") : t(\"optionOverlayFamilyGrowth\");\n const ratioText = item.ratioKind === \"risk\"\n ? t(\"optionOverlayRiskRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio))\n : t(\"optionOverlayBudgetRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio));\n return `${family} ${ratioText}`;\n }).join(\" / \");\n }\n\n function optionOverlayDefaultText(profile) {\n const defaults = optionOverlayDefaultForStrategy(profile);\n if (!defaults) return t(\"optionOverlayNotSupported\");\n const detail = optionOverlayDefaultSummaryDetail(defaults);\n return detail ? t(\"optionOverlayDefault\").replace(\"{detail}\", detail) : t(\"optionOverlayDefaultSimple\");\n }\n\n function currentOptionOverlayText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const configured = cleanOptionalBoolean(entry.option_overlay_enabled);\n if (!optionOverlaySupported(profile)) {\n return configured === null ? t(\"optionOverlayNotSupported\") : optionOverlayText(configured);\n }\n if (configured === null) return optionOverlayDefaultText(profile);\n return optionOverlayText(configured);\n }\n\n function currentIncomeLayerForAccount(platform, account) {\n return incomeLayerFromEntry(currentEntryForAccount(platform, account));\n }\n\n function incomeLayerFromEntry(entry) {\n return {\n enabled: cleanOptionalBoolean(entry?.income_layer_enabled),\n startUsd: cleanDisplayNumber(entry?.income_layer_start_usd),\n maxRatio: cleanDisplayRatio(entry?.income_layer_max_ratio),\n };\n }\n\n function incomeLayerFieldsConfigured(entry) {\n const current = incomeLayerFromEntry(entry);\n return current.enabled !== null || Boolean(current.startUsd || current.maxRatio);\n }\n\n function effectiveIncomeLayerForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return null;\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return null;\n const current = incomeLayerFromEntry(entry);\n if (!incomeLayerFieldsConfigured(entry)) {\n return {\n enabled: true,\n startUsd: String(defaults.startUsd),\n maxRatio: defaults.maxRatio,\n };\n }\n return {\n enabled: current.enabled ?? true,\n startUsd: current.startUsd || String(defaults.startUsd),\n maxRatio: current.maxRatio || defaults.maxRatio,\n };\n }\n\n function currentDcaForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const defaults = dcaConfigForStrategy(profile);\n if (!defaults) return { supported: false, mode: \"\", baseInvestmentUsd: \"\" };\n const entry = currentEntryForAccount(platform, account);\n return {\n supported: true,\n mode: normalizeDcaMode(entry?.dca_mode || account?.dca_mode || defaults.defaultMode),\n baseInvestmentUsd: cleanDisplayPositiveNumber(entry?.dca_base_investment_usd) ||\n cleanDisplayPositiveNumber(account?.dca_base_investment_usd) ||\n defaults.defaultBaseInvestmentUsd,\n };\n }\n\n function normalizeAccountLookupKey(value) {\n return String(value || \"\").trim().toLowerCase();\n }\n\n function collectAccountLookupCandidates(keys) {\n const candidates = new Set();\n for (const rawKey of keys) {\n const key = normalizeAccountLookupKey(rawKey);\n if (!key) continue;\n\n candidates.add(key);\n\n const compact = key.replace(/[^a-z0-9]+/g, \"\");\n if (compact) candidates.add(compact);\n\n const parts = key.split(/[^a-z0-9]+/).filter(Boolean);\n if (parts.length > 1) candidates.add(parts[parts.length - 1]);\n }\n return [...candidates];\n }\n\n function resolveCurrentEntryByKey(byPlatform, keys) {\n const candidates = new Set(collectAccountLookupCandidates(keys));\n if (!candidates.size) return null;\n\n for (const key of keys) {\n const entry = byPlatform[key];\n if (currentEntryHasState(entry)) return entry;\n }\n\n for (const [rawKey, entry] of Object.entries(byPlatform)) {\n if (!currentEntryHasState(entry)) continue;\n const rawCandidates = collectAccountLookupCandidates([rawKey]);\n const hasMatch = rawCandidates.some((candidate) => candidates.has(candidate));\n if (hasMatch) return entry;\n }\n\n return null;\n }\n\n function currentEntryForAccount(platform, account) {\n const byPlatform = state.currentStrategies[platform] || {};\n const keys = [account?.key, account?.target_name, account?.label]\n .filter(Boolean)\n .map((value) => String(value));\n const entry = resolveCurrentEntryByKey(byPlatform, keys);\n if (entry) {\n if (!entry.strategy_profile) {\n const gd = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {};\n const entryCopy = { ...entry };\n entryCopy.strategy_profile = account?.default_strategy_profile || gd.default_strategy_profile || \"\";\n entryCopy.source = (entryCopy.source || \"worker\") + \"+account_defaults\";\n return entryCopy;\n }\n return entry;\n }\n const globalDefaults = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {};\n const merged = { ...globalDefaults, ...(account || {}) };\n const profile = cleanStrategyProfile(merged.default_strategy_profile || merged.strategy_profile || \"\");\n const synth = {\n strategy_profile: profile || merged.default_strategy_profile || \"\",\n source: \"account_defaults\",\n };\n const cashMode = merged.cash_only_execution_mode;\n if (cashMode === \"enabled\") synth.cash_only_execution = true;\n else if (cashMode === \"disabled\") synth.cash_only_execution = false;\n else if (platformSupportsMarginPolicy(platform)) synth.cash_only_execution = true;\n if (merged.min_reserved_cash_usd) synth.min_reserved_cash_usd = merged.min_reserved_cash_usd;\n if (merged.reserved_cash_ratio) synth.reserved_cash_ratio = merged.reserved_cash_ratio;\n synth.runtime_target_enabled = merged.runtime_target_enabled !== false;\n const execMode = merged.default_execution_mode || platformConfig[platform]?.default_execution_mode || \"live\";\n synth.execution_mode = execMode;\n synth.dry_run_only = execMode === \"paper\";\n return synth;\n }\n\n function currentEntryHasState(entry) {\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) return false;\n return Boolean(\n cleanStrategyProfile(entry.strategy_profile) ||\n cleanDisplayNumber(entry.min_reserved_cash_usd ?? entry.reserved_cash_floor_usd) ||\n cleanDisplayRatio(entry.reserved_cash_ratio) ||\n cleanOptionalBoolean(entry.income_layer_enabled) !== null ||\n cleanDisplayNumber(entry.income_layer_start_usd) ||\n cleanDisplayRatio(entry.income_layer_max_ratio) ||\n cleanOptionalBoolean(entry.option_overlay_enabled) !== null ||\n cleanOptionalBoolean(entry.cash_only_execution) !== null ||\n cleanOptionalBoolean(entry.runtime_target_enabled) !== null ||\n normalizeDcaMode(entry.dca_mode || \"\") !== \"fixed\" ||\n cleanDisplayPositiveNumber(entry.dca_base_investment_usd) ||\n normalizeExecutionMode(entry.execution_mode, entry.dry_run_only),\n );\n }\n\n function currentStrategyForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n const profile = cleanStrategyProfile(entry?.strategy_profile);\n if (profile) return profile;\n const fallback = account?.default_strategy_profile || account?.strategy_profile || \"\";\n return cleanStrategyProfile(fallback);\n }\n\n function currentReservePolicyForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n return reservePolicyFromEntry(entry);\n }\n\n function currentPluginModeForAccount(platform, account) {\n void platform;\n return normalizePluginMode(account?.plugin_mode);\n }\n\n function reservePolicyFromEntry(entry) {\n return {\n minReservedCashUsd: cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd),\n reservedCashRatio: cleanDisplayRatio(entry?.reserved_cash_ratio),\n };\n }\n\n function cleanDisplayNumber(value) {\n const text = String(value ?? \"\").trim();\n if (!text || text.length > 32 || !/^(?:\\d+|\\d*\\.\\d+)$/.test(text)) return \"\";\n const numeric = Number(text);\n if (!Number.isFinite(numeric) || numeric < 0) return \"\";\n return text;\n }\n\n function cleanDisplayRatio(value) {\n const text = cleanDisplayNumber(value);\n if (!text) return \"\";\n const numeric = Number(text);\n return numeric >= 0 && numeric <= 1 ? text : \"\";\n }\n\n function cleanDisplayPositiveNumber(value) {\n const text = cleanDisplayNumber(value);\n return text && Number(text) > 0 ? text : \"\";\n }\n\n function normalizeExecutionMode(value, dryRunOnly) {\n const mode = String(value || \"\").trim().toLowerCase();\n if (mode === \"live\" || mode === \"paper\") return mode;\n if (dryRunOnly === true || dryRunOnly === \"true\" || dryRunOnly === \"1\" || dryRunOnly === 1) return \"paper\";\n if (dryRunOnly === false || dryRunOnly === \"false\" || dryRunOnly === \"0\" || dryRunOnly === 0) return \"live\";\n return \"\";\n }\n\n function cleanOptionalBoolean(value) {\n if (value === true || value === 1) return true;\n if (value === false || value === 0) return false;\n if (typeof value === \"string\") {\n const text = value.trim().toLowerCase();\n if (text === \"true\" || text === \"1\") return true;\n if (text === \"false\" || text === \"0\") return false;\n }\n return null;\n }\n\n function defaultExecutionModeForAccount(platform, account, fallback = \"live\") {\n if (platformDryRunOnly(platform)) return \"paper\";\n const currentMode = normalizeExecutionMode(\n currentEntryForAccount(platform, account)?.execution_mode,\n currentEntryForAccount(platform, account)?.dry_run_only,\n );\n if (currentMode) return currentMode;\n const hint = [\n account?.key,\n account?.label,\n account?.target_name,\n account?.deployment_selector,\n account?.account_scope,\n account?.service_name,\n ].join(\" \").toLowerCase();\n if (hint.split(/\\s+/).includes(\"paper\") || hint.includes(\"-paper\") || hint.includes(\"_paper\") || hint.includes(\"dry_run\") || hint.includes(\"dry-run\")) {\n return \"paper\";\n }\n return fallback;\n }\n\n function defaultStrategyForAccount(platform, account, fallback = \"soxl_soxx_trend_income\") {\n const currentProfile = currentStrategyForAccount(platform, account);\n if (currentProfile) return currentProfile;\n const profile = cleanStrategyProfile(account?.default_strategy_profile || account?.strategy_profile);\n if (profile && strategyAllowedForAccount(platform, account, profile)) return profile;\n const hint = [\n account?.key,\n account?.label,\n account?.target_name,\n account?.deployment_selector,\n account?.account_scope,\n account?.service_name,\n ].join(\" \").toLowerCase();\n const hinted = hint.includes(\"dividend\")\n ? \"cn_dividend_quality_snapshot\"\n : (hint.includes(\"industry\") ? \"cn_industry_etf_rotation\" : (hint.includes(\"smart-dca\") || hint.includes(\"smart_dca\")\n ? \"nasdaq_sp500_smart_dca\"\n : (hint.includes(\"soxl\") ? \"soxl_soxx_trend_income\" : (hint.includes(\"tqqq\") ? \"tqqq_growth_income\" : \"\"))));\n if (hinted && strategyAllowedForAccount(platform, account, hinted)) return hinted;\n if (fallback && strategyAllowedForAccount(platform, account, fallback)) return fallback;\n return strategyChoicesForAccount(platform, account)[0] || \"\";\n }\n\n function syncStrategyForAccount(platform) {\n const account = selectedAccount(platform);\n if (!account) return;\n state.forms[platform].strategy = defaultStrategyForAccount(platform, account, state.forms[platform].strategy);\n state.forms[platform].executionMode = defaultExecutionModeForAccount(\n platform,\n account,\n );\n state.forms[platform].pluginMode = currentPluginModeForAccount(platform, account);\n syncRuntimeTargetForAccount(platform);\n syncReservePolicyForAccount(platform);\n syncIncomeLayerForAccount(platform);\n syncOptionOverlayForAccount(platform);\n syncCashOnlyExecutionForAccount(platform);\n reconcileExecutionCashPolicy(state.forms[platform], \"margin\");\n syncDcaForAccount(platform);\n }\n\n function syncRuntimeTargetForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.runtimeTargetTouched) return;\n const current = runtimeTargetEnabledForAccount(platform, selectedAccount(platform));\n form.runtimeTargetMode = current === false ? \"disabled\" : \"enabled\";\n }\n\n function syncReservePolicyForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.reservedCashTouched) return;\n const policy = currentReservePolicyForAccount(platform, selectedAccount(platform));\n const hasFloor = Boolean(policy.minReservedCashUsd);\n const hasRatio = Boolean(policy.reservedCashRatio);\n if (hasFloor && hasRatio) form.reservePolicyMode = \"max\";\n else if (hasFloor) form.reservePolicyMode = \"floor\";\n else if (hasRatio) form.reservePolicyMode = \"ratio\";\n else form.reservePolicyMode = \"none\";\n form.minReservedCashUsd = policy.minReservedCashUsd;\n form.reservedCashRatio = policy.reservedCashRatio;\n }\n\n function syncIncomeLayerForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.incomeLayerTouched) return;\n const defaults = incomeLayerDefaultForStrategy(form.strategy);\n const current = currentIncomeLayerForAccount(platform, selectedAccount(platform));\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n if (entry && incomeLayerFieldsConfigured(entry)) {\n form.incomeLayerMode = current.enabled === false ? \"disabled\" : \"enabled\";\n } else {\n form.incomeLayerMode = incomeLayerSupported(form.strategy) ? \"enabled\" : \"disabled\";\n }\n form.incomeLayerStartUsd = current.startUsd || String(defaults?.startUsd || \"\");\n form.incomeLayerMaxRatio = current.maxRatio || defaults?.maxRatio || \"\";\n }\n\n function syncOptionOverlayForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.optionOverlayTouched) return;\n const configured = normalizeOptionOverlayMode(selectedAccount(platform)?.option_overlay_mode);\n if (configured !== \"current\") {\n form.optionOverlayMode = configured;\n return;\n }\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n const rawValue = cleanOptionalBoolean(entry?.option_overlay_enabled);\n if (rawValue !== null) {\n form.optionOverlayMode = rawValue ? \"enabled\" : \"disabled\";\n return;\n }\n form.optionOverlayMode = optionOverlaySupported(form.strategy) ? \"enabled\" : \"disabled\";\n }\n\n function syncCashOnlyExecutionForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.cashOnlyExecutionTouched) return;\n const configured = normalizeCashOnlyExecutionMode(selectedAccount(platform)?.cash_only_execution_mode);\n if (configured !== \"current\") {\n form.cashOnlyExecutionMode = configured;\n return;\n }\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n const rawValue = cleanOptionalBoolean(entry?.cash_only_execution);\n if (rawValue !== null) {\n form.cashOnlyExecutionMode = rawValue ? \"enabled\" : \"disabled\";\n return;\n }\n // No explicit config — use platform default (cash-only for margin-capable platforms)\n form.cashOnlyExecutionMode = platformSupportsMarginPolicy(platform) ? \"enabled\" : \"disabled\";\n }\n\n function syncDcaForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.dcaTouched) return;\n const current = currentDcaForAccount(platform, selectedAccount(platform), form.strategy);\n form.dcaMode = current.supported ? current.mode : \"fixed\";\n form.dcaBaseInvestmentUsd = current.supported ? current.baseInvestmentUsd : \"\";\n }\n\n function ensureAccountSelection(platform) {\n const options = optionsFor(platform);\n if (!options.length) return;\n if (!options.some((option) => option.key === state.forms[platform].accountKey)) {\n state.forms[platform].accountKey = options[0].key;\n state.forms[platform].runtimeTargetTouched = false;\n state.forms[platform].reservedCashTouched = false;\n state.forms[platform].incomeLayerTouched = false;\n state.forms[platform].optionOverlayTouched = false;\n state.forms[platform].cashOnlyExecutionTouched = false;\n state.forms[platform].dcaTouched = false;\n state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0], state.forms[platform].strategy);\n state.forms[platform].pluginMode = currentPluginModeForAccount(platform, options[0]);\n syncRuntimeTargetForAccount(platform);\n syncReservePolicyForAccount(platform);\n syncIncomeLayerForAccount(platform);\n syncOptionOverlayForAccount(platform);\n syncCashOnlyExecutionForAccount(platform);\n syncDcaForAccount(platform);\n }\n }\n\n function derivedService(platform, targetName) {\n if (platform === \"longbridge\") return `longbridge-quant-${targetName.toLowerCase()}-service`;\n if (platform === \"ibkr\") return `interactive-brokers-${targetName.toLowerCase()}-service`;\n if (platform === \"schwab\") return \"charles-schwab-quant-service\";\n if (platform === \"firstrade\") return \"firstrade-quant-service\";\n if (platform === \"qmt\") return \"qmt-quant-service\";\n return \"\";\n }\n\n function accountMetaText(platform = state.selected) {\n const account = selectedAccount(platform);\n const targetName = account.target_name || account.key;\n const raw = account.service_name || derivedService(platform, targetName);\n const service = raw || (state.lang === \"zh\" ? \"无\" : \"-\");\n return t(\"targetMeta\")\n .replace(\"{target}\", targetName)\n .replace(\"{service}\", service)\n .replace(\"{domains}\", supportedDomainLabel(platform, account));\n }\n\n function hasRunnableStrategySelection(platform = state.selected) {\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n return Boolean(form?.strategy && account && strategyAllowedForAccount(platform, account, form.strategy, form.executionMode));\n }\n\n function hasValidReservePolicy(platform = state.selected) {\n if (!platformSupportsReservedCashPolicy(platform)) return true;\n const form = state.forms[platform];\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n if (mode === \"current\" || mode === \"none\") return true;\n return Boolean(reservePolicyOverrideForForm(form, platform));\n }\n\n function hasValidExecutionCashPolicy(platform = state.selected) {\n if (!platformSupportsMarginPolicy(platform) && !platformSupportsReservedCashPolicy(platform)) return true;\n const form = state.forms[platform];\n return !executionCashPolicyConflict(form) && hasValidReservePolicy(platform);\n }\n\n function hasValidIncomeLayerPolicy(platform = state.selected) {\n const form = state.forms[platform];\n if (!incomeLayerSupported(form?.strategy)) return true;\n const mode = normalizeIncomeLayerMode(form?.incomeLayerMode);\n if (mode === \"current\" || mode === \"disabled\") return true;\n const defaults = incomeLayerDefaultForStrategy(form?.strategy);\n const startUsd = cleanDisplayNumber(form?.incomeLayerStartUsd || defaults?.startUsd);\n const maxRatio = cleanDisplayRatio(form?.incomeLayerMaxRatio || defaults?.maxRatio);\n return Boolean(startUsd && maxRatio);\n }\n\n function hasValidOptionOverlayPolicy(platform = state.selected) {\n const form = state.forms[platform];\n const mode = normalizeOptionOverlayMode(form?.optionOverlayMode);\n return mode !== \"enabled\" || optionOverlaySupported(form?.strategy);\n }\n\n function hasValidDcaPolicy(platform = state.selected) {\n const form = state.forms[platform];\n if (!dcaSupported(form?.strategy) || !platformSupportsDca(platform)) return true;\n return Boolean(dcaModes.includes(normalizeDcaMode(form?.dcaMode)) && cleanDisplayPositiveNumber(form?.dcaBaseInvestmentUsd));\n }\n\n function hasValidStrategySelection(platform = state.selected) {\n return hasRunnableStrategySelection(platform) &&\n hasValidExecutionCashPolicy(platform) &&\n hasValidIncomeLayerPolicy(platform) &&\n hasValidOptionOverlayPolicy(platform) &&\n hasValidDcaPolicy(platform);\n }\n\n function normalizeReservePolicyMode(value) {\n return reservePolicyModes.includes(value) ? value : \"current\";\n }\n\n function reservePolicyOverrideForForm(form, platform = state.selected) {\n if (!platformSupportsReservedCashPolicy(platform)) return null;\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n const floor = cleanDisplayNumber(form?.minReservedCashUsd);\n const ratio = cleanDisplayRatio(form?.reservedCashRatio);\n const extraVariables = {};\n if (mode === \"current\") return null;\n if (mode === \"none\") {\n extraVariables[platformMinReservedCashVariables[platform]] = \"\";\n extraVariables[platformReservedCashRatioVariables[platform]] = \"\";\n return { inputs: {}, extraVariables };\n }\n if (mode === \"ratio\") {\n if (!ratio) return null;\n extraVariables[platformMinReservedCashVariables[platform]] = \"\";\n return { inputs: { reserved_cash_ratio: ratio }, extraVariables };\n }\n if (mode === \"floor\") {\n if (!floor) return null;\n extraVariables[platformReservedCashRatioVariables[platform]] = \"\";\n return { inputs: { min_reserved_cash_usd: floor }, extraVariables };\n }\n if (mode === \"max\") {\n if (!floor || !ratio) return null;\n return { inputs: { min_reserved_cash_usd: floor, reserved_cash_ratio: ratio }, extraVariables };\n }\n return null;\n }\n\n function runtimeTargetOverrideForForm(form) {\n const mode = normalizeRuntimeTargetMode(form?.runtimeTargetMode);\n if (mode === \"current\") return null;\n return {\n inputs: { runtime_target_enabled_mode: mode },\n extraVariables: { [runtimeTargetEnabledVariable]: mode === \"enabled\" ? \"true\" : \"false\" },\n };\n }\n\n function incomeLayerOverrideForForm(form) {\n const defaults = incomeLayerDefaultForStrategy(form?.strategy);\n if (!defaults) return null;\n const mode = normalizeIncomeLayerMode(form?.incomeLayerMode);\n if (mode === \"current\") return null;\n const extraVariables = {};\n if (mode === \"disabled\") {\n extraVariables[incomeLayerEnabledVariable] = \"false\";\n extraVariables[incomeLayerStartUsdVariable] = \"\";\n extraVariables[incomeLayerMaxRatioVariable] = \"\";\n return { inputs: { income_layer_mode: mode }, extraVariables };\n }\n const startUsd = cleanDisplayNumber(form?.incomeLayerStartUsd || defaults.startUsd);\n const maxRatio = cleanDisplayRatio(form?.incomeLayerMaxRatio || defaults.maxRatio);\n if (!startUsd || !maxRatio) return null;\n extraVariables[incomeLayerEnabledVariable] = \"true\";\n extraVariables[incomeLayerStartUsdVariable] = startUsd;\n extraVariables[incomeLayerMaxRatioVariable] = maxRatio;\n return { inputs: { income_layer_mode: mode, income_layer_start_usd: startUsd, income_layer_max_ratio: maxRatio }, extraVariables };\n }\n\n function optionOverlayOverrideForForm(form) {\n const mode = normalizeOptionOverlayMode(form?.optionOverlayMode);\n if (mode === \"current\") return null;\n if (mode === \"enabled\" && !optionOverlaySupported(form?.strategy)) return null;\n return { inputs: { option_overlay_mode: mode } };\n }\n\n function cashOnlyExecutionOverrideForForm(form, platform = state.selected) {\n if (!platformSupportsMarginPolicy(platform)) return null;\n const mode = normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode);\n if (mode === \"current\") return null;\n return { inputs: { cash_only_execution_mode: mode } };\n }\n\n function dcaOverrideForForm(form) {\n if (!dcaSupported(form?.strategy) || !platformSupportsDca(state.selected)) return null;\n const mode = normalizeDcaMode(form?.dcaMode);\n const baseInvestmentUsd = cleanDisplayPositiveNumber(form?.dcaBaseInvestmentUsd);\n if (!baseInvestmentUsd) return null;\n return { inputs: { dca_mode: mode, dca_base_investment_usd: baseInvestmentUsd } };\n }\n\n function mergeExtraVariables(inputs, extraVariables) {\n if (!extraVariables || !Object.keys(extraVariables).length) return;\n const merged = inputs.extra_variables_json ? JSON.parse(inputs.extra_variables_json) : {};\n Object.assign(merged, extraVariables);\n inputs.extra_variables_json = JSON.stringify(merged);\n }\n\n function buildInputs(platform = state.selected) {\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n const targetName = account.target_name || account.key;\n const inputs = {\n platform,\n target_name: targetName,\n strategy_profile: form.strategy,\n execution_mode: form.executionMode,\n variable_scope: account.variable_scope || \"default\",\n plugin_mode: normalizePluginMode(form.pluginMode),\n service_targets_mode: \"auto\",\n apply: \"true\",\n trigger_platform_sync: \"true\",\n confirm_apply: \"APPLY_AND_SYNC\",\n platform_sync_workflow: \"sync-cloud-run-env.yml\",\n };\n for (const field of [\n \"github_environment\",\n \"deployment_selector\",\n \"account_selector\",\n \"account_scope\",\n \"service_name\",\n ]) {\n if (account[field]) inputs[field] = account[field];\n }\n const reserveOverride = platformSupportsReservedCashPolicy(platform)\n ? reservePolicyOverrideForForm(form, platform)\n : null;\n if (platformSupportsReservedCashPolicy(platform)) {\n inputs.reserved_cash_policy_mode = normalizeReservePolicyMode(form.reservePolicyMode);\n if (reserveOverride) {\n Object.assign(inputs, reserveOverride.inputs);\n mergeExtraVariables(inputs, reserveOverride.extraVariables);\n }\n }\n const runtimeTargetOverride = runtimeTargetOverrideForForm(form);\n inputs.runtime_target_enabled_mode = normalizeRuntimeTargetMode(form.runtimeTargetMode);\n if (runtimeTargetOverride) {\n Object.assign(inputs, runtimeTargetOverride.inputs);\n mergeExtraVariables(inputs, runtimeTargetOverride.extraVariables);\n }\n const incomeOverride = incomeLayerOverrideForForm(form);\n inputs.income_layer_mode = normalizeIncomeLayerMode(form.incomeLayerMode);\n if (incomeOverride) {\n Object.assign(inputs, incomeOverride.inputs);\n mergeExtraVariables(inputs, incomeOverride.extraVariables);\n }\n const optionOverlayOverride = optionOverlayOverrideForForm(form);\n inputs.option_overlay_mode = normalizeOptionOverlayMode(form.optionOverlayMode);\n if (optionOverlayOverride) {\n Object.assign(inputs, optionOverlayOverride.inputs);\n }\n const cashOnlyOverride = cashOnlyExecutionOverrideForForm(form, platform);\n if (platformSupportsMarginPolicy(platform)) {\n inputs.cash_only_execution_mode = normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode);\n if (cashOnlyOverride) {\n Object.assign(inputs, cashOnlyOverride.inputs);\n }\n }\n const dcaOverride = dcaOverrideForForm(form);\n if (dcaOverride) {\n Object.assign(inputs, dcaOverride.inputs);\n }\n return inputs;\n }\n\n function reservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform), fallback = t(\"unchanged\")) {\n if (inputs?.reserved_cash_policy_mode === \"none\") return t(\"reservedCashNone\");\n const floor = cleanDisplayNumber(inputs?.min_reserved_cash_usd);\n const ratio = cleanDisplayRatio(inputs?.reserved_cash_ratio);\n const currency = selectedCashCurrency(platform, account);\n const hasEffectiveFloor = Boolean(floor && !(ratio && Number(floor) === 0));\n if (!hasEffectiveFloor && !ratio) return fallback;\n if (hasEffectiveFloor && ratio) return `max(${floor} ${currency}, ${formatRatioPercent(ratio)})`;\n if (hasEffectiveFloor) return `${floor} ${currency}`;\n return formatRatioPercent(ratio);\n }\n\n function platformReservedCashDefaultText(platform = state.selected, account = selectedAccount(platform)) {\n return t(\"reservedCashDefault\").replace(\"{currency}\", selectedCashCurrency(platform, account));\n }\n\n function currentReservedCashPolicyText(platform = state.selected, account = selectedAccount(platform)) {\n const entry = currentEntryForAccount(platform, account);\n const policy = currentReservePolicyForAccount(platform, account);\n return reservedCashPolicyText(\n {\n min_reserved_cash_usd: policy.minReservedCashUsd,\n reserved_cash_ratio: policy.reservedCashRatio,\n },\n platform,\n account,\n entry ? platformReservedCashDefaultText(platform, account) : t(\"notRead\"),\n );\n }\n\n function pendingReservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n return reservedCashPolicyText(pendingReservePolicy(inputs, platform, account).inputs, platform, account, t(\"unchanged\"));\n }\n\n function pendingReservePolicy(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const current = currentReservePolicyForAccount(platform, account);\n const currentFloor = cleanDisplayNumber(current.minReservedCashUsd);\n const currentRatio = cleanDisplayRatio(current.reservedCashRatio);\n const mode = normalizeReservePolicyMode(inputs.reserved_cash_policy_mode);\n const next = {\n min_reserved_cash_usd: cleanDisplayNumber(inputs.min_reserved_cash_usd),\n reserved_cash_ratio: cleanDisplayRatio(inputs.reserved_cash_ratio),\n };\n if (mode === \"none\") {\n next.reserved_cash_policy_mode = \"none\";\n }\n const entry = currentEntryForAccount(platform, account);\n const changed = Boolean(entry && (\n next.min_reserved_cash_usd !== currentFloor ||\n next.reserved_cash_ratio !== currentRatio ||\n (mode === \"none\" && (currentFloor || currentRatio))\n ));\n return { changed, inputs: next };\n }\n\n function currentIncomeLayerText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return t(\"incomeLayerNotSupported\");\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const current = incomeLayerFromEntry(entry);\n if (!incomeLayerFieldsConfigured(entry)) {\n return t(\"incomeLayerDefault\")\n .replace(\"{start}\", formatUsd(defaults.startUsd))\n .replace(\"{ratio}\", formatRatioPercent(defaults.maxRatio));\n }\n const enabled = current.enabled ?? true;\n const startUsd = current.startUsd || String(defaults.startUsd);\n const ratio = current.maxRatio || defaults.maxRatio;\n return enabled\n ? t(\"incomeLayerOn\")\n .replace(\"{start}\", formatUsd(startUsd))\n .replace(\"{ratio}\", formatRatioPercent(ratio))\n : t(\"incomeLayerOff\");\n }\n\n function pendingIncomeLayerText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingIncomeLayer(inputs, platform, account);\n if (!pending.supported) return t(\"incomeLayerNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n if (pending.inputs.income_layer_enabled === false) return t(\"incomeLayerOff\");\n return t(\"incomeLayerOn\")\n .replace(\"{start}\", formatUsd(pending.inputs.income_layer_start_usd))\n .replace(\"{ratio}\", formatRatioPercent(pending.inputs.income_layer_max_ratio));\n }\n\n function pendingOptionOverlayText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingOptionOverlay(inputs, platform, account);\n if (!pending.supported && pending.inputs.option_overlay_enabled !== false) return t(\"optionOverlayNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n return optionOverlayText(pending.inputs.option_overlay_enabled);\n }\n\n function pendingCashOnlyExecutionText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingCashOnlyExecution(inputs, platform, account);\n if (!pending.changed) return t(\"unchanged\");\n return cashOnlyExecutionText(pending.inputs.cash_only_execution);\n }\n\n function pendingRuntimeTargetText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingRuntimeTarget(inputs, platform, account);\n if (!pending.changed) return t(\"unchanged\");\n return runtimeTargetText(pending.inputs.runtime_target_enabled);\n }\n\n function pendingRuntimeTargetTone(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingRuntimeTarget(inputs, platform, account);\n if (!pending.changed) return \"neutral\";\n return runtimeTargetTone(pending.inputs.runtime_target_enabled);\n }\n\n function currentDcaText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const current = currentDcaForAccount(platform, account, profile);\n if (!current.supported) return t(\"dcaNotSupported\");\n return t(\"dcaText\")\n .replace(\"{mode}\", dcaModeLabel(current.mode))\n .replace(\"{amount}\", formatUsd(current.baseInvestmentUsd));\n }\n\n function pendingDcaText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingDca(inputs, platform, account);\n if (!pending.supported) return t(\"dcaNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n return t(\"dcaText\")\n .replace(\"{mode}\", dcaModeLabel(pending.inputs.dca_mode))\n .replace(\"{amount}\", formatUsd(pending.inputs.dca_base_investment_usd));\n }\n\n function pendingRuntimeTarget(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const mode = normalizeRuntimeTargetMode(inputs.runtime_target_enabled_mode);\n if (mode === \"current\") {\n return {\n changed: false,\n inputs: { runtime_target_enabled: runtimeTargetEnabledForAccount(platform, account) ?? true },\n };\n }\n const current = runtimeTargetEnabledForAccount(platform, account);\n const currentEnabled = current ?? true;\n const nextEnabled = mode === \"enabled\";\n const entry = currentEntryForAccount(platform, account);\n return {\n changed: Boolean(entry && current !== null && currentEnabled !== nextEnabled),\n inputs: { runtime_target_enabled: nextEnabled },\n };\n }\n\n function pendingIncomeLayer(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return { supported: false, changed: false, inputs: {} };\n const mode = normalizeIncomeLayerMode(inputs.income_layer_mode);\n const entry = currentEntryForAccount(platform, account);\n const rawCurrent = currentIncomeLayerForAccount(platform, account);\n const effective = effectiveIncomeLayerForAccount(platform, account, profile);\n const currentEnabled = effective?.enabled ?? true;\n const currentStartUsd = effective?.startUsd ?? String(defaults.startUsd);\n const currentRatio = effective?.maxRatio ?? defaults.maxRatio;\n if (mode === \"current\") {\n return {\n supported: true,\n changed: false,\n inputs: {\n income_layer_enabled: rawCurrent.enabled,\n income_layer_start_usd: rawCurrent.startUsd,\n income_layer_max_ratio: rawCurrent.maxRatio,\n },\n };\n }\n if (mode === \"disabled\") {\n if (!entry) {\n return {\n supported: true,\n changed: false,\n inputs: {\n income_layer_enabled: false,\n income_layer_start_usd: \"\",\n income_layer_max_ratio: \"\",\n },\n };\n }\n return {\n supported: true,\n changed: currentEnabled !== false || Boolean(rawCurrent.startUsd || rawCurrent.maxRatio),\n inputs: {\n income_layer_enabled: false,\n income_layer_start_usd: \"\",\n income_layer_max_ratio: \"\",\n },\n };\n }\n const nextStartUsd = cleanDisplayNumber(inputs.income_layer_start_usd || defaults.startUsd);\n const nextRatio = cleanDisplayRatio(inputs.income_layer_max_ratio || defaults.maxRatio);\n return {\n supported: true,\n changed: Boolean(entry && (currentEnabled !== true || nextStartUsd !== currentStartUsd || nextRatio !== currentRatio)),\n inputs: {\n income_layer_enabled: true,\n income_layer_start_usd: nextStartUsd,\n income_layer_max_ratio: nextRatio,\n },\n };\n }\n\n function pendingOptionOverlay(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const supported = optionOverlaySupported(profile);\n const mode = normalizeOptionOverlayMode(inputs.option_overlay_mode);\n const current = effectiveOptionOverlayForAccount(platform, account, profile);\n if (mode === \"current\") {\n return {\n supported,\n changed: false,\n inputs: { option_overlay_enabled: currentOptionOverlayForAccount(platform, account) },\n };\n }\n if (mode === \"enabled\") {\n return {\n supported,\n changed: supported && current !== null && current !== true,\n inputs: { option_overlay_enabled: true },\n };\n }\n return {\n supported,\n changed: current === true,\n inputs: { option_overlay_enabled: false },\n };\n }\n\n function pendingCashOnlyExecution(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const mode = normalizeCashOnlyExecutionMode(inputs.cash_only_execution_mode);\n if (!platformSupportsMarginPolicy(platform) || mode === \"current\") {\n return { changed: false, inputs: {} };\n }\n const current = effectiveCashOnlyExecutionForAccount(platform, account);\n const nextEnabled = mode === \"enabled\";\n const entry = currentEntryForAccount(platform, account);\n return {\n changed: Boolean(entry && current !== null && current !== nextEnabled),\n inputs: { cash_only_execution: nextEnabled },\n };\n }\n\n function pendingDca(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const defaults = dcaConfigForStrategy(profile);\n if (!defaults) return { supported: false, changed: false, inputs: {} };\n const current = currentDcaForAccount(platform, account, profile);\n const nextMode = normalizeDcaMode(inputs.dca_mode || defaults.defaultMode);\n const nextBase = cleanDisplayPositiveNumber(inputs.dca_base_investment_usd || defaults.defaultBaseInvestmentUsd);\n return {\n supported: true,\n changed: Boolean(current.mode !== nextMode || current.baseInvestmentUsd !== nextBase),\n inputs: {\n dca_mode: nextMode,\n dca_base_investment_usd: nextBase,\n },\n };\n }\n\n function pendingChangeState(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const currentProfile = currentStrategyForAccount(platform, account);\n const nextProfile = cleanStrategyProfile(inputs.strategy_profile);\n const currentEntry = currentEntryForAccount(platform, account);\n const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);\n const currentPluginMode = currentPluginModeForAccount(platform, account);\n const nextPluginMode = normalizePluginMode(inputs.plugin_mode);\n const runtimeTarget = pendingRuntimeTarget(inputs, platform, account);\n const reserve = pendingReservePolicy(inputs, platform, account);\n const income = pendingIncomeLayer(inputs, platform, account);\n const optionOverlay = pendingOptionOverlay(inputs, platform, account);\n const cashOnly = pendingCashOnlyExecution(inputs, platform, account);\n const dca = pendingDca(inputs, platform, account);\n return {\n currentProfile,\n nextProfile,\n currentMode,\n currentPluginMode,\n nextPluginMode,\n strategyChanged: Boolean(nextProfile && ((state.forms[platform]?.strategyTouched) || (currentProfile && currentProfile !== nextProfile))),\n modeChanged: Boolean(inputs.execution_mode && currentMode && currentMode !== inputs.execution_mode),\n pluginModeChanged: Boolean(nextPluginMode && currentPluginMode && currentPluginMode !== nextPluginMode),\n runtimeTargetChanged: runtimeTarget.changed,\n reserveCashChanged: reserve.changed,\n incomeLayerChanged: income.changed,\n optionOverlayChanged: optionOverlay.changed,\n cashOnlyChanged: cashOnly.changed,\n dcaChanged: dca.changed,\n runtimeTarget,\n reserve,\n income,\n optionOverlay,\n cashOnly,\n dca,\n };\n }\n\n function hasPendingChanges(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const changes = pendingChangeState(inputs, platform, account);\n return Boolean(\n changes.strategyChanged ||\n changes.modeChanged ||\n changes.pluginModeChanged ||\n changes.runtimeTargetChanged ||\n changes.reserveCashChanged ||\n changes.incomeLayerChanged ||\n changes.optionOverlayChanged ||\n changes.cashOnlyChanged ||\n changes.dcaChanged\n );\n }\n\n function formatRatioPercent(value) {\n const numeric = Number(value);\n if (!Number.isFinite(numeric)) return String(value);\n return `${(numeric * 100).toFixed(2).replace(/\\.?0+$/, \"\")}%`;\n }\n\n function formatUsd(value) {\n const numeric = Number(value);\n if (!Number.isFinite(numeric)) return String(value);\n return `$${numeric.toLocaleString(\"en-US\", { maximumFractionDigits: 0 })}`;\n }\n\n function incomeLayerAllocationText(defaults) {\n if (!defaults?.allocations) return \"\";\n return Object.entries(defaults.allocations)\n .map(([symbol, ratio]) => `${symbol} ${formatRatioPercent(ratio)}`)\n .join(\" / \");\n }\n\n function incomeLayerDefaultMetaText(defaults) {\n if (!defaults) return t(\"incomeLayerModeMeta\");\n return t(\"incomeLayerDefaultMeta\")\n .replace(\"{start}\", formatUsd(defaults.startUsd))\n .replace(\"{ratio}\", formatRatioPercent(defaults.maxRatio));\n }\n\n function optionOverlayDefaultMetaText(defaults) {\n if (!defaults?.families?.length) return t(\"optionOverlayModeMeta\");\n const familyText = defaults.families.map((item) => {\n const family = item.family === \"income\" ? t(\"optionOverlayFamilyIncome\") : t(\"optionOverlayFamilyGrowth\");\n const ratioText = item.ratioKind === \"risk\"\n ? t(\"optionOverlayRiskRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio))\n : t(\"optionOverlayBudgetRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio));\n return `${family}: ${item.recipe}, ${formatUsd(item.startUsd)}, ${ratioText}`;\n }).join(\" / \");\n return t(\"optionOverlayDefaultMeta\").replace(\"{defaults}\", familyText);\n }\n\n function summaryRows(inputs) {\n const account = selectedAccount();\n const changes = pendingChangeState(inputs, state.selected, account);\n const currentStrategyText = changes.currentProfile ? strategyLabel(changes.currentProfile) : t(\"notRead\");\n const rows = [\n [t(\"repository\"), state.repositories[state.selected] || defaultRepositories[state.selected]],\n [t(\"selectedAccount\"), account.label],\n [t(\"currentStrategy\"), currentStrategyText],\n [t(\"selectedMarket\"), supportedDomainLabel(state.selected, account)],\n [\n t(\"currentRuntimeTarget\"),\n currentRuntimeTargetText(state.selected, account),\n \"\",\n currentRuntimeTargetTone(state.selected, account),\n ],\n [t(\"currentPluginMode\"), pluginModeLabel(changes.currentPluginMode)],\n [t(\"reservedCashPolicy\"), currentReservedCashPolicyText(state.selected, account)],\n ];\n if (platformSupportsMarginPolicy(state.selected)) {\n rows.push([t(\"currentCashOnlyExecution\"), currentCashOnlyExecutionText(state.selected, account)]);\n }\n if (incomeLayerSupported(inputs.strategy_profile)) {\n rows.push([t(\"currentIncomeLayer\"), currentIncomeLayerText(state.selected, account, inputs.strategy_profile)]);\n }\n if (optionOverlaySupported(inputs.strategy_profile) || changes.optionOverlayChanged) {\n rows.push([t(\"currentOptionOverlay\"), currentOptionOverlayText(state.selected, account, inputs.strategy_profile)]);\n }\n if (dcaSupported(inputs.strategy_profile)) {\n rows.push([t(\"currentDca\"), currentDcaText(state.selected, account, inputs.strategy_profile)]);\n }\n if (changes.reserveCashChanged) {\n rows.push([t(\"pendingReservedCashPolicy\"), pendingReservedCashPolicyText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.incomeLayerChanged) {\n rows.push([t(\"pendingIncomeLayer\"), pendingIncomeLayerText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.optionOverlayChanged) {\n rows.push([t(\"pendingOptionOverlay\"), pendingOptionOverlayText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.cashOnlyChanged) {\n rows.push([t(\"pendingCashOnlyExecution\"), pendingCashOnlyExecutionText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.dcaChanged) {\n rows.push([t(\"pendingDca\"), pendingDcaText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.modeChanged) {\n rows.push([t(\"pendingMode\"), modeLabel(inputs.execution_mode), \"pending\"]);\n }\n if (changes.pluginModeChanged) {\n rows.push([t(\"pendingPluginMode\"), pluginModeLabel(changes.nextPluginMode), \"pending\"]);\n }\n if (changes.runtimeTargetChanged) {\n rows.push([\n t(\"pendingRuntimeTarget\"),\n pendingRuntimeTargetText(inputs, state.selected, account),\n \"pending\",\n pendingRuntimeTargetTone(inputs, state.selected, account),\n ]);\n }\n if (changes.strategyChanged && changes.nextProfile) {\n rows.push([t(\"nextStrategy\"), strategyLabel(changes.nextProfile), \"pending\"]);\n }\n return rows;\n }\n\n function applyLanguage() {\n document.documentElement.lang = state.lang === \"zh\" ? \"zh-CN\" : \"en\";\n document.querySelectorAll(\"[data-i18n]\").forEach((node) => {\n node.textContent = t(node.dataset.i18n);\n });\n el(\"lang-button\").textContent = state.lang === \"zh\" ? \"EN\" : \"中\";\n }\n\n function renderPlatforms() {\n const strip = el(\"platform-strip\");\n strip.replaceChildren();\n const showPrivateConfig = hasPrivateConfig();\n for (const platform of Object.keys(platformMeta)) {\n ensureAccountSelection(platform);\n const meta = platformMeta[platform];\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n const button = document.createElement(\"button\");\n button.className = \"platform-button\";\n button.type = \"button\";\n button.dataset.platform = platform;\n button.classList.toggle(\"active\", platform === state.selected);\n const mark = document.createElement(\"span\");\n mark.className = \"mark\";\n mark.textContent = meta.code;\n const copyNode = document.createElement(\"span\");\n copyNode.className = \"platform-copy\";\n const labelNode = document.createElement(\"strong\");\n labelNode.textContent = meta.label;\n copyNode.append(labelNode);\n if (showPrivateConfig) {\n const accountNode = document.createElement(\"span\");\n accountNode.textContent = account.label;\n const strategyNode = document.createElement(\"small\");\n strategyNode.textContent = strategyLabel(form.strategy);\n copyNode.append(accountNode, strategyNode);\n }\n button.append(mark, copyNode);\n strip.appendChild(button);\n }\n }\n\n function renderControls() {\n const platform = state.selected;\n const meta = platformMeta[platform];\n const form = state.forms[platform];\n const accounts = optionsFor(platform);\n const account = selectedAccount(platform);\n const choices = strategyChoicesForAccount(platform, account, form.executionMode);\n const currentStrategy = currentStrategyForAccount(platform, account);\n const currentStrategyBlocked = Boolean(\n currentStrategy &&\n !strategyAllowedForAccount(platform, account, currentStrategy, form.executionMode),\n );\n const accountSelect = el(\"account-select\");\n const strategySelect = el(\"strategy-select\");\n const runtimeTargetEnabledSelect = el(\"runtime-target-enabled-select\");\n const pluginModeSelect = el(\"plugin-mode-select\");\n const incomeLayerModeSelect = el(\"income-layer-mode-select\");\n const incomeLayerStartUsdInput = el(\"income-layer-start-usd-input\");\n const incomeLayerMaxRatioInput = el(\"income-layer-max-ratio-input\");\n const optionOverlayModeSelect = el(\"option-overlay-mode-select\");\n const cashOnlyExecutionModeSelect = el(\"cash-only-execution-mode-select\");\n const dcaModeSelect = el(\"dca-mode-select\");\n const dcaBaseInvestmentUsdInput = el(\"dca-base-investment-usd-input\");\n const reservePolicyModeSelect = el(\"reserve-policy-mode-select\");\n const minReservedCashInput = el(\"min-reserved-cash-input\");\n const reservedCashRatioInput = el(\"reserved-cash-ratio-input\");\n const showPrivateControls = hasPrivateConfig();\n\n el(\"switch-panel\").style.setProperty(\"--platform-color\", meta.accent);\n el(\"platform-title\").textContent = meta.label;\n el(\"quick-form\").hidden = !showPrivateControls;\n el(\"run-area\").hidden = !showPrivateControls;\n el(\"public-note\").hidden = showPrivateControls;\n el(\"public-preview\").hidden = showPrivateControls;\n el(\"public-note\").textContent = state.auth.allowed ? t(\"missingConfigNote\") : t(\"publicReadonly\");\n\n if (!showPrivateControls) {\n accountSelect.replaceChildren();\n strategySelect.replaceChildren();\n runtimeTargetEnabledSelect.replaceChildren();\n pluginModeSelect.replaceChildren();\n incomeLayerModeSelect.replaceChildren();\n optionOverlayModeSelect.replaceChildren();\n cashOnlyExecutionModeSelect.replaceChildren();\n dcaModeSelect.replaceChildren();\n reservePolicyModeSelect.replaceChildren();\n incomeLayerStartUsdInput.value = \"\";\n incomeLayerMaxRatioInput.value = \"\";\n dcaBaseInvestmentUsdInput.value = \"\";\n minReservedCashInput.value = \"\";\n reservedCashRatioInput.value = \"\";\n el(\"account-meta\").textContent = \"\";\n el(\"strategy-meta\").textContent = \"\";\n el(\"income-layer-mode-meta\").textContent = \"\";\n el(\"income-layer-start-meta\").textContent = \"\";\n el(\"income-layer-ratio-meta\").textContent = \"\";\n el(\"option-overlay-mode-meta\").textContent = \"\";\n el(\"cash-only-execution-mode-meta\").textContent = \"\";\n el(\"dca-mode-meta\").textContent = \"\";\n el(\"dca-base-meta\").textContent = \"\";\n return;\n }\n\n accountSelect.replaceChildren();\n if (accounts.length) {\n for (const account of accounts) {\n accountSelect.append(new Option(account.label, account.key, false, account.key === form.accountKey));\n }\n } else {\n accountSelect.append(new Option(t(\"noAccount\"), \"\"));\n }\n el(\"account-meta\").textContent = accounts.length ? accountMetaText(platform) : \"\";\n\n if (choices.length && !choices.includes(form.strategy) && !currentStrategyBlocked) {\n form.strategy = choices[0];\n }\n strategySelect.disabled = !choices.length;\n strategySelect.replaceChildren();\n if (currentStrategyBlocked) {\n const blockedOption = new Option(\n strategyChoiceLabel(currentStrategy, platform, account, form.executionMode),\n currentStrategy,\n true,\n currentStrategy === form.strategy,\n );\n blockedOption.disabled = true;\n strategySelect.append(blockedOption);\n }\n if (choices.length) {\n for (const strategy of choices) {\n strategySelect.append(\n new Option(strategyChoiceLabel(strategy, platform, account, form.executionMode), strategy, false, strategy === form.strategy),\n );\n }\n } else {\n strategySelect.append(new Option(t(\"noStrategy\"), \"\"));\n }\n el(\"strategy-meta\").textContent = account\n ? strategyDisplayMetaText(platform, account, form.strategy)\n : \"\";\n runtimeTargetEnabledSelect.replaceChildren();\n for (const mode of runtimeTargetModes) {\n runtimeTargetEnabledSelect.append(\n new Option(runtimeTargetModeLabel(mode), mode, false, mode === normalizeRuntimeTargetMode(form.runtimeTargetMode)),\n );\n }\n pluginModeSelect.replaceChildren();\n for (const mode of pluginModes) {\n pluginModeSelect.append(new Option(pluginModeLabel(mode), mode, false, mode === normalizePluginMode(form.pluginMode)));\n }\n const incomeDefaults = incomeLayerDefaultForStrategy(form.strategy);\n el(\"income-layer-section\").hidden = false;\n el(\"option-overlay-section\").hidden = false;\n incomeLayerModeSelect.replaceChildren();\n if (incomeDefaults) {\n incomeLayerModeSelect.disabled = false;\n for (const mode of incomeLayerModes) {\n incomeLayerModeSelect.append(new Option(incomeLayerModeLabel(mode), mode, false, mode === normalizeIncomeLayerMode(form.incomeLayerMode)));\n }\n el(\"income-layer-mode-meta\").textContent = incomeLayerDefaultMetaText(incomeDefaults);\n el(\"income-layer-start-meta\").textContent = t(\"incomeLayerStartMeta\");\n el(\"income-layer-ratio-meta\").textContent = t(\"incomeLayerAllocationMeta\").replace(\n \"{allocations}\",\n incomeLayerAllocationText(incomeDefaults),\n );\n } else {\n incomeLayerModeSelect.disabled = true;\n incomeLayerModeSelect.append(new Option(t(\"incomeLayerNotSupported\"), \"current\"));\n el(\"income-layer-mode-meta\").textContent = t(\"incomeLayerModeMeta\");\n el(\"income-layer-start-meta\").textContent = t(\"incomeLayerStartMeta\");\n el(\"income-layer-ratio-meta\").textContent = t(\"incomeLayerRatioMeta\");\n }\n const supportsMargin = platformSupportsMarginPolicy(platform);\n const supportsReserve = platformSupportsReservedCashPolicy(platform);\n if (supportsMargin) syncCashOnlyExecutionForAccount(platform);\n reconcileExecutionCashPolicy(form, \"margin\");\n const executionCashPolicyGrid = el(\"execution-cash-policy-grid\");\n const qmtPlatformCashNote = el(\"qmt-platform-cash-note\");\n const executionCashPolicyNote = el(\"execution-cash-policy-note\");\n executionCashPolicyGrid.hidden = !supportsMargin && !supportsReserve;\n qmtPlatformCashNote.hidden = supportsMargin || supportsReserve || platform !== \"qmt\";\n executionCashPolicyNote.hidden = !supportsMargin || !supportsReserve;\n\n const marginBlocksReserve = supportsMargin && supportsReserve && allowMarginExplicitlySelected(form);\n const reserveBlocksMargin = supportsMargin && supportsReserve && reserveCashOverrideActive(form);\n\n if (supportsReserve) {\n reservePolicyModeSelect.replaceChildren();\n for (const mode of reservePolicyModes) {\n reservePolicyModeSelect.append(new Option(t(`reservePolicy${mode[0].toUpperCase()}${mode.slice(1)}`), mode, false, mode === normalizeReservePolicyMode(form.reservePolicyMode)));\n }\n const reserveMode = normalizeReservePolicyMode(form.reservePolicyMode);\n el(\"min-reserved-cash-label\").textContent = t(\"minReservedCash\").replace(\n \"{currency}\",\n selectedCashCurrency(platform, account),\n );\n reservePolicyModeSelect.disabled = false;\n minReservedCashInput.disabled = reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"ratio\";\n reservedCashRatioInput.disabled = reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"floor\";\n minReservedCashInput.value = reserveMode === \"ratio\" || reserveMode === \"none\" ? \"\" : form.minReservedCashUsd;\n reservedCashRatioInput.value = reserveMode === \"floor\" || reserveMode === \"none\" ? \"\" : form.reservedCashRatio;\n el(\"reserve-policy-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"min-reserve-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"reserve-ratio-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"reserve-policy-mode-meta\").textContent = marginBlocksReserve\n ? t(\"executionCashMarginBlocksReserve\")\n : t(\"reservedCashModeMeta\");\n } else {\n reservePolicyModeSelect.replaceChildren();\n minReservedCashInput.value = \"\";\n reservedCashRatioInput.value = \"\";\n }\n const incomeMode = normalizeIncomeLayerMode(form.incomeLayerMode);\n const incomeLayerInputsDisabled = !incomeDefaults || incomeMode === \"disabled\";\n incomeLayerStartUsdInput.disabled = incomeLayerInputsDisabled;\n incomeLayerMaxRatioInput.disabled = incomeLayerInputsDisabled;\n if (incomeDefaults && incomeMode !== \"disabled\" && !cleanDisplayNumber(form.incomeLayerStartUsd)) {\n form.incomeLayerStartUsd = String(incomeDefaults.startUsd);\n }\n if (incomeDefaults && incomeMode !== \"disabled\" && !cleanDisplayRatio(form.incomeLayerMaxRatio)) {\n form.incomeLayerMaxRatio = incomeDefaults.maxRatio;\n }\n incomeLayerStartUsdInput.value = incomeDefaults && incomeMode !== \"disabled\" ? form.incomeLayerStartUsd : \"\";\n incomeLayerMaxRatioInput.value = incomeDefaults && incomeMode !== \"disabled\" ? form.incomeLayerMaxRatio : \"\";\n\n const optionDefaults = optionOverlayDefaultForStrategy(form.strategy);\n optionOverlayModeSelect.replaceChildren();\n if (optionDefaults) {\n optionOverlayModeSelect.disabled = false;\n for (const mode of optionOverlayModes) {\n optionOverlayModeSelect.append(\n new Option(optionOverlayModeLabel(mode), mode, false, mode === normalizeOptionOverlayMode(form.optionOverlayMode)),\n );\n }\n el(\"option-overlay-mode-meta\").textContent = optionOverlayDefaultMetaText(optionDefaults);\n } else {\n optionOverlayModeSelect.disabled = true;\n optionOverlayModeSelect.append(new Option(t(\"optionOverlayNotSupported\"), \"current\"));\n el(\"option-overlay-mode-meta\").textContent = t(\"optionOverlayModeMeta\");\n }\n\n if (supportsMargin) {\n cashOnlyExecutionModeSelect.replaceChildren();\n for (const mode of cashOnlyExecutionModes) {\n const option = new Option(\n mode === \"enabled\" ? t(\"cashOnlyExecutionNo\") : t(\"cashOnlyExecutionYes\"),\n mode,\n false,\n mode === normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode),\n );\n cashOnlyExecutionModeSelect.append(option);\n }\n el(\"cash-only-policy-block\").classList.toggle(\"policy-block-muted\", reserveBlocksMargin);\n el(\"cash-only-execution-mode-meta\").textContent = reserveBlocksMargin\n ? t(\"executionCashReserveBlocksMargin\")\n : t(\"cashOnlyExecutionModeMeta\");\n } else {\n cashOnlyExecutionModeSelect.replaceChildren();\n el(\"cash-only-execution-mode-meta\").textContent = \"\";\n }\n\n const dcaDefaults = dcaConfigForStrategy(form.strategy);\n dcaModeSelect.replaceChildren();\n const dcaAllowed = Boolean(dcaDefaults) && platformSupportsDca(platform);\n if (dcaAllowed) {\n dcaModeSelect.disabled = false;\n for (const mode of dcaModes) {\n dcaModeSelect.append(new Option(dcaModeLabel(mode), mode, false, mode === normalizeDcaMode(form.dcaMode)));\n }\n if (!cleanDisplayPositiveNumber(form.dcaBaseInvestmentUsd)) {\n form.dcaBaseInvestmentUsd = dcaDefaults.defaultBaseInvestmentUsd;\n }\n dcaBaseInvestmentUsdInput.disabled = false;\n dcaBaseInvestmentUsdInput.value = form.dcaBaseInvestmentUsd;\n el(\"dca-mode-meta\").textContent = t(\"dcaDefaultMeta\")\n .replace(\"{mode}\", dcaModeLabel(dcaDefaults.defaultMode))\n .replace(\"{amount}\", formatUsd(dcaDefaults.defaultBaseInvestmentUsd));\n el(\"dca-base-meta\").textContent = t(\"dcaModeMeta\");\n } else {\n dcaModeSelect.disabled = true;\n dcaModeSelect.append(new Option(\n dcaDefaults && !platformSupportsDca(platform) ? t(\"dcaPlatformNotSupported\") : t(\"dcaNotSupported\"),\n \"fixed\",\n ));\n dcaBaseInvestmentUsdInput.disabled = true;\n dcaBaseInvestmentUsdInput.value = \"\";\n el(\"dca-mode-meta\").textContent = t(\"dcaModeMeta\");\n el(\"dca-base-meta\").textContent = t(\"dcaModeMeta\");\n }\n\n if (platformDryRunOnly(platform)) {\n form.executionMode = \"paper\";\n }\n document.querySelectorAll(\"#mode-control [data-mode]\").forEach((button) => {\n const dryRunOnly = platformDryRunOnly(platform);\n button.disabled = dryRunOnly && button.dataset.mode === \"live\";\n button.classList.toggle(\"active\", button.dataset.mode === form.executionMode);\n });\n el(\"mode-meta\").textContent = platformDryRunOnly(platform) ? t(\"qmtDryRunOnlyNote\") : \"\";\n }\n\n function renderSummary() {\n const showSummary = hasPrivateConfig();\n const summaryPanel = document.querySelector(\".summary-panel\");\n const switchSurface = document.querySelector(\".switch-surface\");\n summaryPanel.hidden = !showSummary;\n switchSurface.classList.toggle(\"summary-hidden\", !showSummary);\n if (!showSummary) return;\n\n const inputs = buildInputs();\n const list = el(\"summary-list\");\n list.replaceChildren();\n document.querySelector(\".summary-head h2\").textContent = t(\"summary\");\n for (const [label, value, rowClass, valueTone] of summaryRows(inputs)) {\n const row = document.createElement(\"div\");\n row.className = \"summary-row\";\n row.setAttribute(\"role\", \"listitem\");\n if (rowClass) row.classList.add(rowClass);\n const labelNode = document.createElement(\"div\");\n labelNode.className = \"summary-label\";\n labelNode.textContent = label;\n const valueNode = document.createElement(\"div\");\n valueNode.className = \"summary-value\";\n if (valueTone) {\n const badge = document.createElement(\"span\");\n badge.className = `summary-status ${valueTone}`;\n badge.textContent = value;\n valueNode.appendChild(badge);\n } else {\n valueNode.textContent = value;\n }\n row.append(labelNode, valueNode);\n list.appendChild(row);\n }\n\n const account = selectedAccount();\n const currentEntry = currentEntryForAccount(state.selected, account);\n const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);\n el(\"mode-pill\").textContent = currentMode ? modeLabel(currentMode) : t(\"notRead\");\n }\n\n function renderAuth() {\n const status = el(\"auth-status\");\n const loginLink = el(\"login-link\");\n const logoutButton = el(\"logout-button\");\n const signedIn = Boolean(state.auth.allowed && state.auth.login);\n\n status.hidden = !signedIn;\n status.textContent = signedIn ? t(\"signedInAs\").replace(\"{login}\", state.auth.login) : \"\";\n loginLink.hidden = signedIn;\n loginLink.href = \"/login\";\n loginLink.textContent = t(\"login\");\n logoutButton.hidden = !signedIn;\n logoutButton.textContent = t(\"logout\");\n\n const dispatch = el(\"dispatch-button\");\n const hasPrivateAccounts = state.configSource === \"private\";\n const loadingConfig = state.configSource === \"loading\";\n const hasRunnableStrategy = hasRunnableStrategySelection();\n const hasValidReserve = hasValidExecutionCashPolicy();\n const hasValidIncomeLayer = hasValidIncomeLayerPolicy();\n const hasValidOptionOverlay = hasValidOptionOverlayPolicy();\n const hasValidDca = hasValidDcaPolicy();\n const hasValidStrategy = hasRunnableStrategy &&\n hasValidReserve &&\n hasValidIncomeLayer &&\n hasValidOptionOverlay &&\n hasValidDca;\n const hasPendingChange = hasPrivateAccounts && hasValidStrategy && hasPendingChanges(buildInputs());\n dispatch.disabled = !state.auth.allowed || loadingConfig || !hasPrivateAccounts || !hasValidStrategy || !hasPendingChange;\n dispatch.textContent = state.auth.allowed\n ? (loadingConfig\n ? t(\"loadingConfig\")\n : (hasPrivateAccounts ? (hasValidStrategy ? (hasPendingChange ? t(\"runSwitch\") : t(\"noChanges\")) : t(\"configureAccounts\")) : t(\"configureAccounts\")))\n : t(\"loginToRun\");\n const note = el(\"action-note\");\n note.textContent = state.auth.allowed\n ? (loadingConfig\n ? t(\"loadingConfigNote\")\n : (hasPrivateAccounts\n ? (hasRunnableStrategy\n ? (hasValidReserve\n ? (hasValidIncomeLayer\n ? (hasValidOptionOverlay\n ? (hasValidDca ? (hasPendingChange ? t(\"readyNote\") : \"\") : t(\"invalidDcaNote\"))\n : t(\"invalidOptionOverlayNote\"))\n : t(\"invalidIncomeLayerNote\"))\n : (executionCashPolicyConflict(state.forms[state.selected])\n ? t(\"invalidExecutionCashPolicyNote\")\n : t(\"invalidReservePolicyNote\")))\n : strategyActionNoteText())\n : t(\"missingConfigNote\")))\n : t(\"readonlyNote\");\n note.classList.toggle(\n \"warning\",\n state.auth.allowed && !loadingConfig && (!hasPrivateAccounts || !hasValidStrategy),\n );\n }\n\n function renderAppVisibility() {\n document.body.classList.toggle(\"app-loading\", !state.appReady);\n el(\"boot-message\").textContent = t(state.bootMessageKey);\n }\n\n function render() {\n applyLanguage();\n renderPlatforms();\n renderControls();\n renderSummary();\n renderAuth();\n renderAppVisibility();\n }\n\n async function refreshSession() {\n state.bootMessageKey = \"bootSession\";\n render();\n try {\n const session = await requestJson(\"/api/session\");\n state.auth = {\n available: true,\n allowed: Boolean(session.allowed),\n admin: Boolean(session.admin),\n login: session.login || null,\n };\n } catch {\n state.auth = { available: false, allowed: false, admin: false, login: null };\n }\n if (state.auth.allowed) {\n await refreshConfig();\n } else {\n state.bootMessageKey = \"bootPublic\";\n state.appReady = true;\n render();\n }\n }\n\n async function refreshStrategyProfiles() {\n state.bootMessageKey = \"bootStrategy\";\n render();\n try {\n const payload = await requestJson(\"/api/strategy-profiles\");\n applyStrategyProfiles(payload.strategyProfiles || []);\n if (payload.platformMeta) platformMeta = payload.platformMeta;\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n render();\n } catch {\n applyStrategyProfiles(defaultStrategyProfiles);\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n }\n }\n\n async function refreshConfig() {\n if (!state.auth.available || !state.auth.allowed) return;\n state.configSource = \"loading\";\n state.bootMessageKey = \"bootConfig\";\n render();\n try {\n const payload = await requestJson(\"/api/config\");\n if (payload.accountOptions) {\n applyStrategyProfiles(payload.strategyProfiles || defaultStrategyProfiles);\n state.accountOptions = normalizeAccountOptions(payload.accountOptions);\n if (payload.platformMeta) platformMeta = payload.platformMeta;\n state.repositories = normalizePlatformRepositories(payload.platformRepositories || {});\n state.currentStrategies = normalizeCurrentStrategies(payload.currentStrategies || {});\n state.configSource = \"private\";\n for (const platform of Object.keys(platformMeta)) {\n ensureAccountSelection(platform);\n syncStrategyForAccount(platform);\n }\n } else {\n state.configSource = \"default\";\n state.currentStrategies = {};\n }\n } catch (error) {\n state.configSource = \"default\";\n state.currentStrategies = {};\n if (isRequestTimeoutError(error)) {\n state.bootMessageKey = \"bootTimeout\";\n } else {\n state.bootMessageKey = \"bootPublic\";\n }\n } finally {\n state.appReady = true;\n render();\n }\n }\n\n function normalizeAccountOptions(raw) {\n const normalized = clone(defaultAccountOptions);\n for (const platform of Object.keys(platformMeta)) {\n if (!Array.isArray(raw[platform]) || !raw[platform].length) continue;\n normalized[platform] = raw[platform].map((item, index) => ({\n key: String(item.key || item.target_name || index),\n label: String(item.label || item.target_name || item.key || platform),\n target_name: String(item.target_name || item.key || \"\"),\n account_selector: item.account_selector ? String(item.account_selector) : \"\",\n deployment_selector: item.deployment_selector ? String(item.deployment_selector) : \"\",\n account_scope: item.account_scope ? String(item.account_scope) : \"\",\n service_name: item.service_name ? String(item.service_name) : \"\",\n cash_currency: item.cash_currency || item.market_currency || item.trading_currency\n ? String(item.cash_currency || item.market_currency || item.trading_currency).trim().toUpperCase()\n : \"\",\n supported_domains: normalizeSupportedDomains(platform, item),\n default_strategy_profile: item.default_strategy_profile || item.strategy_profile\n ? String(item.default_strategy_profile || item.strategy_profile)\n : \"\",\n github_environment: item.github_environment ? String(item.github_environment) : \"\",\n variable_scope: item.variable_scope ? String(item.variable_scope) : \"\",\n plugin_mode: item.plugin_mode ? String(item.plugin_mode) : \"\",\n option_overlay_mode: item.option_overlay_mode ? normalizeOptionOverlayMode(item.option_overlay_mode) : \"\",\n cash_only_execution_mode: item.cash_only_execution_mode\n ? normalizeCashOnlyExecutionMode(item.cash_only_execution_mode)\n : \"\",\n dca_mode: item.dca_mode ? normalizeDcaMode(item.dca_mode) : \"\",\n dca_base_investment_usd: cleanDisplayPositiveNumber(item.dca_base_investment_usd),\n }));\n }\n return normalized;\n }\n\n function normalizeSupportedDomains(platform, item) {\n const raw = Array.isArray(item?.supported_domains)\n ? item.supported_domains\n : String(item?.supported_domains || \"\").split(/[\\s,;]+/);\n const cleaned = raw.map(cleanStrategyDomain).filter(Boolean);\n if (cleaned.length) return [...new Set(cleaned)];\n return inferSupportedDomains(platform, item || {});\n }\n\n function normalizeCurrentStrategies(raw) {\n const normalized = {};\n for (const platform of Object.keys(platformMeta)) {\n if (!raw[platform] || typeof raw[platform] !== \"object\" || Array.isArray(raw[platform])) continue;\n normalized[platform] = {};\n for (const [key, entry] of Object.entries(raw[platform])) {\n const profile = cleanStrategyProfile(entry?.strategy_profile);\n const minReservedCashUsd = cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd);\n const reservedCashRatio = cleanDisplayRatio(entry?.reserved_cash_ratio);\n const incomeLayerEnabled = cleanOptionalBoolean(entry?.income_layer_enabled);\n const incomeLayerStartUsd = cleanDisplayNumber(entry?.income_layer_start_usd);\n const incomeLayerMaxRatio = cleanDisplayRatio(entry?.income_layer_max_ratio);\n const optionOverlayEnabled = cleanOptionalBoolean(entry?.option_overlay_enabled);\n const cashOnlyExecution = cleanOptionalBoolean(entry?.cash_only_execution);\n const runtimeTargetEnabled = cleanOptionalBoolean(entry?.runtime_target_enabled);\n const dcaMode = entry?.dca_mode ? normalizeDcaMode(entry.dca_mode) : \"\";\n const dcaBaseInvestmentUsd = cleanDisplayPositiveNumber(entry?.dca_base_investment_usd);\n const executionMode = normalizeExecutionMode(entry?.execution_mode, entry?.dry_run_only);\n if (\n !profile &&\n !minReservedCashUsd &&\n !reservedCashRatio &&\n incomeLayerEnabled === null &&\n !incomeLayerStartUsd &&\n !incomeLayerMaxRatio &&\n optionOverlayEnabled === null &&\n cashOnlyExecution === null &&\n runtimeTargetEnabled === null &&\n !dcaMode &&\n !dcaBaseInvestmentUsd &&\n !executionMode\n ) continue;\n normalized[platform][String(key)] = {\n strategy_profile: profile,\n execution_mode: executionMode,\n dry_run_only: entry?.dry_run_only === true || entry?.dry_run_only === \"true\" || entry?.dry_run_only === \"1\",\n min_reserved_cash_usd: minReservedCashUsd,\n reserved_cash_ratio: reservedCashRatio,\n income_layer_enabled: incomeLayerEnabled,\n income_layer_start_usd: incomeLayerStartUsd,\n income_layer_max_ratio: incomeLayerMaxRatio,\n option_overlay_enabled: optionOverlayEnabled,\n cash_only_execution: cashOnlyExecution,\n runtime_target_enabled: runtimeTargetEnabled,\n dca_mode: dcaMode,\n dca_base_investment_usd: dcaBaseInvestmentUsd,\n source: entry?.source ? String(entry.source) : \"\",\n };\n }\n if (!Object.keys(normalized[platform]).length) delete normalized[platform];\n }\n return normalized;\n }\n\n function normalizePlatformRepositories(raw) {\n const normalized = clone(defaultRepositories);\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return normalized;\n for (const platform of Object.keys(platformMeta)) {\n const repository = String(raw[platform] || \"\").trim();\n if (/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$/.test(repository)) {\n normalized[platform] = repository;\n }\n }\n return normalized;\n }\n\n async function dispatchSwitch() {\n if (!state.auth.allowed) return;\n showToast(t(\"dispatching\"), { duration: 0 });\n try {\n const response = await fetch(\"/api/switch\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildInputs()),\n });\n const payload = await response.json();\n if (!response.ok || !payload.ok) throw new Error(payload.error || t(\"dispatchFailed\"));\n showToast(t(\"dispatched\"), { duration: 4000 });\n if (payload.actions_url) window.open(payload.actions_url, \"_blank\", \"noopener,noreferrer\");\n await refreshConfig();\n } catch (error) {\n showToast(`${t(\"dispatchFailed\")}: ${error.message}`, { duration: 12000 });\n }\n }\n\n async function handleLogout() {\n await fetch(\"/api/logout\", { method: \"POST\" });\n window.location.reload();\n }\n\n function summaryText() {\n const inputs = buildInputs();\n return summaryRows(inputs).map(([label, value]) => `${label}: ${value}`).join(\"\\\n\");\n }\n\n el(\"platform-strip\").addEventListener(\"click\", (event) => {\n const button = event.target.closest(\"[data-platform]\");\n if (!button) return;\n state.selected = button.dataset.platform;\n state.forms[state.selected].strategyTouched = false;\n render();\n });\n\n el(\"account-select\").addEventListener(\"change\", () => {\n state.forms[state.selected].accountKey = el(\"account-select\").value;\n state.forms[state.selected].runtimeTargetTouched = false;\n state.forms[state.selected].reservedCashTouched = false;\n state.forms[state.selected].incomeLayerTouched = false;\n state.forms[state.selected].optionOverlayTouched = false;\n state.forms[state.selected].cashOnlyExecutionTouched = false;\n state.forms[state.selected].dcaTouched = false;\n state.forms[state.selected].strategyTouched = false;\n syncStrategyForAccount(state.selected);\n render();\n });\n\n el(\"strategy-select\").addEventListener(\"change\", () => {\n state.forms[state.selected].strategy = el(\"strategy-select\").value;\n state.forms[state.selected].strategyTouched = true;\n state.forms[state.selected].incomeLayerTouched = false;\n state.forms[state.selected].optionOverlayTouched = false;\n state.forms[state.selected].dcaTouched = false;\n syncIncomeLayerForAccount(state.selected);\n syncOptionOverlayForAccount(state.selected);\n syncDcaForAccount(state.selected);\n render();\n });\n\n el(\"mode-control\").addEventListener(\"click\", (event) => {\n const button = event.target.closest(\"[data-mode]\");\n if (!button || button.disabled) return;\n if (platformDryRunOnly(state.selected) && button.dataset.mode === \"live\") return;\n state.forms[state.selected].executionMode = button.dataset.mode;\n render();\n });\n\n el(\"plugin-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.pluginMode = normalizePluginMode(el(\"plugin-mode-select\").value);\n render();\n });\n\n el(\"runtime-target-enabled-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.runtimeTargetMode = normalizeRuntimeTargetMode(el(\"runtime-target-enabled-select\").value);\n form.runtimeTargetTouched = form.runtimeTargetMode !== \"current\";\n render();\n });\n\n el(\"income-layer-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerMode = normalizeIncomeLayerMode(el(\"income-layer-mode-select\").value);\n form.incomeLayerTouched = form.incomeLayerMode !== \"current\";\n if (form.incomeLayerMode === \"current\") {\n form.incomeLayerTouched = false;\n syncIncomeLayerForAccount(state.selected);\n }\n render();\n });\n\n el(\"income-layer-start-usd-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerTouched = true;\n form.incomeLayerMode = \"enabled\";\n form.incomeLayerStartUsd = el(\"income-layer-start-usd-input\").value.trim();\n render();\n });\n\n el(\"income-layer-max-ratio-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerTouched = true;\n form.incomeLayerMode = \"enabled\";\n form.incomeLayerMaxRatio = el(\"income-layer-max-ratio-input\").value.trim();\n render();\n });\n\n el(\"option-overlay-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.optionOverlayMode = normalizeOptionOverlayMode(el(\"option-overlay-mode-select\").value);\n form.optionOverlayTouched = form.optionOverlayMode !== \"current\";\n render();\n });\n\n el(\"cash-only-execution-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.cashOnlyExecutionMode = normalizeCashOnlyExecutionMode(el(\"cash-only-execution-mode-select\").value);\n form.cashOnlyExecutionTouched = form.cashOnlyExecutionMode !== \"current\";\n if (allowMarginExplicitlySelected(form)) reconcileExecutionCashPolicy(form, \"margin\");\n else restoreReserveAfterMarginDisabled(form);\n render();\n });\n\n el(\"dca-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.dcaTouched = true;\n form.dcaMode = normalizeDcaMode(el(\"dca-mode-select\").value);\n render();\n });\n\n el(\"dca-base-investment-usd-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.dcaTouched = true;\n form.dcaBaseInvestmentUsd = el(\"dca-base-investment-usd-input\").value.trim();\n render();\n });\n\n el(\"reserve-policy-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.reservePolicyMode = normalizeReservePolicyMode(el(\"reserve-policy-mode-select\").value);\n form.reservedCashTouched = form.reservePolicyMode !== \"current\";\n if (form.reservePolicyMode === \"current\") syncReservePolicyForAccount(state.selected);\n reconcileExecutionCashPolicy(form, \"reserve\");\n render();\n });\n\n el(\"min-reserved-cash-input\").addEventListener(\"input\", () => {\n state.forms[state.selected].reservedCashTouched = true;\n state.forms[state.selected].minReservedCashUsd = el(\"min-reserved-cash-input\").value.trim();\n render();\n });\n\n el(\"reserved-cash-ratio-input\").addEventListener(\"input\", () => {\n state.forms[state.selected].reservedCashTouched = true;\n state.forms[state.selected].reservedCashRatio = el(\"reserved-cash-ratio-input\").value.trim();\n render();\n });\n\n el(\"copy-button\").addEventListener(\"click\", async () => {\n try {\n await navigator.clipboard.writeText(summaryText());\n showToast(t(\"copied\"), { duration: 3000 });\n } catch {\n showToast(summaryText(), { duration: 0 });\n }\n });\n\n el(\"dispatch-button\").addEventListener(\"click\", dispatchSwitch);\n el(\"logout-button\").addEventListener(\"click\", handleLogout);\n el(\"lang-button\").addEventListener(\"click\", () => {\n state.lang = state.lang === \"zh\" ? \"en\" : \"zh\";\n localStorage.setItem(\"qsl-switch-lang\", state.lang);\n render();\n });\n\n applyStrategyProfiles(defaultStrategyProfiles);\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n render();\n boot();\n\n async function boot() {\n try {\n await refreshStrategyProfiles();\n await refreshSession();\n } catch {\n state.auth = { available: false, allowed: false, admin: false, login: null };\n state.configSource = \"default\";\n state.currentStrategies = {};\n state.bootMessageKey = \"bootTimeout\";\n state.appReady = true;\n render();\n }\n }\n"; +export const APP_JS = "\n\n let platformMeta = {\n binance: { label: \"Binance\", code: \"BN\", accent: \"var(--bn)\" },\n firstrade: { label: \"Firstrade\", code: \"FT\", accent: \"var(--ft)\" },\n ibkr: { label: \"IBKR\", code: \"IB\", accent: \"var(--ib)\" },\n longbridge: { label: \"LongBridge\", code: \"LB\", accent: \"var(--lb)\" },\n qmt: { label: \"QMT\", code: \"QM\", accent: \"var(--qmt)\" },\n schwab: { label: \"Schwab\", code: \"SW\", accent: \"var(--sw)\" },\n };\n\n const platformRepositories = {\n binance: \"QuantStrategyLab/BinancePlatform\",\n firstrade: \"QuantStrategyLab/FirstradePlatform\",\n ibkr: \"QuantStrategyLab/InteractiveBrokersPlatform\",\n longbridge: \"QuantStrategyLab/LongBridgePlatform\",\n qmt: \"QuantStrategyLab/QmtPlatform\",\n schwab: \"QuantStrategyLab/CharlesSchwabPlatform\",\n };\n // Alias for backward compatibility\n const defaultRepositories = platformRepositories;\n\n const defaultAccountOptions = window.__DEFAULT_ACCOUNT_OPTIONS__ || {\n binance: [{\"key\": \"default\", \"label\": \"Binance\", \"target_name\": \"default\", \"cash_currency\": \"USD\", \"supported_domains\": [\"crypto\"]}],\n firstrade: [{\"key\": \"preview\", \"label\": \"Firstrade\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\", \"service_name\": \"firstrade-quant-service\"}],\n ibkr: [{\"key\": \"preview\", \"label\": \"IBKR\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\", \"hk_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\"}],\n longbridge: [{\"key\": \"preview\", \"label\": \"LongBridge\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\", \"hk_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\"}],\n qmt: [{\"key\": \"default\", \"label\": \"QMT\", \"target_name\": \"default\", \"cash_currency\": \"CNY\", \"supported_domains\": [\"cn_equity\"], \"service_name\": \"qmt-quant-service\"}],\n schwab: [{\"key\": \"preview\", \"label\": \"Schwab\", \"target_name\": \"preview\", \"supported_domains\": [\"us_equity\"], \"cash_currency\": \"USD\", \"default_execution_mode\": \"live\", \"service_name\": \"charles-schwab-quant-service\"}],\n };\n\n const domainLabels = window.__DOMAIN_LABELS__ || {\n cn_equity: { zh: \"A股\", en: \"CN A-share\" },\n crypto: { zh: \"加密\", en: \"Crypto\" },\n hk_equity: { zh: \"港股\", en: \"HK Equity\" },\n us_equity: { zh: \"美股\", en: \"US Equity\" },\n };\n\n const platformConfig = window.__PLATFORM_CONFIG__ || {\n binance: {\n dry_run_only: false,\n margin_policy: false,\n reserved_cash: false,\n income_layer: false,\n option_overlay: false,\n dca: false,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n firstrade: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"firstrade-quant-service\",\n default_execution_mode: \"live\"\n },\n ibkr: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n longbridge: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"\",\n default_execution_mode: \"live\"\n },\n qmt: {\n dry_run_only: true,\n margin_policy: false,\n reserved_cash: false,\n income_layer: false,\n option_overlay: false,\n dca: false,\n execution_mode: \"paper\",\n service_name: \"qmt-quant-service\",\n default_execution_mode: \"paper\"\n },\n schwab: {\n dry_run_only: false,\n margin_policy: true,\n reserved_cash: true,\n income_layer: true,\n option_overlay: true,\n dca: true,\n execution_mode: \"live\",\n service_name: \"charles-schwab-quant-service\",\n default_execution_mode: \"live\"\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 const reservePolicyModes = [\"none\", \"ratio\", \"floor\", \"max\"];\n const incomeLayerModes = [\"enabled\", \"disabled\"];\n const optionOverlayModes = [\"enabled\", \"disabled\"];\n const cashOnlyExecutionModes = [\"enabled\", \"disabled\"];\n const runtimeTargetModes = [\"enabled\", \"disabled\"];\n const pluginModes = [\"auto\", \"none\"];\n const dcaModes = [\"fixed\", \"smart\"];\n const runtimeTargetEnabledVariable = \"RUNTIME_TARGET_ENABLED\";\n const incomeLayerEnabledVariable = \"INCOME_LAYER_ENABLED\";\n const incomeLayerStartUsdVariable = \"INCOME_LAYER_START_USD\";\n const incomeLayerMaxRatioVariable = \"INCOME_LAYER_MAX_RATIO\";\n const dcaProfileDefaults = window.__DCA_PROFILE_DEFAULTS__ || {\n nasdaq_sp500_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n ibit_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n };\n const APP_BOOT_TIMEOUT_MS = 15000;\n const platformMinReservedCashVariables = {\n longbridge: \"LONGBRIDGE_MIN_RESERVED_CASH_USD\",\n ibkr: \"IBKR_MIN_RESERVED_CASH_USD\",\n schwab: \"SCHWAB_MIN_RESERVED_CASH_USD\",\n firstrade: \"FIRSTRADE_MIN_RESERVED_CASH_USD\",\n };\n const platformReservedCashRatioVariables = {\n longbridge: \"LONGBRIDGE_RESERVED_CASH_RATIO\",\n ibkr: \"IBKR_RESERVED_CASH_RATIO\",\n schwab: \"SCHWAB_RESERVED_CASH_RATIO\",\n firstrade: \"FIRSTRADE_RESERVED_CASH_RATIO\",\n };\n\n const defaultStrategyProfiles = window.__DEFAULT_STRATEGY_PROFILES__ || [\n {\n \"profile\": \"tqqq_growth_income\",\n \"label\": \"纳斯达克增长收益\",\n \"label_en\": \"NASDAQ Growth Income\",\n \"label_zh\": \"纳斯达克增长收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"250000\",\n \"income_layer_max_ratio\": \"0.55\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.3,\n \"DGRO\": 0.2,\n \"SGOV\": 0.4,\n \"SPYI\": 0.08,\n \"QQQI\": 0.02\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"tqqq_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"250000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.03\"\n },\n {\n \"profile\": \"soxl_soxx_trend_income\",\n \"label\": \"半导体趋势收益\",\n \"label_en\": \"Semiconductor Trend Income\",\n \"label_zh\": \"半导体趋势收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"150000\",\n \"income_layer_max_ratio\": \"0.95\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.15,\n \"DGRO\": 0.1,\n \"SGOV\": 0.7,\n \"SPYI\": 0.04,\n \"QQQI\": 0.01\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_income_overlay_enabled\": true,\n \"option_income_overlay_recipe\": \"soxx_put_credit_spread_income_v1\",\n \"option_income_overlay_start_usd\": \"150000\",\n \"option_income_overlay_nav_risk_ratio\": \"0.01\"\n },\n {\n \"profile\": \"nasdaq_sp500_smart_dca\",\n \"label\": \"纳指标普定投\",\n \"label_en\": \"NASDAQ/S&P 500 DCA\",\n \"label_zh\": \"纳指标普定投\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"dca_enabled\": true,\n \"dca_default_mode\": \"fixed\",\n \"dca_default_base_investment_usd\": \"1000\"\n },\n {\n \"profile\": \"ibit_smart_dca\",\n \"label\": \"IBIT比特币定投\",\n \"label_en\": \"IBIT Bitcoin DCA\",\n \"label_zh\": \"IBIT比特币定投\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"dca_enabled\": true,\n \"dca_default_mode\": \"fixed\",\n \"dca_default_base_investment_usd\": \"1000\"\n },\n {\n \"profile\": \"global_etf_rotation\",\n \"label\": \"全球ETF轮动\",\n \"label_en\": \"Global ETF Rotation\",\n \"label_zh\": \"全球ETF轮动\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"500000\",\n \"income_layer_max_ratio\": \"0.15\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.4,\n \"DGRO\": 0.25,\n \"SGOV\": 0.3,\n \"SPYI\": 0.05\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"500000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"russell_top50_leader_rotation\",\n \"label\": \"罗素Top50领涨\",\n \"label_en\": \"Russell Top50 Leaders\",\n \"label_zh\": \"罗素Top50领涨\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\",\n \"income_layer_start_usd\": \"300000\",\n \"income_layer_max_ratio\": \"0.25\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.45,\n \"DGRO\": 0.3,\n \"SGOV\": 0.25\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"300000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"tecl_xlk_trend_income\",\n \"label\": \"TECL/XLK趋势收益\",\n \"label_en\": \"TECL/XLK Trend Income\",\n \"label_zh\": \"TECL/XLK趋势收益\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"failed_promotion_vs_live_profiles\"\n },\n {\n \"profile\": \"us_equity_combo\",\n \"label\": \"美股核心组合\",\n \"label_en\": \"US Core Combo\",\n \"label_zh\": \"美股核心组合\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": true,\n \"option_overlay_enabled\": true,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\",\n \"income_layer_start_usd\": \"300000\",\n \"income_layer_max_ratio\": \"0.25\",\n \"income_layer_allocations\": {\n \"SCHD\": 0.25,\n \"DGRO\": 0.25,\n \"SGOV\": 0.2,\n \"SPYI\": 0.15,\n \"QQQI\": 0.15\n },\n \"option_overlay_live_gate\": \"promotion_required\",\n \"option_overlay_live_status\": \"research_only\",\n \"option_growth_overlay_enabled\": true,\n \"option_growth_overlay_recipe\": \"spy_leaps_growth_v1\",\n \"option_growth_overlay_start_usd\": \"300000\",\n \"option_growth_overlay_nav_budget_ratio\": \"0.015\"\n },\n {\n \"profile\": \"us_equity_combo_core\",\n \"label\": \"美股核心组合影子\",\n \"label_en\": \"US Core Combo Shadow\",\n \"label_zh\": \"美股核心组合影子\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"us_equity_combo_leveraged\",\n \"label\": \"美股加速组合\",\n \"label_en\": \"US Alpha Combo\",\n \"label_zh\": \"美股加速组合\",\n \"domain\": \"us_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"hk_global_etf_tactical_rotation\",\n \"label\": \"港股ETF战术轮动\",\n \"label_en\": \"HK ETF Tactical Rotation\",\n \"label_zh\": \"港股ETF战术轮动\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"hk_low_vol_dividend_quality_snapshot\",\n \"label\": \"港股红利质量\",\n \"label_en\": \"HK Dividend Quality\",\n \"label_zh\": \"港股红利质量\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"hk_equity_combo\",\n \"label\": \"港股恒生组合\",\n \"label_en\": \"HK Core Combo\",\n \"label_zh\": \"港股恒生组合\",\n \"domain\": \"hk_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"cn_industry_etf_rotation\",\n \"label\": \"A股行业ETF轮动\",\n \"label_en\": \"CN Industry ETF Rotation\",\n \"label_zh\": \"A股行业ETF轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"cn_industry_etf_rotation_aggressive\",\n \"label\": \"A股ETF轮动\",\n \"label_en\": \"CN ETF Rotation\",\n \"label_zh\": \"A股ETF轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"live_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"live_candidate_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_index_etf_tactical_rotation\",\n \"label\": \"A股宽基ETF战术轮动\",\n \"label_en\": \"CN Index ETF Tactical Rotation\",\n \"label_zh\": \"A股宽基ETF战术轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_tactical_rotation\",\n \"label\": \"创业板战术轮动\",\n \"label_en\": \"CN ChiNext Tactical Rotation\",\n \"label_zh\": \"创业板战术轮动\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_growth_momentum_quality\",\n \"label\": \"创业板成长动量质量\",\n \"label_en\": \"CN ChiNext Growth Momentum Quality\",\n \"label_zh\": \"创业板成长动量质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_dividend_quality_snapshot\",\n \"label\": \"A股红利质量\",\n \"label_en\": \"CN Dividend Quality\",\n \"label_zh\": \"A股红利质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_chinext_growth_momentum_quality_snapshot\",\n \"label\": \"创业板成长质量快照\",\n \"label_en\": \"CN ChiNext Growth Quality Snapshot\",\n \"label_zh\": \"创业板成长质量快照\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_star_growth_momentum_quality\",\n \"label\": \"科创板成长动量质量\",\n \"label_en\": \"CN STAR Growth Momentum Quality\",\n \"label_zh\": \"科创板成长动量质量\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"cn_equity_combo\",\n \"label\": \"A股进取组合\",\n \"label_en\": \"CN Alpha Combo\",\n \"label_zh\": \"A股进取组合\",\n \"domain\": \"cn_equity\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n },\n {\n \"profile\": \"crypto_live_pool_rotation\",\n \"label\": \"加密实时池轮动\",\n \"label_en\": \"Crypto Live Pool Rotation\",\n \"label_zh\": \"加密实时池轮动\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": true,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"runtime_enabled\",\n \"can_switch_live\": true,\n \"allowed_execution_modes\": [\n \"live\",\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"\"\n },\n {\n \"profile\": \"crypto_btc_dca\",\n \"label\": \"BTC定投\",\n \"label_en\": \"BTC DCA\",\n \"label_zh\": \"BTC定投\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"shadow_candidate\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"paper\",\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"shadow_candidate_requires_evidence_package\"\n },\n {\n \"profile\": \"crypto_trend_rotation\",\n \"label\": \"山寨趋势轮动\",\n \"label_en\": \"Altcoin Trend\",\n \"label_zh\": \"山寨趋势轮动\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": false,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\"\n },\n {\n \"profile\": \"crypto_equity_combo\",\n \"label\": \"加密动量组合\",\n \"label_en\": \"Crypto Core Combo\",\n \"label_zh\": \"加密动量组合\",\n \"domain\": \"crypto\",\n \"runtime_enabled\": false,\n \"income_layer_enabled\": false,\n \"option_overlay_enabled\": false,\n \"combo_enabled\": true,\n \"lifecycle_stage\": \"research_backtest_only\",\n \"can_switch_live\": false,\n \"allowed_execution_modes\": [\n \"dry_run\"\n ],\n \"blocked_live_reason\": \"research_backtest_only_requires_evidence_package\",\n \"combo_mode\": \"dynamic\"\n }\n ];\n\n const localStrategyLabels = {\n tqqq_growth_income: { zh: \"纳斯达克增长收益\", en: \"NASDAQ Growth Income\" },\n soxl_soxx_trend_income: { zh: \"半导体趋势收益\", en: \"Semiconductor Trend Income\" },\n nasdaq_sp500_smart_dca: { zh: \"纳指标普定投\", en: \"NASDAQ/S&P 500 DCA\" },\n ibit_smart_dca: { zh: \"IBIT比特币定投\", en: \"IBIT Bitcoin DCA\" },\n global_etf_rotation: { zh: \"全球ETF轮动\", en: \"Global ETF Rotation\" },\n russell_top50_leader_rotation: { zh: \"罗素Top50领涨\", en: \"Russell Top50 Leaders\" },\n hk_global_etf_tactical_rotation: { zh: \"港股ETF战术轮动\", en: \"HK ETF Tactical Rotation\" },\n hk_low_vol_dividend_quality_snapshot: { zh: \"港股红利质量\", en: \"HK Dividend Quality\" },\n cn_industry_etf_rotation: { zh: \"A股行业ETF轮动\", en: \"CN Industry ETF Rotation\" },\n cn_dividend_quality_snapshot: { zh: \"A股红利质量\", en: \"CN Dividend Quality\" },\n us_equity_combo: { zh: \"美股核心组合\", en: \"US Core Combo\" },\n us_equity_combo_leveraged: { zh: \"美股加速组合\", en: \"US Alpha Combo\" },\n hk_equity_combo: { zh: \"港股恒生组合\", en: \"HK Core Combo\" },\n cn_industry_etf_rotation_aggressive: { zh: \"A股ETF轮动\", en: \"CN ETF Rotation\" },\n cn_stock_momentum_rotation: { zh: \"A股个股动量\", en: \"CN Stock Momentum\" },\n cn_equity_combo: { zh: \"A股进取组合\", en: \"CN Alpha Combo\" },\n crypto_btc_dca: { zh: \"BTC定投\", en: \"BTC DCA\" },\n crypto_trend_rotation: { zh: \"山寨趋势轮动\", en: \"Altcoin Trend\" },\n crypto_equity_combo: { zh: \"加密动量组合\", en: \"Crypto Core Combo\" },\n };\n\n const fallbackIncomeLayerDefaults = window.__INCOME_LAYER_DEFAULTS__ || {\n tqqq_growth_income: {\n startUsd: 250000,\n maxRatio: \"0.55\",\n allocations: { SCHD: 0.30, DGRO: 0.20, SGOV: 0.40, SPYI: 0.08, QQQI: 0.02 },\n },\n soxl_soxx_trend_income: {\n startUsd: 150000,\n maxRatio: \"0.95\",\n allocations: { SCHD: 0.15, DGRO: 0.10, SGOV: 0.70, SPYI: 0.04, QQQI: 0.01 },\n },\n global_etf_rotation: {\n startUsd: 500000,\n maxRatio: \"0.15\",\n allocations: { SCHD: 0.40, DGRO: 0.25, SGOV: 0.30, SPYI: 0.05 },\n },\n russell_top50_leader_rotation: {\n startUsd: 300000,\n maxRatio: \"0.25\",\n allocations: { SCHD: 0.45, DGRO: 0.30, SGOV: 0.25 },\n },\n us_equity_combo: {\n startUsd: 300000,\n maxRatio: \"0.25\",\n allocations: { SCHD: 0.25, DGRO: 0.25, SGOV: 0.20, SPYI: 0.15, QQQI: 0.15 },\n }};\n let incomeLayerDefaults = {};\n const fallbackOptionOverlayDefaults = window.__OPTION_OVERLAY_DEFAULTS__ || {\n tqqq_growth_income: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"tqqq_leaps_growth_v1\", startUsd: \"250000\", ratio: \"0.03\", ratioKind: \"budget\" },\n ],\n },\n soxl_soxx_trend_income: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"income\", recipe: \"soxx_put_credit_spread_income_v1\", startUsd: \"150000\", ratio: \"0.01\", ratioKind: \"risk\" },\n ],\n },\n global_etf_rotation: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"500000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n russell_top50_leader_rotation: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"300000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n us_equity_combo: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [\n { family: \"growth\", recipe: \"spy_leaps_growth_v1\", startUsd: \"300000\", ratio: \"0.015\", ratioKind: \"budget\" },\n ],\n },\n us_equity_combo_leveraged: {\n liveGate: \"promotion_required\",\n liveStatus: \"research_only\",\n families: [],\n }};\n let optionOverlayDefaults = {};\n\n const strategyDomains = [\"us_equity\", \"hk_equity\", \"cn_equity\", \"crypto\"];\n let strategyOptions = [];\n let strategyLabels = {};\n let strategyCatalog = {};\n\n\n const copy = {\n zh: {\n appTitle: \"策略切换\",\n appSubtitle: \"选平台、目标账号和策略,一次执行完成切换。\",\n bootKicker: \"初始化控制台\",\n bootTitle: \"读取策略配置\",\n bootMessage: \"正在读取登录状态、账号配置和当前状态。\",\n bootStrategy: \"正在读取策略目录。\",\n bootSession: \"正在验证登录状态。\",\n bootConfig: \"正在读取账号配置和当前状态。\",\n bootTimeout: \"加载超时,已切换到公开预览(登录后可重试)。\",\n bootPublic: \"公开预览已就绪。\",\n login: \"登录\",\n logout: \"退出\",\n signedInAs: \"已登录 {login}\",\n activePlatform: \"当前平台\",\n account: \"目标账号\",\n strategy: \"策略\",\n mode: \"模式\",\n live: \"实盘\",\n paper: \"模拟\",\n runtimeTargetMode: \"账号运行状态\",\n runtimeSectionTitle: \"运行与插件\",\n runtimeTargetCurrent: \"沿用当前状态\",\n runtimeTargetEnabled: \"启用\",\n runtimeTargetDisabled: \"禁用\",\n runtimeTargetModeMeta: \"停用后正式运行会跳过,模拟运行和健康检查仍可用。\",\n pluginMode: \"插件启用范围\",\n pluginModeAuto: \"启用插件\",\n pluginModeNone: \"禁用插件\",\n pluginModeMeta: \"选择是否启用该策略的插件。\",\n incomeLayerMode: \"收入层状态\",\n incomeLayerSectionTitle: \"收入层\",\n incomeLayerCurrent: \"沿用当前配置\",\n incomeLayerEnabled: \"开启收入层\",\n incomeLayerDisabled: \"关闭收入层\",\n incomeLayerNotSupported: \"该策略未定义收入层\",\n incomeLayerStartUsd: \"收入层起始金额\",\n incomeLayerMaxRatio: \"收入层最高比例\",\n incomeLayerModeMeta: \"仅对已定义收入层的美股策略生效。\",\n incomeLayerDefaultMeta: \"策略默认:起始 {start},最高 {ratio}。\",\n incomeLayerAllocationMeta: \"默认分配:{allocations}。\",\n incomeLayerStartMeta: \"总资产达到该金额后启用收入层。\",\n incomeLayerRatioMeta: \"例如 0.55 表示最高 55%。\",\n optionOverlayMode: \"期权层状态\",\n optionOverlaySectionTitle: \"期权层\",\n optionOverlayCurrent: \"沿用当前配置\",\n optionOverlayEnabled: \"启用期权层\",\n optionOverlayDisabled: \"关闭期权层\",\n optionOverlayNotSupported: \"该策略未定义期权层\",\n optionOverlayModeMeta: \"启用时使用策略默认的最佳 recipe 和预算,不在这里手动调比例。\",\n optionOverlayDefaultMeta: \"{defaults}\",\n optionOverlayFamilyGrowth: \"增长\",\n optionOverlayFamilyIncome: \"收入\",\n optionOverlayBudgetRatio: \"预算 {ratio}\",\n optionOverlayRiskRatio: \"风险 {ratio}\",\n cashOnlyExecutionMode: \"允许融资\",\n cashOnlyExecutionCurrent: \"沿用当前配置\",\n cashOnlyExecutionYes: \"是\",\n cashOnlyExecutionNo: \"否\",\n cashOnlyExecutionModeMeta: \"选「否」时只按真实现金下单,不会动用 margin 购买力。\",\n cashOnlyExecutionValueYes: \"是\",\n cashOnlyExecutionValueNo: \"否\",\n currentCashOnlyExecution: \"当前允许融资\",\n pendingCashOnlyExecution: \"待提交允许融资\",\n executionCashPolicyTitle: \"现金与融资\",\n executionCashPolicyNote: \"允许融资与预留现金覆盖不能同时生效;选「是」会清空预留覆盖,设预留覆盖会强制「否」。\",\n executionCashMarginBlocksReserve: \"已选允许融资;提交时会清空预留现金覆盖。\",\n executionCashReserveBlocksMargin: \"已设预留现金覆盖;提交时会强制不允许融资。\",\n qmtPlatformCashNote: \"A 股 QMT 不使用 margin / 平台预留现金;现金约束在策略参数 execution_cash_reserve_ratio 内配置。\",\n qmtDryRunOnlyNote: \"QMT 当前仅支持 dry-run(模拟),尚无 live 券商账号。\",\n binancePlatformNote: \"Binance 平台不使用券商级收入层与期权层;相关功能由策略内部实现。\",\n invalidExecutionCashPolicyNote: \"允许融资与预留现金覆盖冲突,请只保留一种约束。\",\n dcaMode: \"定投模式\",\n dcaSectionTitle: \"定投\",\n dcaModeFixed: \"定额定投\",\n dcaModeSmart: \"智能定投\",\n dcaBaseInvestmentUsd: \"定投基准金额\",\n dcaModeMeta: \"仅定投策略可配置。\",\n dcaDefaultMeta: \"默认:{mode},基准金额 {amount}。\",\n dcaNotSupported: \"该策略不是定投策略\",\n dcaPlatformNotSupported: \"当前平台不支持定投策略\",\n currentDca: \"当前定投设置\",\n pendingDca: \"待提交定投设置\",\n dcaText: \"{mode},基准金额 {amount}\",\n minReservedCash: \"最小预留现金 ({currency})\",\n reservedCashRatio: \"预留现金比例\",\n reservedCashMode: \"预留现金策略\",\n reservePolicyCurrent: \"沿用当前配置\",\n reservePolicyNone: \"不设置平台预留现金\",\n reservePolicyRatio: \"仅按比例\",\n reservePolicyFloor: \"仅按固定金额\",\n reservePolicyMax: \"固定金额和比例取较大值\",\n reservedCashModeMeta: \"选择是否沿用、清空或覆盖平台预留现金。\",\n reservedCashNone: \"不设置\",\n reservedCashDefault: \"未配置(平台默认:0 {currency} / 0%)\",\n reservedCashMeta: \"固定金额下限,可单独设置或与比例取较大值。\",\n reservedCashRatioMeta: \"例如 0.03 表示 3%。\",\n summary: \"当前 / 待提交\",\n copySummary: \"复制状态\",\n loginToRun: \"登录后切换\",\n loadingConfig: \"读取配置中\",\n configureAccounts: \"配置账号后切换\",\n runSwitch: \"一键切换\",\n noChanges: \"无变更\",\n readonlyNote: \"登录后才可执行切换。\",\n publicReadonly: \"登录后查看账号配置。\",\n loadingConfigNote: \"正在读取账号配置和当前状态。\",\n missingConfigNote: \"账号配置未加载,暂时不能执行。\",\n readyNote: \"点击后会触发工作流,并同步目标平台服务。\",\n invalidStrategyNote: \"当前账号没有可执行策略,暂时不能切换。\",\n invalidReservePolicyNote: \"请为当前预留现金策略填写有效金额或比例。\",\n invalidIncomeLayerNote: \"请填写有效的收入层起始金额和最高比例。\",\n invalidOptionOverlayNote: \"当前策略未定义可启用的期权层。\",\n invalidDcaNote: \"请填写有效的定投模式和基准金额。\",\n publicOauthTitle: \"GitHub OAuth 保护\",\n publicOauthText: \"只允许白名单账号进入私有配置。\",\n publicWorkerTitle: \"Worker 端触发\",\n publicWorkerText: \"令牌保留在服务端,浏览器只提交切换意图。\",\n publicAuditTitle: \"变更可回溯\",\n publicAuditText: \"切换由 GitHub Actions 执行,便于审计和回滚。\",\n noAccount: \"没有账号选项\",\n noStrategy: \"没有支持的策略\",\n repository: \"平台仓库\",\n selectedAccount: \"账号\",\n selectedMarket: \"市场\",\n currentRuntimeTarget: \"当前账号状态\",\n pendingRuntimeTarget: \"待提交账号状态\",\n reservedCashPolicy: \"当前预留现金\",\n currentIncomeLayer: \"当前收入层\",\n pendingIncomeLayer: \"待提交收入层\",\n currentOptionOverlay: \"当前期权层\",\n pendingOptionOverlay: \"待提交期权层\",\n pendingReservedCashPolicy: \"待提交预留现金\",\n pendingMode: \"待提交模式\",\n currentPluginMode: \"当前插件范围\",\n pendingPluginMode: \"待提交插件范围\",\n unchanged: \"不变\",\n copied: \"已复制状态\",\n dispatching: \"正在触发工作流...\",\n dispatched: \"已触发工作流\",\n dispatchFailed: \"触发失败\",\n targetMeta: \"目标 {target} · 服务 {service} · 市场 {domains}\",\n strategyMeta: \"该账号仅显示 {domains} 策略\",\n strategyLifecycleMeta: \"当前门槛 {stage}\",\n strategyBlockedCountMeta: \"{count} 个策略未达 live 门槛\",\n strategyDefaultBlockedMeta: \"默认策略 {profile} 已阻断:{reason}\",\n usEquity: \"美股\",\n hkEquity: \"港股\",\n cnEquity: \"A股\",\n cryptoEquity: \"加密\",\n currentStrategy: \"当前策略\",\n nextStrategy: \"切换策略\",\n notRead: \"读取失败\",\n runtimeTargetOn: \"启用\",\n runtimeTargetOff: \"禁用\",\n incomeLayerDefault: \"开启,{start}起 {ratio}\",\n incomeLayerOff: \"关闭\",\n incomeLayerOn: \"开启,起始 {start},最高 {ratio}\",\n optionOverlayOff: \"关闭\",\n optionOverlayOn: \"开启\",\n optionOverlayDefaultSimple: \"开启\",\n optionOverlayDefault: \"开启,{detail}\",\n cashOnlyExecutionDefault: \"仅用现金\",\n },\n en: {\n appTitle: \"Strategy Switch\",\n appSubtitle: \"Pick platform, target account, and strategy. One action switches everything.\",\n bootKicker: \"Starting console\",\n bootTitle: \"Loading strategy config\",\n bootMessage: \"Reading session, account config, and current state.\",\n bootStrategy: \"Reading strategy catalog.\",\n bootSession: \"Checking sign-in status.\",\n bootConfig: \"Reading account config and current state.\",\n bootTimeout: \"Loading timed out; switched to public preview. Retry after signing in.\",\n bootPublic: \"Public preview is ready.\",\n login: \"Sign in\",\n logout: \"Sign out\",\n signedInAs: \"Signed in as {login}\",\n activePlatform: \"Active Platform\",\n account: \"Target account\",\n strategy: \"Strategy\",\n mode: \"Mode\",\n live: \"Live\",\n paper: \"Dry run\",\n runtimeTargetMode: \"Account status\",\n runtimeSectionTitle: \"Runtime and plugins\",\n runtimeTargetCurrent: \"Keep current status\",\n runtimeTargetEnabled: \"Enabled\",\n runtimeTargetDisabled: \"Disabled\",\n runtimeTargetModeMeta: \"Disabled accounts skip live runs; dry runs and health checks still work.\",\n pluginMode: \"Plugin scope\",\n pluginModeAuto: \"Enabled\",\n pluginModeNone: \"Disabled\",\n pluginModeMeta: \"Choose whether to enable this strategy's plugins.\",\n incomeLayerMode: \"Income layer\",\n incomeLayerSectionTitle: \"Income layer\",\n incomeLayerCurrent: \"Keep current config\",\n incomeLayerEnabled: \"Enable income layer\",\n incomeLayerDisabled: \"Disable income layer\",\n incomeLayerNotSupported: \"No income layer for this strategy\",\n incomeLayerStartUsd: \"Income layer start amount\",\n incomeLayerMaxRatio: \"Income layer max ratio\",\n incomeLayerModeMeta: \"Only applies to US equity strategies with an income layer.\",\n incomeLayerDefaultMeta: \"Strategy default: starts at {start}, max {ratio}.\",\n incomeLayerAllocationMeta: \"Default allocation: {allocations}.\",\n incomeLayerStartMeta: \"Income layer activates after total assets reach this amount.\",\n incomeLayerRatioMeta: \"Use 0.55 for a 55% cap.\",\n optionOverlayMode: \"Option layer\",\n optionOverlaySectionTitle: \"Option layer\",\n optionOverlayCurrent: \"Keep current config\",\n optionOverlayEnabled: \"Enable option layer\",\n optionOverlayDisabled: \"Disable option layer\",\n optionOverlayNotSupported: \"No option layer for this strategy\",\n optionOverlayModeMeta: \"Enabled mode uses the strategy's default recipe and budget; ratios are not edited here.\",\n optionOverlayDefaultMeta: \"{defaults}\",\n optionOverlayFamilyGrowth: \"Growth\",\n optionOverlayFamilyIncome: \"Income\",\n optionOverlayBudgetRatio: \"budget {ratio}\",\n optionOverlayRiskRatio: \"risk {ratio}\",\n cashOnlyExecutionMode: \"Allow margin\",\n cashOnlyExecutionCurrent: \"Keep current config\",\n cashOnlyExecutionYes: \"Yes\",\n cashOnlyExecutionNo: \"No\",\n cashOnlyExecutionModeMeta: \"Choose No to use available cash only and avoid margin buying power.\",\n cashOnlyExecutionValueYes: \"Yes\",\n cashOnlyExecutionValueNo: \"No\",\n currentCashOnlyExecution: \"Current allow margin\",\n pendingCashOnlyExecution: \"Pending allow margin\",\n executionCashPolicyTitle: \"Cash and margin\",\n executionCashPolicyNote: \"Allow margin and reserve-cash overrides cannot both apply. Yes clears reserve overrides; reserve overrides force No.\",\n executionCashMarginBlocksReserve: \"Allow margin is selected; submitting will clear reserve-cash overrides.\",\n executionCashReserveBlocksMargin: \"Reserve-cash override is active; submitting will force allow margin to No.\",\n qmtPlatformCashNote: \"QMT A-share does not use margin or platform reserve cash; cash constraints live in strategy execution_cash_reserve_ratio.\",\n qmtDryRunOnlyNote: \"QMT is dry-run only for now; no live broker accounts are configured.\",\n binancePlatformNote: \"Binance does not use broker-level income/option layers; features are implemented inside strategies.\",\n invalidExecutionCashPolicyNote: \"Allow margin and reserve-cash overrides conflict. Keep only one constraint.\",\n dcaMode: \"DCA mode\",\n dcaSectionTitle: \"DCA\",\n dcaModeFixed: \"Fixed DCA\",\n dcaModeSmart: \"Smart DCA\",\n dcaBaseInvestmentUsd: \"Base DCA amount\",\n dcaModeMeta: \"Only DCA strategies can use this.\",\n dcaDefaultMeta: \"Default: {mode}, base amount {amount}.\",\n dcaNotSupported: \"This is not a DCA strategy\",\n dcaPlatformNotSupported: \"DCA not supported on this platform\",\n currentDca: \"Current DCA settings\",\n pendingDca: \"Pending DCA settings\",\n dcaText: \"{mode}, base amount {amount}\",\n minReservedCash: \"Minimum reserved cash ({currency})\",\n reservedCashRatio: \"Reserved cash ratio\",\n reservedCashMode: \"Reserved cash policy\",\n reservePolicyCurrent: \"Keep current config\",\n reservePolicyNone: \"No platform reserve\",\n reservePolicyRatio: \"Ratio only\",\n reservePolicyFloor: \"Fixed amount only\",\n reservePolicyMax: \"Max of amount and ratio\",\n reservedCashModeMeta: \"Choose whether to keep, clear, or override platform reserved cash.\",\n reservedCashNone: \"None\",\n reservedCashDefault: \"Not configured (platform default: 0 {currency} / 0%)\",\n reservedCashMeta: \"Fixed cash floor. Use alone or with a ratio.\",\n reservedCashRatioMeta: \"Use 0.03 for 3%.\",\n summary: \"Current / Pending\",\n copySummary: \"Copy state\",\n loginToRun: \"Sign in to switch\",\n loadingConfig: \"Loading config\",\n configureAccounts: \"Configure accounts\",\n runSwitch: \"Switch now\",\n noChanges: \"No changes\",\n readonlyNote: \"Sign in to switch.\",\n publicReadonly: \"Sign in to view account config.\",\n loadingConfigNote: \"Reading account config and current state.\",\n missingConfigNote: \"Account config is not loaded, so switching is disabled.\",\n readyNote: \"This dispatches the workflow and syncs the target platform service.\",\n invalidStrategyNote: \"This account has no runnable strategy, so switching is disabled.\",\n invalidReservePolicyNote: \"Enter a valid amount or ratio for the selected reserved-cash policy.\",\n invalidIncomeLayerNote: \"Enter a valid income layer start amount and max ratio.\",\n invalidOptionOverlayNote: \"This strategy does not define an option layer to enable.\",\n invalidDcaNote: \"Enter a valid DCA mode and base amount.\",\n publicOauthTitle: \"Protected by GitHub OAuth\",\n publicOauthText: \"Only allowlisted accounts can open private config.\",\n publicWorkerTitle: \"Worker-side dispatch\",\n publicWorkerText: \"Tokens stay server-side; the browser submits intent only.\",\n publicAuditTitle: \"Traceable changes\",\n publicAuditText: \"Switches run through GitHub Actions for audit and rollback.\",\n noAccount: \"No accounts\",\n noStrategy: \"No supported strategies\",\n repository: \"Repository\",\n selectedAccount: \"Account\",\n selectedMarket: \"Market\",\n currentRuntimeTarget: \"Current account status\",\n pendingRuntimeTarget: \"Pending account status\",\n reservedCashPolicy: \"Current reserved cash\",\n currentIncomeLayer: \"Current income layer\",\n pendingIncomeLayer: \"Pending income layer\",\n currentOptionOverlay: \"Current option layer\",\n pendingOptionOverlay: \"Pending option layer\",\n pendingReservedCashPolicy: \"Pending reserved cash\",\n pendingMode: \"Pending mode\",\n currentPluginMode: \"Current plugin scope\",\n pendingPluginMode: \"Pending plugin scope\",\n unchanged: \"Unchanged\",\n copied: \"State copied\",\n dispatching: \"Dispatching workflow...\",\n dispatched: \"Workflow dispatched\",\n dispatchFailed: \"Dispatch failed\",\n targetMeta: \"target {target} · service {service} · market {domains}\",\n strategyMeta: \"This account only shows {domains} strategies\",\n strategyLifecycleMeta: \"current gate {stage}\",\n strategyBlockedCountMeta: \"{count} strategies are blocked from live\",\n strategyDefaultBlockedMeta: \"Default strategy {profile} is blocked: {reason}\",\n usEquity: \"US equity\",\n hkEquity: \"HK equity\",\n cnEquity: \"CN A-share\",\n cryptoEquity: \"Crypto\",\n currentStrategy: \"Current strategy\",\n nextStrategy: \"Switch strategy\",\n notRead: \"Not read\",\n runtimeTargetOn: \"Enabled\",\n runtimeTargetOff: \"Disabled\",\n incomeLayerDefault: \"Enabled, {start} start, {ratio} max\",\n incomeLayerOff: \"Disabled\",\n incomeLayerOn: \"Enabled, starts at {start}, max {ratio}\",\n optionOverlayOff: \"Disabled\",\n optionOverlayOn: \"Enabled\",\n optionOverlayDefaultSimple: \"Strategy default: enabled\",\n optionOverlayDefault: \"Enabled, {detail}\",\n cashOnlyExecutionDefault: \"Cash only\",\n },\n };\n\n const storedLang = localStorage.getItem(\"qsl-switch-lang\");\n const initialLang = storedLang === \"zh\" || storedLang === \"en\"\n ? storedLang\n : ((navigator.language || \"\").toLowerCase().startsWith(\"zh\") ? \"zh\" : \"en\");\n const clone = (value) => JSON.parse(JSON.stringify(value));\n const defaultReserveForm = () => ({\n reservePolicyMode: \"current\",\n minReservedCashUsd: \"\",\n reservedCashRatio: \"\",\n reservedCashTouched: false,\n incomeLayerMode: \"current\",\n incomeLayerStartUsd: \"\",\n incomeLayerMaxRatio: \"\",\n incomeLayerTouched: false,\n optionOverlayMode: \"current\",\n optionOverlayTouched: false,\n cashOnlyExecutionMode: \"current\",\n cashOnlyExecutionTouched: false,\n runtimeTargetMode: \"current\",\n runtimeTargetTouched: false,\n dcaMode: \"fixed\",\n dcaBaseInvestmentUsd: \"\",\n dcaTouched: false,\n strategyTouched: false,\n });\n\n const state = {\n selected: \"longbridge\",\n lang: initialLang,\n appReady: false,\n bootMessageKey: \"bootMessage\",\n auth: { available: false, allowed: false, admin: false, login: null },\n accountOptions: clone(defaultAccountOptions),\n currentStrategies: {},\n configSource: \"default\",\n repositories: clone(defaultRepositories),\n forms: {\n longbridge: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n ibkr: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n schwab: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n firstrade: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\", ...defaultReserveForm() },\n qmt: { accountKey: \"preview\", strategy: \"\", executionMode: \"paper\", pluginMode: \"auto\", ...defaultReserveForm() },\n binance: { accountKey: \"preview\", strategy: \"\", executionMode: \"live\", pluginMode: \"auto\" },\n },\n };\n\n const el = (id) => document.getElementById(id);\n const t = (key) => copy[state.lang][key] || copy.en[key] || key;\n let toastTimer = null;\n\n function showToast(message, { duration = 4000 } = {}) {\n const node = el(\"toast\");\n if (toastTimer) {\n window.clearTimeout(toastTimer);\n toastTimer = null;\n }\n node.textContent = message || \"\";\n if (message && duration > 0) {\n toastTimer = window.setTimeout(() => {\n node.textContent = \"\";\n toastTimer = null;\n }, duration);\n }\n }\n\n async function fetchWithTimeout(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) {\n const controller = new AbortController();\n const timeoutId = window.setTimeout(() => controller.abort(), timeoutMs);\n try {\n return await fetch(url, { ...init, signal: controller.signal });\n } catch (error) {\n if (error?.name === \"AbortError\") {\n throw new Error(\"request timeout\");\n }\n throw error;\n } finally {\n window.clearTimeout(timeoutId);\n }\n }\n\n async function requestJson(url, init = {}, timeoutMs = APP_BOOT_TIMEOUT_MS) {\n const response = await fetchWithTimeout(url, { ...init, cache: \"no-store\" }, timeoutMs);\n if (!response.ok) throw new Error(\"request failed\");\n return response.json();\n }\n\n function isRequestTimeoutError(error) {\n return String(error?.message || \"\").toLowerCase() === \"request timeout\";\n }\n\n function optionsFor(platform) {\n return state.accountOptions[platform] && state.accountOptions[platform].length\n ? state.accountOptions[platform]\n : defaultAccountOptions[platform];\n }\n\n function selectedAccount(platform = state.selected) {\n const options = optionsFor(platform);\n const form = state.forms[platform];\n return options.find((option) => option.key === form.accountKey) || options[0];\n }\n\n function hasPrivateConfig() {\n return Boolean(state.auth.allowed && state.configSource === \"private\");\n }\n\n function cleanStrategyProfile(value) {\n const profile = String(value || \"\").trim();\n return /^[a-z0-9._=-]{1,120}$/.test(profile) ? profile : \"\";\n }\n\n function cleanStrategyDomain(value) {\n const domain = String(value || \"\").trim();\n return strategyDomains.includes(domain) ? domain : \"\";\n }\n\n function domainLabel(domain) {\n const entry = domainLabels[domain];\n if (entry) return state.lang === \"zh\" ? entry.zh : entry.en;\n return domain;\n }\n\n function platformSupportsMarginPolicy(platform = state.selected) {\n return platformConfig[platform]?.margin_policy ?? true;\n }\n\n function platformSupportsReservedCashPolicy(platform = state.selected) {\n return platformConfig[platform]?.reserved_cash ?? true;\n }\n\n function platformDryRunOnly(platform = state.selected) {\n return platformConfig[platform]?.dry_run_only ?? false;\n }\n\n function allowMarginExplicitlySelected(form) {\n return normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode) === \"disabled\";\n }\n\n function reserveCashOverrideActive(form) {\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n return mode === \"ratio\" || mode === \"floor\" || mode === \"max\";\n }\n\n function executionCashPolicyConflict(form) {\n return allowMarginExplicitlySelected(form) && reserveCashOverrideActive(form);\n }\n\n function reconcileExecutionCashPolicy(form, changed) {\n if (!form) return;\n if (changed === \"margin\" && allowMarginExplicitlySelected(form)) {\n if (form.reservePolicyMode !== \"none\" && (form.minReservedCashUsd || form.reservedCashRatio)) {\n form._prevReserve = {\n mode: form.reservePolicyMode,\n floor: form.minReservedCashUsd,\n ratio: form.reservedCashRatio,\n };\n }\n form.reservePolicyMode = \"none\";\n form.reservedCashTouched = true;\n } else if (changed === \"reserve\" && reserveCashOverrideActive(form)) {\n form.cashOnlyExecutionMode = \"enabled\";\n form.cashOnlyExecutionTouched = true;\n }\n }\n\n function restoreReserveAfterMarginDisabled(form) {\n if (!form || allowMarginExplicitlySelected(form) || !form._prevReserve) return;\n form.reservePolicyMode = form._prevReserve.mode;\n form.minReservedCashUsd = form._prevReserve.floor;\n form.reservedCashRatio = form._prevReserve.ratio;\n form.reservedCashTouched = true;\n delete form._prevReserve;\n }\n\n function strategyDomain(profile) {\n return strategyCatalog[profile]?.domain || \"\";\n }\n\n function selectedCashCurrency(platform = state.selected, account = selectedAccount(platform)) {\n const configured = String(account?.cash_currency || \"\").trim().toUpperCase();\n if (configured === \"USD\" || configured === \"HKD\" || configured === \"CNY\") return configured;\n const domain = strategyDomain(state.forms[platform]?.strategy);\n if (domain === \"hk_equity\") return \"HKD\";\n if (domain === \"cn_equity\") return \"CNY\";\n return \"USD\";\n }\n\n function applyStrategyProfiles(rawProfiles) {\n const profiles = Array.isArray(rawProfiles) && rawProfiles.length\n ? rawProfiles\n : defaultStrategyProfiles;\n const nextOptions = [];\n const nextLabels = {};\n const nextCatalog = {};\n const nextIncomeLayerDefaults = {};\n const nextOptionOverlayDefaults = {};\n for (const item of profiles) {\n const profile = cleanStrategyProfile(item?.profile || item?.strategy_profile);\n if (!profile || nextOptions.includes(profile)) continue;\n const domain = cleanStrategyDomain(item?.domain || \"us_equity\");\n if (!domain) continue;\n nextOptions.push(profile);\n nextLabels[profile] = strategyLabelSet(profile, item);\n nextCatalog[profile] = {\n profile,\n label: nextLabels[profile].en || nextLabels[profile].zh || profile,\n label_en: nextLabels[profile].en || \"\",\n label_zh: nextLabels[profile].zh || \"\",\n domain,\n runtime_enabled: cleanOptionalBoolean(item?.runtime_enabled ?? item?.live_enabled ?? true) !== false,\n };\n const lifecycleStage = normalizeLifecycleStage(item?.lifecycle_stage ?? item?.lifecycleStage);\n if (lifecycleStage) nextCatalog[profile].lifecycle_stage = lifecycleStage;\n const allowedExecutionModes = normalizeAllowedExecutionModes(item?.allowed_execution_modes);\n if (allowedExecutionModes.length) nextCatalog[profile].allowed_execution_modes = allowedExecutionModes;\n const canSwitchLive = cleanOptionalBoolean(item?.can_switch_live);\n if (canSwitchLive !== null) nextCatalog[profile].can_switch_live = canSwitchLive;\n const blockedLiveReason = cleanDisplayText(item?.blocked_live_reason);\n if (blockedLiveReason) nextCatalog[profile].blocked_live_reason = blockedLiveReason;\n const latestEvidenceStatus = cleanDisplayText(item?.latest_evidence_status);\n if (latestEvidenceStatus) nextCatalog[profile].latest_evidence_status = latestEvidenceStatus;\n const pluginGateStatus = cleanDisplayText(item?.plugin_gate_status);\n if (pluginGateStatus) nextCatalog[profile].plugin_gate_status = pluginGateStatus;\n const dcaDefaults = dcaProfileDefaults[profile] || null;\n if (dcaDefaults || item?.dca_enabled === true) {\n nextCatalog[profile].dca_enabled = true;\n nextCatalog[profile].dca_default_mode = normalizeDcaMode(\n item?.dca_default_mode || item?.default_dca_mode || dcaDefaults?.defaultMode || \"fixed\",\n );\n nextCatalog[profile].dca_default_base_investment_usd = cleanDisplayPositiveNumber(\n item?.dca_default_base_investment_usd ||\n item?.default_dca_base_investment_usd ||\n dcaDefaults?.defaultBaseInvestmentUsd ||\n \"1000\",\n ) || \"1000\";\n }\n const profileIncomeDefaults = incomeLayerDefaultsFromProfileItem(item);\n const incomeDefaults = profileIncomeDefaults === false\n ? null\n : (profileIncomeDefaults || fallbackIncomeLayerDefaults[profile] || null);\n if (incomeDefaults) {\n nextIncomeLayerDefaults[profile] = incomeDefaults;\n nextCatalog[profile].income_layer_enabled = true;\n nextCatalog[profile].income_layer_start_usd = String(incomeDefaults.startUsd);\n nextCatalog[profile].income_layer_max_ratio = incomeDefaults.maxRatio;\n nextCatalog[profile].income_layer_allocations = incomeDefaults.allocations;\n }\n const profileOptionDefaults = optionOverlayDefaultsFromProfileItem(item);\n const optionDefaults = profileOptionDefaults === false\n ? null\n : (profileOptionDefaults || fallbackOptionOverlayDefaults[profile] || null);\n if (optionDefaults) {\n nextOptionOverlayDefaults[profile] = optionDefaults;\n nextCatalog[profile].option_overlay_enabled = true;\n nextCatalog[profile].option_overlay_live_gate = optionDefaults.liveGate || \"\";\n nextCatalog[profile].option_overlay_live_status = optionDefaults.liveStatus || \"\";\n }\n }\n if (!nextOptions.length && profiles !== defaultStrategyProfiles) return applyStrategyProfiles(defaultStrategyProfiles);\n strategyOptions = nextOptions;\n strategyLabels = nextLabels;\n strategyCatalog = nextCatalog;\n incomeLayerDefaults = nextIncomeLayerDefaults;\n optionOverlayDefaults = nextOptionOverlayDefaults;\n }\n\n function incomeLayerDefaultsFromProfileItem(item) {\n const enabled = cleanOptionalBoolean(item?.income_layer_enabled);\n const hasConfig = enabled !== null ||\n item?.income_layer_start_usd !== undefined ||\n item?.income_layer_max_ratio !== undefined ||\n item?.income_layer_allocations !== undefined;\n if (!hasConfig) return null;\n if (enabled === false) return false;\n const startUsd = cleanDisplayNumber(item?.income_layer_start_usd);\n const maxRatio = cleanDisplayRatio(item?.income_layer_max_ratio);\n const allocations = cleanIncomeLayerAllocations(item?.income_layer_allocations);\n if (!startUsd || !maxRatio || !allocations) return null;\n return { startUsd, maxRatio, allocations };\n }\n\n function cleanIncomeLayerAllocations(value) {\n if (!value || Array.isArray(value) || typeof value !== \"object\") return null;\n const allocations = {};\n let total = 0;\n for (const [rawSymbol, rawRatio] of Object.entries(value)) {\n const symbol = String(rawSymbol || \"\").trim().toUpperCase();\n const ratio = cleanDisplayPositiveNumber(rawRatio);\n if (!/^[A-Z0-9.-]{1,12}$/.test(symbol) || !ratio) continue;\n allocations[symbol] = Number(ratio);\n total += Number(ratio);\n }\n return total > 0 && Object.keys(allocations).length ? allocations : null;\n }\n\n function optionOverlayDefaultsFromProfileItem(item) {\n const enabled = cleanOptionalBoolean(item?.option_overlay_enabled);\n const hasConfig = enabled !== null ||\n item?.option_growth_overlay_enabled !== undefined ||\n item?.option_income_overlay_enabled !== undefined ||\n item?.option_overlay_live_gate !== undefined ||\n item?.option_overlay_live_status !== undefined;\n if (!hasConfig) return null;\n if (enabled === false) return false;\n const families = [\n optionOverlayFamilyDefaultsFromProfileItem(item, \"growth\"),\n optionOverlayFamilyDefaultsFromProfileItem(item, \"income\"),\n ].filter(Boolean);\n if (!families.length) return null;\n return {\n liveGate: String(item?.option_overlay_live_gate || \"promotion_required\"),\n liveStatus: String(item?.option_overlay_live_status || \"research_only\"),\n families,\n };\n }\n\n function optionOverlayFamilyDefaultsFromProfileItem(item, family) {\n const prefix = `option_${family}_overlay`;\n const enabled = cleanOptionalBoolean(item?.[`${prefix}_enabled`]);\n if (enabled !== true) return null;\n const recipe = cleanStrategyProfile(item?.[`${prefix}_recipe`]);\n const startUsd = cleanDisplayNumber(item?.[`${prefix}_start_usd`]);\n const ratioField = family === \"growth\"\n ? \"option_growth_overlay_nav_budget_ratio\"\n : \"option_income_overlay_nav_risk_ratio\";\n const ratio = cleanDisplayRatio(item?.[ratioField]);\n if (!recipe || !startUsd || !ratio) return null;\n return {\n family,\n recipe,\n startUsd,\n ratio,\n ratioKind: family === \"growth\" ? \"budget\" : \"risk\",\n };\n }\n\n function supportedDomainsForAccount(platform, account) {\n if (Array.isArray(account?.supported_domains) && account.supported_domains.length) {\n const cleaned = account.supported_domains.map(cleanStrategyDomain).filter(Boolean);\n if (cleaned.length) return [...new Set(cleaned)];\n }\n return inferSupportedDomains(platform, account);\n }\n\n function inferSupportedDomains(platform, account) {\n void account;\n if (platform === \"qmt\") return [\"cn_equity\"];\n if (platform === \"longbridge\" || platform === \"ibkr\") return [\"us_equity\", \"hk_equity\"];\n return [\"us_equity\"];\n }\n\n function supportedDomainLabel(platform, account) {\n return supportedDomainsForAccount(platform, account).map(domainLabel).join(\" / \");\n }\n\n function platformSupportsDca(platform = state.selected) {\n return platformConfig[platform]?.dca ?? false;\n }\n\n function strategyCatalogEntry(profile) {\n return strategyCatalog[cleanStrategyProfile(profile)] || {};\n }\n\n function normalizeLifecycleStage(value) {\n const text = String(value || \"\").trim().toLowerCase();\n if (!text || text.length > 40 || !/^[a-z0-9._-]+$/.test(text)) return \"\";\n return text;\n }\n\n function normalizeAllowedExecutionModes(value) {\n const items = Array.isArray(value)\n ? value\n : String(value || \"\").split(/[,\\s/|]+/);\n const modes = [];\n for (const item of items) {\n const mode = String(item || \"\").trim().toLowerCase();\n if (!mode || mode.length > 32 || !/^[a-z0-9._-]+$/.test(mode)) continue;\n if (!modes.includes(mode)) modes.push(mode);\n }\n return modes;\n }\n\n function cleanDisplayText(value) {\n const text = String(value ?? \"\").trim();\n if (!text || text.length > 120 || /[<>]/.test(text)) return \"\";\n return text;\n }\n\n function strategyCanSwitchLive(entry) {\n if (!entry || typeof entry !== \"object\") return false;\n if (entry.runtime_enabled === false) return false;\n const allowedModes = normalizeAllowedExecutionModes(entry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(\"live\")) return false;\n if (cleanOptionalBoolean(entry.can_switch_live) === false) return false;\n const lifecycleStage = normalizeLifecycleStage(entry.lifecycle_stage);\n if (lifecycleStage && [\"research\", \"draft\", \"blocked\", \"archived\", \"disabled\"].includes(lifecycleStage)) return false;\n const blockedReason = cleanDisplayText(entry.blocked_live_reason);\n if (blockedReason) return false;\n const evidenceStatus = cleanDisplayText(entry.latest_evidence_status);\n if (evidenceStatus && [\"research_only\", \"blocked\", \"pending\"].includes(evidenceStatus.toLowerCase())) return false;\n const pluginGateStatus = cleanDisplayText(entry.plugin_gate_status);\n if (pluginGateStatus && [\"blocked\", \"locked\", \"disabled\"].includes(pluginGateStatus.toLowerCase())) return false;\n return true;\n }\n\n function strategyBlockedLiveReason(entry) {\n if (!entry || typeof entry !== \"object\") return \"\";\n const explicit = cleanDisplayText(entry.blocked_live_reason);\n if (explicit) return explicit;\n if (entry.runtime_enabled === false) return \"runtime_enabled=false\";\n const allowedModes = normalizeAllowedExecutionModes(entry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(\"live\")) {\n return `allowed_execution_modes=${allowedModes.join(\",\")}`;\n }\n if (cleanOptionalBoolean(entry.can_switch_live) === false) return \"can_switch_live=false\";\n const lifecycleStage = normalizeLifecycleStage(entry.lifecycle_stage);\n if (lifecycleStage && [\"research\", \"draft\", \"blocked\", \"archived\", \"disabled\"].includes(lifecycleStage)) {\n return `lifecycle_stage=${lifecycleStage}`;\n }\n const evidenceStatus = cleanDisplayText(entry.latest_evidence_status);\n if (evidenceStatus && evidenceStatus.toLowerCase() !== \"live_allowed\") {\n return `latest_evidence_status=${evidenceStatus}`;\n }\n const pluginGateStatus = cleanDisplayText(entry.plugin_gate_status);\n if (pluginGateStatus && pluginGateStatus.toLowerCase() !== \"live_allowed\") {\n return `plugin_gate_status=${pluginGateStatus}`;\n }\n return \"\";\n }\n\n function strategyDisplayMetaText(platform, account, profile) {\n const entry = strategyCatalogEntry(profile);\n if (!entry.profile) {\n return t(\"strategyMeta\").replace(\"{domains}\", supportedDomainLabel(platform, account));\n }\n const lines = [];\n if (entry.lifecycle_stage) lines.push(`lifecycle_stage: ${entry.lifecycle_stage}`);\n lines.push(`runtime_enabled: ${entry.runtime_enabled === false ? \"false\" : \"true\"}`);\n lines.push(`can_switch_live: ${strategyCanSwitchLive(entry) ? \"true\" : \"false\"}`);\n if (entry.allowed_execution_modes?.length) {\n lines.push(`allowed_execution_modes: ${entry.allowed_execution_modes.join(\", \")}`);\n }\n const blockedReason = strategyBlockedLiveReason(entry);\n if (blockedReason) lines.push(`blocked_live_reason: ${blockedReason}`);\n if (entry.latest_evidence_status) lines.push(`latest_evidence_status: ${entry.latest_evidence_status}`);\n if (entry.plugin_gate_status) lines.push(`plugin_gate_status: ${entry.plugin_gate_status}`);\n lines.push(`domains: ${supportedDomainLabel(platform, account)}`);\n return lines.join(\"\\n\");\n }\n\n function strategyActionNoteText(platform = state.selected, account = selectedAccount(platform)) {\n const profile = state.forms[platform]?.strategy || \"\";\n const meta = strategyDisplayMetaText(platform, account, profile);\n return meta ? `${t(\"invalidStrategyNote\")}\\n${meta}` : t(\"invalidStrategyNote\");\n }\n\n function strategyChoiceLabel(profile, platform = state.selected, account = selectedAccount(platform), executionMode = state.forms[platform]?.executionMode) {\n const label = strategyLabel(profile);\n const entry = strategyCatalogEntry(profile);\n if (!entry.profile) return label;\n if (normalizeExecutionMode(executionMode, false) === \"live\" && !strategyCanSwitchLive(entry)) {\n return `${label} [blocked live]`;\n }\n if (entry.runtime_enabled === false) {\n return `${label} [disabled]`;\n }\n return label;\n }\n\n function strategyAllowedForAccount(platform, account, profile, executionMode = state.forms[platform]?.executionMode) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalogEntry = strategyCatalogEntry(cleanProfile);\n if (!catalogEntry.profile) return false;\n if (dcaConfigForStrategy(cleanProfile) && !platformSupportsDca(platform)) return false;\n if (!supportedDomainsForAccount(platform, account).includes(catalogEntry.domain)) return false;\n const mode = normalizeExecutionMode(executionMode, false);\n if (mode === \"live\") return strategyCanSwitchLive(catalogEntry);\n const allowedModes = normalizeAllowedExecutionModes(catalogEntry.allowed_execution_modes);\n if (allowedModes.length && !allowedModes.includes(mode)) return false;\n if (catalogEntry.runtime_enabled !== true && !allowedModes.length) return false;\n return true;\n }\n\n function strategyChoicesForAccount(platform = state.selected, account = selectedAccount(platform), executionMode = state.forms[platform]?.executionMode) {\n const choices = strategyOptions.filter((profile) => strategyAllowedForAccount(platform, account, profile, executionMode));\n const addChoice = (value) => {\n const profile = cleanStrategyProfile(value);\n if (profile && !choices.includes(profile) && strategyAllowedForAccount(platform, account, profile, executionMode)) {\n choices.push(profile);\n }\n };\n return choices;\n }\n\n function strategyLabel(profile) {\n const labels = strategyLabels[profile] || localStrategyLabels[profile];\n if (!labels) return profile;\n return state.lang === \"zh\"\n ? (labels.zh || labels.en || profile)\n : (labels.en || labels.zh || profile);\n }\n\n function strategyLabelSet(profile, item) {\n const local = localStrategyLabels[profile] || {};\n const label = String(item?.label || item?.display_name || \"\").trim();\n const labelEn = String(item?.label_en || item?.display_name_en || \"\").trim();\n const labelZh = String(item?.label_zh || item?.display_name_zh || \"\").trim();\n return {\n zh: labelZh || local.zh || label || local.en || profile,\n en: labelEn || label || local.en || labelZh || local.zh || profile,\n };\n }\n\n function modeLabel(mode) {\n return mode === \"paper\" ? t(\"paper\") : t(\"live\");\n }\n\n function normalizePluginMode(value) {\n return pluginModes.includes(value) ? value : \"auto\";\n }\n\n function pluginModeLabel(mode) {\n return mode === \"none\" ? t(\"pluginModeNone\") : t(\"pluginModeAuto\");\n }\n\n function dcaConfigForStrategy(profile) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalog = strategyCatalog[cleanProfile] || {};\n if (catalog.dca_enabled === true) {\n return {\n defaultMode: normalizeDcaMode(catalog.dca_default_mode || \"fixed\"),\n defaultBaseInvestmentUsd: cleanDisplayPositiveNumber(catalog.dca_default_base_investment_usd) || \"1000\",\n };\n }\n return dcaProfileDefaults[cleanProfile] || null;\n }\n\n function dcaSupported(profile) {\n return Boolean(dcaConfigForStrategy(profile));\n }\n\n function normalizeDcaMode(value) {\n const mode = String(value || \"\").trim().toLowerCase();\n if (mode === \"smart_dca\") return \"smart\";\n if (mode === \"fixed_dca\" || mode === \"ordinary\" || mode === \"ordinary_dca\") return \"fixed\";\n return dcaModes.includes(mode) ? mode : \"fixed\";\n }\n\n function dcaModeLabel(mode) {\n return normalizeDcaMode(mode) === \"smart\" ? t(\"dcaModeSmart\") : t(\"dcaModeFixed\");\n }\n\n function normalizeRuntimeTargetMode(value) {\n return runtimeTargetModes.includes(value) ? value : \"current\";\n }\n\n function runtimeTargetModeLabel(mode) {\n if (mode === \"enabled\") return t(\"runtimeTargetEnabled\");\n if (mode === \"disabled\") return t(\"runtimeTargetDisabled\");\n return t(\"runtimeTargetCurrent\");\n }\n\n function runtimeTargetEnabledForAccount(platform, account) {\n return cleanOptionalBoolean(currentEntryForAccount(platform, account)?.runtime_target_enabled);\n }\n\n function runtimeTargetStateForAccount(platform = state.selected, account = selectedAccount(platform)) {\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return { known: false, enabled: null };\n const configured = cleanOptionalBoolean(entry.runtime_target_enabled);\n return { known: true, enabled: configured ?? true };\n }\n\n function runtimeTargetText(enabled) {\n return enabled ? t(\"runtimeTargetOn\") : t(\"runtimeTargetOff\");\n }\n\n function runtimeTargetTone(enabled) {\n return enabled ? \"enabled\" : \"disabled\";\n }\n\n function currentRuntimeTargetText(platform = state.selected, account = selectedAccount(platform)) {\n const target = runtimeTargetStateForAccount(platform, account);\n if (!target.known) return t(\"notRead\");\n return runtimeTargetText(target.enabled);\n }\n\n function currentRuntimeTargetTone(platform = state.selected, account = selectedAccount(platform)) {\n const target = runtimeTargetStateForAccount(platform, account);\n if (!target.known) return \"neutral\";\n return runtimeTargetTone(target.enabled);\n }\n\n function incomeLayerDefaultForStrategy(profile) {\n return incomeLayerDefaults[cleanStrategyProfile(profile)] || null;\n }\n\n function incomeLayerSupported(profile) {\n return Boolean(incomeLayerDefaultForStrategy(profile));\n }\n\n function normalizeIncomeLayerMode(value) {\n return incomeLayerModes.includes(value) ? value : \"current\";\n }\n\n function incomeLayerModeLabel(mode) {\n if (mode === \"enabled\") return t(\"incomeLayerEnabled\");\n if (mode === \"disabled\") return t(\"incomeLayerDisabled\");\n return t(\"incomeLayerCurrent\");\n }\n\n function optionOverlayDefaultForStrategy(profile) {\n return optionOverlayDefaults[cleanStrategyProfile(profile)] || null;\n }\n\n function optionOverlaySupported(profile) {\n return Boolean(optionOverlayDefaultForStrategy(profile));\n }\n\n function normalizeOptionOverlayMode(value) {\n return optionOverlayModes.includes(value) ? value : \"current\";\n }\n\n function optionOverlayModeLabel(mode) {\n if (mode === \"enabled\") return t(\"optionOverlayEnabled\");\n if (mode === \"disabled\") return t(\"optionOverlayDisabled\");\n return t(\"optionOverlayCurrent\");\n }\n\n function optionOverlayText(enabled) {\n return enabled ? t(\"optionOverlayOn\") : t(\"optionOverlayOff\");\n }\n\n function normalizeCashOnlyExecutionMode(value) {\n return cashOnlyExecutionModes.includes(value) ? value : \"current\";\n }\n\n function cashOnlyExecutionModeLabel(mode) {\n if (mode === \"enabled\") return t(\"cashOnlyExecutionNo\");\n if (mode === \"disabled\") return t(\"cashOnlyExecutionYes\");\n return t(\"cashOnlyExecutionCurrent\");\n }\n\n function cashOnlyExecutionText(enabled) {\n if (enabled === null) return t(\"notRead\");\n return enabled ? t(\"cashOnlyExecutionValueNo\") : t(\"cashOnlyExecutionValueYes\");\n }\n\n function platformCashOnlyExecutionDefault() {\n return true;\n }\n\n function effectiveCashOnlyExecutionForAccount(platform, account) {\n const configured = currentCashOnlyExecutionForAccount(platform, account);\n if (configured !== null) return configured;\n if (!platformSupportsMarginPolicy(platform)) return null;\n return platformCashOnlyExecutionDefault();\n }\n\n function currentCashOnlyExecutionForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n if (entry) {\n const val = cleanOptionalBoolean(entry.cash_only_execution);\n if (val !== null) return val;\n }\n return platformCashOnlyExecutionDefault();\n }\n\n function currentCashOnlyExecutionText(platform = state.selected, account = selectedAccount(platform)) {\n if (!platformSupportsMarginPolicy(platform)) return t(\"notRead\");\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const configured = cleanOptionalBoolean(entry.cash_only_execution);\n if (configured === null) return t(\"cashOnlyExecutionDefault\");\n return cashOnlyExecutionText(configured);\n }\n\n function currentOptionOverlayForAccount(platform, account) {\n return cleanOptionalBoolean(currentEntryForAccount(platform, account)?.option_overlay_enabled);\n }\n\n function effectiveOptionOverlayForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const configured = currentOptionOverlayForAccount(platform, account);\n if (configured !== null) return configured;\n if (!optionOverlaySupported(profile)) return null;\n return true;\n }\n\n function optionOverlayDefaultSummaryDetail(defaults) {\n if (!defaults?.families?.length) return \"\";\n return defaults.families.map((item) => {\n const family = item.family === \"income\" ? t(\"optionOverlayFamilyIncome\") : t(\"optionOverlayFamilyGrowth\");\n const ratioText = item.ratioKind === \"risk\"\n ? t(\"optionOverlayRiskRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio))\n : t(\"optionOverlayBudgetRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio));\n return `${family} ${ratioText}`;\n }).join(\" / \");\n }\n\n function optionOverlayDefaultText(profile) {\n const defaults = optionOverlayDefaultForStrategy(profile);\n if (!defaults) return t(\"optionOverlayNotSupported\");\n const detail = optionOverlayDefaultSummaryDetail(defaults);\n return detail ? t(\"optionOverlayDefault\").replace(\"{detail}\", detail) : t(\"optionOverlayDefaultSimple\");\n }\n\n function currentOptionOverlayText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const configured = cleanOptionalBoolean(entry.option_overlay_enabled);\n if (!optionOverlaySupported(profile)) {\n return configured === null ? t(\"optionOverlayNotSupported\") : optionOverlayText(configured);\n }\n if (configured === null) return optionOverlayDefaultText(profile);\n return optionOverlayText(configured);\n }\n\n function currentIncomeLayerForAccount(platform, account) {\n return incomeLayerFromEntry(currentEntryForAccount(platform, account));\n }\n\n function incomeLayerFromEntry(entry) {\n return {\n enabled: cleanOptionalBoolean(entry?.income_layer_enabled),\n startUsd: cleanDisplayNumber(entry?.income_layer_start_usd),\n maxRatio: cleanDisplayRatio(entry?.income_layer_max_ratio),\n };\n }\n\n function incomeLayerFieldsConfigured(entry) {\n const current = incomeLayerFromEntry(entry);\n return current.enabled !== null || Boolean(current.startUsd || current.maxRatio);\n }\n\n function effectiveIncomeLayerForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return null;\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return null;\n const current = incomeLayerFromEntry(entry);\n if (!incomeLayerFieldsConfigured(entry)) {\n return {\n enabled: true,\n startUsd: String(defaults.startUsd),\n maxRatio: defaults.maxRatio,\n };\n }\n return {\n enabled: current.enabled ?? true,\n startUsd: current.startUsd || String(defaults.startUsd),\n maxRatio: current.maxRatio || defaults.maxRatio,\n };\n }\n\n function currentDcaForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n const defaults = dcaConfigForStrategy(profile);\n if (!defaults) return { supported: false, mode: \"\", baseInvestmentUsd: \"\" };\n const entry = currentEntryForAccount(platform, account);\n return {\n supported: true,\n mode: normalizeDcaMode(entry?.dca_mode || account?.dca_mode || defaults.defaultMode),\n baseInvestmentUsd: cleanDisplayPositiveNumber(entry?.dca_base_investment_usd) ||\n cleanDisplayPositiveNumber(account?.dca_base_investment_usd) ||\n defaults.defaultBaseInvestmentUsd,\n };\n }\n\n function normalizeAccountLookupKey(value) {\n return String(value || \"\").trim().toLowerCase();\n }\n\n function collectAccountLookupCandidates(keys) {\n const candidates = new Set();\n for (const rawKey of keys) {\n const key = normalizeAccountLookupKey(rawKey);\n if (!key) continue;\n\n candidates.add(key);\n\n const compact = key.replace(/[^a-z0-9]+/g, \"\");\n if (compact) candidates.add(compact);\n\n const parts = key.split(/[^a-z0-9]+/).filter(Boolean);\n if (parts.length > 1) candidates.add(parts[parts.length - 1]);\n }\n return [...candidates];\n }\n\n function resolveCurrentEntryByKey(byPlatform, keys) {\n const candidates = new Set(collectAccountLookupCandidates(keys));\n if (!candidates.size) return null;\n\n for (const key of keys) {\n const entry = byPlatform[key];\n if (currentEntryHasState(entry)) return entry;\n }\n\n for (const [rawKey, entry] of Object.entries(byPlatform)) {\n if (!currentEntryHasState(entry)) continue;\n const rawCandidates = collectAccountLookupCandidates([rawKey]);\n const hasMatch = rawCandidates.some((candidate) => candidates.has(candidate));\n if (hasMatch) return entry;\n }\n\n return null;\n }\n\n function currentEntryForAccount(platform, account) {\n const byPlatform = state.currentStrategies[platform] || {};\n const keys = [account?.key, account?.target_name, account?.label]\n .filter(Boolean)\n .map((value) => String(value));\n const entry = resolveCurrentEntryByKey(byPlatform, keys);\n if (entry) return entry;\n const globalDefaults = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {};\n const merged = { ...globalDefaults, ...(account || {}) };\n const synth = {\n strategy_profile: \"\",\n source: \"account_defaults\",\n };\n const cashMode = merged.cash_only_execution_mode;\n if (cashMode === \"enabled\") synth.cash_only_execution = true;\n else if (cashMode === \"disabled\") synth.cash_only_execution = false;\n else if (platformSupportsMarginPolicy(platform)) synth.cash_only_execution = true;\n if (merged.min_reserved_cash_usd) synth.min_reserved_cash_usd = merged.min_reserved_cash_usd;\n if (merged.reserved_cash_ratio) synth.reserved_cash_ratio = merged.reserved_cash_ratio;\n synth.runtime_target_enabled = merged.runtime_target_enabled !== false;\n const execMode = merged.default_execution_mode || platformConfig[platform]?.default_execution_mode || \"live\";\n synth.execution_mode = execMode;\n synth.dry_run_only = execMode === \"paper\";\n return synth;\n }\n\n function currentEntryHasState(entry) {\n if (!entry || typeof entry !== \"object\" || Array.isArray(entry)) return false;\n return Boolean(\n cleanStrategyProfile(entry.strategy_profile) ||\n cleanDisplayNumber(entry.min_reserved_cash_usd ?? entry.reserved_cash_floor_usd) ||\n cleanDisplayRatio(entry.reserved_cash_ratio) ||\n cleanOptionalBoolean(entry.income_layer_enabled) !== null ||\n cleanDisplayNumber(entry.income_layer_start_usd) ||\n cleanDisplayRatio(entry.income_layer_max_ratio) ||\n cleanOptionalBoolean(entry.option_overlay_enabled) !== null ||\n cleanOptionalBoolean(entry.cash_only_execution) !== null ||\n cleanOptionalBoolean(entry.runtime_target_enabled) !== null ||\n normalizeDcaMode(entry.dca_mode || \"\") !== \"fixed\" ||\n cleanDisplayPositiveNumber(entry.dca_base_investment_usd) ||\n normalizeExecutionMode(entry.execution_mode, entry.dry_run_only),\n );\n }\n\n function currentStrategyForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n return cleanStrategyProfile(entry?.strategy_profile) || \"\";\n }\n\n function currentReservePolicyForAccount(platform, account) {\n const entry = currentEntryForAccount(platform, account);\n return reservePolicyFromEntry(entry);\n }\n\n function currentPluginModeForAccount(platform, account) {\n void platform;\n return normalizePluginMode(account?.plugin_mode);\n }\n\n function reservePolicyFromEntry(entry) {\n return {\n minReservedCashUsd: cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd),\n reservedCashRatio: cleanDisplayRatio(entry?.reserved_cash_ratio),\n };\n }\n\n function cleanDisplayNumber(value) {\n const text = String(value ?? \"\").trim();\n if (!text || text.length > 32 || !/^(?:\\d+|\\d*\\.\\d+)$/.test(text)) return \"\";\n const numeric = Number(text);\n if (!Number.isFinite(numeric) || numeric < 0) return \"\";\n return text;\n }\n\n function cleanDisplayRatio(value) {\n const text = cleanDisplayNumber(value);\n if (!text) return \"\";\n const numeric = Number(text);\n return numeric >= 0 && numeric <= 1 ? text : \"\";\n }\n\n function cleanDisplayPositiveNumber(value) {\n const text = cleanDisplayNumber(value);\n return text && Number(text) > 0 ? text : \"\";\n }\n\n function normalizeExecutionMode(value, dryRunOnly) {\n const mode = String(value || \"\").trim().toLowerCase();\n if (mode === \"live\" || mode === \"paper\") return mode;\n if (dryRunOnly === true || dryRunOnly === \"true\" || dryRunOnly === \"1\" || dryRunOnly === 1) return \"paper\";\n if (dryRunOnly === false || dryRunOnly === \"false\" || dryRunOnly === \"0\" || dryRunOnly === 0) return \"live\";\n return \"\";\n }\n\n function cleanOptionalBoolean(value) {\n if (value === true || value === 1) return true;\n if (value === false || value === 0) return false;\n if (typeof value === \"string\") {\n const text = value.trim().toLowerCase();\n if (text === \"true\" || text === \"1\") return true;\n if (text === \"false\" || text === \"0\") return false;\n }\n return null;\n }\n\n function defaultExecutionModeForAccount(platform, account, fallback = \"live\") {\n if (platformDryRunOnly(platform)) return \"paper\";\n const currentMode = normalizeExecutionMode(\n currentEntryForAccount(platform, account)?.execution_mode,\n currentEntryForAccount(platform, account)?.dry_run_only,\n );\n if (currentMode) return currentMode;\n const hint = [\n account?.key,\n account?.label,\n account?.target_name,\n account?.deployment_selector,\n account?.account_scope,\n account?.service_name,\n ].join(\" \").toLowerCase();\n if (hint.split(/\\s+/).includes(\"paper\") || hint.includes(\"-paper\") || hint.includes(\"_paper\") || hint.includes(\"dry_run\") || hint.includes(\"dry-run\")) {\n return \"paper\";\n }\n return fallback;\n }\n\n function defaultStrategyForAccount(platform, account) {\n const currentProfile = currentStrategyForAccount(platform, account);\n if (currentProfile) return currentProfile;\n return \"\";\n }\n\n function syncStrategyForAccount(platform) {\n const account = selectedAccount(platform);\n if (!account) return;\n state.forms[platform].strategy = defaultStrategyForAccount(platform, account);\n state.forms[platform].executionMode = defaultExecutionModeForAccount(\n platform,\n account,\n );\n state.forms[platform].pluginMode = currentPluginModeForAccount(platform, account);\n syncRuntimeTargetForAccount(platform);\n syncReservePolicyForAccount(platform);\n syncIncomeLayerForAccount(platform);\n syncOptionOverlayForAccount(platform);\n syncCashOnlyExecutionForAccount(platform);\n reconcileExecutionCashPolicy(state.forms[platform], \"margin\");\n syncDcaForAccount(platform);\n }\n\n function syncRuntimeTargetForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.runtimeTargetTouched) return;\n const current = runtimeTargetEnabledForAccount(platform, selectedAccount(platform));\n form.runtimeTargetMode = current === false ? \"disabled\" : \"enabled\";\n }\n\n function syncReservePolicyForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.reservedCashTouched) return;\n const policy = currentReservePolicyForAccount(platform, selectedAccount(platform));\n const hasFloor = Boolean(policy.minReservedCashUsd);\n const hasRatio = Boolean(policy.reservedCashRatio);\n if (hasFloor && hasRatio) form.reservePolicyMode = \"max\";\n else if (hasFloor) form.reservePolicyMode = \"floor\";\n else if (hasRatio) form.reservePolicyMode = \"ratio\";\n else form.reservePolicyMode = \"none\";\n form.minReservedCashUsd = policy.minReservedCashUsd;\n form.reservedCashRatio = policy.reservedCashRatio;\n }\n\n function syncIncomeLayerForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.incomeLayerTouched) return;\n const defaults = incomeLayerDefaultForStrategy(form.strategy);\n const current = currentIncomeLayerForAccount(platform, selectedAccount(platform));\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n if (entry && incomeLayerFieldsConfigured(entry)) {\n form.incomeLayerMode = current.enabled === false ? \"disabled\" : \"enabled\";\n } else {\n form.incomeLayerMode = incomeLayerSupported(form.strategy) ? \"enabled\" : \"disabled\";\n }\n form.incomeLayerStartUsd = current.startUsd || String(defaults?.startUsd || \"\");\n form.incomeLayerMaxRatio = current.maxRatio || defaults?.maxRatio || \"\";\n }\n\n function syncOptionOverlayForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.optionOverlayTouched) return;\n const configured = normalizeOptionOverlayMode(selectedAccount(platform)?.option_overlay_mode);\n if (configured !== \"current\") {\n form.optionOverlayMode = configured;\n return;\n }\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n const rawValue = cleanOptionalBoolean(entry?.option_overlay_enabled);\n if (rawValue !== null) {\n form.optionOverlayMode = rawValue ? \"enabled\" : \"disabled\";\n return;\n }\n form.optionOverlayMode = optionOverlaySupported(form.strategy) ? \"enabled\" : \"disabled\";\n }\n\n function syncCashOnlyExecutionForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.cashOnlyExecutionTouched) return;\n const configured = normalizeCashOnlyExecutionMode(selectedAccount(platform)?.cash_only_execution_mode);\n if (configured !== \"current\") {\n form.cashOnlyExecutionMode = configured;\n return;\n }\n const entry = currentEntryForAccount(platform, selectedAccount(platform));\n const rawValue = cleanOptionalBoolean(entry?.cash_only_execution);\n if (rawValue !== null) {\n form.cashOnlyExecutionMode = rawValue ? \"enabled\" : \"disabled\";\n return;\n }\n // No explicit config — use platform default (cash-only for margin-capable platforms)\n form.cashOnlyExecutionMode = platformSupportsMarginPolicy(platform) ? \"enabled\" : \"disabled\";\n }\n\n function syncDcaForAccount(platform) {\n const form = state.forms[platform];\n if (!form || form.dcaTouched) return;\n const current = currentDcaForAccount(platform, selectedAccount(platform), form.strategy);\n form.dcaMode = current.supported ? current.mode : \"fixed\";\n form.dcaBaseInvestmentUsd = current.supported ? current.baseInvestmentUsd : \"\";\n }\n\n function ensureAccountSelection(platform) {\n const options = optionsFor(platform);\n if (!options.length) return;\n if (!options.some((option) => option.key === state.forms[platform].accountKey)) {\n state.forms[platform].accountKey = options[0].key;\n state.forms[platform].runtimeTargetTouched = false;\n state.forms[platform].reservedCashTouched = false;\n state.forms[platform].incomeLayerTouched = false;\n state.forms[platform].optionOverlayTouched = false;\n state.forms[platform].cashOnlyExecutionTouched = false;\n state.forms[platform].dcaTouched = false;\n state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0]);\n state.forms[platform].pluginMode = currentPluginModeForAccount(platform, options[0]);\n syncRuntimeTargetForAccount(platform);\n syncReservePolicyForAccount(platform);\n syncIncomeLayerForAccount(platform);\n syncOptionOverlayForAccount(platform);\n syncCashOnlyExecutionForAccount(platform);\n syncDcaForAccount(platform);\n }\n }\n\n function derivedService(platform, targetName) {\n if (platform === \"longbridge\") return `longbridge-quant-${targetName.toLowerCase()}-service`;\n if (platform === \"ibkr\") return `interactive-brokers-${targetName.toLowerCase()}-service`;\n if (platform === \"schwab\") return \"charles-schwab-quant-service\";\n if (platform === \"firstrade\") return \"firstrade-quant-service\";\n if (platform === \"qmt\") return \"qmt-quant-service\";\n return \"\";\n }\n\n function accountMetaText(platform = state.selected) {\n const account = selectedAccount(platform);\n const targetName = account.target_name || account.key;\n const raw = account.service_name || derivedService(platform, targetName);\n const service = raw || (state.lang === \"zh\" ? \"无\" : \"-\");\n return t(\"targetMeta\")\n .replace(\"{target}\", targetName)\n .replace(\"{service}\", service)\n .replace(\"{domains}\", supportedDomainLabel(platform, account));\n }\n\n function hasRunnableStrategySelection(platform = state.selected) {\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n return Boolean(form?.strategy && account && strategyAllowedForAccount(platform, account, form.strategy, form.executionMode));\n }\n\n function hasValidReservePolicy(platform = state.selected) {\n if (!platformSupportsReservedCashPolicy(platform)) return true;\n const form = state.forms[platform];\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n if (mode === \"current\" || mode === \"none\") return true;\n return Boolean(reservePolicyOverrideForForm(form, platform));\n }\n\n function hasValidExecutionCashPolicy(platform = state.selected) {\n if (!platformSupportsMarginPolicy(platform) && !platformSupportsReservedCashPolicy(platform)) return true;\n const form = state.forms[platform];\n return !executionCashPolicyConflict(form) && hasValidReservePolicy(platform);\n }\n\n function hasValidIncomeLayerPolicy(platform = state.selected) {\n const form = state.forms[platform];\n if (!incomeLayerSupported(form?.strategy)) return true;\n const mode = normalizeIncomeLayerMode(form?.incomeLayerMode);\n if (mode === \"current\" || mode === \"disabled\") return true;\n const defaults = incomeLayerDefaultForStrategy(form?.strategy);\n const startUsd = cleanDisplayNumber(form?.incomeLayerStartUsd || defaults?.startUsd);\n const maxRatio = cleanDisplayRatio(form?.incomeLayerMaxRatio || defaults?.maxRatio);\n return Boolean(startUsd && maxRatio);\n }\n\n function hasValidOptionOverlayPolicy(platform = state.selected) {\n const form = state.forms[platform];\n const mode = normalizeOptionOverlayMode(form?.optionOverlayMode);\n return mode !== \"enabled\" || optionOverlaySupported(form?.strategy);\n }\n\n function hasValidDcaPolicy(platform = state.selected) {\n const form = state.forms[platform];\n if (!dcaSupported(form?.strategy) || !platformSupportsDca(platform)) return true;\n return Boolean(dcaModes.includes(normalizeDcaMode(form?.dcaMode)) && cleanDisplayPositiveNumber(form?.dcaBaseInvestmentUsd));\n }\n\n function hasValidStrategySelection(platform = state.selected) {\n return hasRunnableStrategySelection(platform) &&\n hasValidExecutionCashPolicy(platform) &&\n hasValidIncomeLayerPolicy(platform) &&\n hasValidOptionOverlayPolicy(platform) &&\n hasValidDcaPolicy(platform);\n }\n\n function normalizeReservePolicyMode(value) {\n return reservePolicyModes.includes(value) ? value : \"current\";\n }\n\n function reservePolicyOverrideForForm(form, platform = state.selected) {\n if (!platformSupportsReservedCashPolicy(platform)) return null;\n const mode = normalizeReservePolicyMode(form?.reservePolicyMode);\n const floor = cleanDisplayNumber(form?.minReservedCashUsd);\n const ratio = cleanDisplayRatio(form?.reservedCashRatio);\n const extraVariables = {};\n if (mode === \"current\") return null;\n if (mode === \"none\") {\n extraVariables[platformMinReservedCashVariables[platform]] = \"\";\n extraVariables[platformReservedCashRatioVariables[platform]] = \"\";\n return { inputs: {}, extraVariables };\n }\n if (mode === \"ratio\") {\n if (!ratio) return null;\n extraVariables[platformMinReservedCashVariables[platform]] = \"\";\n return { inputs: { reserved_cash_ratio: ratio }, extraVariables };\n }\n if (mode === \"floor\") {\n if (!floor) return null;\n extraVariables[platformReservedCashRatioVariables[platform]] = \"\";\n return { inputs: { min_reserved_cash_usd: floor }, extraVariables };\n }\n if (mode === \"max\") {\n if (!floor || !ratio) return null;\n return { inputs: { min_reserved_cash_usd: floor, reserved_cash_ratio: ratio }, extraVariables };\n }\n return null;\n }\n\n function runtimeTargetOverrideForForm(form) {\n const mode = normalizeRuntimeTargetMode(form?.runtimeTargetMode);\n if (mode === \"current\") return null;\n return {\n inputs: { runtime_target_enabled_mode: mode },\n extraVariables: { [runtimeTargetEnabledVariable]: mode === \"enabled\" ? \"true\" : \"false\" },\n };\n }\n\n function incomeLayerOverrideForForm(form) {\n const defaults = incomeLayerDefaultForStrategy(form?.strategy);\n if (!defaults) return null;\n const mode = normalizeIncomeLayerMode(form?.incomeLayerMode);\n if (mode === \"current\") return null;\n const extraVariables = {};\n if (mode === \"disabled\") {\n extraVariables[incomeLayerEnabledVariable] = \"false\";\n extraVariables[incomeLayerStartUsdVariable] = \"\";\n extraVariables[incomeLayerMaxRatioVariable] = \"\";\n return { inputs: { income_layer_mode: mode }, extraVariables };\n }\n const startUsd = cleanDisplayNumber(form?.incomeLayerStartUsd || defaults.startUsd);\n const maxRatio = cleanDisplayRatio(form?.incomeLayerMaxRatio || defaults.maxRatio);\n if (!startUsd || !maxRatio) return null;\n extraVariables[incomeLayerEnabledVariable] = \"true\";\n extraVariables[incomeLayerStartUsdVariable] = startUsd;\n extraVariables[incomeLayerMaxRatioVariable] = maxRatio;\n return { inputs: { income_layer_mode: mode, income_layer_start_usd: startUsd, income_layer_max_ratio: maxRatio }, extraVariables };\n }\n\n function optionOverlayOverrideForForm(form) {\n const mode = normalizeOptionOverlayMode(form?.optionOverlayMode);\n if (mode === \"current\") return null;\n if (mode === \"enabled\" && !optionOverlaySupported(form?.strategy)) return null;\n return { inputs: { option_overlay_mode: mode } };\n }\n\n function cashOnlyExecutionOverrideForForm(form, platform = state.selected) {\n if (!platformSupportsMarginPolicy(platform)) return null;\n const mode = normalizeCashOnlyExecutionMode(form?.cashOnlyExecutionMode);\n if (mode === \"current\") return null;\n return { inputs: { cash_only_execution_mode: mode } };\n }\n\n function dcaOverrideForForm(form) {\n if (!dcaSupported(form?.strategy) || !platformSupportsDca(state.selected)) return null;\n const mode = normalizeDcaMode(form?.dcaMode);\n const baseInvestmentUsd = cleanDisplayPositiveNumber(form?.dcaBaseInvestmentUsd);\n if (!baseInvestmentUsd) return null;\n return { inputs: { dca_mode: mode, dca_base_investment_usd: baseInvestmentUsd } };\n }\n\n function mergeExtraVariables(inputs, extraVariables) {\n if (!extraVariables || !Object.keys(extraVariables).length) return;\n const merged = inputs.extra_variables_json ? JSON.parse(inputs.extra_variables_json) : {};\n Object.assign(merged, extraVariables);\n inputs.extra_variables_json = JSON.stringify(merged);\n }\n\n function buildInputs(platform = state.selected) {\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n const targetName = account.target_name || account.key;\n const inputs = {\n platform,\n target_name: targetName,\n strategy_profile: form.strategy,\n execution_mode: form.executionMode,\n variable_scope: account.variable_scope || \"default\",\n plugin_mode: normalizePluginMode(form.pluginMode),\n service_targets_mode: \"auto\",\n apply: \"true\",\n trigger_platform_sync: \"true\",\n confirm_apply: \"APPLY_AND_SYNC\",\n platform_sync_workflow: \"sync-cloud-run-env.yml\",\n };\n for (const field of [\n \"github_environment\",\n \"deployment_selector\",\n \"account_selector\",\n \"account_scope\",\n \"service_name\",\n ]) {\n if (account[field]) inputs[field] = account[field];\n }\n const reserveOverride = platformSupportsReservedCashPolicy(platform)\n ? reservePolicyOverrideForForm(form, platform)\n : null;\n if (platformSupportsReservedCashPolicy(platform)) {\n inputs.reserved_cash_policy_mode = normalizeReservePolicyMode(form.reservePolicyMode);\n if (reserveOverride) {\n Object.assign(inputs, reserveOverride.inputs);\n mergeExtraVariables(inputs, reserveOverride.extraVariables);\n }\n }\n const runtimeTargetOverride = runtimeTargetOverrideForForm(form);\n inputs.runtime_target_enabled_mode = normalizeRuntimeTargetMode(form.runtimeTargetMode);\n if (runtimeTargetOverride) {\n Object.assign(inputs, runtimeTargetOverride.inputs);\n mergeExtraVariables(inputs, runtimeTargetOverride.extraVariables);\n }\n const incomeOverride = incomeLayerOverrideForForm(form);\n inputs.income_layer_mode = normalizeIncomeLayerMode(form.incomeLayerMode);\n if (incomeOverride) {\n Object.assign(inputs, incomeOverride.inputs);\n mergeExtraVariables(inputs, incomeOverride.extraVariables);\n }\n const optionOverlayOverride = optionOverlayOverrideForForm(form);\n inputs.option_overlay_mode = normalizeOptionOverlayMode(form.optionOverlayMode);\n if (optionOverlayOverride) {\n Object.assign(inputs, optionOverlayOverride.inputs);\n }\n const cashOnlyOverride = cashOnlyExecutionOverrideForForm(form, platform);\n if (platformSupportsMarginPolicy(platform)) {\n inputs.cash_only_execution_mode = normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode);\n if (cashOnlyOverride) {\n Object.assign(inputs, cashOnlyOverride.inputs);\n }\n }\n const dcaOverride = dcaOverrideForForm(form);\n if (dcaOverride) {\n Object.assign(inputs, dcaOverride.inputs);\n }\n return inputs;\n }\n\n function reservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform), fallback = t(\"unchanged\")) {\n if (inputs?.reserved_cash_policy_mode === \"none\") return t(\"reservedCashNone\");\n const floor = cleanDisplayNumber(inputs?.min_reserved_cash_usd);\n const ratio = cleanDisplayRatio(inputs?.reserved_cash_ratio);\n const currency = selectedCashCurrency(platform, account);\n const hasEffectiveFloor = Boolean(floor && !(ratio && Number(floor) === 0));\n if (!hasEffectiveFloor && !ratio) return fallback;\n if (hasEffectiveFloor && ratio) return `max(${floor} ${currency}, ${formatRatioPercent(ratio)})`;\n if (hasEffectiveFloor) return `${floor} ${currency}`;\n return formatRatioPercent(ratio);\n }\n\n function platformReservedCashDefaultText(platform = state.selected, account = selectedAccount(platform)) {\n return t(\"reservedCashDefault\").replace(\"{currency}\", selectedCashCurrency(platform, account));\n }\n\n function currentReservedCashPolicyText(platform = state.selected, account = selectedAccount(platform)) {\n const entry = currentEntryForAccount(platform, account);\n const policy = currentReservePolicyForAccount(platform, account);\n return reservedCashPolicyText(\n {\n min_reserved_cash_usd: policy.minReservedCashUsd,\n reserved_cash_ratio: policy.reservedCashRatio,\n },\n platform,\n account,\n entry ? platformReservedCashDefaultText(platform, account) : t(\"notRead\"),\n );\n }\n\n function pendingReservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n return reservedCashPolicyText(pendingReservePolicy(inputs, platform, account).inputs, platform, account, t(\"unchanged\"));\n }\n\n function pendingReservePolicy(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const current = currentReservePolicyForAccount(platform, account);\n const currentFloor = cleanDisplayNumber(current.minReservedCashUsd);\n const currentRatio = cleanDisplayRatio(current.reservedCashRatio);\n const mode = normalizeReservePolicyMode(inputs.reserved_cash_policy_mode);\n const next = {\n min_reserved_cash_usd: cleanDisplayNumber(inputs.min_reserved_cash_usd),\n reserved_cash_ratio: cleanDisplayRatio(inputs.reserved_cash_ratio),\n };\n if (mode === \"none\") {\n next.reserved_cash_policy_mode = \"none\";\n }\n const entry = currentEntryForAccount(platform, account);\n const changed = Boolean(entry && (\n next.min_reserved_cash_usd !== currentFloor ||\n next.reserved_cash_ratio !== currentRatio ||\n (mode === \"none\" && (currentFloor || currentRatio))\n ));\n return { changed, inputs: next };\n }\n\n function currentIncomeLayerText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return t(\"incomeLayerNotSupported\");\n const entry = currentEntryForAccount(platform, account);\n if (!entry) return t(\"notRead\");\n const current = incomeLayerFromEntry(entry);\n if (!incomeLayerFieldsConfigured(entry)) {\n return t(\"incomeLayerDefault\")\n .replace(\"{start}\", formatUsd(defaults.startUsd))\n .replace(\"{ratio}\", formatRatioPercent(defaults.maxRatio));\n }\n const enabled = current.enabled ?? true;\n const startUsd = current.startUsd || String(defaults.startUsd);\n const ratio = current.maxRatio || defaults.maxRatio;\n return enabled\n ? t(\"incomeLayerOn\")\n .replace(\"{start}\", formatUsd(startUsd))\n .replace(\"{ratio}\", formatRatioPercent(ratio))\n : t(\"incomeLayerOff\");\n }\n\n function pendingIncomeLayerText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingIncomeLayer(inputs, platform, account);\n if (!pending.supported) return t(\"incomeLayerNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n if (pending.inputs.income_layer_enabled === false) return t(\"incomeLayerOff\");\n return t(\"incomeLayerOn\")\n .replace(\"{start}\", formatUsd(pending.inputs.income_layer_start_usd))\n .replace(\"{ratio}\", formatRatioPercent(pending.inputs.income_layer_max_ratio));\n }\n\n function pendingOptionOverlayText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingOptionOverlay(inputs, platform, account);\n if (!pending.supported && pending.inputs.option_overlay_enabled !== false) return t(\"optionOverlayNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n return optionOverlayText(pending.inputs.option_overlay_enabled);\n }\n\n function pendingCashOnlyExecutionText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingCashOnlyExecution(inputs, platform, account);\n if (!pending.changed) return t(\"unchanged\");\n return cashOnlyExecutionText(pending.inputs.cash_only_execution);\n }\n\n function pendingRuntimeTargetText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingRuntimeTarget(inputs, platform, account);\n if (!pending.changed) return t(\"unchanged\");\n return runtimeTargetText(pending.inputs.runtime_target_enabled);\n }\n\n function pendingRuntimeTargetTone(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingRuntimeTarget(inputs, platform, account);\n if (!pending.changed) return \"neutral\";\n return runtimeTargetTone(pending.inputs.runtime_target_enabled);\n }\n\n function currentDcaText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const current = currentDcaForAccount(platform, account, profile);\n if (!current.supported) return t(\"dcaNotSupported\");\n return t(\"dcaText\")\n .replace(\"{mode}\", dcaModeLabel(current.mode))\n .replace(\"{amount}\", formatUsd(current.baseInvestmentUsd));\n }\n\n function pendingDcaText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingDca(inputs, platform, account);\n if (!pending.supported) return t(\"dcaNotSupported\");\n if (!pending.changed) return t(\"unchanged\");\n return t(\"dcaText\")\n .replace(\"{mode}\", dcaModeLabel(pending.inputs.dca_mode))\n .replace(\"{amount}\", formatUsd(pending.inputs.dca_base_investment_usd));\n }\n\n function pendingRuntimeTarget(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const mode = normalizeRuntimeTargetMode(inputs.runtime_target_enabled_mode);\n if (mode === \"current\") {\n return {\n changed: false,\n inputs: { runtime_target_enabled: runtimeTargetEnabledForAccount(platform, account) ?? true },\n };\n }\n const current = runtimeTargetEnabledForAccount(platform, account);\n const currentEnabled = current ?? true;\n const nextEnabled = mode === \"enabled\";\n const entry = currentEntryForAccount(platform, account);\n return {\n changed: Boolean(entry && current !== null && currentEnabled !== nextEnabled),\n inputs: { runtime_target_enabled: nextEnabled },\n };\n }\n\n function pendingIncomeLayer(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const defaults = incomeLayerDefaultForStrategy(profile);\n if (!defaults) return { supported: false, changed: false, inputs: {} };\n const mode = normalizeIncomeLayerMode(inputs.income_layer_mode);\n const entry = currentEntryForAccount(platform, account);\n const rawCurrent = currentIncomeLayerForAccount(platform, account);\n const effective = effectiveIncomeLayerForAccount(platform, account, profile);\n const currentEnabled = effective?.enabled ?? true;\n const currentStartUsd = effective?.startUsd ?? String(defaults.startUsd);\n const currentRatio = effective?.maxRatio ?? defaults.maxRatio;\n if (mode === \"current\") {\n return {\n supported: true,\n changed: false,\n inputs: {\n income_layer_enabled: rawCurrent.enabled,\n income_layer_start_usd: rawCurrent.startUsd,\n income_layer_max_ratio: rawCurrent.maxRatio,\n },\n };\n }\n if (mode === \"disabled\") {\n if (!entry) {\n return {\n supported: true,\n changed: false,\n inputs: {\n income_layer_enabled: false,\n income_layer_start_usd: \"\",\n income_layer_max_ratio: \"\",\n },\n };\n }\n return {\n supported: true,\n changed: currentEnabled !== false || Boolean(rawCurrent.startUsd || rawCurrent.maxRatio),\n inputs: {\n income_layer_enabled: false,\n income_layer_start_usd: \"\",\n income_layer_max_ratio: \"\",\n },\n };\n }\n const nextStartUsd = cleanDisplayNumber(inputs.income_layer_start_usd || defaults.startUsd);\n const nextRatio = cleanDisplayRatio(inputs.income_layer_max_ratio || defaults.maxRatio);\n return {\n supported: true,\n changed: Boolean(entry && (currentEnabled !== true || nextStartUsd !== currentStartUsd || nextRatio !== currentRatio)),\n inputs: {\n income_layer_enabled: true,\n income_layer_start_usd: nextStartUsd,\n income_layer_max_ratio: nextRatio,\n },\n };\n }\n\n function pendingOptionOverlay(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const supported = optionOverlaySupported(profile);\n const mode = normalizeOptionOverlayMode(inputs.option_overlay_mode);\n const current = effectiveOptionOverlayForAccount(platform, account, profile);\n if (mode === \"current\") {\n return {\n supported,\n changed: false,\n inputs: { option_overlay_enabled: currentOptionOverlayForAccount(platform, account) },\n };\n }\n if (mode === \"enabled\") {\n return {\n supported,\n changed: supported && current !== null && current !== true,\n inputs: { option_overlay_enabled: true },\n };\n }\n return {\n supported,\n changed: current === true,\n inputs: { option_overlay_enabled: false },\n };\n }\n\n function pendingCashOnlyExecution(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const mode = normalizeCashOnlyExecutionMode(inputs.cash_only_execution_mode);\n if (!platformSupportsMarginPolicy(platform) || mode === \"current\") {\n return { changed: false, inputs: {} };\n }\n const current = effectiveCashOnlyExecutionForAccount(platform, account);\n const nextEnabled = mode === \"enabled\";\n const entry = currentEntryForAccount(platform, account);\n return {\n changed: Boolean(entry && current !== null && current !== nextEnabled),\n inputs: { cash_only_execution: nextEnabled },\n };\n }\n\n function pendingDca(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n const defaults = dcaConfigForStrategy(profile);\n if (!defaults) return { supported: false, changed: false, inputs: {} };\n const current = currentDcaForAccount(platform, account, profile);\n const nextMode = normalizeDcaMode(inputs.dca_mode || defaults.defaultMode);\n const nextBase = cleanDisplayPositiveNumber(inputs.dca_base_investment_usd || defaults.defaultBaseInvestmentUsd);\n return {\n supported: true,\n changed: Boolean(current.mode !== nextMode || current.baseInvestmentUsd !== nextBase),\n inputs: {\n dca_mode: nextMode,\n dca_base_investment_usd: nextBase,\n },\n };\n }\n\n function pendingChangeState(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const currentProfile = currentStrategyForAccount(platform, account);\n const nextProfile = cleanStrategyProfile(inputs.strategy_profile);\n const currentEntry = currentEntryForAccount(platform, account);\n const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);\n const currentPluginMode = currentPluginModeForAccount(platform, account);\n const nextPluginMode = normalizePluginMode(inputs.plugin_mode);\n const runtimeTarget = pendingRuntimeTarget(inputs, platform, account);\n const reserve = pendingReservePolicy(inputs, platform, account);\n const income = pendingIncomeLayer(inputs, platform, account);\n const optionOverlay = pendingOptionOverlay(inputs, platform, account);\n const cashOnly = pendingCashOnlyExecution(inputs, platform, account);\n const dca = pendingDca(inputs, platform, account);\n return {\n currentProfile,\n nextProfile,\n currentMode,\n currentPluginMode,\n nextPluginMode,\n strategyChanged: Boolean(nextProfile && ((state.forms[platform]?.strategyTouched) || (currentProfile && currentProfile !== nextProfile))),\n modeChanged: Boolean(inputs.execution_mode && currentMode && currentMode !== inputs.execution_mode),\n pluginModeChanged: Boolean(nextPluginMode && currentPluginMode && currentPluginMode !== nextPluginMode),\n runtimeTargetChanged: runtimeTarget.changed,\n reserveCashChanged: reserve.changed,\n incomeLayerChanged: income.changed,\n optionOverlayChanged: optionOverlay.changed,\n cashOnlyChanged: cashOnly.changed,\n dcaChanged: dca.changed,\n runtimeTarget,\n reserve,\n income,\n optionOverlay,\n cashOnly,\n dca,\n };\n }\n\n function hasPendingChanges(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const changes = pendingChangeState(inputs, platform, account);\n return Boolean(\n changes.strategyChanged ||\n changes.modeChanged ||\n changes.pluginModeChanged ||\n changes.runtimeTargetChanged ||\n changes.reserveCashChanged ||\n changes.incomeLayerChanged ||\n changes.optionOverlayChanged ||\n changes.cashOnlyChanged ||\n changes.dcaChanged\n );\n }\n\n function formatRatioPercent(value) {\n const numeric = Number(value);\n if (!Number.isFinite(numeric)) return String(value);\n return `${(numeric * 100).toFixed(2).replace(/\\.?0+$/, \"\")}%`;\n }\n\n function formatUsd(value) {\n const numeric = Number(value);\n if (!Number.isFinite(numeric)) return String(value);\n return `$${numeric.toLocaleString(\"en-US\", { maximumFractionDigits: 0 })}`;\n }\n\n function incomeLayerAllocationText(defaults) {\n if (!defaults?.allocations) return \"\";\n return Object.entries(defaults.allocations)\n .map(([symbol, ratio]) => `${symbol} ${formatRatioPercent(ratio)}`)\n .join(\" / \");\n }\n\n function incomeLayerDefaultMetaText(defaults) {\n if (!defaults) return t(\"incomeLayerModeMeta\");\n return t(\"incomeLayerDefaultMeta\")\n .replace(\"{start}\", formatUsd(defaults.startUsd))\n .replace(\"{ratio}\", formatRatioPercent(defaults.maxRatio));\n }\n\n function optionOverlayDefaultMetaText(defaults) {\n if (!defaults?.families?.length) return t(\"optionOverlayModeMeta\");\n const familyText = defaults.families.map((item) => {\n const family = item.family === \"income\" ? t(\"optionOverlayFamilyIncome\") : t(\"optionOverlayFamilyGrowth\");\n const ratioText = item.ratioKind === \"risk\"\n ? t(\"optionOverlayRiskRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio))\n : t(\"optionOverlayBudgetRatio\").replace(\"{ratio}\", formatRatioPercent(item.ratio));\n return `${family}: ${item.recipe}, ${formatUsd(item.startUsd)}, ${ratioText}`;\n }).join(\" / \");\n return t(\"optionOverlayDefaultMeta\").replace(\"{defaults}\", familyText);\n }\n\n function summaryRows(inputs) {\n const account = selectedAccount();\n const changes = pendingChangeState(inputs, state.selected, account);\n const currentStrategyText = changes.currentProfile ? strategyLabel(changes.currentProfile) : t(\"notRead\");\n const rows = [\n [t(\"repository\"), state.repositories[state.selected] || defaultRepositories[state.selected]],\n [t(\"selectedAccount\"), account.label],\n [t(\"currentStrategy\"), currentStrategyText],\n [t(\"selectedMarket\"), supportedDomainLabel(state.selected, account)],\n [\n t(\"currentRuntimeTarget\"),\n currentRuntimeTargetText(state.selected, account),\n \"\",\n currentRuntimeTargetTone(state.selected, account),\n ],\n [t(\"currentPluginMode\"), pluginModeLabel(changes.currentPluginMode)],\n [t(\"reservedCashPolicy\"), currentReservedCashPolicyText(state.selected, account)],\n ];\n if (platformSupportsMarginPolicy(state.selected)) {\n rows.push([t(\"currentCashOnlyExecution\"), currentCashOnlyExecutionText(state.selected, account)]);\n }\n if (incomeLayerSupported(inputs.strategy_profile)) {\n rows.push([t(\"currentIncomeLayer\"), currentIncomeLayerText(state.selected, account, inputs.strategy_profile)]);\n }\n if (optionOverlaySupported(inputs.strategy_profile) || changes.optionOverlayChanged) {\n rows.push([t(\"currentOptionOverlay\"), currentOptionOverlayText(state.selected, account, inputs.strategy_profile)]);\n }\n if (dcaSupported(inputs.strategy_profile)) {\n rows.push([t(\"currentDca\"), currentDcaText(state.selected, account, inputs.strategy_profile)]);\n }\n if (changes.reserveCashChanged) {\n rows.push([t(\"pendingReservedCashPolicy\"), pendingReservedCashPolicyText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.incomeLayerChanged) {\n rows.push([t(\"pendingIncomeLayer\"), pendingIncomeLayerText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.optionOverlayChanged) {\n rows.push([t(\"pendingOptionOverlay\"), pendingOptionOverlayText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.cashOnlyChanged) {\n rows.push([t(\"pendingCashOnlyExecution\"), pendingCashOnlyExecutionText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.dcaChanged) {\n rows.push([t(\"pendingDca\"), pendingDcaText(inputs, state.selected, account), \"pending\"]);\n }\n if (changes.modeChanged) {\n rows.push([t(\"pendingMode\"), modeLabel(inputs.execution_mode), \"pending\"]);\n }\n if (changes.pluginModeChanged) {\n rows.push([t(\"pendingPluginMode\"), pluginModeLabel(changes.nextPluginMode), \"pending\"]);\n }\n if (changes.runtimeTargetChanged) {\n rows.push([\n t(\"pendingRuntimeTarget\"),\n pendingRuntimeTargetText(inputs, state.selected, account),\n \"pending\",\n pendingRuntimeTargetTone(inputs, state.selected, account),\n ]);\n }\n if (changes.strategyChanged && changes.nextProfile) {\n rows.push([t(\"nextStrategy\"), strategyLabel(changes.nextProfile), \"pending\"]);\n }\n return rows;\n }\n\n function applyLanguage() {\n document.documentElement.lang = state.lang === \"zh\" ? \"zh-CN\" : \"en\";\n document.querySelectorAll(\"[data-i18n]\").forEach((node) => {\n node.textContent = t(node.dataset.i18n);\n });\n el(\"lang-button\").textContent = state.lang === \"zh\" ? \"EN\" : \"中\";\n }\n\n function renderPlatforms() {\n const strip = el(\"platform-strip\");\n strip.replaceChildren();\n const showPrivateConfig = hasPrivateConfig();\n for (const platform of Object.keys(platformMeta)) {\n ensureAccountSelection(platform);\n const meta = platformMeta[platform];\n const form = state.forms[platform];\n const account = selectedAccount(platform);\n const button = document.createElement(\"button\");\n button.className = \"platform-button\";\n button.type = \"button\";\n button.dataset.platform = platform;\n button.classList.toggle(\"active\", platform === state.selected);\n const mark = document.createElement(\"span\");\n mark.className = \"mark\";\n mark.textContent = meta.code;\n const copyNode = document.createElement(\"span\");\n copyNode.className = \"platform-copy\";\n const labelNode = document.createElement(\"strong\");\n labelNode.textContent = meta.label;\n copyNode.append(labelNode);\n if (showPrivateConfig) {\n const accountNode = document.createElement(\"span\");\n accountNode.textContent = account.label;\n const strategyNode = document.createElement(\"small\");\n strategyNode.textContent = strategyLabel(form.strategy);\n copyNode.append(accountNode, strategyNode);\n }\n button.append(mark, copyNode);\n strip.appendChild(button);\n }\n }\n\n function renderControls() {\n const platform = state.selected;\n const meta = platformMeta[platform];\n const form = state.forms[platform];\n const accounts = optionsFor(platform);\n const account = selectedAccount(platform);\n const choices = strategyChoicesForAccount(platform, account, form.executionMode);\n const currentStrategy = currentStrategyForAccount(platform, account);\n const currentStrategyBlocked = Boolean(\n currentStrategy &&\n !strategyAllowedForAccount(platform, account, currentStrategy, form.executionMode),\n );\n const accountSelect = el(\"account-select\");\n const strategySelect = el(\"strategy-select\");\n const runtimeTargetEnabledSelect = el(\"runtime-target-enabled-select\");\n const pluginModeSelect = el(\"plugin-mode-select\");\n const incomeLayerModeSelect = el(\"income-layer-mode-select\");\n const incomeLayerStartUsdInput = el(\"income-layer-start-usd-input\");\n const incomeLayerMaxRatioInput = el(\"income-layer-max-ratio-input\");\n const optionOverlayModeSelect = el(\"option-overlay-mode-select\");\n const cashOnlyExecutionModeSelect = el(\"cash-only-execution-mode-select\");\n const dcaModeSelect = el(\"dca-mode-select\");\n const dcaBaseInvestmentUsdInput = el(\"dca-base-investment-usd-input\");\n const reservePolicyModeSelect = el(\"reserve-policy-mode-select\");\n const minReservedCashInput = el(\"min-reserved-cash-input\");\n const reservedCashRatioInput = el(\"reserved-cash-ratio-input\");\n const showPrivateControls = hasPrivateConfig();\n\n el(\"switch-panel\").style.setProperty(\"--platform-color\", meta.accent);\n el(\"platform-title\").textContent = meta.label;\n el(\"quick-form\").hidden = !showPrivateControls;\n el(\"run-area\").hidden = !showPrivateControls;\n el(\"public-note\").hidden = showPrivateControls;\n el(\"public-preview\").hidden = showPrivateControls;\n el(\"public-note\").textContent = state.auth.allowed ? t(\"missingConfigNote\") : t(\"publicReadonly\");\n\n if (!showPrivateControls) {\n accountSelect.replaceChildren();\n strategySelect.replaceChildren();\n runtimeTargetEnabledSelect.replaceChildren();\n pluginModeSelect.replaceChildren();\n incomeLayerModeSelect.replaceChildren();\n optionOverlayModeSelect.replaceChildren();\n cashOnlyExecutionModeSelect.replaceChildren();\n dcaModeSelect.replaceChildren();\n reservePolicyModeSelect.replaceChildren();\n incomeLayerStartUsdInput.value = \"\";\n incomeLayerMaxRatioInput.value = \"\";\n dcaBaseInvestmentUsdInput.value = \"\";\n minReservedCashInput.value = \"\";\n reservedCashRatioInput.value = \"\";\n el(\"account-meta\").textContent = \"\";\n el(\"strategy-meta\").textContent = \"\";\n el(\"income-layer-mode-meta\").textContent = \"\";\n el(\"income-layer-start-meta\").textContent = \"\";\n el(\"income-layer-ratio-meta\").textContent = \"\";\n el(\"option-overlay-mode-meta\").textContent = \"\";\n el(\"cash-only-execution-mode-meta\").textContent = \"\";\n el(\"dca-mode-meta\").textContent = \"\";\n el(\"dca-base-meta\").textContent = \"\";\n return;\n }\n\n accountSelect.replaceChildren();\n if (accounts.length) {\n for (const account of accounts) {\n accountSelect.append(new Option(account.label, account.key, false, account.key === form.accountKey));\n }\n } else {\n accountSelect.append(new Option(t(\"noAccount\"), \"\"));\n }\n el(\"account-meta\").textContent = accounts.length ? accountMetaText(platform) : \"\";\n\n if (choices.length && !choices.includes(form.strategy) && !currentStrategyBlocked) {\n form.strategy = choices[0];\n }\n strategySelect.disabled = !choices.length;\n strategySelect.replaceChildren();\n if (currentStrategyBlocked) {\n const blockedOption = new Option(\n strategyChoiceLabel(currentStrategy, platform, account, form.executionMode),\n currentStrategy,\n true,\n currentStrategy === form.strategy,\n );\n blockedOption.disabled = true;\n strategySelect.append(blockedOption);\n }\n if (choices.length) {\n for (const strategy of choices) {\n strategySelect.append(\n new Option(strategyChoiceLabel(strategy, platform, account, form.executionMode), strategy, false, strategy === form.strategy),\n );\n }\n } else {\n strategySelect.append(new Option(t(\"noStrategy\"), \"\"));\n }\n el(\"strategy-meta\").textContent = account\n ? strategyDisplayMetaText(platform, account, form.strategy)\n : \"\";\n runtimeTargetEnabledSelect.replaceChildren();\n for (const mode of runtimeTargetModes) {\n runtimeTargetEnabledSelect.append(\n new Option(runtimeTargetModeLabel(mode), mode, false, mode === normalizeRuntimeTargetMode(form.runtimeTargetMode)),\n );\n }\n pluginModeSelect.replaceChildren();\n for (const mode of pluginModes) {\n pluginModeSelect.append(new Option(pluginModeLabel(mode), mode, false, mode === normalizePluginMode(form.pluginMode)));\n }\n const incomeDefaults = incomeLayerDefaultForStrategy(form.strategy);\n el(\"income-layer-section\").hidden = false;\n el(\"option-overlay-section\").hidden = false;\n incomeLayerModeSelect.replaceChildren();\n if (incomeDefaults) {\n incomeLayerModeSelect.disabled = false;\n for (const mode of incomeLayerModes) {\n incomeLayerModeSelect.append(new Option(incomeLayerModeLabel(mode), mode, false, mode === normalizeIncomeLayerMode(form.incomeLayerMode)));\n }\n el(\"income-layer-mode-meta\").textContent = incomeLayerDefaultMetaText(incomeDefaults);\n el(\"income-layer-start-meta\").textContent = t(\"incomeLayerStartMeta\");\n el(\"income-layer-ratio-meta\").textContent = t(\"incomeLayerAllocationMeta\").replace(\n \"{allocations}\",\n incomeLayerAllocationText(incomeDefaults),\n );\n } else {\n incomeLayerModeSelect.disabled = true;\n incomeLayerModeSelect.append(new Option(t(\"incomeLayerNotSupported\"), \"current\"));\n el(\"income-layer-mode-meta\").textContent = t(\"incomeLayerModeMeta\");\n el(\"income-layer-start-meta\").textContent = t(\"incomeLayerStartMeta\");\n el(\"income-layer-ratio-meta\").textContent = t(\"incomeLayerRatioMeta\");\n }\n const supportsMargin = platformSupportsMarginPolicy(platform);\n const supportsReserve = platformSupportsReservedCashPolicy(platform);\n if (supportsMargin) syncCashOnlyExecutionForAccount(platform);\n reconcileExecutionCashPolicy(form, \"margin\");\n const executionCashPolicyGrid = el(\"execution-cash-policy-grid\");\n const qmtPlatformCashNote = el(\"qmt-platform-cash-note\");\n const executionCashPolicyNote = el(\"execution-cash-policy-note\");\n executionCashPolicyGrid.hidden = !supportsMargin && !supportsReserve;\n qmtPlatformCashNote.hidden = supportsMargin || supportsReserve || platform !== \"qmt\";\n executionCashPolicyNote.hidden = !supportsMargin || !supportsReserve;\n\n const marginBlocksReserve = supportsMargin && supportsReserve && allowMarginExplicitlySelected(form);\n const reserveBlocksMargin = supportsMargin && supportsReserve && reserveCashOverrideActive(form);\n\n if (supportsReserve) {\n reservePolicyModeSelect.replaceChildren();\n for (const mode of reservePolicyModes) {\n reservePolicyModeSelect.append(new Option(t(`reservePolicy${mode[0].toUpperCase()}${mode.slice(1)}`), mode, false, mode === normalizeReservePolicyMode(form.reservePolicyMode)));\n }\n const reserveMode = normalizeReservePolicyMode(form.reservePolicyMode);\n el(\"min-reserved-cash-label\").textContent = t(\"minReservedCash\").replace(\n \"{currency}\",\n selectedCashCurrency(platform, account),\n );\n reservePolicyModeSelect.disabled = false;\n minReservedCashInput.disabled = reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"ratio\";\n reservedCashRatioInput.disabled = reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"floor\";\n minReservedCashInput.value = reserveMode === \"ratio\" || reserveMode === \"none\" ? \"\" : form.minReservedCashUsd;\n reservedCashRatioInput.value = reserveMode === \"floor\" || reserveMode === \"none\" ? \"\" : form.reservedCashRatio;\n el(\"reserve-policy-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"min-reserve-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"reserve-ratio-block\").classList.toggle(\"policy-block-muted\", marginBlocksReserve);\n el(\"reserve-policy-mode-meta\").textContent = marginBlocksReserve\n ? t(\"executionCashMarginBlocksReserve\")\n : t(\"reservedCashModeMeta\");\n } else {\n reservePolicyModeSelect.replaceChildren();\n minReservedCashInput.value = \"\";\n reservedCashRatioInput.value = \"\";\n }\n const incomeMode = normalizeIncomeLayerMode(form.incomeLayerMode);\n const incomeLayerInputsDisabled = !incomeDefaults || incomeMode === \"disabled\";\n incomeLayerStartUsdInput.disabled = incomeLayerInputsDisabled;\n incomeLayerMaxRatioInput.disabled = incomeLayerInputsDisabled;\n if (incomeDefaults && incomeMode !== \"disabled\" && !cleanDisplayNumber(form.incomeLayerStartUsd)) {\n form.incomeLayerStartUsd = String(incomeDefaults.startUsd);\n }\n if (incomeDefaults && incomeMode !== \"disabled\" && !cleanDisplayRatio(form.incomeLayerMaxRatio)) {\n form.incomeLayerMaxRatio = incomeDefaults.maxRatio;\n }\n incomeLayerStartUsdInput.value = incomeDefaults && incomeMode !== \"disabled\" ? form.incomeLayerStartUsd : \"\";\n incomeLayerMaxRatioInput.value = incomeDefaults && incomeMode !== \"disabled\" ? form.incomeLayerMaxRatio : \"\";\n\n const optionDefaults = optionOverlayDefaultForStrategy(form.strategy);\n optionOverlayModeSelect.replaceChildren();\n if (optionDefaults) {\n optionOverlayModeSelect.disabled = false;\n for (const mode of optionOverlayModes) {\n optionOverlayModeSelect.append(\n new Option(optionOverlayModeLabel(mode), mode, false, mode === normalizeOptionOverlayMode(form.optionOverlayMode)),\n );\n }\n el(\"option-overlay-mode-meta\").textContent = optionOverlayDefaultMetaText(optionDefaults);\n } else {\n optionOverlayModeSelect.disabled = true;\n optionOverlayModeSelect.append(new Option(t(\"optionOverlayNotSupported\"), \"current\"));\n el(\"option-overlay-mode-meta\").textContent = t(\"optionOverlayModeMeta\");\n }\n\n if (supportsMargin) {\n cashOnlyExecutionModeSelect.replaceChildren();\n for (const mode of cashOnlyExecutionModes) {\n const option = new Option(\n mode === \"enabled\" ? t(\"cashOnlyExecutionNo\") : t(\"cashOnlyExecutionYes\"),\n mode,\n false,\n mode === normalizeCashOnlyExecutionMode(form.cashOnlyExecutionMode),\n );\n cashOnlyExecutionModeSelect.append(option);\n }\n el(\"cash-only-policy-block\").classList.toggle(\"policy-block-muted\", reserveBlocksMargin);\n el(\"cash-only-execution-mode-meta\").textContent = reserveBlocksMargin\n ? t(\"executionCashReserveBlocksMargin\")\n : t(\"cashOnlyExecutionModeMeta\");\n } else {\n cashOnlyExecutionModeSelect.replaceChildren();\n el(\"cash-only-execution-mode-meta\").textContent = \"\";\n }\n\n const dcaDefaults = dcaConfigForStrategy(form.strategy);\n dcaModeSelect.replaceChildren();\n const dcaAllowed = Boolean(dcaDefaults) && platformSupportsDca(platform);\n if (dcaAllowed) {\n dcaModeSelect.disabled = false;\n for (const mode of dcaModes) {\n dcaModeSelect.append(new Option(dcaModeLabel(mode), mode, false, mode === normalizeDcaMode(form.dcaMode)));\n }\n if (!cleanDisplayPositiveNumber(form.dcaBaseInvestmentUsd)) {\n form.dcaBaseInvestmentUsd = dcaDefaults.defaultBaseInvestmentUsd;\n }\n dcaBaseInvestmentUsdInput.disabled = false;\n dcaBaseInvestmentUsdInput.value = form.dcaBaseInvestmentUsd;\n el(\"dca-mode-meta\").textContent = t(\"dcaDefaultMeta\")\n .replace(\"{mode}\", dcaModeLabel(dcaDefaults.defaultMode))\n .replace(\"{amount}\", formatUsd(dcaDefaults.defaultBaseInvestmentUsd));\n el(\"dca-base-meta\").textContent = t(\"dcaModeMeta\");\n } else {\n dcaModeSelect.disabled = true;\n dcaModeSelect.append(new Option(\n dcaDefaults && !platformSupportsDca(platform) ? t(\"dcaPlatformNotSupported\") : t(\"dcaNotSupported\"),\n \"fixed\",\n ));\n dcaBaseInvestmentUsdInput.disabled = true;\n dcaBaseInvestmentUsdInput.value = \"\";\n el(\"dca-mode-meta\").textContent = t(\"dcaModeMeta\");\n el(\"dca-base-meta\").textContent = t(\"dcaModeMeta\");\n }\n\n if (platformDryRunOnly(platform)) {\n form.executionMode = \"paper\";\n }\n document.querySelectorAll(\"#mode-control [data-mode]\").forEach((button) => {\n const dryRunOnly = platformDryRunOnly(platform);\n button.disabled = dryRunOnly && button.dataset.mode === \"live\";\n button.classList.toggle(\"active\", button.dataset.mode === form.executionMode);\n });\n el(\"mode-meta\").textContent = platformDryRunOnly(platform) ? t(\"qmtDryRunOnlyNote\") : \"\";\n }\n\n function renderSummary() {\n const showSummary = hasPrivateConfig();\n const summaryPanel = document.querySelector(\".summary-panel\");\n const switchSurface = document.querySelector(\".switch-surface\");\n summaryPanel.hidden = !showSummary;\n switchSurface.classList.toggle(\"summary-hidden\", !showSummary);\n if (!showSummary) return;\n\n const inputs = buildInputs();\n const list = el(\"summary-list\");\n list.replaceChildren();\n document.querySelector(\".summary-head h2\").textContent = t(\"summary\");\n for (const [label, value, rowClass, valueTone] of summaryRows(inputs)) {\n const row = document.createElement(\"div\");\n row.className = \"summary-row\";\n row.setAttribute(\"role\", \"listitem\");\n if (rowClass) row.classList.add(rowClass);\n const labelNode = document.createElement(\"div\");\n labelNode.className = \"summary-label\";\n labelNode.textContent = label;\n const valueNode = document.createElement(\"div\");\n valueNode.className = \"summary-value\";\n if (valueTone) {\n const badge = document.createElement(\"span\");\n badge.className = `summary-status ${valueTone}`;\n badge.textContent = value;\n valueNode.appendChild(badge);\n } else {\n valueNode.textContent = value;\n }\n row.append(labelNode, valueNode);\n list.appendChild(row);\n }\n\n const account = selectedAccount();\n const currentEntry = currentEntryForAccount(state.selected, account);\n const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);\n el(\"mode-pill\").textContent = currentMode ? modeLabel(currentMode) : t(\"notRead\");\n }\n\n function renderAuth() {\n const status = el(\"auth-status\");\n const loginLink = el(\"login-link\");\n const logoutButton = el(\"logout-button\");\n const signedIn = Boolean(state.auth.allowed && state.auth.login);\n\n status.hidden = !signedIn;\n status.textContent = signedIn ? t(\"signedInAs\").replace(\"{login}\", state.auth.login) : \"\";\n loginLink.hidden = signedIn;\n loginLink.href = \"/login\";\n loginLink.textContent = t(\"login\");\n logoutButton.hidden = !signedIn;\n logoutButton.textContent = t(\"logout\");\n\n const dispatch = el(\"dispatch-button\");\n const hasPrivateAccounts = state.configSource === \"private\";\n const loadingConfig = state.configSource === \"loading\";\n const hasRunnableStrategy = hasRunnableStrategySelection();\n const hasValidReserve = hasValidExecutionCashPolicy();\n const hasValidIncomeLayer = hasValidIncomeLayerPolicy();\n const hasValidOptionOverlay = hasValidOptionOverlayPolicy();\n const hasValidDca = hasValidDcaPolicy();\n const hasValidStrategy = hasRunnableStrategy &&\n hasValidReserve &&\n hasValidIncomeLayer &&\n hasValidOptionOverlay &&\n hasValidDca;\n const hasPendingChange = hasPrivateAccounts && hasValidStrategy && hasPendingChanges(buildInputs());\n dispatch.disabled = !state.auth.allowed || loadingConfig || !hasPrivateAccounts || !hasValidStrategy || !hasPendingChange;\n dispatch.textContent = state.auth.allowed\n ? (loadingConfig\n ? t(\"loadingConfig\")\n : (hasPrivateAccounts ? (hasValidStrategy ? (hasPendingChange ? t(\"runSwitch\") : t(\"noChanges\")) : t(\"configureAccounts\")) : t(\"configureAccounts\")))\n : t(\"loginToRun\");\n const note = el(\"action-note\");\n note.textContent = state.auth.allowed\n ? (loadingConfig\n ? t(\"loadingConfigNote\")\n : (hasPrivateAccounts\n ? (hasRunnableStrategy\n ? (hasValidReserve\n ? (hasValidIncomeLayer\n ? (hasValidOptionOverlay\n ? (hasValidDca ? (hasPendingChange ? t(\"readyNote\") : \"\") : t(\"invalidDcaNote\"))\n : t(\"invalidOptionOverlayNote\"))\n : t(\"invalidIncomeLayerNote\"))\n : (executionCashPolicyConflict(state.forms[state.selected])\n ? t(\"invalidExecutionCashPolicyNote\")\n : t(\"invalidReservePolicyNote\")))\n : strategyActionNoteText())\n : t(\"missingConfigNote\")))\n : t(\"readonlyNote\");\n note.classList.toggle(\n \"warning\",\n state.auth.allowed && !loadingConfig && (!hasPrivateAccounts || !hasValidStrategy),\n );\n }\n\n function renderAppVisibility() {\n document.body.classList.toggle(\"app-loading\", !state.appReady);\n el(\"boot-message\").textContent = t(state.bootMessageKey);\n }\n\n function render() {\n applyLanguage();\n renderPlatforms();\n renderControls();\n renderSummary();\n renderAuth();\n renderAppVisibility();\n }\n\n async function refreshSession() {\n state.bootMessageKey = \"bootSession\";\n render();\n try {\n const session = await requestJson(\"/api/session\");\n state.auth = {\n available: true,\n allowed: Boolean(session.allowed),\n admin: Boolean(session.admin),\n login: session.login || null,\n };\n } catch {\n state.auth = { available: false, allowed: false, admin: false, login: null };\n }\n if (state.auth.allowed) {\n await refreshConfig();\n } else {\n state.bootMessageKey = \"bootPublic\";\n state.appReady = true;\n render();\n }\n }\n\n async function refreshStrategyProfiles() {\n state.bootMessageKey = \"bootStrategy\";\n render();\n try {\n const payload = await requestJson(\"/api/strategy-profiles\");\n applyStrategyProfiles(payload.strategyProfiles || []);\n if (payload.platformMeta) platformMeta = payload.platformMeta;\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n render();\n } catch {\n applyStrategyProfiles(defaultStrategyProfiles);\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n }\n }\n\n async function refreshConfig() {\n if (!state.auth.available || !state.auth.allowed) return;\n state.configSource = \"loading\";\n state.bootMessageKey = \"bootConfig\";\n render();\n try {\n const payload = await requestJson(\"/api/config\");\n if (payload.accountOptions) {\n applyStrategyProfiles(payload.strategyProfiles || defaultStrategyProfiles);\n state.accountOptions = normalizeAccountOptions(payload.accountOptions);\n if (payload.platformMeta) platformMeta = payload.platformMeta;\n state.repositories = normalizePlatformRepositories(payload.platformRepositories || {});\n state.currentStrategies = normalizeCurrentStrategies(payload.currentStrategies || {});\n state.configSource = \"private\";\n for (const platform of Object.keys(platformMeta)) {\n ensureAccountSelection(platform);\n syncStrategyForAccount(platform);\n }\n } else {\n state.configSource = \"default\";\n state.currentStrategies = {};\n }\n } catch (error) {\n state.configSource = \"default\";\n state.currentStrategies = {};\n if (isRequestTimeoutError(error)) {\n state.bootMessageKey = \"bootTimeout\";\n } else {\n state.bootMessageKey = \"bootPublic\";\n }\n } finally {\n state.appReady = true;\n render();\n }\n }\n\n function normalizeAccountOptions(raw) {\n const normalized = clone(defaultAccountOptions);\n for (const platform of Object.keys(platformMeta)) {\n if (!Array.isArray(raw[platform]) || !raw[platform].length) continue;\n normalized[platform] = raw[platform].map((item, index) => ({\n key: String(item.key || item.target_name || index),\n label: String(item.label || item.target_name || item.key || platform),\n target_name: String(item.target_name || item.key || \"\"),\n account_selector: item.account_selector ? String(item.account_selector) : \"\",\n deployment_selector: item.deployment_selector ? String(item.deployment_selector) : \"\",\n account_scope: item.account_scope ? String(item.account_scope) : \"\",\n service_name: item.service_name ? String(item.service_name) : \"\",\n cash_currency: item.cash_currency || item.market_currency || item.trading_currency\n ? String(item.cash_currency || item.market_currency || item.trading_currency).trim().toUpperCase()\n : \"\",\n supported_domains: normalizeSupportedDomains(platform, item),\n github_environment: item.github_environment ? String(item.github_environment) : \"\",\n variable_scope: item.variable_scope ? String(item.variable_scope) : \"\",\n plugin_mode: item.plugin_mode ? String(item.plugin_mode) : \"\",\n option_overlay_mode: item.option_overlay_mode ? normalizeOptionOverlayMode(item.option_overlay_mode) : \"\",\n cash_only_execution_mode: item.cash_only_execution_mode\n ? normalizeCashOnlyExecutionMode(item.cash_only_execution_mode)\n : \"\",\n dca_mode: item.dca_mode ? normalizeDcaMode(item.dca_mode) : \"\",\n dca_base_investment_usd: cleanDisplayPositiveNumber(item.dca_base_investment_usd),\n }));\n }\n return normalized;\n }\n\n function normalizeSupportedDomains(platform, item) {\n const raw = Array.isArray(item?.supported_domains)\n ? item.supported_domains\n : String(item?.supported_domains || \"\").split(/[\\s,;]+/);\n const cleaned = raw.map(cleanStrategyDomain).filter(Boolean);\n if (cleaned.length) return [...new Set(cleaned)];\n return inferSupportedDomains(platform, item || {});\n }\n\n function normalizeCurrentStrategies(raw) {\n const normalized = {};\n for (const platform of Object.keys(platformMeta)) {\n if (!raw[platform] || typeof raw[platform] !== \"object\" || Array.isArray(raw[platform])) continue;\n normalized[platform] = {};\n for (const [key, entry] of Object.entries(raw[platform])) {\n const profile = cleanStrategyProfile(entry?.strategy_profile);\n const minReservedCashUsd = cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd);\n const reservedCashRatio = cleanDisplayRatio(entry?.reserved_cash_ratio);\n const incomeLayerEnabled = cleanOptionalBoolean(entry?.income_layer_enabled);\n const incomeLayerStartUsd = cleanDisplayNumber(entry?.income_layer_start_usd);\n const incomeLayerMaxRatio = cleanDisplayRatio(entry?.income_layer_max_ratio);\n const optionOverlayEnabled = cleanOptionalBoolean(entry?.option_overlay_enabled);\n const cashOnlyExecution = cleanOptionalBoolean(entry?.cash_only_execution);\n const runtimeTargetEnabled = cleanOptionalBoolean(entry?.runtime_target_enabled);\n const dcaMode = entry?.dca_mode ? normalizeDcaMode(entry.dca_mode) : \"\";\n const dcaBaseInvestmentUsd = cleanDisplayPositiveNumber(entry?.dca_base_investment_usd);\n const executionMode = normalizeExecutionMode(entry?.execution_mode, entry?.dry_run_only);\n if (\n !profile &&\n !minReservedCashUsd &&\n !reservedCashRatio &&\n incomeLayerEnabled === null &&\n !incomeLayerStartUsd &&\n !incomeLayerMaxRatio &&\n optionOverlayEnabled === null &&\n cashOnlyExecution === null &&\n runtimeTargetEnabled === null &&\n !dcaMode &&\n !dcaBaseInvestmentUsd &&\n !executionMode\n ) continue;\n normalized[platform][String(key)] = {\n strategy_profile: profile,\n execution_mode: executionMode,\n dry_run_only: entry?.dry_run_only === true || entry?.dry_run_only === \"true\" || entry?.dry_run_only === \"1\",\n min_reserved_cash_usd: minReservedCashUsd,\n reserved_cash_ratio: reservedCashRatio,\n income_layer_enabled: incomeLayerEnabled,\n income_layer_start_usd: incomeLayerStartUsd,\n income_layer_max_ratio: incomeLayerMaxRatio,\n option_overlay_enabled: optionOverlayEnabled,\n cash_only_execution: cashOnlyExecution,\n runtime_target_enabled: runtimeTargetEnabled,\n dca_mode: dcaMode,\n dca_base_investment_usd: dcaBaseInvestmentUsd,\n source: entry?.source ? String(entry.source) : \"\",\n };\n }\n if (!Object.keys(normalized[platform]).length) delete normalized[platform];\n }\n return normalized;\n }\n\n function normalizePlatformRepositories(raw) {\n const normalized = clone(defaultRepositories);\n if (!raw || typeof raw !== \"object\" || Array.isArray(raw)) return normalized;\n for (const platform of Object.keys(platformMeta)) {\n const repository = String(raw[platform] || \"\").trim();\n if (/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$/.test(repository)) {\n normalized[platform] = repository;\n }\n }\n return normalized;\n }\n\n async function dispatchSwitch() {\n if (!state.auth.allowed) return;\n showToast(t(\"dispatching\"), { duration: 0 });\n try {\n const response = await fetch(\"/api/switch\", {\n method: \"POST\",\n headers: { \"Content-Type\": \"application/json\" },\n body: JSON.stringify(buildInputs()),\n });\n const payload = await response.json();\n if (!response.ok || !payload.ok) throw new Error(payload.error || t(\"dispatchFailed\"));\n showToast(t(\"dispatched\"), { duration: 4000 });\n if (payload.actions_url) window.open(payload.actions_url, \"_blank\", \"noopener,noreferrer\");\n await refreshConfig();\n } catch (error) {\n showToast(`${t(\"dispatchFailed\")}: ${error.message}`, { duration: 12000 });\n }\n }\n\n async function handleLogout() {\n await fetch(\"/api/logout\", { method: \"POST\" });\n window.location.reload();\n }\n\n function summaryText() {\n const inputs = buildInputs();\n return summaryRows(inputs).map(([label, value]) => `${label}: ${value}`).join(\"\\\n\");\n }\n\n el(\"platform-strip\").addEventListener(\"click\", (event) => {\n const button = event.target.closest(\"[data-platform]\");\n if (!button) return;\n state.selected = button.dataset.platform;\n state.forms[state.selected].strategyTouched = false;\n render();\n });\n\n el(\"account-select\").addEventListener(\"change\", () => {\n state.forms[state.selected].accountKey = el(\"account-select\").value;\n state.forms[state.selected].runtimeTargetTouched = false;\n state.forms[state.selected].reservedCashTouched = false;\n state.forms[state.selected].incomeLayerTouched = false;\n state.forms[state.selected].optionOverlayTouched = false;\n state.forms[state.selected].cashOnlyExecutionTouched = false;\n state.forms[state.selected].dcaTouched = false;\n state.forms[state.selected].strategyTouched = false;\n syncStrategyForAccount(state.selected);\n render();\n });\n\n el(\"strategy-select\").addEventListener(\"change\", () => {\n state.forms[state.selected].strategy = el(\"strategy-select\").value;\n state.forms[state.selected].strategyTouched = true;\n state.forms[state.selected].incomeLayerTouched = false;\n state.forms[state.selected].optionOverlayTouched = false;\n state.forms[state.selected].dcaTouched = false;\n syncIncomeLayerForAccount(state.selected);\n syncOptionOverlayForAccount(state.selected);\n syncDcaForAccount(state.selected);\n render();\n });\n\n el(\"mode-control\").addEventListener(\"click\", (event) => {\n const button = event.target.closest(\"[data-mode]\");\n if (!button || button.disabled) return;\n if (platformDryRunOnly(state.selected) && button.dataset.mode === \"live\") return;\n state.forms[state.selected].executionMode = button.dataset.mode;\n render();\n });\n\n el(\"plugin-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.pluginMode = normalizePluginMode(el(\"plugin-mode-select\").value);\n render();\n });\n\n el(\"runtime-target-enabled-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.runtimeTargetMode = normalizeRuntimeTargetMode(el(\"runtime-target-enabled-select\").value);\n form.runtimeTargetTouched = form.runtimeTargetMode !== \"current\";\n render();\n });\n\n el(\"income-layer-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerMode = normalizeIncomeLayerMode(el(\"income-layer-mode-select\").value);\n form.incomeLayerTouched = form.incomeLayerMode !== \"current\";\n if (form.incomeLayerMode === \"current\") {\n form.incomeLayerTouched = false;\n syncIncomeLayerForAccount(state.selected);\n }\n render();\n });\n\n el(\"income-layer-start-usd-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerTouched = true;\n form.incomeLayerMode = \"enabled\";\n form.incomeLayerStartUsd = el(\"income-layer-start-usd-input\").value.trim();\n render();\n });\n\n el(\"income-layer-max-ratio-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.incomeLayerTouched = true;\n form.incomeLayerMode = \"enabled\";\n form.incomeLayerMaxRatio = el(\"income-layer-max-ratio-input\").value.trim();\n render();\n });\n\n el(\"option-overlay-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.optionOverlayMode = normalizeOptionOverlayMode(el(\"option-overlay-mode-select\").value);\n form.optionOverlayTouched = form.optionOverlayMode !== \"current\";\n render();\n });\n\n el(\"cash-only-execution-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.cashOnlyExecutionMode = normalizeCashOnlyExecutionMode(el(\"cash-only-execution-mode-select\").value);\n form.cashOnlyExecutionTouched = form.cashOnlyExecutionMode !== \"current\";\n if (allowMarginExplicitlySelected(form)) reconcileExecutionCashPolicy(form, \"margin\");\n else restoreReserveAfterMarginDisabled(form);\n render();\n });\n\n el(\"dca-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.dcaTouched = true;\n form.dcaMode = normalizeDcaMode(el(\"dca-mode-select\").value);\n render();\n });\n\n el(\"dca-base-investment-usd-input\").addEventListener(\"input\", () => {\n const form = state.forms[state.selected];\n form.dcaTouched = true;\n form.dcaBaseInvestmentUsd = el(\"dca-base-investment-usd-input\").value.trim();\n render();\n });\n\n el(\"reserve-policy-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.reservePolicyMode = normalizeReservePolicyMode(el(\"reserve-policy-mode-select\").value);\n form.reservedCashTouched = form.reservePolicyMode !== \"current\";\n if (form.reservePolicyMode === \"current\") syncReservePolicyForAccount(state.selected);\n reconcileExecutionCashPolicy(form, \"reserve\");\n render();\n });\n\n el(\"min-reserved-cash-input\").addEventListener(\"input\", () => {\n state.forms[state.selected].reservedCashTouched = true;\n state.forms[state.selected].minReservedCashUsd = el(\"min-reserved-cash-input\").value.trim();\n render();\n });\n\n el(\"reserved-cash-ratio-input\").addEventListener(\"input\", () => {\n state.forms[state.selected].reservedCashTouched = true;\n state.forms[state.selected].reservedCashRatio = el(\"reserved-cash-ratio-input\").value.trim();\n render();\n });\n\n el(\"copy-button\").addEventListener(\"click\", async () => {\n try {\n await navigator.clipboard.writeText(summaryText());\n showToast(t(\"copied\"), { duration: 3000 });\n } catch {\n showToast(summaryText(), { duration: 0 });\n }\n });\n\n el(\"dispatch-button\").addEventListener(\"click\", dispatchSwitch);\n el(\"logout-button\").addEventListener(\"click\", handleLogout);\n el(\"lang-button\").addEventListener(\"click\", () => {\n state.lang = state.lang === \"zh\" ? \"en\" : \"zh\";\n localStorage.setItem(\"qsl-switch-lang\", state.lang);\n render();\n });\n\n applyStrategyProfiles(defaultStrategyProfiles);\n for (const platform of Object.keys(platformMeta)) syncStrategyForAccount(platform);\n render();\n boot();\n\n async function boot() {\n try {\n await refreshStrategyProfiles();\n await refreshSession();\n } catch {\n state.auth = { available: false, allowed: false, admin: false, login: null };\n state.configSource = \"default\";\n state.currentStrategies = {};\n state.bootMessageKey = \"bootTimeout\";\n state.appReady = true;\n render();\n }\n }\n"; diff --git a/web/strategy-switch-console/config.js b/web/strategy-switch-console/config.js index 0efe2cc..34c4784 100644 --- a/web/strategy-switch-console/config.js +++ b/web/strategy-switch-console/config.js @@ -81,7 +81,6 @@ export const DEFAULT_ACCOUNT_OPTIONS = { "hk_equity" ], "cash_currency": "USD", - "default_strategy_profile": "soxl_soxx_trend_income", "default_execution_mode": "live", "service_name": "" } @@ -96,7 +95,6 @@ export const DEFAULT_ACCOUNT_OPTIONS = { "hk_equity" ], "cash_currency": "USD", - "default_strategy_profile": "soxl_soxx_trend_income", "default_execution_mode": "live", "service_name": "" } @@ -110,7 +108,6 @@ export const DEFAULT_ACCOUNT_OPTIONS = { "us_equity" ], "cash_currency": "USD", - "default_strategy_profile": "soxl_soxx_trend_income", "default_execution_mode": "live", "service_name": "charles-schwab-quant-service" } @@ -124,7 +121,6 @@ export const DEFAULT_ACCOUNT_OPTIONS = { "us_equity" ], "cash_currency": "USD", - "default_strategy_profile": "ibit_smart_dca", "default_execution_mode": "live", "service_name": "firstrade-quant-service" } @@ -138,7 +134,6 @@ export const DEFAULT_ACCOUNT_OPTIONS = { "cn_equity" ], "cash_currency": "CNY", - "default_strategy_profile": "cn_industry_etf_rotation", "service_name": "qmt-quant-service", "default_execution_mode": "paper" } @@ -152,7 +147,6 @@ export const DEFAULT_ACCOUNT_OPTIONS = { "crypto" ], "cash_currency": "USD", - "default_strategy_profile": "crypto_live_pool_rotation", "service_name": "", "default_execution_mode": "live" } diff --git a/web/strategy-switch-console/index.html b/web/strategy-switch-console/index.html index 74477c5..905f5b9 100644 --- a/web/strategy-switch-console/index.html +++ b/web/strategy-switch-console/index.html @@ -11,7 +11,7 @@ \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"; +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
\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"; diff --git a/web/strategy-switch-console/worker.js b/web/strategy-switch-console/worker.js index 5abe894..8f6ffa6 100644 --- a/web/strategy-switch-console/worker.js +++ b/web/strategy-switch-console/worker.js @@ -1125,10 +1125,6 @@ function updateAccountOptionsDefaultStrategy(accountOptions, inputs) { const nextOption = { ...option }; let optionChanged = false; if ("ibit_zscore_exit_mode" in rawOption) optionChanged = true; - if (nextOption.default_strategy_profile !== inputs.strategy_profile) { - nextOption.default_strategy_profile = inputs.strategy_profile; - optionChanged = true; - } if (inputs.plugin_mode === "auto" || inputs.plugin_mode === "none") { const currentPluginMode = nextOption.plugin_mode || "auto"; if (currentPluginMode !== inputs.plugin_mode) { @@ -1665,7 +1661,6 @@ function cleanAccountOption(item, platform, index) { item.cash_currency || item.market_currency || item.trading_currency, cleanCashCurrency, ); - addConfigOptional(option, "default_strategy_profile", item.default_strategy_profile || item.strategy_profile, cleanSlug); addConfigOptional(option, "github_environment", item.github_environment, cleanSlug); addConfigOptional(option, "variable_scope", item.variable_scope, (value, field) => cleanChoice(value || "default", ["default", "repository", "environment"], field),