读取策略配置
\n正在读取登录状态、账号配置和当前状态。
\n \ndiff --git a/scripts/build_runtime_switch.py b/scripts/build_runtime_switch.py index cb3e2de..d04f33d 100644 --- a/scripts/build_runtime_switch.py +++ b/scripts/build_runtime_switch.py @@ -177,6 +177,7 @@ "ibit_smart_dca", } ) +DCA_SUPPORTED_PLATFORMS = frozenset({"firstrade"}) DCA_MODES = frozenset({"fixed", "smart"}) DCA_MODE_VARIABLE = "DCA_MODE" DCA_BASE_INVESTMENT_VARIABLE = "DCA_BASE_INVESTMENT_USD" @@ -329,6 +330,15 @@ def _normalize_dca_mode(value: str) -> str: return mode +def _validate_dca_platform(platform: str, strategy_profile: str) -> None: + profile = str(strategy_profile or "").strip().lower() + if profile in DCA_PROFILES and platform not in DCA_SUPPORTED_PLATFORMS: + raise ValueError( + "DCA strategy profiles are only supported on firstrade; " + f"got platform={platform!r}, strategy_profile={profile!r}" + ) + + def _normalize_positive_decimal(value: str, *, field_name: str) -> str: text = str(value or "").strip() if not text or not re.fullmatch(r"(?:\d+|\d*\.\d+)", text): @@ -810,6 +820,7 @@ def _build_runtime_target(args: argparse.Namespace) -> dict[str, Any]: account_selector = _split_csv(args.account_selector) or _account_selector_default(platform, account_scope) service_name = args.service_name.strip() if args.service_name else _default_service_name(platform, target_name) strategy_profile = args.strategy_profile.strip().lower() + _validate_dca_platform(platform, strategy_profile) runtime_target: dict[str, Any] = { "platform_id": platform, "strategy_profile": strategy_profile, diff --git a/tests/strategy_switch_worker_validation.mjs b/tests/strategy_switch_worker_validation.mjs index e33cdf8..1985dda 100644 --- a/tests/strategy_switch_worker_validation.mjs +++ b/tests/strategy_switch_worker_validation.mjs @@ -541,8 +541,8 @@ const normalizedPluginInputs = __test.normalizeSwitchInputs({ }); assert.equal(normalizedPluginInputs.plugin_mode, "none"); const normalizedDcaInputs = __test.normalizeSwitchInputs({ - platform: "ibkr", - target_name: "ibkr-primary", + platform: "firstrade", + target_name: "default", strategy_profile: "nasdaq_sp500_smart_dca", execution_mode: "live", plugin_mode: "auto", @@ -555,9 +555,21 @@ assert.deepEqual(JSON.parse(normalizedDcaInputs.extra_variables_json), { dca_mode: "smart", dca_base_investment_usd: "500", }); +assert.throws( + () => __test.normalizeSwitchInputs({ + platform: "ibkr", + target_name: "ibkr-primary", + strategy_profile: "nasdaq_sp500_smart_dca", + execution_mode: "live", + plugin_mode: "auto", + dca_mode: "smart", + dca_base_investment_usd: "500", + }), + /DCA strategy profiles are only supported on firstrade/, +); const normalizedDcaJsonInputs = __test.normalizeSwitchInputs({ - platform: "ibkr", - target_name: "ibkr-primary", + platform: "firstrade", + target_name: "default", strategy_profile: "nasdaq_sp500_smart_dca", execution_mode: "live", plugin_mode: "auto", @@ -571,8 +583,8 @@ assert.deepEqual(JSON.parse(normalizedDcaJsonInputs.extra_variables_json), { dca_base_investment_usd: "500", }); const normalizedIbitZscoreInputs = __test.normalizeSwitchInputs({ - platform: "ibkr", - target_name: "ibit-primary", + platform: "firstrade", + target_name: "default", strategy_profile: "ibit_smart_dca", execution_mode: "live", plugin_mode: "auto", @@ -581,6 +593,17 @@ const normalizedIbitZscoreInputs = __test.normalizeSwitchInputs({ assert.deepEqual(JSON.parse(normalizedIbitZscoreInputs.extra_variables_json), { ibit_zscore_exit_mode: "live", }); +assert.throws( + () => __test.normalizeSwitchInputs({ + platform: "ibkr", + target_name: "ibit-primary", + strategy_profile: "ibit_smart_dca", + execution_mode: "live", + plugin_mode: "auto", + ibit_zscore_exit_mode: "live", + }), + /DCA strategy profiles are only supported on firstrade/, +); assert.throws( () => __test.normalizeSwitchInputs({ platform: "ibkr", @@ -588,7 +611,7 @@ assert.throws( strategy_profile: "nasdaq_sp500_smart_dca", ibit_zscore_exit_mode: "live", }), - /IBIT Z-Score exit settings/, + /DCA strategy profiles are only supported on firstrade/, ); assert.throws( () => __test.normalizeSwitchInputs({ @@ -617,8 +640,8 @@ assert.throws( ); assert.throws( () => __test.normalizeSwitchInputs({ - platform: "ibkr", - target_name: "ibkr-primary", + platform: "firstrade", + target_name: "default", strategy_profile: "nasdaq_sp500_smart_dca", dca_mode: "smart", dca_base_investment_usd: "0", diff --git a/tests/test_runtime_settings.py b/tests/test_runtime_settings.py index c47c400..1a86fa2 100644 --- a/tests/test_runtime_settings.py +++ b/tests/test_runtime_settings.py @@ -542,7 +542,7 @@ def test_build_switch_target_uses_dca_monthly_scheduler_window(self): args = parser.parse_args( [ "--platform", - "ibkr", + "firstrade", "--target-name", "dca", "--strategy-profile", @@ -569,7 +569,7 @@ def test_build_switch_target_uses_daily_scheduler_when_ibit_zscore_plugin_is_aut args = parser.parse_args( [ "--platform", - "ibkr", + "firstrade", "--target-name", "ibit", "--strategy-profile", @@ -579,7 +579,7 @@ def test_build_switch_target_uses_daily_scheduler_when_ibit_zscore_plugin_is_aut 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["IBKR_STRATEGY_PLUGIN_MOUNTS_JSON"]) + plugin_payload = json.loads(assignments["FIRSTRADE_STRATEGY_PLUGIN_MOUNTS_JSON"]) self.assertEqual( target["runtime_target"]["scheduler"], @@ -601,7 +601,7 @@ def test_build_switch_target_sets_ibit_zscore_exit_runtime_controls(self): args = parser.parse_args( [ "--platform", - "ibkr", + "firstrade", "--target-name", "ibit", "--strategy-profile", @@ -625,7 +625,7 @@ def test_build_switch_target_disables_ibit_zscore_exit_when_plugins_are_disabled args = parser.parse_args( [ "--platform", - "ibkr", + "firstrade", "--target-name", "ibit", "--strategy-profile", @@ -647,7 +647,7 @@ def test_build_switch_target_rejects_ibit_zscore_controls_for_other_profiles(sel args = parser.parse_args( [ "--platform", - "ibkr", + "firstrade", "--target-name", "dca", "--strategy-profile", @@ -665,7 +665,7 @@ def test_build_switch_target_sets_dca_settings_for_dca_profile(self): args = parser.parse_args( [ "--platform", - "ibkr", + "firstrade", "--target-name", "dca", "--strategy-profile", @@ -690,7 +690,7 @@ def test_build_switch_target_accepts_dca_control_fields_from_extra_variables_jso args = parser.parse_args( [ "--platform", - "ibkr", + "firstrade", "--target-name", "dca", "--strategy-profile", @@ -728,7 +728,7 @@ def test_build_switch_target_rejects_dca_settings_for_non_dca_profile(self): with self.assertRaisesRegex(ValueError, "DCA settings are only supported"): build_runtime_switch.build_switch_target(args) - def test_build_switch_target_rejects_direct_dca_extra_variables(self): + def test_build_switch_target_rejects_dca_profile_on_non_firstrade_platform(self): parser = build_runtime_switch.build_parser() args = parser.parse_args( [ @@ -738,6 +738,24 @@ def test_build_switch_target_rejects_direct_dca_extra_variables(self): "dca", "--strategy-profile", "nasdaq_sp500_smart_dca", + "--plugin-mode", + "none", + ] + ) + + with self.assertRaisesRegex(ValueError, "DCA strategy profiles are only supported on firstrade"): + build_runtime_switch.build_switch_target(args) + + def test_build_switch_target_rejects_direct_dca_extra_variables(self): + parser = build_runtime_switch.build_parser() + args = parser.parse_args( + [ + "--platform", + "firstrade", + "--target-name", + "dca", + "--strategy-profile", + "nasdaq_sp500_smart_dca", "--extra-variables-json", '{"DCA_MODE":"smart"}', ] @@ -799,7 +817,7 @@ def test_build_switch_target_can_disable_option_overlay(self): "--target-name", "live", "--strategy-profile", - "nasdaq_sp500_smart_dca", + "tqqq_growth_income", "--option-overlay-mode", "disabled", ] @@ -838,9 +856,9 @@ def test_build_switch_target_rejects_enabled_option_overlay_without_profile_defa args = parser.parse_args( [ "--platform", - "schwab", + "firstrade", "--target-name", - "live", + "default", "--strategy-profile", "nasdaq_sp500_smart_dca", "--option-overlay-mode", @@ -873,18 +891,18 @@ def test_build_switch_target_preserves_dca_fields_in_service_targets_when_omitte existing = { "targets": [ { - "service": "interactive-brokers-demo-ibkr-dca-service", - "ACCOUNT_GROUP": "demo-ibkr-dca", + "service": "firstrade-quant-service", + "ACCOUNT_GROUP": "firstrade", "DCA_MODE": "smart", "DCA_BASE_INVESTMENT_USD": "500", "runtime_target": { - "platform_id": "ibkr", + "platform_id": "firstrade", "strategy_profile": "nasdaq_sp500_smart_dca", "dry_run_only": False, - "deployment_selector": "demo-ibkr-dca", - "account_selector": ["DEMO_IBKR_DCA"], - "account_scope": "demo-ibkr-dca", - "service_name": "interactive-brokers-demo-ibkr-dca-service", + "deployment_selector": "firstrade", + "account_selector": ["firstrade"], + "account_scope": "US", + "service_name": "firstrade-quant-service", "execution_mode": "live", }, }, @@ -897,15 +915,15 @@ def test_build_switch_target_preserves_dca_fields_in_service_targets_when_omitte args = parser.parse_args( [ "--platform", - "ibkr", + "firstrade", "--target-name", - "demo-ibkr-dca", + "default", "--strategy-profile", "nasdaq_sp500_smart_dca", "--account-selector", - "DEMO_IBKR_DCA", + "firstrade", "--service-name", - "interactive-brokers-demo-ibkr-dca-service", + "firstrade-quant-service", "--plugin-mode", "none", "--existing-service-targets-json-file", @@ -954,7 +972,7 @@ def test_build_switch_target_preserves_market_signal_fields_in_service_targets_w "--target-name", "demo-ibkr-dca", "--strategy-profile", - "ibit_smart_dca", + "tqqq_growth_income", "--account-selector", "DEMO_IBKR_DCA", "--service-name", @@ -970,7 +988,7 @@ def test_build_switch_target_preserves_market_signal_fields_in_service_targets_w assignments = {item.name: item.value for item in runtime_settings.build_assignments(target)} selected = json.loads(assignments["CLOUD_RUN_SERVICE_TARGETS_JSON"])["targets"][0] - self.assertEqual(selected["runtime_target"]["strategy_profile"], "ibit_smart_dca") + self.assertEqual(selected["runtime_target"]["strategy_profile"], "tqqq_growth_income") self.assertEqual(selected["IBKR_MARKET_SIGNAL_HANDOFF_INDEX_URI"], "gs://signals/index.json") self.assertEqual(selected["IBKR_MARKET_SIGNAL_REQUIRED"], "true") self.assertEqual(selected["IBKR_MARKET_SIGNAL_FALLBACK_MODE"], "last_valid") diff --git a/web/strategy-switch-console/index.html b/web/strategy-switch-console/index.html index 2eedd4f..5ead7a6 100644 --- a/web/strategy-switch-console/index.html +++ b/web/strategy-switch-console/index.html @@ -1711,6 +1711,7 @@
选平台、目标账号和策略,一次执行完成切换。
\n正在读取登录状态、账号配置和当前状态。
\n \n登录后查看账号配置。
\n\n允许融资与预留现金覆盖不能同时生效。
\nA 股 QMT 不使用 margin / 平台预留现金;现金约束在策略参数 execution_cash_reserve_ratio 内配置。
\n登录后才可执行切换。
\n \n选平台、目标账号和策略,一次执行完成切换。
\n正在读取登录状态、账号配置和当前状态。
\n \n登录后查看账号配置。
\n\n允许融资与预留现金覆盖不能同时生效。
\nA 股 QMT 不使用 margin / 平台预留现金;现金约束在策略参数 execution_cash_reserve_ratio 内配置。
\n登录后才可执行切换。
\n \n