From b2afbebe8bfd29a1fbd844339f192c5144c93460 Mon Sep 17 00:00:00 2001 From: Pigbibi <20649888+Pigbibi@users.noreply.github.com> Date: Fri, 3 Jul 2026 01:26:10 +0800 Subject: [PATCH] fix strategy switch DCA-derived IBIT zscore settings Co-Authored-By: Codex --- platform-config.json | 8 +- python/scripts/build_runtime_switch.py | 117 +++-------- python/scripts/runtime_settings.py | 1 - python/tests/test_runtime_settings.py | 45 +++- tests/strategy_switch_worker_validation.mjs | 62 +++--- web/strategy-switch-console/app.js | 198 +----------------- web/strategy-switch-console/app_js.js | 2 +- web/strategy-switch-console/config.js | 10 +- web/strategy-switch-console/index.html | 9 +- web/strategy-switch-console/page_asset.js | 2 +- .../strategy-profiles.example.json | 5 +- .../strategy_profiles_asset.js | 2 +- web/strategy-switch-console/worker.js | 143 ++++--------- 13 files changed, 159 insertions(+), 445 deletions(-) diff --git a/platform-config.json b/platform-config.json index 7a0c7e8..a465766 100644 --- a/platform-config.json +++ b/platform-config.json @@ -240,7 +240,7 @@ "reserved_cash": false, "income_layer": false, "option_overlay": false, - "dca": true + "dca": false }, "default_account": { "key": "default", @@ -573,12 +573,8 @@ "features": { "income_layer": false, "option_overlay": false, - "dca": true, + "dca": false, "combo": false - }, - "dca_defaults": { - "default_mode": "fixed", - "default_base_investment_usd": "100" } }, "crypto_trend_rotation": { diff --git a/python/scripts/build_runtime_switch.py b/python/scripts/build_runtime_switch.py index 95a1fb1..2c59f67 100644 --- a/python/scripts/build_runtime_switch.py +++ b/python/scripts/build_runtime_switch.py @@ -394,25 +394,6 @@ def _normalize_option_recipe(value: str, *, field_name: str) -> str: return text -def _normalize_ibit_zscore_exit_mode(value: str) -> str: - mode = str(value or "").strip().lower() - aliases = { - "off": "disabled", - "none": "disabled", - "false": "disabled", - "0": "disabled", - "disable": "disabled", - "enabled": "live", - "shadow": "paper", - "dry_run": "paper", - "dry-run": "paper", - } - mode = aliases.get(mode, mode) - if mode not in {"disabled", "paper", "live"}: - raise ValueError("ibit_zscore_exit_mode must be disabled, paper, or live") - return mode - - def _normalize_symbol_text(value: str, *, field_name: str) -> str: text = str(value or "").strip().upper().removesuffix(".US") if not text or not re.fullmatch(r"[A-Z0-9.-]{1,12}", text): @@ -573,6 +554,18 @@ def _dca_extra_variables( return extra_variables +def _effective_dca_mode( + args: argparse.Namespace, + strategy_profile: str, + controls: dict[str, Any] | None = None, +) -> str: + if strategy_profile not in DCA_PROFILES: + return "" + controls = dict(controls or {}) + raw_mode = args.dca_mode if str(args.dca_mode or "").strip() else controls.get(DCA_MODE_CONTROL_FIELD, "") + return _normalize_dca_mode(raw_mode) if str(raw_mode or "").strip() else "fixed" + + def _reject_direct_dca_extra_variables(extra_variables: dict[str, Any]) -> None: provided = [ variable @@ -594,7 +587,10 @@ def _reject_direct_ibit_zscore_exit_extra_variables(extra_variables: dict[str, A ] if provided: names = ", ".join(provided) - raise ValueError(f"use ibit_zscore_exit_* control fields instead of extra_variables_json for {names}") + raise ValueError( + "IBIT_ZSCORE_EXIT variables are derived from ibit_smart_dca smart DCA mode; " + f"do not set them directly: {names}" + ) def _reject_research_only_extra_variables(extra_variables: dict[str, Any]) -> None: @@ -620,81 +616,36 @@ def _ibit_zscore_exit_extra_variables( args: argparse.Namespace, strategy_profile: str, plugin_mode: str, - controls: dict[str, Any] | None = None, + dca_mode: str, ) -> dict[str, Any]: - controls = dict(controls or {}) - cli_mode = str(getattr(args, "ibit_zscore_exit_mode", "") or "").strip() - mode_value = cli_mode or controls.get("ibit_zscore_exit_mode", "") - has_controls = bool(mode_value) or any( - str(controls.get(field, "") or "").strip() - for field in IBIT_ZSCORE_EXIT_CONTROL_FIELDS - if field != "ibit_zscore_exit_mode" - ) - has_cli_controls = any( - str(getattr(args, attr, "") or "").strip() - for attr in ( - "ibit_zscore_exit_parking_symbol", - "ibit_zscore_exit_risk_reduced_exposure", - "ibit_zscore_exit_risk_off_exposure", - "ibit_zscore_exit_allow_outside_execution_window", - ) - ) is_ibit_profile = strategy_profile == IBIT_ZSCORE_EXIT_STRATEGY_PROFILE if not is_ibit_profile: - if has_controls or has_cli_controls: - raise ValueError("IBIT Z-Score exit settings are only supported for ibit_smart_dca") return {variable: "" for variable in IBIT_ZSCORE_EXIT_RUNTIME_VARIABLES} - if not mode_value: - mode = "disabled" if plugin_mode == "none" else "live" - else: - mode = _normalize_ibit_zscore_exit_mode(mode_value) - if plugin_mode == "none" and mode != "disabled": - raise ValueError("IBIT Z-Score exit live/paper modes require plugin_mode auto or custom") - - parking_symbol = ( - getattr(args, "ibit_zscore_exit_parking_symbol", "") - or controls.get("ibit_zscore_exit_parking_symbol") - or "BOXX" - ) - risk_reduced_exposure = ( - getattr(args, "ibit_zscore_exit_risk_reduced_exposure", "") - or controls.get("ibit_zscore_exit_risk_reduced_exposure") - or "0.50" - ) - risk_off_exposure = ( - getattr(args, "ibit_zscore_exit_risk_off_exposure", "") - or controls.get("ibit_zscore_exit_risk_off_exposure") - or "0.25" - ) - allow_outside_window = ( - getattr(args, "ibit_zscore_exit_allow_outside_execution_window", "") - or controls.get("ibit_zscore_exit_allow_outside_execution_window") - or "true" - ) + mode = "live" if plugin_mode != "none" and dca_mode == "smart" else "disabled" return { IBIT_ZSCORE_EXIT_ENABLED_VARIABLE: "true" if mode != "disabled" else "false", IBIT_ZSCORE_EXIT_MODE_VARIABLE: "paper" if mode == "disabled" else mode, IBIT_ZSCORE_EXIT_PARKING_SYMBOL_VARIABLE: _normalize_symbol_text( - parking_symbol, + "BOXX", field_name="ibit_zscore_exit_parking_symbol", ), IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE_VARIABLE: _normalize_ratio_decimal( - risk_reduced_exposure, + "0.50", field_name="ibit_zscore_exit_risk_reduced_exposure", ), IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE_VARIABLE: _normalize_ratio_decimal( - risk_off_exposure, + "0.25", field_name="ibit_zscore_exit_risk_off_exposure", ), IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_WINDOW_VARIABLE: _normalize_optional_bool_text( - allow_outside_window, + "true", field_name="ibit_zscore_exit_allow_outside_execution_window", ), } -def _auto_plugin_mounts(strategy_profile: str, artifact_bucket_uri: str) -> list[dict[str, Any]]: +def _auto_plugin_mounts(strategy_profile: str, artifact_bucket_uri: str, dca_mode: str = "") -> list[dict[str, Any]]: prefix = artifact_bucket_uri.rstrip("/") mounts: list[dict[str, Any]] = [] if strategy_profile in MARKET_REGIME_CONTROL_PROFILES: @@ -711,7 +662,7 @@ def _auto_plugin_mounts(strategy_profile: str, artifact_bucket_uri: str) -> list "expected_schema_version": "market_regime_control.v1", } ) - if strategy_profile == IBIT_ZSCORE_EXIT_STRATEGY_PROFILE: + if strategy_profile == IBIT_ZSCORE_EXIT_STRATEGY_PROFILE and dca_mode == "smart": mounts.append( { "strategy": strategy_profile, @@ -743,12 +694,12 @@ def _custom_plugin_mounts(raw_json: str) -> list[dict[str, Any]]: return [dict(item) for item in payload] -def _plugin_mounts(args: argparse.Namespace, strategy_profile: str) -> list[dict[str, Any]]: +def _plugin_mounts(args: argparse.Namespace, strategy_profile: str, dca_mode: str = "") -> list[dict[str, Any]]: mode = str(args.plugin_mode or "auto").strip().lower() if mode == "none": return [] if mode == "auto": - return _auto_plugin_mounts(strategy_profile, args.artifact_bucket_uri) + return _auto_plugin_mounts(strategy_profile, args.artifact_bucket_uri, dca_mode) if mode == "custom": return _custom_plugin_mounts(args.custom_plugin_mounts_json) raise ValueError(f"unsupported plugin_mode {args.plugin_mode!r}") @@ -935,13 +886,14 @@ def build_switch_target(args: argparse.Namespace) -> dict[str, Any]: raise ValueError("variable_scope must be repository or environment") github_environment = args.github_environment or _default_github_environment(platform, target_name, variable_scope) runtime_target = _build_runtime_target(args) - mounts = _plugin_mounts(args, runtime_target["strategy_profile"]) - runtime_target["scheduler"] = _scheduler_plan_for_strategy(runtime_target["strategy_profile"], mounts) - mounts_variable = f"{SUPPORTED_PLATFORMS[platform]['plugin_mounts_prefix']}STRATEGY_PLUGIN_MOUNTS_JSON" extra_variables = _parse_extra_variables(args.extra_variable, args.extra_variables_json) cash_only_controls = _extract_cash_only_control_fields(extra_variables) dca_controls = _extract_dca_control_fields(extra_variables) - ibit_zscore_exit_controls = _extract_ibit_zscore_exit_control_fields(extra_variables) + _extract_ibit_zscore_exit_control_fields(extra_variables) + effective_dca_mode = _effective_dca_mode(args, runtime_target["strategy_profile"], dca_controls) + mounts = _plugin_mounts(args, runtime_target["strategy_profile"], effective_dca_mode) + runtime_target["scheduler"] = _scheduler_plan_for_strategy(runtime_target["strategy_profile"], mounts) + mounts_variable = f"{SUPPORTED_PLATFORMS[platform]['plugin_mounts_prefix']}STRATEGY_PLUGIN_MOUNTS_JSON" if cash_only_controls.get(CASH_ONLY_EXECUTION_CONTROL_FIELD): args.cash_only_execution_mode = str(cash_only_controls[CASH_ONLY_EXECUTION_CONTROL_FIELD]).strip().lower() _reject_direct_dca_extra_variables(extra_variables) @@ -966,7 +918,7 @@ def build_switch_target(args: argparse.Namespace) -> dict[str, Any]: args, runtime_target["strategy_profile"], str(args.plugin_mode or "auto").strip().lower(), - ibit_zscore_exit_controls, + effective_dca_mode, ) ) @@ -1040,11 +992,6 @@ def build_parser() -> argparse.ArgumentParser: parser.add_argument("--option-overlay-mode", choices=sorted(OPTION_OVERLAY_MODES), default="current") parser.add_argument("--dca-mode", default="") parser.add_argument("--dca-base-investment-usd", default="") - parser.add_argument("--ibit-zscore-exit-mode", choices=("disabled", "paper", "live"), default="") - parser.add_argument("--ibit-zscore-exit-parking-symbol", default="") - parser.add_argument("--ibit-zscore-exit-risk-reduced-exposure", default="") - parser.add_argument("--ibit-zscore-exit-risk-off-exposure", default="") - parser.add_argument("--ibit-zscore-exit-allow-outside-execution-window", default="") parser.add_argument("--existing-service-targets-json-file", default="") parser.add_argument("--no-platform-dry-run-variable", dest="set_platform_dry_run_variable", action="store_false") parser.set_defaults(set_platform_dry_run_variable=True) diff --git a/python/scripts/runtime_settings.py b/python/scripts/runtime_settings.py index cf6e5d7..4dc8cbd 100644 --- a/python/scripts/runtime_settings.py +++ b/python/scripts/runtime_settings.py @@ -714,7 +714,6 @@ def command_repository(args: argparse.Namespace) -> int: ACCOUNT_SYNC_CONTROL_FIELDS = { "DCA_MODE": "dca_mode", "DCA_BASE_INVESTMENT_USD": "dca_base_investment_usd", - "IBIT_ZSCORE_EXIT_MODE": "ibit_zscore_exit_mode", } diff --git a/python/tests/test_runtime_settings.py b/python/tests/test_runtime_settings.py index 3deca3d..a1ce76d 100644 --- a/python/tests/test_runtime_settings.py +++ b/python/tests/test_runtime_settings.py @@ -247,7 +247,6 @@ def test_extract_account_sync_controls_reads_ibkr_service_targets(self): { "dca_mode": "smart", "dca_base_investment_usd": "500", - "ibit_zscore_exit_mode": "paper", }, ) @@ -609,7 +608,7 @@ def test_build_switch_target_uses_dca_monthly_scheduler_window(self): }, ) - def test_build_switch_target_uses_daily_scheduler_when_ibit_zscore_plugin_is_auto_mounted(self): + def test_build_switch_target_uses_daily_scheduler_when_ibit_smart_dca_is_smart(self): parser = build_runtime_switch.build_parser() args = parser.parse_args( [ @@ -619,6 +618,8 @@ def test_build_switch_target_uses_daily_scheduler_when_ibit_zscore_plugin_is_aut "ibit", "--strategy-profile", "ibit_smart_dca", + "--dca-mode", + "smart", ] ) @@ -641,7 +642,7 @@ def test_build_switch_target_uses_daily_scheduler_when_ibit_zscore_plugin_is_aut self.assertEqual(assignments["IBIT_ZSCORE_EXIT_ENABLED"], "true") self.assertEqual(assignments["IBIT_ZSCORE_EXIT_MODE"], "live") - def test_build_switch_target_sets_ibit_zscore_exit_runtime_controls(self): + def test_build_switch_target_ignores_legacy_ibit_zscore_controls(self): parser = build_runtime_switch.build_parser() args = parser.parse_args( [ @@ -651,6 +652,8 @@ def test_build_switch_target_sets_ibit_zscore_exit_runtime_controls(self): "ibit", "--strategy-profile", "ibit_smart_dca", + "--dca-mode", + "smart", "--extra-variables-json", '{"ibit_zscore_exit_mode":"live","ibit_zscore_exit_parking_symbol":"SGOV"}', ] @@ -661,10 +664,32 @@ def test_build_switch_target_sets_ibit_zscore_exit_runtime_controls(self): self.assertEqual(assignments["IBIT_ZSCORE_EXIT_ENABLED"], "true") self.assertEqual(assignments["IBIT_ZSCORE_EXIT_MODE"], "live") - self.assertEqual(assignments["IBIT_ZSCORE_EXIT_PARKING_SYMBOL"], "SGOV") + self.assertEqual(assignments["IBIT_ZSCORE_EXIT_PARKING_SYMBOL"], "BOXX") self.assertNotIn("ibit_zscore_exit_mode", target["extra_variables"]) self.assertNotIn("ibit_zscore_exit_parking_symbol", target["extra_variables"]) + def test_build_switch_target_disables_ibit_zscore_exit_for_fixed_dca(self): + parser = build_runtime_switch.build_parser() + args = parser.parse_args( + [ + "--platform", + "firstrade", + "--target-name", + "ibit", + "--strategy-profile", + "ibit_smart_dca", + ] + ) + + target = build_runtime_switch.build_switch_target(args) + assignments = {item.name: item.value for item in runtime_settings.build_assignments(target)} + plugin_payload = json.loads(assignments["FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON"]) + + self.assertEqual(target["runtime_target"]["scheduler"], build_runtime_switch.US_DCA_SCHEDULER) + self.assertEqual(plugin_payload["strategy_plugins"], []) + self.assertEqual(assignments["IBIT_ZSCORE_EXIT_ENABLED"], "false") + self.assertEqual(assignments["IBIT_ZSCORE_EXIT_MODE"], "paper") + def test_build_switch_target_disables_ibit_zscore_exit_when_plugins_are_disabled(self): parser = build_runtime_switch.build_parser() args = parser.parse_args( @@ -675,6 +700,8 @@ def test_build_switch_target_disables_ibit_zscore_exit_when_plugins_are_disabled "ibit", "--strategy-profile", "ibit_smart_dca", + "--dca-mode", + "smart", "--plugin-mode", "none", ] @@ -687,7 +714,7 @@ def test_build_switch_target_disables_ibit_zscore_exit_when_plugins_are_disabled self.assertEqual(assignments["IBIT_ZSCORE_EXIT_ENABLED"], "false") self.assertEqual(assignments["IBIT_ZSCORE_EXIT_MODE"], "paper") - def test_build_switch_target_rejects_ibit_zscore_controls_for_other_profiles(self): + def test_build_switch_target_ignores_legacy_ibit_zscore_controls_for_other_profiles(self): parser = build_runtime_switch.build_parser() args = parser.parse_args( [ @@ -702,8 +729,12 @@ def test_build_switch_target_rejects_ibit_zscore_controls_for_other_profiles(sel ] ) - with self.assertRaisesRegex(ValueError, "IBIT Z-Score exit settings"): - build_runtime_switch.build_switch_target(args) + target = build_runtime_switch.build_switch_target(args) + assignments = {item.name: item.value for item in runtime_settings.build_assignments(target)} + + self.assertEqual(assignments["IBIT_ZSCORE_EXIT_ENABLED"], "") + self.assertEqual(assignments["IBIT_ZSCORE_EXIT_MODE"], "") + self.assertNotIn("ibit_zscore_exit_mode", target["extra_variables"]) def test_build_switch_target_sets_dca_settings_for_dca_profile(self): parser = build_runtime_switch.build_parser() diff --git a/tests/strategy_switch_worker_validation.mjs b/tests/strategy_switch_worker_validation.mjs index d5383da..b69ed04 100644 --- a/tests/strategy_switch_worker_validation.mjs +++ b/tests/strategy_switch_worker_validation.mjs @@ -32,7 +32,7 @@ assert.ok(indexHtml.includes('id="reserved-cash-ratio-input"')); assert.ok(indexHtml.includes('id="reserve-policy-mode-select"')); assert.ok(indexHtml.includes('id="runtime-target-enabled-select"')); assert.ok(indexHtml.includes('id="plugin-mode-select"')); -assert.ok(indexHtml.includes('id="ibit-zscore-exit-mode-select"')); +assert.equal(indexHtml.includes('id="ibit-zscore-exit-mode-select"'), false); assert.ok(indexHtml.includes('id="income-layer-start-usd-input"')); assert.ok(indexHtml.includes('incomeLayerStartUsd: "收入层起始金额"')); assert.ok(indexHtml.includes('incomeLayerStartUsd: "Income layer start amount"')); @@ -123,11 +123,9 @@ assert.ok(indexHtml.includes('pluginModeAuto: "启用插件"')); assert.ok(indexHtml.includes('pluginModeNone: "禁用插件"')); assert.ok(indexHtml.includes('pluginModeAuto: "Enabled"')); assert.ok(indexHtml.includes('pluginMode: "Plugin scope"')); -assert.ok(indexHtml.includes('ibitZscoreExitMode: "IBIT Z-Score 逃顶"')); -assert.ok(indexHtml.includes('ibitZscoreExitLive: "实盘执行(按信号调仓)"')); -assert.ok(indexHtml.includes('ibitZscoreExitMode: "IBIT Z-Score exit"')); -assert.ok(indexHtml.includes('changes.ibitZscoreExitChanged')); -assert.ok(indexHtml.includes('el("ibit-zscore-exit-mode-select").addEventListener("change"')); +assert.equal(indexHtml.includes('id="ibit-zscore-exit-mode-select"'), false); +assert.equal(indexHtml.includes("ibitZscoreExit"), false); +assert.equal(indexHtml.includes("ibit_zscore_exit_mode"), false); assert.ok(indexHtml.includes('reservedCashDefault')); assert.ok(indexHtml.includes('paper: "模拟"')); assert.ok(indexHtml.includes('paper: "Dry run"')); @@ -174,7 +172,8 @@ const servedAppResponse = await worker.fetch(new Request("https://switch.example const servedAppJs = await servedAppResponse.text(); assert.equal(servedAppResponse.status, 200); assert.ok(servedAppJs.includes("function hasPrivateConfig()")); -assert.ok(servedAppJs.includes("changes.ibitZscoreExitChanged")); +assert.equal(servedAppJs.includes("ibitZscoreExit"), false); +assert.equal(servedAppJs.includes("ibit_zscore_exit_mode"), false); assert.equal(servedAppJs.includes("ibkr-primary"), false); assert.equal(servedAppJs.includes("longbridge-quant-sg-service"), false); assert.equal(servedAppJs.includes('account_selector: "SG"'), false); @@ -604,39 +603,48 @@ assert.deepEqual(JSON.parse(normalizedDcaJsonInputs.extra_variables_json), { dca_base_investment_usd: "500", cash_only_execution_mode: "enabled", }); -const normalizedIbitZscoreInputs = __test.normalizeSwitchInputs({ +const normalizedIbitSmartDcaInputs = __test.normalizeSwitchInputs({ platform: "firstrade", target_name: "default", strategy_profile: "ibit_smart_dca", execution_mode: "live", plugin_mode: "auto", + dca_mode: "smart", + dca_base_investment_usd: "500", ibit_zscore_exit_mode: "live", }); -assert.deepEqual(JSON.parse(normalizedIbitZscoreInputs.extra_variables_json), { - ibit_zscore_exit_mode: "live", +assert.deepEqual(JSON.parse(normalizedIbitSmartDcaInputs.extra_variables_json), { + dca_mode: "smart", + dca_base_investment_usd: "500", cash_only_execution_mode: "enabled", }); -const normalizedIbkrIbitZscoreInputs = __test.normalizeSwitchInputs({ +const normalizedIbkrIbitSmartDcaInputs = __test.normalizeSwitchInputs({ platform: "ibkr", target_name: "ibit-primary", strategy_profile: "ibit_smart_dca", execution_mode: "live", plugin_mode: "auto", - ibit_zscore_exit_mode: "live", + extra_variables_json: JSON.stringify({ + dca_mode: "smart", + dca_base_investment_usd: "500", + ibit_zscore_exit_mode: "live", + }), }); -assert.deepEqual(JSON.parse(normalizedIbkrIbitZscoreInputs.extra_variables_json), { - ibit_zscore_exit_mode: "live", +assert.deepEqual(JSON.parse(normalizedIbkrIbitSmartDcaInputs.extra_variables_json), { + dca_mode: "smart", + dca_base_investment_usd: "500", + cash_only_execution_mode: "enabled", +}); +const normalizedNonIbitLegacyZscoreInputs = __test.normalizeSwitchInputs({ + platform: "ibkr", + target_name: "ibkr-primary", + strategy_profile: "tqqq_growth_income", + execution_mode: "live", + extra_variables_json: JSON.stringify({ ibit_zscore_exit_mode: "live" }), +}); +assert.deepEqual(JSON.parse(normalizedNonIbitLegacyZscoreInputs.extra_variables_json), { cash_only_execution_mode: "enabled", }); -assert.throws( - () => __test.normalizeSwitchInputs({ - platform: "ibkr", - target_name: "ibkr-primary", - strategy_profile: "nasdaq_sp500_smart_dca", - ibit_zscore_exit_mode: "live", - }), - /IBIT Z-Score exit settings/, -); assert.throws( () => __test.normalizeSwitchInputs({ platform: "qmt", @@ -826,6 +834,7 @@ const updatedIbitZscoreModeOptions = __test.updateAccountOptionsDefaultStrategy( target_name: "ibit-primary", default_strategy_profile: "ibit_smart_dca", supported_domains: ["us_equity"], + ibit_zscore_exit_mode: "live", }, ], }, @@ -836,11 +845,10 @@ const updatedIbitZscoreModeOptions = __test.updateAccountOptionsDefaultStrategy( execution_mode: "live", variable_scope: "repository", plugin_mode: "auto", - extra_variables_json: JSON.stringify({ ibit_zscore_exit_mode: "live" }), }, ); assert.equal(updatedIbitZscoreModeOptions.changed, true); -assert.equal(updatedIbitZscoreModeOptions.options.ibkr[0].ibit_zscore_exit_mode, "live"); +assert.equal("ibit_zscore_exit_mode" in updatedIbitZscoreModeOptions.options.ibkr[0], false); const kvWrites = new Map(); const syncResult = await __test.syncDefaultStrategyForAccount( @@ -1004,7 +1012,7 @@ try { assert.equal(currentStrategies.ibkr["ibkr-primary"].runtime_target_enabled, false); assert.equal(currentStrategies.ibkr["ibkr-primary"].dca_mode, "smart"); assert.equal(currentStrategies.ibkr["ibkr-primary"].dca_base_investment_usd, "700"); - assert.equal(currentStrategies.ibkr["ibkr-primary"].ibit_zscore_exit_mode, "live"); + assert.equal("ibit_zscore_exit_mode" in currentStrategies.ibkr["ibkr-primary"], false); assert.equal(currentStrategies.ibkr["ibkr-primary"].source, "CLOUD_RUN_SERVICE_TARGETS_JSON"); } finally { globalThis.fetch = originalFetch; @@ -1203,7 +1211,7 @@ try { assert.equal(currentStrategies.firstrade.default.reserved_cash_ratio, "0.02"); assert.equal(currentStrategies.firstrade.default.dca_mode, "fixed"); assert.equal(currentStrategies.firstrade.default.dca_base_investment_usd, "50"); - assert.equal(currentStrategies.firstrade.default.ibit_zscore_exit_mode, "live"); + assert.equal("ibit_zscore_exit_mode" in currentStrategies.firstrade.default, false); } finally { globalThis.fetch = originalFetch; } diff --git a/web/strategy-switch-console/app.js b/web/strategy-switch-console/app.js index fabf95b..1c90d7a 100644 --- a/web/strategy-switch-console/app.js +++ b/web/strategy-switch-console/app.js @@ -43,7 +43,7 @@ reserved_cash: false, income_layer: false, option_overlay: false, - dca: true, + dca: false, execution_mode: "live", service_name: "", default_execution_mode: "live" @@ -145,7 +145,6 @@ const runtimeTargetModes = ["enabled", "disabled"]; const pluginModes = ["auto", "none"]; const dcaModes = ["fixed", "smart"]; - const ibitZscoreExitModes = ["paper", "live", "disabled"]; const runtimeTargetEnabledVariable = "RUNTIME_TARGET_ENABLED"; const incomeLayerEnabledVariable = "INCOME_LAYER_ENABLED"; const incomeLayerStartUsdVariable = "INCOME_LAYER_START_USD"; @@ -153,7 +152,6 @@ const dcaProfileDefaults = { nasdaq_sp500_smart_dca: { defaultMode: "fixed", defaultBaseInvestmentUsd: "1000" }, ibit_smart_dca: { defaultMode: "fixed", defaultBaseInvestmentUsd: "1000" }, - crypto_btc_dca: { defaultMode: "fixed", defaultBaseInvestmentUsd: "100" }, }; const platformMinReservedCashVariables = { longbridge: "LONGBRIDGE_MIN_RESERVED_CASH_USD", @@ -308,11 +306,6 @@ pluginModeAuto: "启用插件", pluginModeNone: "禁用插件", pluginModeMeta: "选择是否启用该策略的插件。", - ibitZscoreExitMode: "IBIT Z-Score 逃顶", - ibitZscoreExitPaper: "观察模式(不卖出)", - ibitZscoreExitLive: "实盘执行(按信号调仓)", - ibitZscoreExitDisabled: "禁用逃顶(只买不卖)", - ibitZscoreExitModeMeta: "插件启用后默认实盘消费动态 Z-Score 信号;可改为观察模式,或禁用后只买不卖。", incomeLayerMode: "收入层状态", incomeLayerSectionTitle: "收入层", incomeLayerCurrent: "沿用当前配置", @@ -397,7 +390,6 @@ invalidIncomeLayerNote: "请填写有效的收入层起始金额和最高比例。", invalidOptionOverlayNote: "当前策略未定义可启用的期权层。", invalidDcaNote: "请填写有效的定投模式和基准金额。", - invalidIbitZscoreExitNote: "请选择有效的 IBIT Z-Score 逃顶模式。", publicOauthTitle: "GitHub OAuth 保护", publicOauthText: "只允许白名单账号进入私有配置。", publicWorkerTitle: "Worker 端触发", @@ -420,8 +412,6 @@ pendingMode: "待提交模式", currentPluginMode: "当前插件范围", pendingPluginMode: "待提交插件范围", - currentIbitZscoreExit: "当前 Z-Score 逃顶", - pendingIbitZscoreExit: "待提交 Z-Score 逃顶", unchanged: "不变", copied: "已复制状态", dispatching: "正在触发工作流...", @@ -476,11 +466,6 @@ pluginModeAuto: "Enabled", pluginModeNone: "Disabled", pluginModeMeta: "Choose whether to enable this strategy's plugins.", - ibitZscoreExitMode: "IBIT Z-Score exit", - ibitZscoreExitPaper: "Paper mode (signals only)", - ibitZscoreExitLive: "Live execution (rebalance on signals)", - ibitZscoreExitDisabled: "Disabled (buy only)", - ibitZscoreExitModeMeta: "When plugins are enabled, live consumption is the default. Paper mode observes only; disabled mode keeps buy-only DCA.", incomeLayerMode: "Income layer", incomeLayerSectionTitle: "Income layer", incomeLayerCurrent: "Keep current config", @@ -565,7 +550,6 @@ invalidIncomeLayerNote: "Enter a valid income layer start amount and max ratio.", invalidOptionOverlayNote: "This strategy does not define an option layer to enable.", invalidDcaNote: "Enter a valid DCA mode and base amount.", - invalidIbitZscoreExitNote: "Choose a valid IBIT Z-Score exit mode.", publicOauthTitle: "Protected by GitHub OAuth", publicOauthText: "Only allowlisted accounts can open private config.", publicWorkerTitle: "Worker-side dispatch", @@ -588,8 +572,6 @@ pendingMode: "Pending mode", currentPluginMode: "Current plugin scope", pendingPluginMode: "Pending plugin scope", - currentIbitZscoreExit: "Current Z-Score exit", - pendingIbitZscoreExit: "Pending Z-Score exit", unchanged: "Unchanged", copied: "State copied", dispatching: "Dispatching workflow...", @@ -641,8 +623,6 @@ dcaBaseInvestmentUsd: "", dcaTouched: false, strategyTouched: false, - ibitZscoreExitMode: "live", - ibitZscoreExitTouched: false, }); const state = { @@ -982,37 +962,6 @@ return mode === "none" ? t("pluginModeNone") : t("pluginModeAuto"); } - function ibitZscoreExitSupported(profile) { - return cleanStrategyProfile(profile) === "ibit_smart_dca"; - } - - function cleanIbitZscoreExitMode(value) { - const mode = String(value || "").trim().toLowerCase(); - const aliases = { - off: "disabled", - none: "disabled", - false: "disabled", - disable: "disabled", - enabled: "live", - shadow: "paper", - dry_run: "paper", - "dry-run": "paper", - }; - const normalized = aliases[mode] || mode; - return ibitZscoreExitModes.includes(normalized) ? normalized : ""; - } - - function normalizeIbitZscoreExitMode(value) { - return cleanIbitZscoreExitMode(value) || "live"; - } - - function ibitZscoreExitModeLabel(mode) { - const normalized = normalizeIbitZscoreExitMode(mode); - if (normalized === "live") return t("ibitZscoreExitLive"); - if (normalized === "disabled") return t("ibitZscoreExitDisabled"); - return t("ibitZscoreExitPaper"); - } - function dcaConfigForStrategy(profile) { const cleanProfile = cleanStrategyProfile(profile); const catalog = strategyCatalog[cleanProfile] || {}; @@ -1305,7 +1254,6 @@ cleanOptionalBoolean(entry.runtime_target_enabled) !== null || normalizeDcaMode(entry.dca_mode || "") !== "fixed" || cleanDisplayPositiveNumber(entry.dca_base_investment_usd) || - cleanIbitZscoreExitMode(entry.ibit_zscore_exit_mode) || normalizeExecutionMode(entry.execution_mode, entry.dry_run_only), ); } @@ -1328,18 +1276,6 @@ return normalizePluginMode(account?.plugin_mode); } - function currentIbitZscoreExitForAccount(platform, account, profile = state.forms[platform]?.strategy) { - if (!ibitZscoreExitSupported(profile)) return { supported: false, mode: "" }; - const entry = currentEntryForAccount(platform, account); - const mode = cleanIbitZscoreExitMode(entry?.ibit_zscore_exit_mode) || - cleanIbitZscoreExitMode(account?.ibit_zscore_exit_mode); - if (mode) return { supported: true, mode }; - if (currentPluginModeForAccount(platform, account) === "none") { - return { supported: true, mode: "disabled" }; - } - return { supported: true, mode: "" }; - } - function reservePolicyFromEntry(entry) { return { minReservedCashUsd: cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd), @@ -1440,7 +1376,6 @@ syncOptionOverlayForAccount(platform); syncCashOnlyExecutionForAccount(platform); syncDcaForAccount(platform); - syncIbitZscoreExitForAccount(platform); } function syncRuntimeTargetForAccount(platform) { @@ -1512,18 +1447,6 @@ form.dcaBaseInvestmentUsd = current.supported ? current.baseInvestmentUsd : ""; } - function syncIbitZscoreExitForAccount(platform) { - const form = state.forms[platform]; - if (!form) return; - if (!ibitZscoreExitSupported(form.strategy) || normalizePluginMode(form.pluginMode) === "none") { - form.ibitZscoreExitMode = "disabled"; - return; - } - if (form.ibitZscoreExitTouched) return; - const current = currentIbitZscoreExitForAccount(platform, selectedAccount(platform), form.strategy); - form.ibitZscoreExitMode = current.mode || "live"; - } - function ensureAccountSelection(platform) { const options = optionsFor(platform); if (!options.length) return; @@ -1535,7 +1458,6 @@ state.forms[platform].optionOverlayTouched = false; state.forms[platform].cashOnlyExecutionTouched = false; state.forms[platform].dcaTouched = false; - state.forms[platform].ibitZscoreExitTouched = false; state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0], state.forms[platform].strategy); state.forms[platform].pluginMode = currentPluginModeForAccount(platform, options[0]); syncRuntimeTargetForAccount(platform); @@ -1544,7 +1466,6 @@ syncOptionOverlayForAccount(platform); syncCashOnlyExecutionForAccount(platform); syncDcaForAccount(platform); - syncIbitZscoreExitForAccount(platform); } } @@ -1611,15 +1532,12 @@ return Boolean(dcaModes.includes(normalizeDcaMode(form?.dcaMode)) && cleanDisplayPositiveNumber(form?.dcaBaseInvestmentUsd)); } - function hasValidIbitZscoreExitPolicy() { return true; } - -function hasValidStrategySelection(platform = state.selected) { + function hasValidStrategySelection(platform = state.selected) { return hasRunnableStrategySelection(platform) && hasValidExecutionCashPolicy(platform) && hasValidIncomeLayerPolicy(platform) && hasValidOptionOverlayPolicy(platform) && - hasValidDcaPolicy(platform) && - hasValidIbitZscoreExitPolicy(platform); + hasValidDcaPolicy(platform); } function normalizeReservePolicyMode(value) { @@ -1707,14 +1625,6 @@ function hasValidStrategySelection(platform = state.selected) { return { inputs: { dca_mode: mode, dca_base_investment_usd: baseInvestmentUsd } }; } - function ibitZscoreExitOverrideForForm(form) { - if (!ibitZscoreExitSupported(form?.strategy)) return null; - const mode = normalizePluginMode(form?.pluginMode) === "none" - ? "disabled" - : normalizeIbitZscoreExitMode(form?.ibitZscoreExitMode); - return { inputs: {}, extraVariables: { ibit_zscore_exit_mode: mode } }; - } - function mergeExtraVariables(inputs, extraVariables) { if (!extraVariables || !Object.keys(extraVariables).length) return; const merged = inputs.extra_variables_json ? JSON.parse(inputs.extra_variables_json) : {}; @@ -1786,7 +1696,6 @@ function hasValidStrategySelection(platform = state.selected) { if (dcaOverride) { Object.assign(inputs, dcaOverride.inputs); } - inputs.ibit_zscore_exit_mode = normalizeDcaMode(form.dcaMode) === "smart" ? "enabled" : "disabled"; return inputs; } @@ -1918,28 +1827,6 @@ function hasValidStrategySelection(platform = state.selected) { .replace("{amount}", formatUsd(pending.inputs.dca_base_investment_usd)); } - function currentIbitZscoreExitText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) { - const current = currentIbitZscoreExitForAccount(platform, account, profile); - if (!current.supported) return ""; - return current.mode ? ibitZscoreExitModeLabel(current.mode) : t("notRead"); - } - - function pendingIbitZscoreExitText(inputs, platform = state.selected, account = selectedAccount(platform)) { - const pending = pendingIbitZscoreExit(inputs, platform, account); - if (!pending.supported) return ""; - if (!pending.changed) return t("unchanged"); - return ibitZscoreExitModeLabel(pending.inputs.ibit_zscore_exit_mode); - } - - function ibitZscoreExitModeFromInputs(inputs) { - try { - const payload = inputs?.extra_variables_json ? JSON.parse(inputs.extra_variables_json) : {}; - return cleanIbitZscoreExitMode(payload.ibit_zscore_exit_mode || inputs?.ibit_zscore_exit_mode); - } catch { - return cleanIbitZscoreExitMode(inputs?.ibit_zscore_exit_mode); - } - } - function pendingRuntimeTarget(inputs, platform = state.selected, account = selectedAccount(platform)) { const mode = normalizeRuntimeTargetMode(inputs.runtime_target_enabled_mode); if (mode === "current") { @@ -2069,19 +1956,6 @@ function hasValidStrategySelection(platform = state.selected) { }; } - function pendingIbitZscoreExit(inputs, platform = state.selected, account = selectedAccount(platform)) { - const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy); - if (!ibitZscoreExitSupported(profile)) return { supported: false, changed: false, inputs: {} }; - const current = currentIbitZscoreExitForAccount(platform, account, profile); - const nextMode = ibitZscoreExitModeFromInputs(inputs) || - (normalizePluginMode(inputs.plugin_mode) === "none" ? "disabled" : "live"); - return { - supported: true, - changed: current.mode ? current.mode !== nextMode : true, - inputs: { ibit_zscore_exit_mode: nextMode }, - }; - } - function pendingChangeState(inputs, platform = state.selected, account = selectedAccount(platform)) { const currentProfile = currentStrategyForAccount(platform, account); const nextProfile = cleanStrategyProfile(inputs.strategy_profile); @@ -2095,7 +1969,6 @@ function hasValidStrategySelection(platform = state.selected) { const optionOverlay = pendingOptionOverlay(inputs, platform, account); const cashOnly = pendingCashOnlyExecution(inputs, platform, account); const dca = pendingDca(inputs, platform, account); - const ibitZscoreExit = pendingIbitZscoreExit(inputs, platform, account); return { currentProfile, nextProfile, @@ -2111,14 +1984,12 @@ function hasValidStrategySelection(platform = state.selected) { optionOverlayChanged: optionOverlay.changed, cashOnlyChanged: cashOnly.changed, dcaChanged: dca.changed, - ibitZscoreExitChanged: ibitZscoreExit.changed, runtimeTarget, reserve, income, optionOverlay, cashOnly, dca, - ibitZscoreExit, }; } @@ -2133,8 +2004,7 @@ function hasValidStrategySelection(platform = state.selected) { changes.incomeLayerChanged || changes.optionOverlayChanged || changes.cashOnlyChanged || - changes.dcaChanged || - changes.ibitZscoreExitChanged, + changes.dcaChanged ); } @@ -2206,10 +2076,6 @@ function hasValidStrategySelection(platform = state.selected) { if (dcaSupported(inputs.strategy_profile)) { rows.push([t("currentDca"), currentDcaText(state.selected, account, inputs.strategy_profile)]); } - if (ibitZscoreExitSupported(inputs.strategy_profile)) { - rows.push([t("currentIbitZscoreExit"), currentIbitZscoreExitText(state.selected, account, inputs.strategy_profile)]); - } - if (changes.reserveCashChanged) { rows.push([t("pendingReservedCashPolicy"), pendingReservedCashPolicyText(inputs, state.selected, account), "pending"]); } @@ -2225,10 +2091,6 @@ function hasValidStrategySelection(platform = state.selected) { if (changes.dcaChanged) { rows.push([t("pendingDca"), pendingDcaText(inputs, state.selected, account), "pending"]); } - if (changes.ibitZscoreExitChanged) { - rows.push([t("pendingIbitZscoreExit"), pendingIbitZscoreExitText(inputs, state.selected, account), "pending"]); - } - if (changes.modeChanged) { rows.push([t("pendingMode"), modeLabel(inputs.execution_mode), "pending"]); } @@ -2302,7 +2164,6 @@ function hasValidStrategySelection(platform = state.selected) { const strategySelect = el("strategy-select"); const runtimeTargetEnabledSelect = el("runtime-target-enabled-select"); const pluginModeSelect = el("plugin-mode-select"); - const ibitZscoreExitModeSelect = el("ibit-zscore-exit-mode-select"); const incomeLayerModeSelect = el("income-layer-mode-select"); const incomeLayerStartUsdInput = el("income-layer-start-usd-input"); const incomeLayerMaxRatioInput = el("income-layer-max-ratio-input"); @@ -2328,7 +2189,6 @@ function hasValidStrategySelection(platform = state.selected) { strategySelect.replaceChildren(); runtimeTargetEnabledSelect.replaceChildren(); pluginModeSelect.replaceChildren(); - ibitZscoreExitModeSelect.replaceChildren(); incomeLayerModeSelect.replaceChildren(); optionOverlayModeSelect.replaceChildren(); cashOnlyExecutionModeSelect.replaceChildren(); @@ -2341,8 +2201,6 @@ function hasValidStrategySelection(platform = state.selected) { reservedCashRatioInput.value = ""; el("account-meta").textContent = ""; el("strategy-meta").textContent = ""; - el("ibit-zscore-exit-mode-block").hidden = true; - el("ibit-zscore-exit-mode-meta").textContent = ""; el("income-layer-mode-meta").textContent = ""; el("income-layer-start-meta").textContent = ""; el("income-layer-ratio-meta").textContent = ""; @@ -2388,30 +2246,6 @@ function hasValidStrategySelection(platform = state.selected) { for (const mode of pluginModes) { pluginModeSelect.append(new Option(pluginModeLabel(mode), mode, false, mode === normalizePluginMode(form.pluginMode))); } - syncIbitZscoreExitForAccount(platform); - const ibitZscoreSupported = ibitZscoreExitSupported(form.strategy); - const ibitZscoreBlock = el("ibit-zscore-exit-mode-block"); - ibitZscoreBlock.hidden = true; - ibitZscoreExitModeSelect.replaceChildren(); - if (ibitZscoreSupported) { - const pluginsDisabled = normalizePluginMode(form.pluginMode) === "none"; - if (pluginsDisabled) form.ibitZscoreExitMode = "disabled"; - form.ibitZscoreExitMode = pluginsDisabled ? "disabled" : normalizeIbitZscoreExitMode(form.ibitZscoreExitMode); - ibitZscoreExitModeSelect.disabled = pluginsDisabled; - for (const mode of ibitZscoreExitModes) { - ibitZscoreExitModeSelect.append(new Option( - ibitZscoreExitModeLabel(mode), - mode, - false, - mode === form.ibitZscoreExitMode, - )); - } - el("ibit-zscore-exit-mode-meta").textContent = t("ibitZscoreExitModeMeta"); - } else { - ibitZscoreExitModeSelect.disabled = true; - ibitZscoreExitModeSelect.append(new Option(t("ibitZscoreExitDisabled"), "disabled")); - el("ibit-zscore-exit-mode-meta").textContent = ""; - } const incomeDefaults = incomeLayerDefaultForStrategy(form.strategy); el("income-layer-section").hidden = false; el("option-overlay-section").hidden = false; @@ -2625,13 +2459,11 @@ function hasValidStrategySelection(platform = state.selected) { const hasValidIncomeLayer = hasValidIncomeLayerPolicy(); const hasValidOptionOverlay = hasValidOptionOverlayPolicy(); const hasValidDca = hasValidDcaPolicy(); - const hasValidIbitZscoreExit = hasValidIbitZscoreExitPolicy(); const hasValidStrategy = hasRunnableStrategy && hasValidReserve && hasValidIncomeLayer && hasValidOptionOverlay && - hasValidDca && - hasValidIbitZscoreExit; + hasValidDca; const hasPendingChange = hasPrivateAccounts && hasValidStrategy && hasPendingChanges(buildInputs()); dispatch.disabled = !state.auth.allowed || loadingConfig || !hasPrivateAccounts || !hasValidStrategy || !hasPendingChange; dispatch.textContent = state.auth.allowed @@ -2648,9 +2480,7 @@ function hasValidStrategySelection(platform = state.selected) { ? (hasValidReserve ? (hasValidIncomeLayer ? (hasValidOptionOverlay - ? (hasValidDca - ? (hasValidIbitZscoreExit ? (hasPendingChange ? t("readyNote") : "") : t("invalidIbitZscoreExitNote")) - : t("invalidDcaNote")) + ? (hasValidDca ? (hasPendingChange ? t("readyNote") : "") : t("invalidDcaNote")) : t("invalidOptionOverlayNote")) : t("invalidIncomeLayerNote")) : (executionCashPolicyConflict(state.forms[state.selected]) @@ -2782,7 +2612,6 @@ function hasValidStrategySelection(platform = state.selected) { : "", dca_mode: item.dca_mode ? normalizeDcaMode(item.dca_mode) : "", dca_base_investment_usd: cleanDisplayPositiveNumber(item.dca_base_investment_usd), - ibit_zscore_exit_mode: cleanIbitZscoreExitMode(item.ibit_zscore_exit_mode), })); } return normalized; @@ -2814,7 +2643,6 @@ function hasValidStrategySelection(platform = state.selected) { const runtimeTargetEnabled = cleanOptionalBoolean(entry?.runtime_target_enabled); const dcaMode = entry?.dca_mode ? normalizeDcaMode(entry.dca_mode) : ""; const dcaBaseInvestmentUsd = cleanDisplayPositiveNumber(entry?.dca_base_investment_usd); - const ibitZscoreExitMode = cleanIbitZscoreExitMode(entry?.ibit_zscore_exit_mode); const executionMode = normalizeExecutionMode(entry?.execution_mode, entry?.dry_run_only); if ( !profile && @@ -2828,7 +2656,6 @@ function hasValidStrategySelection(platform = state.selected) { runtimeTargetEnabled === null && !dcaMode && !dcaBaseInvestmentUsd && - !ibitZscoreExitMode && !executionMode ) continue; normalized[platform][String(key)] = { @@ -2845,7 +2672,6 @@ function hasValidStrategySelection(platform = state.selected) { runtime_target_enabled: runtimeTargetEnabled, dca_mode: dcaMode, dca_base_investment_usd: dcaBaseInvestmentUsd, - ibit_zscore_exit_mode: ibitZscoreExitMode, source: entry?.source ? String(entry.source) : "", }; } @@ -2912,7 +2738,6 @@ function hasValidStrategySelection(platform = state.selected) { state.forms[state.selected].optionOverlayTouched = false; state.forms[state.selected].cashOnlyExecutionTouched = false; state.forms[state.selected].dcaTouched = false; - state.forms[state.selected].ibitZscoreExitTouched = false; state.forms[state.selected].strategyTouched = false; syncStrategyForAccount(state.selected); render(); @@ -2924,11 +2749,9 @@ function hasValidStrategySelection(platform = state.selected) { state.forms[state.selected].incomeLayerTouched = false; state.forms[state.selected].optionOverlayTouched = false; state.forms[state.selected].dcaTouched = false; - state.forms[state.selected].ibitZscoreExitTouched = false; syncIncomeLayerForAccount(state.selected); syncOptionOverlayForAccount(state.selected); syncDcaForAccount(state.selected); - syncIbitZscoreExitForAccount(state.selected); render(); }); @@ -2943,15 +2766,6 @@ function hasValidStrategySelection(platform = state.selected) { el("plugin-mode-select").addEventListener("change", () => { const form = state.forms[state.selected]; form.pluginMode = normalizePluginMode(el("plugin-mode-select").value); - form.ibitZscoreExitTouched = false; - syncIbitZscoreExitForAccount(state.selected); - render(); - }); - - el("ibit-zscore-exit-mode-select").addEventListener("change", () => { - const form = state.forms[state.selected]; - form.ibitZscoreExitTouched = true; - form.ibitZscoreExitMode = normalizeIbitZscoreExitMode(el("ibit-zscore-exit-mode-select").value); render(); }); diff --git a/web/strategy-switch-console/app_js.js b/web/strategy-switch-console/app_js.js index bf5d907..5344a5a 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 = {\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 = {\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 = {\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: true,\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 ibitZscoreExitModes = [\"paper\", \"live\", \"disabled\"];\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 = {\n nasdaq_sp500_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n ibit_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n crypto_btc_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"100\" },\n };\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 = [\n { profile: \"tqqq_growth_income\", label: \"NASDAQ Growth Income\", label_en: \"TQQQ Growth Income\", label_zh: \"纳斯达克增长收益\", domain: \"us_equity\", runtime_enabled: true, income_layer_enabled: true, income_layer_start_usd: \"250000\", income_layer_max_ratio: \"0.55\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: true, option_growth_overlay_recipe: \"tqqq_leaps_growth_v1\", option_growth_overlay_start_usd: \"250000\", option_growth_overlay_nav_budget_ratio: \"0.03\", income_layer_allocations: { SCHD: 0.3, DGRO: 0.2, SGOV: 0.4, SPYI: 0.08, QQQI: 0.02 } },\n { profile: \"soxl_soxx_trend_income\", label: \"Semiconductor Trend Income\", label_en: \"SOXL/SOXX Semiconductor Trend Income\", label_zh: \"半导体趋势收益\", domain: \"us_equity\", runtime_enabled: true, income_layer_enabled: true, income_layer_start_usd: \"150000\", income_layer_max_ratio: \"0.95\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: false, income_layer_allocations: { SCHD: 0.15, DGRO: 0.1, SGOV: 0.7, SPYI: 0.04, QQQI: 0.01 } },\n { profile: \"nasdaq_sp500_smart_dca\", label: \"NASDAQ/S&P 500 DCA\", label_en: \"Nasdaq 100 / S&P 500 DCA\", label_zh: \"纳指标普定投\", domain: \"us_equity\", runtime_enabled: true, dca_enabled: true, dca_default_mode: \"fixed\", dca_default_base_investment_usd: \"1000\" },\n { profile: \"ibit_smart_dca\", label: \"IBIT Bitcoin DCA\", label_en: \"IBIT Bitcoin ETF DCA\", label_zh: \"IBIT比特币定投\", domain: \"us_equity\", runtime_enabled: true, dca_enabled: true, dca_default_mode: \"fixed\", dca_default_base_investment_usd: \"1000\" },\n { profile: \"global_etf_rotation\", label: \"Global ETF Rotation\", label_en: \"Global ETF Rotation\", label_zh: \"全球ETF轮动\", domain: \"us_equity\", runtime_enabled: true, income_layer_enabled: true, income_layer_start_usd: \"500000\", income_layer_max_ratio: \"0.15\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: true, option_growth_overlay_recipe: \"spy_leaps_growth_v1\", option_growth_overlay_start_usd: \"500000\", option_growth_overlay_nav_budget_ratio: \"0.015\", income_layer_allocations: { SCHD: 0.4, DGRO: 0.25, SGOV: 0.3, SPYI: 0.05 } },\n { profile: \"russell_top50_leader_rotation\", label: \"Russell Top50 Leaders\", label_en: \"Russell Top50 Leader Rotation\", label_zh: \"罗素Top50领涨\", domain: \"us_equity\", runtime_enabled: true, income_layer_enabled: true, income_layer_start_usd: \"300000\", income_layer_max_ratio: \"0.25\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: true, option_growth_overlay_recipe: \"spy_leaps_growth_v1\", option_growth_overlay_start_usd: \"300000\", option_growth_overlay_nav_budget_ratio: \"0.015\", income_layer_allocations: { SCHD: 0.45, DGRO: 0.3, SGOV: 0.25 } },\n { profile: \"us_equity_combo\", label: \"US Core Combo\", label_en: \"US Equity Combo\", label_zh: \"美股核心组合\", domain: \"us_equity\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\", income_layer_enabled: true, income_layer_start_usd: \"300000\", income_layer_max_ratio: \"0.25\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: true, option_growth_overlay_recipe: \"spy_leaps_growth_v1\", option_growth_overlay_start_usd: \"300000\", option_growth_overlay_nav_budget_ratio: \"0.015\", income_layer_allocations: { SCHD: 0.25, DGRO: 0.25, SGOV: 0.2, SPYI: 0.15, QQQI: 0.15 } },\n { profile: \"us_equity_combo_leveraged\", label: \"US Alpha Combo\", label_en: \"US Equity Combo Leveraged\", label_zh: \"美股Alpha组合\", domain: \"us_equity\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\" },\n { profile: \"hk_global_etf_tactical_rotation\", label: \"HK ETF Tactical Rotation\", label_en: \"HK Global ETF Tactical Rotation\", label_zh: \"港股ETF战术轮动\", domain: \"hk_equity\", runtime_enabled: true },\n { profile: \"hk_low_vol_dividend_quality_snapshot\", label: \"HK Dividend Quality\", label_en: \"HK Low-Vol Dividend Quality Snapshot\", label_zh: \"港股红利质量\", domain: \"hk_equity\", runtime_enabled: true },\n { profile: \"hk_equity_combo\", label: \"HK Core Combo\", label_en: \"HK Equity Combo\", label_zh: \"港股核心组合\", domain: \"hk_equity\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\" },\n { profile: \"cn_industry_etf_rotation_aggressive\", label: \"CN ETF Rotation\", label_en: \"CN Industry ETF Rotation Aggressive\", label_zh: \"A股ETF轮动\", domain: \"cn_equity\", runtime_enabled: true },\n { profile: \"cn_stock_momentum_rotation\", label: \"CN Stock Momentum\", label_en: \"CN Stock Momentum\", label_zh: \"A股个股动量\", domain: \"cn_equity\", runtime_enabled: true },\n { profile: \"cn_dividend_quality_snapshot\", label: \"CN Dividend Quality\", label_en: \"CN Dividend Quality Snapshot\", label_zh: \"A股红利质量\", domain: \"cn_equity\", runtime_enabled: true },\n { profile: \"cn_equity_combo\", label: \"CN Alpha Combo\", label_en: \"CN Equity Combo\", label_zh: \"A股Alpha组合\", domain: \"cn_equity\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\" },\n { profile: \"crypto_btc_dca\", label: \"BTC DCA\", label_en: \"Crypto BTC DCA\", label_zh: \"BTC定投\", domain: \"crypto\", runtime_enabled: true },\n { profile: \"crypto_trend_rotation\", label: \"Altcoin Trend\", label_en: \"Crypto Trend Rotation\", label_zh: \"山寨趋势轮动\", domain: \"crypto\", runtime_enabled: true },\n { profile: \"crypto_equity_combo\", label: \"Crypto Core Combo\", label_en: \"Crypto Equity Combo\", label_zh: \"加密核心组合\", domain: \"crypto\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\" }\n ];\n\n const localStrategyLabels = {\n tqqq_growth_income: { zh: \"TQQQ 增长收益\", en: \"TQQQ Growth Income\" },\n soxl_soxx_trend_income: { zh: \"SOXL/SOXX 半导体趋势收益\", en: \"SOXL/SOXX Semiconductor Trend Income\" },\n nasdaq_sp500_smart_dca: { zh: \"纳指100 / 标普500 定投\", en: \"Nasdaq 100 / S&P 500 DCA\" },\n ibit_smart_dca: { zh: \"IBIT 比特币定投\", en: \"IBIT Bitcoin ETF DCA\" },\n global_etf_rotation: { zh: \"全球 ETF 轮动\", en: \"Global ETF Rotation\" },\n russell_top50_leader_rotation: { zh: \"罗素 Top50 领涨轮动\", en: \"Russell Top50 Leader Rotation\" },\n hk_global_etf_tactical_rotation: { zh: \"港股全球 ETF 战术轮动\", en: \"HK Global ETF Tactical Rotation\" },\n hk_low_vol_dividend_quality_snapshot: { zh: \"港股低波红利质量快照\", en: \"HK Low-Vol Dividend Quality Snapshot\" },\n cn_industry_etf_rotation: { zh: \"A股行业 ETF 轮动\", en: \"CN Industry ETF Rotation\" },\n cn_dividend_quality_snapshot: { zh: \"A股红利质量快照\", en: \"CN Dividend Quality Snapshot\" },\n };\n\n const fallbackIncomeLayerDefaults = {\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 = {\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 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 ibitZscoreExitMode: \"IBIT Z-Score 逃顶\",\n ibitZscoreExitPaper: \"观察模式(不卖出)\",\n ibitZscoreExitLive: \"实盘执行(按信号调仓)\",\n ibitZscoreExitDisabled: \"禁用逃顶(只买不卖)\",\n ibitZscoreExitModeMeta: \"插件启用后默认实盘消费动态 Z-Score 信号;可改为观察模式,或禁用后只买不卖。\",\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 invalidIbitZscoreExitNote: \"请选择有效的 IBIT Z-Score 逃顶模式。\",\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 currentIbitZscoreExit: \"当前 Z-Score 逃顶\",\n pendingIbitZscoreExit: \"待提交 Z-Score 逃顶\",\n unchanged: \"不变\",\n copied: \"已复制状态\",\n dispatching: \"正在触发工作流...\",\n dispatched: \"已触发工作流\",\n dispatchFailed: \"触发失败\",\n targetMeta: \"目标 {target} · 服务 {service} · 市场 {domains}\",\n strategyMeta: \"该账号仅显示 {domains} 策略\",\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 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 ibitZscoreExitMode: \"IBIT Z-Score exit\",\n ibitZscoreExitPaper: \"Paper mode (signals only)\",\n ibitZscoreExitLive: \"Live execution (rebalance on signals)\",\n ibitZscoreExitDisabled: \"Disabled (buy only)\",\n ibitZscoreExitModeMeta: \"When plugins are enabled, live consumption is the default. Paper mode observes only; disabled mode keeps buy-only DCA.\",\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; reserve-cash overrides are disabled.\",\n executionCashReserveBlocksMargin: \"Reserve-cash override is active; allow margin Yes is disabled.\",\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 invalidIbitZscoreExitNote: \"Choose a valid IBIT Z-Score exit mode.\",\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 currentIbitZscoreExit: \"Current Z-Score exit\",\n pendingIbitZscoreExit: \"Pending Z-Score exit\",\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 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 ibitZscoreExitMode: \"live\",\n ibitZscoreExitTouched: 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 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 || !executionCashPolicyConflict(form)) return;\n if (changed === \"margin\") {\n form.reservePolicyMode = \"none\";\n form.reservedCashTouched = true;\n form.minReservedCashUsd = \"\";\n form.reservedCashRatio = \"\";\n } else if (changed === \"reserve\") {\n form.cashOnlyExecutionMode = \"enabled\";\n form.cashOnlyExecutionTouched = true;\n }\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 if (item?.runtime_enabled === false || item?.live_enabled === false) 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: true,\n };\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 strategyAllowedForAccount(platform, account, profile) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalogEntry = strategyCatalog[cleanProfile];\n if (!catalogEntry || catalogEntry.runtime_enabled !== true) return false;\n if (dcaConfigForStrategy(cleanProfile) && !platformSupportsDca(platform)) return false;\n return supportedDomainsForAccount(platform, account).includes(catalogEntry.domain);\n }\n\n function strategyChoicesForAccount(platform = state.selected, account = selectedAccount(platform)) {\n const choices = strategyOptions.filter((profile) => strategyAllowedForAccount(platform, account, profile));\n const addChoice = (value) => {\n const profile = cleanStrategyProfile(value);\n if (profile && !choices.includes(profile) && strategyAllowedForAccount(platform, account, profile)) {\n choices.push(profile);\n }\n };\n if (account) {\n addChoice(account.default_strategy_profile || account.strategy_profile);\n addChoice(currentStrategyForAccount(platform, account));\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 ibitZscoreExitSupported(profile) {\n return cleanStrategyProfile(profile) === \"ibit_smart_dca\";\n }\n\n function cleanIbitZscoreExitMode(value) {\n const mode = String(value || \"\").trim().toLowerCase();\n const aliases = {\n off: \"disabled\",\n none: \"disabled\",\n false: \"disabled\",\n disable: \"disabled\",\n enabled: \"live\",\n shadow: \"paper\",\n dry_run: \"paper\",\n \"dry-run\": \"paper\",\n };\n const normalized = aliases[mode] || mode;\n return ibitZscoreExitModes.includes(normalized) ? normalized : \"\";\n }\n\n function normalizeIbitZscoreExitMode(value) {\n return cleanIbitZscoreExitMode(value) || \"live\";\n }\n\n function ibitZscoreExitModeLabel(mode) {\n const normalized = normalizeIbitZscoreExitMode(mode);\n if (normalized === \"live\") return t(\"ibitZscoreExitLive\");\n if (normalized === \"disabled\") return t(\"ibitZscoreExitDisabled\");\n return t(\"ibitZscoreExitPaper\");\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 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 for (const key of keys) {\n const entry = byPlatform[key];\n if (currentEntryHasState(entry)) {\n if (!entry.strategy_profile) {\n const gd = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {};\n entry.strategy_profile = account?.default_strategy_profile || gd.default_strategy_profile || \"\";\n entry.source = (entry.source || \"worker\") + \"+account_defaults\";\n }\n return entry;\n }\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 cleanIbitZscoreExitMode(entry.ibit_zscore_exit_mode) ||\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 currentIbitZscoreExitForAccount(platform, account, profile = state.forms[platform]?.strategy) {\n if (!ibitZscoreExitSupported(profile)) return { supported: false, mode: \"\" };\n const entry = currentEntryForAccount(platform, account);\n const mode = cleanIbitZscoreExitMode(entry?.ibit_zscore_exit_mode) ||\n cleanIbitZscoreExitMode(account?.ibit_zscore_exit_mode);\n if (mode) return { supported: true, mode };\n if (currentPluginModeForAccount(platform, account) === \"none\") {\n return { supported: true, mode: \"disabled\" };\n }\n return { supported: true, 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 === \"true\" || value === \"1\" || value === 1) return true;\n if (value === false || value === \"false\" || value === \"0\" || value === 0) return false;\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 = \"tqqq_growth_income\") {\n const currentProfile = currentStrategyForAccount(platform, account);\n if (currentProfile && strategyAllowedForAccount(platform, account, 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 syncDcaForAccount(platform);\n syncIbitZscoreExitForAccount(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 ? \"enabled\" : \"disabled\";\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 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 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 syncIbitZscoreExitForAccount(platform) {\n const form = state.forms[platform];\n if (!form) return;\n if (!ibitZscoreExitSupported(form.strategy) || normalizePluginMode(form.pluginMode) === \"none\") {\n form.ibitZscoreExitMode = \"disabled\";\n return;\n }\n if (form.ibitZscoreExitTouched) return;\n const current = currentIbitZscoreExitForAccount(platform, selectedAccount(platform), form.strategy);\n form.ibitZscoreExitMode = current.mode || \"live\";\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].ibitZscoreExitTouched = 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 syncIbitZscoreExitForAccount(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));\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 hasValidIbitZscoreExitPolicy() { return true; }\n\nfunction hasValidStrategySelection(platform = state.selected) {\n return hasRunnableStrategySelection(platform) &&\n hasValidExecutionCashPolicy(platform) &&\n hasValidIncomeLayerPolicy(platform) &&\n hasValidOptionOverlayPolicy(platform) &&\n hasValidDcaPolicy(platform) &&\n hasValidIbitZscoreExitPolicy(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 ibitZscoreExitOverrideForForm(form) {\n if (!ibitZscoreExitSupported(form?.strategy)) return null;\n const mode = normalizePluginMode(form?.pluginMode) === \"none\"\n ? \"disabled\"\n : normalizeIbitZscoreExitMode(form?.ibitZscoreExitMode);\n return { inputs: {}, extraVariables: { ibit_zscore_exit_mode: mode } };\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 inputs.ibit_zscore_exit_mode = normalizeDcaMode(form.dcaMode) === \"smart\" ? \"enabled\" : \"disabled\";\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 currentIbitZscoreExitText(platform = state.selected, account = selectedAccount(platform), profile = state.forms[platform]?.strategy) {\n const current = currentIbitZscoreExitForAccount(platform, account, profile);\n if (!current.supported) return \"\";\n return current.mode ? ibitZscoreExitModeLabel(current.mode) : t(\"notRead\");\n }\n\n function pendingIbitZscoreExitText(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const pending = pendingIbitZscoreExit(inputs, platform, account);\n if (!pending.supported) return \"\";\n if (!pending.changed) return t(\"unchanged\");\n return ibitZscoreExitModeLabel(pending.inputs.ibit_zscore_exit_mode);\n }\n\n function ibitZscoreExitModeFromInputs(inputs) {\n try {\n const payload = inputs?.extra_variables_json ? JSON.parse(inputs.extra_variables_json) : {};\n return cleanIbitZscoreExitMode(payload.ibit_zscore_exit_mode || inputs?.ibit_zscore_exit_mode);\n } catch {\n return cleanIbitZscoreExitMode(inputs?.ibit_zscore_exit_mode);\n }\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 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 pendingIbitZscoreExit(inputs, platform = state.selected, account = selectedAccount(platform)) {\n const profile = cleanStrategyProfile(inputs.strategy_profile || state.forms[platform]?.strategy);\n if (!ibitZscoreExitSupported(profile)) return { supported: false, changed: false, inputs: {} };\n const current = currentIbitZscoreExitForAccount(platform, account, profile);\n const nextMode = ibitZscoreExitModeFromInputs(inputs) ||\n (normalizePluginMode(inputs.plugin_mode) === \"none\" ? \"disabled\" : \"live\");\n return {\n supported: true,\n changed: current.mode ? current.mode !== nextMode : true,\n inputs: { ibit_zscore_exit_mode: nextMode },\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 const ibitZscoreExit = pendingIbitZscoreExit(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 ibitZscoreExitChanged: ibitZscoreExit.changed,\n runtimeTarget,\n reserve,\n income,\n optionOverlay,\n cashOnly,\n dca,\n ibitZscoreExit,\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 changes.ibitZscoreExitChanged,\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 (ibitZscoreExitSupported(inputs.strategy_profile)) {\n rows.push([t(\"currentIbitZscoreExit\"), currentIbitZscoreExitText(state.selected, account, inputs.strategy_profile)]);\n }\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.ibitZscoreExitChanged) {\n rows.push([t(\"pendingIbitZscoreExit\"), pendingIbitZscoreExitText(inputs, state.selected, account), \"pending\"]);\n }\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);\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 ibitZscoreExitModeSelect = el(\"ibit-zscore-exit-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 ibitZscoreExitModeSelect.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(\"ibit-zscore-exit-mode-block\").hidden = true;\n el(\"ibit-zscore-exit-mode-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)) {\n form.strategy = choices[0];\n }\n strategySelect.disabled = !choices.length;\n strategySelect.replaceChildren();\n if (choices.length) {\n for (const strategy of choices) {\n strategySelect.append(new Option(strategyLabel(strategy), strategy, false, strategy === form.strategy));\n }\n } else {\n strategySelect.append(new Option(t(\"noStrategy\"), \"\"));\n }\n el(\"strategy-meta\").textContent = account\n ? t(\"strategyMeta\").replace(\"{domains}\", supportedDomainLabel(platform, account))\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 syncIbitZscoreExitForAccount(platform);\n const ibitZscoreSupported = ibitZscoreExitSupported(form.strategy);\n const ibitZscoreBlock = el(\"ibit-zscore-exit-mode-block\");\n ibitZscoreBlock.hidden = true;\n ibitZscoreExitModeSelect.replaceChildren();\n if (ibitZscoreSupported) {\n const pluginsDisabled = normalizePluginMode(form.pluginMode) === \"none\";\n if (pluginsDisabled) form.ibitZscoreExitMode = \"disabled\";\n form.ibitZscoreExitMode = pluginsDisabled ? \"disabled\" : normalizeIbitZscoreExitMode(form.ibitZscoreExitMode);\n ibitZscoreExitModeSelect.disabled = pluginsDisabled;\n for (const mode of ibitZscoreExitModes) {\n ibitZscoreExitModeSelect.append(new Option(\n ibitZscoreExitModeLabel(mode),\n mode,\n false,\n mode === form.ibitZscoreExitMode,\n ));\n }\n el(\"ibit-zscore-exit-mode-meta\").textContent = t(\"ibitZscoreExitModeMeta\");\n } else {\n ibitZscoreExitModeSelect.disabled = true;\n ibitZscoreExitModeSelect.append(new Option(t(\"ibitZscoreExitDisabled\"), \"disabled\"));\n el(\"ibit-zscore-exit-mode-meta\").textContent = \"\";\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 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 = marginBlocksReserve;\n minReservedCashInput.disabled = marginBlocksReserve || reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"ratio\";\n reservedCashRatioInput.disabled = marginBlocksReserve || 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 syncCashOnlyExecutionForAccount(platform);\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 if (mode === \"disabled\" && reserveBlocksMargin) option.disabled = true;\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 hasValidIbitZscoreExit = hasValidIbitZscoreExitPolicy();\n const hasValidStrategy = hasRunnableStrategy &&\n hasValidReserve &&\n hasValidIncomeLayer &&\n hasValidOptionOverlay &&\n hasValidDca &&\n hasValidIbitZscoreExit;\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\n ? (hasValidIbitZscoreExit ? (hasPendingChange ? t(\"readyNote\") : \"\") : t(\"invalidIbitZscoreExitNote\"))\n : t(\"invalidDcaNote\"))\n : t(\"invalidOptionOverlayNote\"))\n : t(\"invalidIncomeLayerNote\"))\n : (executionCashPolicyConflict(state.forms[state.selected])\n ? t(\"invalidExecutionCashPolicyNote\")\n : t(\"invalidReservePolicyNote\")))\n : t(\"invalidStrategyNote\"))\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 response = await fetch(\"/api/session\", { cache: \"no-store\" });\n if (!response.ok) throw new Error(\"no backend\");\n const session = await response.json();\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 response = await fetch(\"/api/strategy-profiles\", { cache: \"no-store\" });\n if (!response.ok) throw new Error(\"no strategy profiles\");\n const payload = await response.json();\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 response = await fetch(\"/api/config\", { cache: \"no-store\" });\n if (!response.ok) throw new Error(\"no config\");\n const payload = await response.json();\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 {\n state.configSource = \"default\";\n state.currentStrategies = {};\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 ibit_zscore_exit_mode: cleanIbitZscoreExitMode(item.ibit_zscore_exit_mode),\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 ibitZscoreExitMode = cleanIbitZscoreExitMode(entry?.ibit_zscore_exit_mode);\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 !ibitZscoreExitMode &&\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 ibit_zscore_exit_mode: ibitZscoreExitMode,\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].ibitZscoreExitTouched = 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 state.forms[state.selected].ibitZscoreExitTouched = false;\n syncIncomeLayerForAccount(state.selected);\n syncOptionOverlayForAccount(state.selected);\n syncDcaForAccount(state.selected);\n syncIbitZscoreExitForAccount(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 form.ibitZscoreExitTouched = false;\n syncIbitZscoreExitForAccount(state.selected);\n render();\n });\n\n el(\"ibit-zscore-exit-mode-select\").addEventListener(\"change\", () => {\n const form = state.forms[state.selected];\n form.ibitZscoreExitTouched = true;\n form.ibitZscoreExitMode = normalizeIbitZscoreExitMode(el(\"ibit-zscore-exit-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 reconcileExecutionCashPolicy(form, \"margin\");\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 await refreshStrategyProfiles();\n await refreshSession();\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 = {\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 = {\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 = {\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 = {\n nasdaq_sp500_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n ibit_smart_dca: { defaultMode: \"fixed\", defaultBaseInvestmentUsd: \"1000\" },\n };\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 = [\n { profile: \"tqqq_growth_income\", label: \"NASDAQ Growth Income\", label_en: \"TQQQ Growth Income\", label_zh: \"纳斯达克增长收益\", domain: \"us_equity\", runtime_enabled: true, income_layer_enabled: true, income_layer_start_usd: \"250000\", income_layer_max_ratio: \"0.55\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: true, option_growth_overlay_recipe: \"tqqq_leaps_growth_v1\", option_growth_overlay_start_usd: \"250000\", option_growth_overlay_nav_budget_ratio: \"0.03\", income_layer_allocations: { SCHD: 0.3, DGRO: 0.2, SGOV: 0.4, SPYI: 0.08, QQQI: 0.02 } },\n { profile: \"soxl_soxx_trend_income\", label: \"Semiconductor Trend Income\", label_en: \"SOXL/SOXX Semiconductor Trend Income\", label_zh: \"半导体趋势收益\", domain: \"us_equity\", runtime_enabled: true, income_layer_enabled: true, income_layer_start_usd: \"150000\", income_layer_max_ratio: \"0.95\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: false, income_layer_allocations: { SCHD: 0.15, DGRO: 0.1, SGOV: 0.7, SPYI: 0.04, QQQI: 0.01 } },\n { profile: \"nasdaq_sp500_smart_dca\", label: \"NASDAQ/S&P 500 DCA\", label_en: \"Nasdaq 100 / S&P 500 DCA\", label_zh: \"纳指标普定投\", domain: \"us_equity\", runtime_enabled: true, dca_enabled: true, dca_default_mode: \"fixed\", dca_default_base_investment_usd: \"1000\" },\n { profile: \"ibit_smart_dca\", label: \"IBIT Bitcoin DCA\", label_en: \"IBIT Bitcoin ETF DCA\", label_zh: \"IBIT比特币定投\", domain: \"us_equity\", runtime_enabled: true, dca_enabled: true, dca_default_mode: \"fixed\", dca_default_base_investment_usd: \"1000\" },\n { profile: \"global_etf_rotation\", label: \"Global ETF Rotation\", label_en: \"Global ETF Rotation\", label_zh: \"全球ETF轮动\", domain: \"us_equity\", runtime_enabled: true, income_layer_enabled: true, income_layer_start_usd: \"500000\", income_layer_max_ratio: \"0.15\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: true, option_growth_overlay_recipe: \"spy_leaps_growth_v1\", option_growth_overlay_start_usd: \"500000\", option_growth_overlay_nav_budget_ratio: \"0.015\", income_layer_allocations: { SCHD: 0.4, DGRO: 0.25, SGOV: 0.3, SPYI: 0.05 } },\n { profile: \"russell_top50_leader_rotation\", label: \"Russell Top50 Leaders\", label_en: \"Russell Top50 Leader Rotation\", label_zh: \"罗素Top50领涨\", domain: \"us_equity\", runtime_enabled: true, income_layer_enabled: true, income_layer_start_usd: \"300000\", income_layer_max_ratio: \"0.25\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: true, option_growth_overlay_recipe: \"spy_leaps_growth_v1\", option_growth_overlay_start_usd: \"300000\", option_growth_overlay_nav_budget_ratio: \"0.015\", income_layer_allocations: { SCHD: 0.45, DGRO: 0.3, SGOV: 0.25 } },\n { profile: \"us_equity_combo\", label: \"US Core Combo\", label_en: \"US Equity Combo\", label_zh: \"美股核心组合\", domain: \"us_equity\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\", income_layer_enabled: true, income_layer_start_usd: \"300000\", income_layer_max_ratio: \"0.25\", option_overlay_enabled: true, option_overlay_live_gate: \"promotion_required\", option_overlay_live_status: \"research_only\", option_growth_overlay_enabled: true, option_growth_overlay_recipe: \"spy_leaps_growth_v1\", option_growth_overlay_start_usd: \"300000\", option_growth_overlay_nav_budget_ratio: \"0.015\", income_layer_allocations: { SCHD: 0.25, DGRO: 0.25, SGOV: 0.2, SPYI: 0.15, QQQI: 0.15 } },\n { profile: \"us_equity_combo_leveraged\", label: \"US Alpha Combo\", label_en: \"US Equity Combo Leveraged\", label_zh: \"美股Alpha组合\", domain: \"us_equity\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\" },\n { profile: \"hk_global_etf_tactical_rotation\", label: \"HK ETF Tactical Rotation\", label_en: \"HK Global ETF Tactical Rotation\", label_zh: \"港股ETF战术轮动\", domain: \"hk_equity\", runtime_enabled: true },\n { profile: \"hk_low_vol_dividend_quality_snapshot\", label: \"HK Dividend Quality\", label_en: \"HK Low-Vol Dividend Quality Snapshot\", label_zh: \"港股红利质量\", domain: \"hk_equity\", runtime_enabled: true },\n { profile: \"hk_equity_combo\", label: \"HK Core Combo\", label_en: \"HK Equity Combo\", label_zh: \"港股核心组合\", domain: \"hk_equity\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\" },\n { profile: \"cn_industry_etf_rotation_aggressive\", label: \"CN ETF Rotation\", label_en: \"CN Industry ETF Rotation Aggressive\", label_zh: \"A股ETF轮动\", domain: \"cn_equity\", runtime_enabled: true },\n { profile: \"cn_stock_momentum_rotation\", label: \"CN Stock Momentum\", label_en: \"CN Stock Momentum\", label_zh: \"A股个股动量\", domain: \"cn_equity\", runtime_enabled: true },\n { profile: \"cn_dividend_quality_snapshot\", label: \"CN Dividend Quality\", label_en: \"CN Dividend Quality Snapshot\", label_zh: \"A股红利质量\", domain: \"cn_equity\", runtime_enabled: true },\n { profile: \"cn_equity_combo\", label: \"CN Alpha Combo\", label_en: \"CN Equity Combo\", label_zh: \"A股Alpha组合\", domain: \"cn_equity\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\" },\n { profile: \"crypto_btc_dca\", label: \"BTC DCA\", label_en: \"Crypto BTC DCA\", label_zh: \"BTC定投\", domain: \"crypto\", runtime_enabled: true },\n { profile: \"crypto_trend_rotation\", label: \"Altcoin Trend\", label_en: \"Crypto Trend Rotation\", label_zh: \"山寨趋势轮动\", domain: \"crypto\", runtime_enabled: true },\n { profile: \"crypto_equity_combo\", label: \"Crypto Core Combo\", label_en: \"Crypto Equity Combo\", label_zh: \"加密核心组合\", domain: \"crypto\", runtime_enabled: true, combo_enabled: true, combo_mode: \"dynamic\" }\n ];\n\n const localStrategyLabels = {\n tqqq_growth_income: { zh: \"TQQQ 增长收益\", en: \"TQQQ Growth Income\" },\n soxl_soxx_trend_income: { zh: \"SOXL/SOXX 半导体趋势收益\", en: \"SOXL/SOXX Semiconductor Trend Income\" },\n nasdaq_sp500_smart_dca: { zh: \"纳指100 / 标普500 定投\", en: \"Nasdaq 100 / S&P 500 DCA\" },\n ibit_smart_dca: { zh: \"IBIT 比特币定投\", en: \"IBIT Bitcoin ETF DCA\" },\n global_etf_rotation: { zh: \"全球 ETF 轮动\", en: \"Global ETF Rotation\" },\n russell_top50_leader_rotation: { zh: \"罗素 Top50 领涨轮动\", en: \"Russell Top50 Leader Rotation\" },\n hk_global_etf_tactical_rotation: { zh: \"港股全球 ETF 战术轮动\", en: \"HK Global ETF Tactical Rotation\" },\n hk_low_vol_dividend_quality_snapshot: { zh: \"港股低波红利质量快照\", en: \"HK Low-Vol Dividend Quality Snapshot\" },\n cn_industry_etf_rotation: { zh: \"A股行业 ETF 轮动\", en: \"CN Industry ETF Rotation\" },\n cn_dividend_quality_snapshot: { zh: \"A股红利质量快照\", en: \"CN Dividend Quality Snapshot\" },\n };\n\n const fallbackIncomeLayerDefaults = {\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 = {\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 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 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 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; reserve-cash overrides are disabled.\",\n executionCashReserveBlocksMargin: \"Reserve-cash override is active; allow margin Yes is disabled.\",\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 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 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 || !executionCashPolicyConflict(form)) return;\n if (changed === \"margin\") {\n form.reservePolicyMode = \"none\";\n form.reservedCashTouched = true;\n form.minReservedCashUsd = \"\";\n form.reservedCashRatio = \"\";\n } else if (changed === \"reserve\") {\n form.cashOnlyExecutionMode = \"enabled\";\n form.cashOnlyExecutionTouched = true;\n }\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 if (item?.runtime_enabled === false || item?.live_enabled === false) 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: true,\n };\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 strategyAllowedForAccount(platform, account, profile) {\n const cleanProfile = cleanStrategyProfile(profile);\n const catalogEntry = strategyCatalog[cleanProfile];\n if (!catalogEntry || catalogEntry.runtime_enabled !== true) return false;\n if (dcaConfigForStrategy(cleanProfile) && !platformSupportsDca(platform)) return false;\n return supportedDomainsForAccount(platform, account).includes(catalogEntry.domain);\n }\n\n function strategyChoicesForAccount(platform = state.selected, account = selectedAccount(platform)) {\n const choices = strategyOptions.filter((profile) => strategyAllowedForAccount(platform, account, profile));\n const addChoice = (value) => {\n const profile = cleanStrategyProfile(value);\n if (profile && !choices.includes(profile) && strategyAllowedForAccount(platform, account, profile)) {\n choices.push(profile);\n }\n };\n if (account) {\n addChoice(account.default_strategy_profile || account.strategy_profile);\n addChoice(currentStrategyForAccount(platform, account));\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 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 for (const key of keys) {\n const entry = byPlatform[key];\n if (currentEntryHasState(entry)) {\n if (!entry.strategy_profile) {\n const gd = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {};\n entry.strategy_profile = account?.default_strategy_profile || gd.default_strategy_profile || \"\";\n entry.source = (entry.source || \"worker\") + \"+account_defaults\";\n }\n return entry;\n }\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 === \"true\" || value === \"1\" || value === 1) return true;\n if (value === false || value === \"false\" || value === \"0\" || value === 0) return false;\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 = \"tqqq_growth_income\") {\n const currentProfile = currentStrategyForAccount(platform, account);\n if (currentProfile && strategyAllowedForAccount(platform, account, 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 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 ? \"enabled\" : \"disabled\";\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 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 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));\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 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);\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)) {\n form.strategy = choices[0];\n }\n strategySelect.disabled = !choices.length;\n strategySelect.replaceChildren();\n if (choices.length) {\n for (const strategy of choices) {\n strategySelect.append(new Option(strategyLabel(strategy), strategy, false, strategy === form.strategy));\n }\n } else {\n strategySelect.append(new Option(t(\"noStrategy\"), \"\"));\n }\n el(\"strategy-meta\").textContent = account\n ? t(\"strategyMeta\").replace(\"{domains}\", supportedDomainLabel(platform, account))\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 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 = marginBlocksReserve;\n minReservedCashInput.disabled = marginBlocksReserve || reserveMode === \"current\" || reserveMode === \"none\" || reserveMode === \"ratio\";\n reservedCashRatioInput.disabled = marginBlocksReserve || 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 syncCashOnlyExecutionForAccount(platform);\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 if (mode === \"disabled\" && reserveBlocksMargin) option.disabled = true;\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 : t(\"invalidStrategyNote\"))\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 response = await fetch(\"/api/session\", { cache: \"no-store\" });\n if (!response.ok) throw new Error(\"no backend\");\n const session = await response.json();\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 response = await fetch(\"/api/strategy-profiles\", { cache: \"no-store\" });\n if (!response.ok) throw new Error(\"no strategy profiles\");\n const payload = await response.json();\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 response = await fetch(\"/api/config\", { cache: \"no-store\" });\n if (!response.ok) throw new Error(\"no config\");\n const payload = await response.json();\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 {\n state.configSource = \"default\";\n state.currentStrategies = {};\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 reconcileExecutionCashPolicy(form, \"margin\");\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 await refreshStrategyProfiles();\n await refreshSession();\n }\n"; diff --git a/web/strategy-switch-console/config.js b/web/strategy-switch-console/config.js index bfc43f7..b6ef0bb 100644 --- a/web/strategy-switch-console/config.js +++ b/web/strategy-switch-console/config.js @@ -63,7 +63,7 @@ export const PLATFORM_CONFIG = { "reserved_cash": false, "income_layer": false, "option_overlay": false, - "dca": true, + "dca": false, "execution_mode": "live", "service_name": "", "default_execution_mode": "live" @@ -176,7 +176,7 @@ export const PLATFORM_REPOSITORIES = { "binance": "QuantStrategyLab/BinancePlatform" }; -export const DCA_SUPPORTED_PLATFORMS = new Set(["longbridge", "ibkr", "schwab", "firstrade", "binance"]); +export const DCA_SUPPORTED_PLATFORMS = new Set(["longbridge", "ibkr", "schwab", "firstrade"]); export const DEFAULT_VARIABLE_SCOPES = { "longbridge": "environment", @@ -337,10 +337,6 @@ export const DCA_PROFILE_DEFAULTS = { "ibit_smart_dca": { "defaultMode": "fixed", "defaultBaseInvestmentUsd": "1000" - }, - "crypto_btc_dca": { - "defaultMode": "fixed", - "defaultBaseInvestmentUsd": "100" } }; @@ -444,7 +440,7 @@ export const STRATEGY_FEATURES = { "crypto_btc_dca": { "income_layer": false, "option_overlay": false, - "dca": true, + "dca": false, "combo": false }, "crypto_trend_rotation": { diff --git a/web/strategy-switch-console/index.html b/web/strategy-switch-console/index.html index 6ef504a..035044c 100644 --- a/web/strategy-switch-console/index.html +++ b/web/strategy-switch-console/index.html @@ -10,9 +10,9 @@ @@ -102,11 +102,6 @@

LongBridge

选择是否启用该策略的插件。 -
diff --git a/web/strategy-switch-console/page_asset.js b/web/strategy-switch-console/page_asset.js index 539bf74..a6ac7de 100644 --- a/web/strategy-switch-console/page_asset.js +++ b/web/strategy-switch-console/page_asset.js @@ -1,2 +1,2 @@ // Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand. -export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch\n \n \n\n\n\n\n\n
\n
\n

策略切换

\n

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

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

读取策略配置

\n

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

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

LongBridge

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

现金与融资

\n

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

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

登录后才可执行切换。

\n

\n
\n
\n\n \n
\n
\n\n \n\n\n"; +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/strategy-profiles.example.json b/web/strategy-switch-console/strategy-profiles.example.json index da76c38..3bfffe7 100644 --- a/web/strategy-switch-console/strategy-profiles.example.json +++ b/web/strategy-switch-console/strategy-profiles.example.json @@ -229,10 +229,7 @@ "label_en": "BTC DCA", "label_zh": "BTC定投", "domain": "crypto", - "runtime_enabled": true, - "dca_enabled": true, - "dca_default_mode": "fixed", - "dca_default_base_investment_usd": "100" + "runtime_enabled": true }, { "profile": "crypto_equity_combo", diff --git a/web/strategy-switch-console/strategy_profiles_asset.js b/web/strategy-switch-console/strategy_profiles_asset.js index 459c7b1..c540011 100644 --- a/web/strategy-switch-console/strategy_profiles_asset.js +++ b/web/strategy-switch-console/strategy_profiles_asset.js @@ -1,2 +1,2 @@ // Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand. -export const DEFAULT_STRATEGY_PROFILES = [{"profile": "ibit_smart_dca", "label": "IBIT Bitcoin DCA", "label_en": "IBIT Bitcoin DCA", "label_zh": "IBIT比特币定投", "domain": "us_equity", "runtime_enabled": true, "dca_enabled": true, "dca_default_mode": "fixed", "dca_default_base_investment_usd": "1000"}, {"profile": "global_etf_rotation", "label": "Global ETF Rotation", "label_en": "Global ETF Rotation", "label_zh": "全球ETF轮动", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "500000", "income_layer_max_ratio": "0.15", "income_layer_allocations": {"SCHD": 0.4, "DGRO": 0.25, "SGOV": 0.3, "SPYI": 0.05}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "500000", "option_growth_overlay_nav_budget_ratio": 0.015}, {"profile": "soxl_soxx_trend_income", "label": "Semiconductor Trend Income", "label_en": "Semiconductor Trend Income", "label_zh": "半导体趋势收益", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "150000", "income_layer_max_ratio": "0.95", "income_layer_allocations": {"SCHD": 0.15, "DGRO": 0.1, "SGOV": 0.7, "SPYI": 0.04, "QQQI": 0.01}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_income_overlay_enabled": true, "option_income_overlay_recipe": "soxx_put_credit_spread_income_v1", "option_income_overlay_start_usd": "150000", "option_income_overlay_nav_risk_ratio": 0.01}, {"profile": "nasdaq_sp500_smart_dca", "label": "NASDAQ/S&P 500 DCA", "label_en": "NASDAQ/S&P 500 DCA", "label_zh": "纳指标普定投", "domain": "us_equity", "runtime_enabled": true, "dca_enabled": true, "dca_default_mode": "fixed", "dca_default_base_investment_usd": "1000"}, {"profile": "tqqq_growth_income", "label": "NASDAQ Growth Income", "label_en": "NASDAQ Growth Income", "label_zh": "纳斯达克增长收益", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "250000", "income_layer_max_ratio": "0.55", "income_layer_allocations": {"SCHD": 0.3, "DGRO": 0.2, "SGOV": 0.4, "SPYI": 0.08, "QQQI": 0.02}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "tqqq_leaps_growth_v1", "option_growth_overlay_start_usd": "250000", "option_growth_overlay_nav_budget_ratio": 0.03}, {"profile": "russell_top50_leader_rotation", "label": "Russell Top50 Leaders", "label_en": "Russell Top50 Leaders", "label_zh": "罗素Top50领涨", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "300000", "income_layer_max_ratio": "0.25", "income_layer_allocations": {"SCHD": 0.45, "DGRO": 0.3, "SGOV": 0.25}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "300000", "option_growth_overlay_nav_budget_ratio": 0.015}, {"profile": "us_equity_combo_leveraged", "label": "US Alpha Combo", "label_en": "US Alpha Combo", "label_zh": "美股加速组合", "domain": "us_equity", "runtime_enabled": true, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "us_equity_combo", "label": "US Core Combo", "label_en": "US Core Combo", "label_zh": "美股核心组合", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "300000", "income_layer_max_ratio": "0.25", "income_layer_allocations": {"SCHD": 0.25, "DGRO": 0.25, "SGOV": 0.2, "SPYI": 0.15, "QQQI": 0.15}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "300000", "option_growth_overlay_nav_budget_ratio": 0.015, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "hk_global_etf_tactical_rotation", "label": "HK ETF Tactical Rotation", "label_en": "HK ETF Tactical Rotation", "label_zh": "港股ETF战术轮动", "domain": "hk_equity", "runtime_enabled": true}, {"profile": "hk_equity_combo", "label": "HK Core Combo", "label_en": "HK Core Combo", "label_zh": "港股恒生组合", "domain": "hk_equity", "runtime_enabled": true, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "hk_low_vol_dividend_quality_snapshot", "label": "HK Dividend Quality", "label_en": "HK Dividend Quality", "label_zh": "港股红利质量", "domain": "hk_equity", "runtime_enabled": true}, {"profile": "cn_industry_etf_rotation_aggressive", "label": "CN ETF Rotation", "label_en": "CN ETF Rotation", "label_zh": "A股ETF轮动", "domain": "cn_equity", "runtime_enabled": true}, {"profile": "cn_stock_momentum_rotation", "label": "CN Stock Momentum", "label_en": "CN Stock Momentum", "label_zh": "A股个股动量", "domain": "cn_equity", "runtime_enabled": true}, {"profile": "cn_dividend_quality_snapshot", "label": "CN Dividend Quality", "label_en": "CN Dividend Quality", "label_zh": "A股红利质量", "domain": "cn_equity", "runtime_enabled": true}, {"profile": "cn_industry_etf_rotation", "label": "CN Industry ETF Rotation", "label_en": "CN Industry ETF Rotation", "label_zh": "A股行业ETF轮动", "domain": "cn_equity", "runtime_enabled": true}, {"profile": "cn_equity_combo", "label": "CN Alpha Combo", "label_en": "CN Alpha Combo", "label_zh": "A股进取组合", "domain": "cn_equity", "runtime_enabled": true, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "crypto_btc_dca", "label": "BTC DCA", "label_en": "BTC DCA", "label_zh": "BTC定投", "domain": "crypto", "runtime_enabled": true, "dca_enabled": true, "dca_default_mode": "fixed", "dca_default_base_investment_usd": "100"}, {"profile": "crypto_equity_combo", "label": "Crypto Core Combo", "label_en": "Crypto Core Combo", "label_zh": "加密动量组合", "domain": "crypto", "runtime_enabled": true, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "crypto_trend_rotation", "label": "Altcoin Trend", "label_en": "Altcoin Trend", "label_zh": "山寨趋势轮动", "domain": "crypto", "runtime_enabled": true}]; +export const DEFAULT_STRATEGY_PROFILES = [{"profile": "ibit_smart_dca", "label": "IBIT Bitcoin DCA", "label_en": "IBIT Bitcoin DCA", "label_zh": "IBIT比特币定投", "domain": "us_equity", "runtime_enabled": true, "dca_enabled": true, "dca_default_mode": "fixed", "dca_default_base_investment_usd": "1000"}, {"profile": "global_etf_rotation", "label": "Global ETF Rotation", "label_en": "Global ETF Rotation", "label_zh": "全球ETF轮动", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "500000", "income_layer_max_ratio": "0.15", "income_layer_allocations": {"SCHD": 0.4, "DGRO": 0.25, "SGOV": 0.3, "SPYI": 0.05}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "500000", "option_growth_overlay_nav_budget_ratio": 0.015}, {"profile": "soxl_soxx_trend_income", "label": "Semiconductor Trend Income", "label_en": "Semiconductor Trend Income", "label_zh": "半导体趋势收益", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "150000", "income_layer_max_ratio": "0.95", "income_layer_allocations": {"SCHD": 0.15, "DGRO": 0.1, "SGOV": 0.7, "SPYI": 0.04, "QQQI": 0.01}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_income_overlay_enabled": true, "option_income_overlay_recipe": "soxx_put_credit_spread_income_v1", "option_income_overlay_start_usd": "150000", "option_income_overlay_nav_risk_ratio": 0.01}, {"profile": "nasdaq_sp500_smart_dca", "label": "NASDAQ/S&P 500 DCA", "label_en": "NASDAQ/S&P 500 DCA", "label_zh": "纳指标普定投", "domain": "us_equity", "runtime_enabled": true, "dca_enabled": true, "dca_default_mode": "fixed", "dca_default_base_investment_usd": "1000"}, {"profile": "tqqq_growth_income", "label": "NASDAQ Growth Income", "label_en": "NASDAQ Growth Income", "label_zh": "纳斯达克增长收益", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "250000", "income_layer_max_ratio": "0.55", "income_layer_allocations": {"SCHD": 0.3, "DGRO": 0.2, "SGOV": 0.4, "SPYI": 0.08, "QQQI": 0.02}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "tqqq_leaps_growth_v1", "option_growth_overlay_start_usd": "250000", "option_growth_overlay_nav_budget_ratio": 0.03}, {"profile": "russell_top50_leader_rotation", "label": "Russell Top50 Leaders", "label_en": "Russell Top50 Leaders", "label_zh": "罗素Top50领涨", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "300000", "income_layer_max_ratio": "0.25", "income_layer_allocations": {"SCHD": 0.45, "DGRO": 0.3, "SGOV": 0.25}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "300000", "option_growth_overlay_nav_budget_ratio": 0.015}, {"profile": "us_equity_combo_leveraged", "label": "US Alpha Combo", "label_en": "US Alpha Combo", "label_zh": "美股加速组合", "domain": "us_equity", "runtime_enabled": true, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "us_equity_combo", "label": "US Core Combo", "label_en": "US Core Combo", "label_zh": "美股核心组合", "domain": "us_equity", "runtime_enabled": true, "income_layer_enabled": true, "income_layer_start_usd": "300000", "income_layer_max_ratio": "0.25", "income_layer_allocations": {"SCHD": 0.25, "DGRO": 0.25, "SGOV": 0.2, "SPYI": 0.15, "QQQI": 0.15}, "option_overlay_enabled": true, "option_overlay_live_gate": "promotion_required", "option_overlay_live_status": "research_only", "option_growth_overlay_enabled": true, "option_growth_overlay_recipe": "spy_leaps_growth_v1", "option_growth_overlay_start_usd": "300000", "option_growth_overlay_nav_budget_ratio": 0.015, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "hk_global_etf_tactical_rotation", "label": "HK ETF Tactical Rotation", "label_en": "HK ETF Tactical Rotation", "label_zh": "港股ETF战术轮动", "domain": "hk_equity", "runtime_enabled": true}, {"profile": "hk_equity_combo", "label": "HK Core Combo", "label_en": "HK Core Combo", "label_zh": "港股恒生组合", "domain": "hk_equity", "runtime_enabled": true, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "hk_low_vol_dividend_quality_snapshot", "label": "HK Dividend Quality", "label_en": "HK Dividend Quality", "label_zh": "港股红利质量", "domain": "hk_equity", "runtime_enabled": true}, {"profile": "cn_industry_etf_rotation_aggressive", "label": "CN ETF Rotation", "label_en": "CN ETF Rotation", "label_zh": "A股ETF轮动", "domain": "cn_equity", "runtime_enabled": true}, {"profile": "cn_stock_momentum_rotation", "label": "CN Stock Momentum", "label_en": "CN Stock Momentum", "label_zh": "A股个股动量", "domain": "cn_equity", "runtime_enabled": true}, {"profile": "cn_dividend_quality_snapshot", "label": "CN Dividend Quality", "label_en": "CN Dividend Quality", "label_zh": "A股红利质量", "domain": "cn_equity", "runtime_enabled": true}, {"profile": "cn_industry_etf_rotation", "label": "CN Industry ETF Rotation", "label_en": "CN Industry ETF Rotation", "label_zh": "A股行业ETF轮动", "domain": "cn_equity", "runtime_enabled": true}, {"profile": "cn_equity_combo", "label": "CN Alpha Combo", "label_en": "CN Alpha Combo", "label_zh": "A股进取组合", "domain": "cn_equity", "runtime_enabled": true, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "crypto_btc_dca", "label": "BTC DCA", "label_en": "BTC DCA", "label_zh": "BTC定投", "domain": "crypto", "runtime_enabled": true}, {"profile": "crypto_equity_combo", "label": "Crypto Core Combo", "label_en": "Crypto Core Combo", "label_zh": "加密动量组合", "domain": "crypto", "runtime_enabled": true, "combo_enabled": true, "combo_mode": "dynamic"}, {"profile": "crypto_trend_rotation", "label": "Altcoin Trend", "label_en": "Altcoin Trend", "label_zh": "山寨趋势轮动", "domain": "crypto", "runtime_enabled": true}]; diff --git a/web/strategy-switch-console/worker.js b/web/strategy-switch-console/worker.js index c4b4712..1c37514 100644 --- a/web/strategy-switch-console/worker.js +++ b/web/strategy-switch-console/worker.js @@ -100,11 +100,15 @@ const OPTION_OVERLAY_PROFILE_FIELDS = [ "option_overlay_live_status", ]; const OPTION_OVERLAY_MODES = ["enabled", "disabled"]; -const DCA_PROFILE_CONFIG = { - nasdaq_sp500_smart_dca: { default_mode: "fixed", default_base_investment_usd: "1000" }, - ibit_smart_dca: { default_mode: "fixed", default_base_investment_usd: "1000" }, - crypto_btc_dca: { default_mode: "fixed", default_base_investment_usd: "100" }, -}; +const DCA_PROFILE_CONFIG = Object.fromEntries( + Object.entries(DCA_PROFILE_DEFAULTS).map(([profile, defaults]) => [ + profile, + { + default_mode: defaults.defaultMode || "fixed", + default_base_investment_usd: defaults.defaultBaseInvestmentUsd || "1000", + }, + ]), +); const SECURITY_HEADERS = { "Content-Security-Policy": [ "default-src 'self'", @@ -779,7 +783,6 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount const serviceTargetOptionOverlayPayload = optionOverlayPayloadFromObject(serviceTarget); const serviceTargetRuntimeTargetEnabledPayload = runtimeTargetEnabledPayloadFromObject(serviceTarget); const serviceTargetDcaPayload = dcaPayloadFromObject(serviceTarget); - const serviceTargetIbitZscorePayload = ibitZscoreExitPayloadFromObject(serviceTarget); const serviceTargetCashOnlyPayload = cashOnlyPayloadFromObject(platform, serviceTarget); if (serviceTargetProfile) { return { @@ -791,7 +794,6 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount ...serviceTargetRuntimeTargetEnabledPayload, ...serviceTargetCashOnlyPayload, ...dcaPayloadForProfile(serviceTargetProfile, serviceTargetDcaPayload), - ...ibitZscoreExitPayloadForProfile(serviceTargetProfile, serviceTargetIbitZscorePayload), source: "CLOUD_RUN_SERVICE_TARGETS_JSON", variable_scope: "repository", }; @@ -801,8 +803,7 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount Object.keys(serviceTargetIncomeLayerPayload).length || Object.keys(serviceTargetOptionOverlayPayload).length || Object.keys(serviceTargetCashOnlyPayload).length || - Object.keys(serviceTargetRuntimeTargetEnabledPayload).length || - Object.keys(serviceTargetIbitZscorePayload).length + Object.keys(serviceTargetRuntimeTargetEnabledPayload).length ) { return { ...runtimeModePayload(serviceTarget), @@ -811,7 +812,6 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount ...serviceTargetOptionOverlayPayload, ...serviceTargetCashOnlyPayload, ...serviceTargetRuntimeTargetEnabledPayload, - ...serviceTargetIbitZscorePayload, source: "CLOUD_RUN_SERVICE_TARGETS_JSON", variable_scope: "repository", }; @@ -850,12 +850,6 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount githubEnvironment, readVariable, }); - const ibitZscorePayloadPromise = readIbitZscoreExitVariables({ - repository, - variableScope, - githubEnvironment, - readVariable, - }); const cashOnlyPayloadPromise = readCashOnlyVariables({ platform, repository, @@ -877,7 +871,6 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount optionOverlayPayload, runtimeTargetEnabledPayload, dcaPayload, - ibitZscorePayload, cashOnlyPayload, ] = await Promise.all([ reservedCashPayloadPromise, @@ -885,7 +878,6 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount optionOverlayPayloadPromise, runtimeTargetEnabledPayloadPromise, dcaPayloadPromise, - ibitZscorePayloadPromise, cashOnlyPayloadPromise, ]); const runtimeTarget = parseJsonObject(runtimeTargetValue); @@ -900,7 +892,6 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount ...optionOverlayPayload, ...runtimeTargetEnabledPayload, ...dcaPayloadForProfile(runtimeTargetProfile, dcaPayload), - ...ibitZscoreExitPayloadForProfile(runtimeTargetProfile, ibitZscorePayload), ...cashOnlyPayload, source: "RUNTIME_TARGET_JSON", variable_scope: variableScope, @@ -919,7 +910,6 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount ...optionOverlayPayload, ...runtimeTargetEnabledPayload, ...dcaPayloadForProfile(profile, dcaPayload), - ...ibitZscoreExitPayloadForProfile(profile, ibitZscorePayload), ...cashOnlyPayload, source: "STRATEGY_PROFILE", variable_scope: variableScope, @@ -936,24 +926,20 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount Object.keys(reservedCashPayload).length || Object.keys(incomeLayerPayload).length || Object.keys(optionOverlayPayload).length || - Object.keys(runtimeTargetEnabledPayload).length || - Object.keys(ibitZscorePayload).length + Object.keys(runtimeTargetEnabledPayload).length ) { return { ...reservedCashPayload, ...incomeLayerPayload, ...optionOverlayPayload, ...runtimeTargetEnabledPayload, - ...ibitZscorePayload, source: Object.keys(reservedCashPayload).length ? "RESERVED_CASH_VARIABLES" : (Object.keys(incomeLayerPayload).length ? "INCOME_LAYER_VARIABLES" : (Object.keys(runtimeTargetEnabledPayload).length ? "RUNTIME_TARGET_ENABLED_VARIABLE" - : (Object.keys(optionOverlayPayload).length - ? "OPTION_OVERLAY_VARIABLES" - : "IBIT_ZSCORE_EXIT_VARIABLES"))), + : "OPTION_OVERLAY_VARIABLES")), variable_scope: variableScope, github_environment: githubEnvironment || "", }; @@ -1123,13 +1109,18 @@ async function syncStrategyProfilesConfig(env, session) { function updateAccountOptionsDefaultStrategy(accountOptions, inputs) { const options = normalizeAccountOptionsPayload(accountOptions || {}, ACCOUNT_OPTIONS_KEY); const platformOptions = options[inputs.platform] || []; + const rawPlatformOptions = Array.isArray(accountOptions?.[inputs.platform]) ? accountOptions[inputs.platform] : []; let matched = false; let changed = false; - const updatedPlatformOptions = platformOptions.map((option) => { + const updatedPlatformOptions = platformOptions.map((option, index) => { if (!accountOptionMatchesInputs(option, inputs)) return option; matched = true; + const rawOption = rawPlatformOptions[index] && typeof rawPlatformOptions[index] === "object" + ? rawPlatformOptions[index] + : {}; 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; @@ -1175,13 +1166,7 @@ function updateAccountOptionsDefaultStrategy(accountOptions, inputs) { } } } - const ibitZscoreMode = ibitZscoreExitModeFromInputs(inputs); - if (inputs.strategy_profile === "ibit_smart_dca" && ibitZscoreMode) { - if (nextOption.ibit_zscore_exit_mode !== ibitZscoreMode) { - nextOption.ibit_zscore_exit_mode = ibitZscoreMode; - optionChanged = true; - } - } else if ("ibit_zscore_exit_mode" in nextOption) { + if ("ibit_zscore_exit_mode" in nextOption) { delete nextOption.ibit_zscore_exit_mode; optionChanged = true; } @@ -1231,9 +1216,14 @@ function normalizeSwitchInputs(raw) { IBIT_ZSCORE_EXIT_ENABLED_VARIABLE, IBIT_ZSCORE_EXIT_MODE_VARIABLE, IBIT_ZSCORE_EXIT_PARKING_SYMBOL_VARIABLE, + "IBIT_ZSCORE_EXIT_RISK_REDUCED_EXPOSURE", + "IBIT_ZSCORE_EXIT_RISK_OFF_EXPOSURE", + "IBIT_ZSCORE_EXIT_ALLOW_OUTSIDE_EXECUTION_WINDOW", ].filter((name) => extraVariables[name] !== undefined && String(extraVariables[name] || "").trim() !== ""); if (directIbitZscoreVariables.length) { - throw new Error("use ibit_zscore_exit_* control fields instead of IBIT_ZSCORE_EXIT variables"); + throw new Error( + "IBIT_ZSCORE_EXIT variables are derived from ibit_smart_dca smart DCA mode; do not set them directly", + ); } rejectResearchOnlyExtraVariables(extraVariables); const directCashOnlyVariables = [ @@ -1244,7 +1234,7 @@ function normalizeSwitchInputs(raw) { throw new Error("use cash_only_execution_mode instead of CASH_ONLY_EXECUTION variables"); } const dcaExtraControls = dcaPayloadFromObject(extraVariables); - const ibitZscoreExtraControls = ibitZscoreExitPayloadFromObject(extraVariables); + stripLegacyIbitZscoreExitControls(extraVariables); const inputs = { platform, @@ -1291,19 +1281,6 @@ function normalizeSwitchInputs(raw) { "dca_base_investment_usd", ); } - const rawHasIbitZscoreMode = raw.ibit_zscore_exit_mode !== undefined && - raw.ibit_zscore_exit_mode !== null && - String(raw.ibit_zscore_exit_mode).trim() !== ""; - const ibitZscoreModeValue = rawHasIbitZscoreMode - ? raw.ibit_zscore_exit_mode - : ibitZscoreExtraControls.ibit_zscore_exit_mode; - const hasIbitZscoreMode = Boolean(String(ibitZscoreModeValue || "").trim()); - if (strategyProfile !== "ibit_smart_dca" && hasIbitZscoreMode) { - throw new Error("IBIT Z-Score exit settings are only supported for ibit_smart_dca"); - } - if (strategyProfile === "ibit_smart_dca" && hasIbitZscoreMode) { - extraVariables.ibit_zscore_exit_mode = cleanIbitZscoreExitMode(ibitZscoreModeValue); - } const cashOnlyMode = cleanChoice( raw.cash_only_execution_mode || extraVariables.cash_only_execution_mode || "enabled", CASH_ONLY_EXECUTION_MODES, @@ -1675,7 +1652,6 @@ function cleanAccountOption(item, platform, index) { addConfigOptional(option, "cash_only_execution_mode", item.cash_only_execution_mode, (value, field) => cleanChoice(value || "enabled", CASH_ONLY_EXECUTION_MODES, field), ); - addConfigOptional(option, "ibit_zscore_exit_mode", item.ibit_zscore_exit_mode, cleanIbitZscoreExitMode); addConfigOptional(option, "dca_mode", item.dca_mode, cleanDcaMode); addConfigOptional(option, "dca_base_investment_usd", item.dca_base_investment_usd, cleanPositiveNumber); option.supported_domains = shouldInferSupportedDomains(item.supported_domains) @@ -1802,21 +1778,6 @@ function cleanDcaMode(value, field = "dca_mode") { return cleanChoice(normalized, ["fixed", "smart"], field); } -function cleanIbitZscoreExitMode(value, field = "ibit_zscore_exit_mode") { - const mode = String(value || "").trim().toLowerCase(); - const aliases = { - off: "disabled", - none: "disabled", - false: "disabled", - disable: "disabled", - enabled: "live", - shadow: "paper", - dry_run: "paper", - "dry-run": "paper", - }; - return cleanChoice(aliases[mode] || mode, ["disabled", "paper", "live"], field); -} - function cleanBoolean(value) { if (value === true || value === "true") return true; if (value === false || value === "false" || value === "" || value === undefined || value === null) return false; @@ -1947,7 +1908,6 @@ function runtimeTargetFromServiceTargets(rawValue, platform, option) { ...optionOverlayPayloadFromObject(entry), ...runtimeTargetEnabledPayloadFromObject(entry), ...dcaPayloadFromObject(entry), - ...ibitZscoreExitPayloadFromObject(entry), ...cashOnlyPayloadFromObject(platform, entry), }; } @@ -2013,17 +1973,6 @@ async function readDcaVariables({ repository, variableScope, githubEnvironment, return dcaPayloadFromValues(modeValue, baseInvestmentValue); } -async function readIbitZscoreExitVariables({ repository, variableScope, githubEnvironment, readVariable }) { - const [enabledValue, modeValue] = await Promise.all([ - readVariable(repository, variableScope, githubEnvironment, IBIT_ZSCORE_EXIT_ENABLED_VARIABLE), - readVariable(repository, variableScope, githubEnvironment, IBIT_ZSCORE_EXIT_MODE_VARIABLE), - ]); - return ibitZscoreExitPayloadFromObject({ - [IBIT_ZSCORE_EXIT_ENABLED_VARIABLE]: enabledValue, - [IBIT_ZSCORE_EXIT_MODE_VARIABLE]: modeValue, - }); -} - function reservedCashPayloadFromObject(platform, payload) { if (!payload || Array.isArray(payload) || typeof payload !== "object") return {}; return reservedCashPayloadFromValues( @@ -2098,34 +2047,20 @@ function dcaPayloadFromObject(payload) { ); } -function ibitZscoreExitPayloadFromObject(payload) { - if (!payload || Array.isArray(payload) || typeof payload !== "object") return {}; - const mode = cleanCurrentIbitZscoreExitMode( - payload.ibit_zscore_exit_mode ?? - payload[IBIT_ZSCORE_EXIT_MODE_VARIABLE] ?? - payload.ibit_zscore_exit_mode, - payload.ibit_zscore_exit_enabled ?? payload[IBIT_ZSCORE_EXIT_ENABLED_VARIABLE], - ); - return mode ? { ibit_zscore_exit_mode: mode } : {}; -} - -function cleanCurrentIbitZscoreExitMode(modeValue, enabledValue) { - const enabled = cleanOptionalBoolean(enabledValue); - if (enabled === false) return "disabled"; - const text = String(modeValue || "").trim(); - if (!text) return enabled === true ? "live" : ""; - try { - return cleanIbitZscoreExitMode(text); - } catch { - return ""; +function stripLegacyIbitZscoreExitControls(payload) { + if (!payload || Array.isArray(payload) || typeof payload !== "object") return; + for (const field of [ + "ibit_zscore_exit_mode", + "ibit_zscore_exit_enabled", + "ibit_zscore_exit_parking_symbol", + "ibit_zscore_exit_risk_reduced_exposure", + "ibit_zscore_exit_risk_off_exposure", + "ibit_zscore_exit_allow_outside_execution_window", + ]) { + delete payload[field]; } } -function ibitZscoreExitModeFromInputs(inputs) { - const payload = inputs?.extra_variables_json ? JSON.parse(inputs.extra_variables_json) : {}; - return ibitZscoreExitPayloadFromObject(payload).ibit_zscore_exit_mode || ""; -} - function dcaPayloadFromValues(modeValue, baseInvestmentValue) { const result = {}; const mode = cleanCurrentDcaMode(modeValue); @@ -2159,10 +2094,6 @@ function dcaPayloadForProfile(profile, payload) { return isDcaProfile(profile) ? payload : {}; } -function ibitZscoreExitPayloadForProfile(profile, payload) { - return cleanCurrentStrategy(profile) === "ibit_smart_dca" ? payload : {}; -} - function runtimeModePayload(runtimeTarget) { const executionMode = normalizeRuntimeExecutionMode(runtimeTarget?.execution_mode, runtimeTarget?.dry_run_only); const payload = {};