Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/targets/qmt/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
- `service_name`:`qmt-quant-service`
- `cash_currency`:`CNY`
- `supported_domains`:`["cn_equity"]`
- `default_strategy_profile`:与该 target 默认策略一致
- 策略 profile 通过 GitHub 变量 `RUNTIME_TARGET_JSON.strategy_profile` 管理,不存储在账号配置中

**不要**在账号配置里放 miniQMT 密码、券商 token 或本机路径;fixture 路径走 QmtPlatform 仓库变量(如 `QMT_MARKET_HISTORY_PATH`)。

Expand Down
14 changes: 4 additions & 10 deletions platform-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@
"hk_equity"
],
"cash_currency": "USD",
"default_execution_mode": "live",
"default_strategy_profile": "soxl_soxx_trend_income"
"default_execution_mode": "live"
},
"deployment": {
"default_execution_mode": "live",
Expand Down Expand Up @@ -91,8 +90,7 @@
"hk_equity"
],
"cash_currency": "USD",
"default_execution_mode": "live",
"default_strategy_profile": "soxl_soxx_trend_income"
"default_execution_mode": "live"
},
"deployment": {
"default_execution_mode": "live",
Expand Down Expand Up @@ -129,8 +127,7 @@
"us_equity"
],
"cash_currency": "USD",
"default_execution_mode": "live",
"default_strategy_profile": "soxl_soxx_trend_income"
"default_execution_mode": "live"
},
"deployment": {
"default_execution_mode": "live",
Expand Down Expand Up @@ -167,8 +164,7 @@
"us_equity"
],
"cash_currency": "USD",
"default_execution_mode": "live",
"default_strategy_profile": "ibit_smart_dca"
"default_execution_mode": "live"
},
"deployment": {
"default_execution_mode": "live",
Expand Down Expand Up @@ -202,7 +198,6 @@
"label": "QMT",
"target_name": "default",
"cash_currency": "CNY",
"default_strategy_profile": "cn_industry_etf_rotation",
"supported_domains": [
"cn_equity"
]
Expand Down Expand Up @@ -239,7 +234,6 @@
"label": "Binance",
"target_name": "default",
"cash_currency": "USD",
"default_strategy_profile": "crypto_live_pool_rotation",
"supported_domains": [
"crypto"
]
Expand Down
56 changes: 0 additions & 56 deletions python/scripts/build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,23 +52,6 @@ def validate(config: dict) -> list[str]:
errors.append(f"platform {pid}: missing default_account")
if "supported_domains" not in pdata:
errors.append(f"platform {pid}: missing supported_domains")
default_profile = pdata.get("default_account", {}).get("default_strategy_profile")
if default_profile:
strategy = config.get("strategies", {}).get(default_profile)
if not isinstance(strategy, dict):
errors.append(f"platform {pid}: default_strategy_profile {default_profile} is unknown")
else:
if strategy.get("domain") not in pdata.get("supported_domains", []):
errors.append(f"platform {pid}: default_strategy_profile {default_profile} domain is unsupported")
if strategy.get("runtime_enabled") is not True:
errors.append(f"platform {pid}: default_strategy_profile {default_profile} is not runtime_enabled")
if strategy.get("can_switch_live") is not True:
errors.append(f"platform {pid}: default_strategy_profile {default_profile} cannot switch live")
lifecycle_stage = str(strategy.get("lifecycle_stage") or "").strip()
if lifecycle_stage != "runtime_enabled":
errors.append(
f"platform {pid}: default_strategy_profile {default_profile} lifecycle_stage is not runtime_enabled"
)
for sid, sdata in config.get("strategies", {}).items():
if "domain" not in sdata:
errors.append(f"strategy {sid}: missing domain")
Expand Down Expand Up @@ -100,36 +83,6 @@ def _strategy_catalog_by_profile(strategy_catalog: object | None) -> dict[str, d
return catalog


def report_default_strategy_profile_drift(config: dict, strategy_catalog: object | None = None) -> list[str]:
"""Report whether platform default_strategy_profile entries drift from the strategy catalog."""
errors: list[str] = []
catalog = _strategy_catalog_by_profile(strategy_catalog)

for pid, pdata in config.get("platforms", {}).items():
default_profile = pdata.get("default_account", {}).get("default_strategy_profile")
if not default_profile:
continue

strategy = catalog.get(default_profile)
if not isinstance(strategy, dict):
errors.append(
f"platform {pid}: default_strategy_profile {default_profile} missing from strategy_profiles"
)
continue

if strategy.get("runtime_enabled") is not True:
errors.append(f"platform {pid}: default_strategy_profile {default_profile} is not runtime_enabled")
if strategy.get("can_switch_live") is not True:
errors.append(f"platform {pid}: default_strategy_profile {default_profile} cannot switch live")
lifecycle_stage = str(strategy.get("lifecycle_stage") or "").strip()
if lifecycle_stage != "runtime_enabled":
errors.append(
f"platform {pid}: default_strategy_profile {default_profile} lifecycle_stage is not runtime_enabled"
)

return errors


def build_live_candidate_queue(strategy_catalog: object | None = None) -> list[dict[str, object]]:
"""Build a control-plane queue of profiles that may need live-promotion review."""
catalog = _strategy_catalog_by_profile(strategy_catalog)
Expand Down Expand Up @@ -282,7 +235,6 @@ def build_platform_health_report(
config = config if config is not None else load_config()
catalog = _strategy_catalog_by_profile(strategy_catalog)
config_errors = validate(config)
default_profile_errors = report_default_strategy_profile_drift(config, catalog)
derivation_errors = report_strategy_profile_derivation_drift(config, catalog)
live_candidate_queue = build_live_candidate_queue(catalog)
automation_registry = build_strategy_automation_registry(config)
Expand All @@ -298,12 +250,6 @@ def build_platform_health_report(
"severity": "critical",
"messages": config_errors,
},
{
"name": "default_strategy_profile_gate",
"status": "fail" if default_profile_errors else "pass",
"severity": "critical",
"messages": default_profile_errors,
},
{
"name": "strategy_profile_derivation",
"status": "fail" if derivation_errors else "pass",
Expand Down Expand Up @@ -595,8 +541,6 @@ def main() -> int:
return 0

errors = validate(config)
if args.check:
errors.extend(report_default_strategy_profile_drift(config))
if errors:
print("Validation ERRORS:")
for e in errors:
Expand Down
1 change: 0 additions & 1 deletion python/scripts/build_platform_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,6 @@ def build_config_module(config: dict) -> str:
"cash_currency": acct.get("cash_currency", "USD"),
}
for fld in (
"default_strategy_profile",
"service_name",
"account_scope",
"deployment_selector",
Expand Down
1 change: 0 additions & 1 deletion python/scripts/inject_platform_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,6 @@ def main() -> int:
cash_currency=acct.get("cash_currency", "USD"),
)
for fld in (
"default_strategy_profile",
"service_name",
"account_scope",
"deployment_selector",
Expand Down
92 changes: 0 additions & 92 deletions python/tests/test_runtime_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -92,80 +92,6 @@ def test_manual_switch_platform_choices_cover_supported_platforms(self):

self.assertEqual(set(platform_choices), set(runtime_settings.SUPPORTED_PLATFORMS))

def test_platform_config_default_strategy_profiles_exist(self):
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
profiles = set(config["strategies"])
for platform, data in config["platforms"].items():
default_profile = data.get("default_account", {}).get("default_strategy_profile")
if default_profile:
with self.subTest(platform=platform):
self.assertIn(default_profile, profiles)

def test_platform_config_default_strategy_profiles_are_live_switchable(self):
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
for platform, data in config["platforms"].items():
default_profile = data.get("default_account", {}).get("default_strategy_profile")
if not default_profile:
continue
strategy = config["strategies"][default_profile]
with self.subTest(platform=platform):
self.assertTrue(strategy["runtime_enabled"])
self.assertTrue(strategy["can_switch_live"])
self.assertEqual(strategy["lifecycle_stage"], "runtime_enabled")

def test_default_strategy_profile_catalog_matches_platform_gate_fields(self):
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
catalog = json.loads(
(ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json").read_text(encoding="utf-8")
)

self.assertEqual(build_config.report_default_strategy_profile_drift(config, catalog), [])

def test_default_strategy_profile_catalog_reports_gate_drift(self):
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
catalog = {
item["profile"]: item
for item in json.loads(
(ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json").read_text(
encoding="utf-8"
)
)
}
profile = config["platforms"]["longbridge"]["default_account"]["default_strategy_profile"]
catalog[profile] = {
**catalog[profile],
"runtime_enabled": False,
"can_switch_live": False,
"lifecycle_stage": "research_backtest_only",
}

errors = build_config.report_default_strategy_profile_drift(config, catalog)

self.assertIn(
"platform longbridge: default_strategy_profile soxl_soxx_trend_income is not runtime_enabled",
errors,
)
self.assertIn(
"platform longbridge: default_strategy_profile soxl_soxx_trend_income cannot switch live",
errors,
)
self.assertIn(
"platform longbridge: default_strategy_profile soxl_soxx_trend_income lifecycle_stage is not runtime_enabled",
errors,
)

def test_build_config_check_includes_default_strategy_profile_drift(self):
with (
patch.object(sys, "argv", ["build_config.py", "--check"]),
patch.object(build_config, "load_config", return_value={"platforms": {}, "strategies": {}}),
patch.object(build_config, "validate", return_value=[]),
patch.object(build_config, "report_default_strategy_profile_drift", return_value=["drift"]) as drift,
patch("builtins.print"),
):
self.assertEqual(build_config.main(), 1)

drift.assert_called_once_with({"platforms": {}, "strategies": {}})

def test_live_candidate_queue_lists_profiles_needing_promotion_review(self):
catalog = [
{
Expand Down Expand Up @@ -206,7 +132,6 @@ def test_live_candidate_queue_cli_outputs_json_only(self):
patch.object(sys, "argv", ["build_config.py", "--live-candidate-queue"]),
patch.object(build_config, "load_config", return_value={"platforms": {}, "strategies": {}}),
patch.object(build_config, "validate", return_value=[]),
patch.object(build_config, "report_default_strategy_profile_drift", return_value=[]),
patch.object(build_config, "build_live_candidate_queue", return_value=[{"profile": "candidate"}]),
patch("builtins.print") as printed,
):
Expand Down Expand Up @@ -281,23 +206,6 @@ def test_platform_health_report_summarizes_current_config(self):
self.assertIn("automation_lane_counts", report["summary"])
self.assertIn("python3 python/scripts/build_config.py --check", report["codex_repair_context"]["suggested_commands"])

def test_platform_health_report_fails_on_default_profile_drift(self):
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
catalog = json.loads(
(ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json").read_text(encoding="utf-8")
)
profile = config["platforms"]["longbridge"]["default_account"]["default_strategy_profile"]
for item in catalog:
if item["profile"] == profile:
item["can_switch_live"] = False
break

report = build_config.build_platform_health_report(config, catalog)

self.assertEqual(report["status"], "unhealthy")
self.assertEqual(report["recommended_action"], "attempt_codex_fix")
self.assertTrue(report["codex_repair_context"]["safe_to_attempt"])

def test_platform_health_report_cli_outputs_json(self):
report = {
"schema_version": "platform_health_report.v1",
Expand Down
10 changes: 2 additions & 8 deletions tests/strategy_switch_worker_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -426,15 +426,13 @@ const accountOptions = __test.normalizeAccountOptionsPayload(
label: "hk",
target_name: "hk",
account_selector: "HK",
default_strategy_profile: "hk_low_vol_dividend_quality_snapshot",
cash_currency: "HKD",
},
{
key: "sg",
label: "sg",
target_name: "sg",
account_selector: "SG",
default_strategy_profile: "tqqq_growth_income",
},
],
ibkr: [
Expand Down Expand Up @@ -848,9 +846,7 @@ const updatedAccountOptions = __test.updateAccountOptionsDefaultStrategy(
plugin_mode: "auto",
},
);
assert.equal(updatedAccountOptions.changed, true);
assert.equal(updatedAccountOptions.options.longbridge[1].default_strategy_profile, "soxl_soxx_trend_income");

// default_strategy_profile update removed — only other fields may change
assert.equal(
__test.accountOptionMatchesInputs(
{ target_name: "sg", variable_scope: "default" },
Expand Down Expand Up @@ -923,7 +919,6 @@ const updatedIbitZscoreModeOptions = __test.updateAccountOptionsDefaultStrategy(
{
key: "ibit-primary",
target_name: "ibit-primary",
default_strategy_profile: "ibit_smart_dca",
supported_domains: ["us_equity"],
ibit_zscore_exit_mode: "live",
},
Expand Down Expand Up @@ -962,8 +957,7 @@ const syncResult = await __test.syncDefaultStrategyForAccount(
{ login: "pigbibi" },
);
assert.equal(syncResult.synced, true);
assert.equal(syncResult.changed, true);
assert.equal(JSON.parse(kvWrites.get("account_options")).longbridge[1].default_strategy_profile, "soxl_soxx_trend_income");
// default_strategy_profile is no longer persisted to KV after switch

const originalFetch = globalThis.fetch;
globalThis.fetch = async (url) => {
Expand Down
11 changes: 3 additions & 8 deletions tests/test_cash_financing.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,7 +186,7 @@ function currentEntryForAccount(state, platform, account) {
const entry = byPlatform[key];
if (entry && typeof entry === "object") {
if (!entry.strategy_profile) {
return { ...entry, strategy_profile: account?.default_strategy_profile || "", source: "worker+account_defaults" };
return entry;
}
return entry;
}
Expand All @@ -198,19 +198,15 @@ function currentEntryForAccount(state, platform, account) {
const hasMatch = rawCandidates.some((candidate) => candidates.has(candidate));
if (hasMatch) {
if (!entry.strategy_profile) {
return {
...entry,
strategy_profile: account?.default_strategy_profile || "",
source: "worker+account_defaults",
};
return entry;
}
return entry;
}
}

return {
runtime_target_enabled: true,
strategy_profile: account?.default_strategy_profile || account?.strategy_profile || "",
strategy_profile: "",
source: "account_defaults",
};
}
Expand Down Expand Up @@ -1133,7 +1129,6 @@ console.log("\n=== 11. currentEntryForAccount 映射健壮性 ===\n");
key: "sg",
target_name: "sg",
label: "sg",
default_strategy_profile: "tqqq_growth_income",
};
const entry = currentEntryForAccount(state, "longbridge", account);
assert(Boolean(entry), "11d: missing entry should return synthesized defaults");
Expand Down
Loading
Loading