Skip to content

Commit bd82faa

Browse files
Pigbibiclaude
andauthored
refactor: remove default_strategy_profile — single source of truth is GitHub variables (#173)
Problem: default_strategy_profile in platform-config.json created a false impression that changing it would change the running strategy. In reality, the runtime reads RUNTIME_TARGET_JSON from GitHub variables, and the two sources could diverge silently (LongBridge TQQQ bug). Changes: - platform-config.json: delete default_strategy_profile from all 6 platforms - build_config.py: delete report_default_strategy_profile_drift() and gate - build_platform_config.py / inject_platform_config.py: stop generating it - app.js: simplify defaultStrategyForAccount → reads only GitHub variables; remove hardcoded fallback; remove account.default_strategy_profile reads - worker.js: remove default_strategy_profile from KV sync/clean logic - account-options.example.json: remove all default_strategy_profile fields - tests: delete 6 test methods; update assertions and test data - docs: update README.md / README.zh-CN.md / qmt README - Regenerated: config.js, index.html, page_asset.js, app_js.js Architecture after: one platform = one strategy source = GitHub variable RUNTIME_TARGET_JSON. platform-config.json owns capabilities and deployment config only. No more silent divergence between config and runtime. Co-authored-by: Claude <noreply@anthropic.com>
1 parent d3f7fc5 commit bd82faa

17 files changed

Lines changed: 28 additions & 255 deletions

examples/targets/qmt/README.zh-CN.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@
3434
- `service_name``qmt-quant-service`
3535
- `cash_currency``CNY`
3636
- `supported_domains``["cn_equity"]`
37-
- `default_strategy_profile`:与该 target 默认策略一致
37+
- 策略 profile 通过 GitHub 变量 `RUNTIME_TARGET_JSON.strategy_profile` 管理,不存储在账号配置中
3838

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

platform-config.json

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@
5151
"hk_equity"
5252
],
5353
"cash_currency": "USD",
54-
"default_execution_mode": "live",
55-
"default_strategy_profile": "soxl_soxx_trend_income"
54+
"default_execution_mode": "live"
5655
},
5756
"deployment": {
5857
"default_execution_mode": "live",
@@ -91,8 +90,7 @@
9190
"hk_equity"
9291
],
9392
"cash_currency": "USD",
94-
"default_execution_mode": "live",
95-
"default_strategy_profile": "soxl_soxx_trend_income"
93+
"default_execution_mode": "live"
9694
},
9795
"deployment": {
9896
"default_execution_mode": "live",
@@ -129,8 +127,7 @@
129127
"us_equity"
130128
],
131129
"cash_currency": "USD",
132-
"default_execution_mode": "live",
133-
"default_strategy_profile": "soxl_soxx_trend_income"
130+
"default_execution_mode": "live"
134131
},
135132
"deployment": {
136133
"default_execution_mode": "live",
@@ -167,8 +164,7 @@
167164
"us_equity"
168165
],
169166
"cash_currency": "USD",
170-
"default_execution_mode": "live",
171-
"default_strategy_profile": "ibit_smart_dca"
167+
"default_execution_mode": "live"
172168
},
173169
"deployment": {
174170
"default_execution_mode": "live",
@@ -202,7 +198,6 @@
202198
"label": "QMT",
203199
"target_name": "default",
204200
"cash_currency": "CNY",
205-
"default_strategy_profile": "cn_industry_etf_rotation",
206201
"supported_domains": [
207202
"cn_equity"
208203
]
@@ -239,7 +234,6 @@
239234
"label": "Binance",
240235
"target_name": "default",
241236
"cash_currency": "USD",
242-
"default_strategy_profile": "crypto_live_pool_rotation",
243237
"supported_domains": [
244238
"crypto"
245239
]

python/scripts/build_config.py

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -52,23 +52,6 @@ def validate(config: dict) -> list[str]:
5252
errors.append(f"platform {pid}: missing default_account")
5353
if "supported_domains" not in pdata:
5454
errors.append(f"platform {pid}: missing supported_domains")
55-
default_profile = pdata.get("default_account", {}).get("default_strategy_profile")
56-
if default_profile:
57-
strategy = config.get("strategies", {}).get(default_profile)
58-
if not isinstance(strategy, dict):
59-
errors.append(f"platform {pid}: default_strategy_profile {default_profile} is unknown")
60-
else:
61-
if strategy.get("domain") not in pdata.get("supported_domains", []):
62-
errors.append(f"platform {pid}: default_strategy_profile {default_profile} domain is unsupported")
63-
if strategy.get("runtime_enabled") is not True:
64-
errors.append(f"platform {pid}: default_strategy_profile {default_profile} is not runtime_enabled")
65-
if strategy.get("can_switch_live") is not True:
66-
errors.append(f"platform {pid}: default_strategy_profile {default_profile} cannot switch live")
67-
lifecycle_stage = str(strategy.get("lifecycle_stage") or "").strip()
68-
if lifecycle_stage != "runtime_enabled":
69-
errors.append(
70-
f"platform {pid}: default_strategy_profile {default_profile} lifecycle_stage is not runtime_enabled"
71-
)
7255
for sid, sdata in config.get("strategies", {}).items():
7356
if "domain" not in sdata:
7457
errors.append(f"strategy {sid}: missing domain")
@@ -100,36 +83,6 @@ def _strategy_catalog_by_profile(strategy_catalog: object | None) -> dict[str, d
10083
return catalog
10184

10285

103-
def report_default_strategy_profile_drift(config: dict, strategy_catalog: object | None = None) -> list[str]:
104-
"""Report whether platform default_strategy_profile entries drift from the strategy catalog."""
105-
errors: list[str] = []
106-
catalog = _strategy_catalog_by_profile(strategy_catalog)
107-
108-
for pid, pdata in config.get("platforms", {}).items():
109-
default_profile = pdata.get("default_account", {}).get("default_strategy_profile")
110-
if not default_profile:
111-
continue
112-
113-
strategy = catalog.get(default_profile)
114-
if not isinstance(strategy, dict):
115-
errors.append(
116-
f"platform {pid}: default_strategy_profile {default_profile} missing from strategy_profiles"
117-
)
118-
continue
119-
120-
if strategy.get("runtime_enabled") is not True:
121-
errors.append(f"platform {pid}: default_strategy_profile {default_profile} is not runtime_enabled")
122-
if strategy.get("can_switch_live") is not True:
123-
errors.append(f"platform {pid}: default_strategy_profile {default_profile} cannot switch live")
124-
lifecycle_stage = str(strategy.get("lifecycle_stage") or "").strip()
125-
if lifecycle_stage != "runtime_enabled":
126-
errors.append(
127-
f"platform {pid}: default_strategy_profile {default_profile} lifecycle_stage is not runtime_enabled"
128-
)
129-
130-
return errors
131-
132-
13386
def build_live_candidate_queue(strategy_catalog: object | None = None) -> list[dict[str, object]]:
13487
"""Build a control-plane queue of profiles that may need live-promotion review."""
13588
catalog = _strategy_catalog_by_profile(strategy_catalog)
@@ -282,7 +235,6 @@ def build_platform_health_report(
282235
config = config if config is not None else load_config()
283236
catalog = _strategy_catalog_by_profile(strategy_catalog)
284237
config_errors = validate(config)
285-
default_profile_errors = report_default_strategy_profile_drift(config, catalog)
286238
derivation_errors = report_strategy_profile_derivation_drift(config, catalog)
287239
live_candidate_queue = build_live_candidate_queue(catalog)
288240
automation_registry = build_strategy_automation_registry(config)
@@ -298,12 +250,6 @@ def build_platform_health_report(
298250
"severity": "critical",
299251
"messages": config_errors,
300252
},
301-
{
302-
"name": "default_strategy_profile_gate",
303-
"status": "fail" if default_profile_errors else "pass",
304-
"severity": "critical",
305-
"messages": default_profile_errors,
306-
},
307253
{
308254
"name": "strategy_profile_derivation",
309255
"status": "fail" if derivation_errors else "pass",
@@ -595,8 +541,6 @@ def main() -> int:
595541
return 0
596542

597543
errors = validate(config)
598-
if args.check:
599-
errors.extend(report_default_strategy_profile_drift(config))
600544
if errors:
601545
print("Validation ERRORS:")
602546
for e in errors:

python/scripts/build_platform_config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,6 @@ def build_config_module(config: dict) -> str:
6262
"cash_currency": acct.get("cash_currency", "USD"),
6363
}
6464
for fld in (
65-
"default_strategy_profile",
6665
"service_name",
6766
"account_scope",
6867
"deployment_selector",

python/scripts/inject_platform_config.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def main() -> int:
4242
cash_currency=acct.get("cash_currency", "USD"),
4343
)
4444
for fld in (
45-
"default_strategy_profile",
4645
"service_name",
4746
"account_scope",
4847
"deployment_selector",

python/tests/test_runtime_settings.py

Lines changed: 0 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -92,80 +92,6 @@ def test_manual_switch_platform_choices_cover_supported_platforms(self):
9292

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

95-
def test_platform_config_default_strategy_profiles_exist(self):
96-
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
97-
profiles = set(config["strategies"])
98-
for platform, data in config["platforms"].items():
99-
default_profile = data.get("default_account", {}).get("default_strategy_profile")
100-
if default_profile:
101-
with self.subTest(platform=platform):
102-
self.assertIn(default_profile, profiles)
103-
104-
def test_platform_config_default_strategy_profiles_are_live_switchable(self):
105-
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
106-
for platform, data in config["platforms"].items():
107-
default_profile = data.get("default_account", {}).get("default_strategy_profile")
108-
if not default_profile:
109-
continue
110-
strategy = config["strategies"][default_profile]
111-
with self.subTest(platform=platform):
112-
self.assertTrue(strategy["runtime_enabled"])
113-
self.assertTrue(strategy["can_switch_live"])
114-
self.assertEqual(strategy["lifecycle_stage"], "runtime_enabled")
115-
116-
def test_default_strategy_profile_catalog_matches_platform_gate_fields(self):
117-
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
118-
catalog = json.loads(
119-
(ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json").read_text(encoding="utf-8")
120-
)
121-
122-
self.assertEqual(build_config.report_default_strategy_profile_drift(config, catalog), [])
123-
124-
def test_default_strategy_profile_catalog_reports_gate_drift(self):
125-
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
126-
catalog = {
127-
item["profile"]: item
128-
for item in json.loads(
129-
(ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json").read_text(
130-
encoding="utf-8"
131-
)
132-
)
133-
}
134-
profile = config["platforms"]["longbridge"]["default_account"]["default_strategy_profile"]
135-
catalog[profile] = {
136-
**catalog[profile],
137-
"runtime_enabled": False,
138-
"can_switch_live": False,
139-
"lifecycle_stage": "research_backtest_only",
140-
}
141-
142-
errors = build_config.report_default_strategy_profile_drift(config, catalog)
143-
144-
self.assertIn(
145-
"platform longbridge: default_strategy_profile soxl_soxx_trend_income is not runtime_enabled",
146-
errors,
147-
)
148-
self.assertIn(
149-
"platform longbridge: default_strategy_profile soxl_soxx_trend_income cannot switch live",
150-
errors,
151-
)
152-
self.assertIn(
153-
"platform longbridge: default_strategy_profile soxl_soxx_trend_income lifecycle_stage is not runtime_enabled",
154-
errors,
155-
)
156-
157-
def test_build_config_check_includes_default_strategy_profile_drift(self):
158-
with (
159-
patch.object(sys, "argv", ["build_config.py", "--check"]),
160-
patch.object(build_config, "load_config", return_value={"platforms": {}, "strategies": {}}),
161-
patch.object(build_config, "validate", return_value=[]),
162-
patch.object(build_config, "report_default_strategy_profile_drift", return_value=["drift"]) as drift,
163-
patch("builtins.print"),
164-
):
165-
self.assertEqual(build_config.main(), 1)
166-
167-
drift.assert_called_once_with({"platforms": {}, "strategies": {}})
168-
16995
def test_live_candidate_queue_lists_profiles_needing_promotion_review(self):
17096
catalog = [
17197
{
@@ -206,7 +132,6 @@ def test_live_candidate_queue_cli_outputs_json_only(self):
206132
patch.object(sys, "argv", ["build_config.py", "--live-candidate-queue"]),
207133
patch.object(build_config, "load_config", return_value={"platforms": {}, "strategies": {}}),
208134
patch.object(build_config, "validate", return_value=[]),
209-
patch.object(build_config, "report_default_strategy_profile_drift", return_value=[]),
210135
patch.object(build_config, "build_live_candidate_queue", return_value=[{"profile": "candidate"}]),
211136
patch("builtins.print") as printed,
212137
):
@@ -281,23 +206,6 @@ def test_platform_health_report_summarizes_current_config(self):
281206
self.assertIn("automation_lane_counts", report["summary"])
282207
self.assertIn("python3 python/scripts/build_config.py --check", report["codex_repair_context"]["suggested_commands"])
283208

284-
def test_platform_health_report_fails_on_default_profile_drift(self):
285-
config = json.loads((ROOT / "platform-config.json").read_text(encoding="utf-8"))
286-
catalog = json.loads(
287-
(ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json").read_text(encoding="utf-8")
288-
)
289-
profile = config["platforms"]["longbridge"]["default_account"]["default_strategy_profile"]
290-
for item in catalog:
291-
if item["profile"] == profile:
292-
item["can_switch_live"] = False
293-
break
294-
295-
report = build_config.build_platform_health_report(config, catalog)
296-
297-
self.assertEqual(report["status"], "unhealthy")
298-
self.assertEqual(report["recommended_action"], "attempt_codex_fix")
299-
self.assertTrue(report["codex_repair_context"]["safe_to_attempt"])
300-
301209
def test_platform_health_report_cli_outputs_json(self):
302210
report = {
303211
"schema_version": "platform_health_report.v1",

tests/strategy_switch_worker_validation.mjs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -426,15 +426,13 @@ const accountOptions = __test.normalizeAccountOptionsPayload(
426426
label: "hk",
427427
target_name: "hk",
428428
account_selector: "HK",
429-
default_strategy_profile: "hk_low_vol_dividend_quality_snapshot",
430429
cash_currency: "HKD",
431430
},
432431
{
433432
key: "sg",
434433
label: "sg",
435434
target_name: "sg",
436435
account_selector: "SG",
437-
default_strategy_profile: "tqqq_growth_income",
438436
},
439437
],
440438
ibkr: [
@@ -848,9 +846,7 @@ const updatedAccountOptions = __test.updateAccountOptionsDefaultStrategy(
848846
plugin_mode: "auto",
849847
},
850848
);
851-
assert.equal(updatedAccountOptions.changed, true);
852-
assert.equal(updatedAccountOptions.options.longbridge[1].default_strategy_profile, "soxl_soxx_trend_income");
853-
849+
// default_strategy_profile update removed — only other fields may change
854850
assert.equal(
855851
__test.accountOptionMatchesInputs(
856852
{ target_name: "sg", variable_scope: "default" },
@@ -923,7 +919,6 @@ const updatedIbitZscoreModeOptions = __test.updateAccountOptionsDefaultStrategy(
923919
{
924920
key: "ibit-primary",
925921
target_name: "ibit-primary",
926-
default_strategy_profile: "ibit_smart_dca",
927922
supported_domains: ["us_equity"],
928923
ibit_zscore_exit_mode: "live",
929924
},
@@ -962,8 +957,7 @@ const syncResult = await __test.syncDefaultStrategyForAccount(
962957
{ login: "pigbibi" },
963958
);
964959
assert.equal(syncResult.synced, true);
965-
assert.equal(syncResult.changed, true);
966-
assert.equal(JSON.parse(kvWrites.get("account_options")).longbridge[1].default_strategy_profile, "soxl_soxx_trend_income");
960+
// default_strategy_profile is no longer persisted to KV after switch
967961

968962
const originalFetch = globalThis.fetch;
969963
globalThis.fetch = async (url) => {

tests/test_cash_financing.js

Lines changed: 3 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,7 @@ function currentEntryForAccount(state, platform, account) {
186186
const entry = byPlatform[key];
187187
if (entry && typeof entry === "object") {
188188
if (!entry.strategy_profile) {
189-
return { ...entry, strategy_profile: account?.default_strategy_profile || "", source: "worker+account_defaults" };
189+
return entry;
190190
}
191191
return entry;
192192
}
@@ -198,19 +198,15 @@ function currentEntryForAccount(state, platform, account) {
198198
const hasMatch = rawCandidates.some((candidate) => candidates.has(candidate));
199199
if (hasMatch) {
200200
if (!entry.strategy_profile) {
201-
return {
202-
...entry,
203-
strategy_profile: account?.default_strategy_profile || "",
204-
source: "worker+account_defaults",
205-
};
201+
return entry;
206202
}
207203
return entry;
208204
}
209205
}
210206

211207
return {
212208
runtime_target_enabled: true,
213-
strategy_profile: account?.default_strategy_profile || account?.strategy_profile || "",
209+
strategy_profile: "",
214210
source: "account_defaults",
215211
};
216212
}
@@ -1133,7 +1129,6 @@ console.log("\n=== 11. currentEntryForAccount 映射健壮性 ===\n");
11331129
key: "sg",
11341130
target_name: "sg",
11351131
label: "sg",
1136-
default_strategy_profile: "tqqq_growth_income",
11371132
};
11381133
const entry = currentEntryForAccount(state, "longbridge", account);
11391134
assert(Boolean(entry), "11d: missing entry should return synthesized defaults");

0 commit comments

Comments
 (0)