Skip to content

Commit 707bd2a

Browse files
Pigbibiclaude
andcommitted
feat: unified config — platform-config.json is single source of truth
Architecture change (Phase 1): - build_platform_config.py generates config.js from platform-config.json - worker.js now imports DCA_SUPPORTED_PLATFORMS, PLATFORM_*_VARIABLES from config.js instead of hardcoding - Fix platform-config.json: Schwab dca=true, LongBridge dca=true, add default_strategy_profile for all platforms - Deploy workflow: run build_platform_config.py instead of build_config.py Eliminates duplicate config between worker.js and page_asset.js. Next phase: migrate index.html hardcoded defaults to import config.js. Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 593e63d commit 707bd2a

6 files changed

Lines changed: 1037 additions & 28 deletions

File tree

.github/workflows/deploy-strategy-switch-console.yml

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,10 @@ jobs:
5151
with:
5252
node-version: "22"
5353

54-
- name: Build config from platform-config.json
55-
run: |
56-
set -euo pipefail
57-
python3 scripts/build_config.py
58-
59-
- name: Regenerate bundled assets
54+
- name: Build config & assets from platform-config.json
6055
run: |
6156
set -euo pipefail
57+
python3 scripts/build_platform_config.py
6258
python3 scripts/sync_strategy_switch_page_asset.py
6359
6460
- name: Validate Worker assets

platform-config.json

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@
4040
"reserved_cash": true,
4141
"income_layer": true,
4242
"option_overlay": true,
43-
"dca": false
43+
"dca": true
4444
},
4545
"default_account": {
4646
"key": "preview",
@@ -51,7 +51,10 @@
5151
"hk_equity"
5252
],
5353
"cash_currency": "USD",
54-
"default_execution_mode": "live"
54+
"default_execution_mode": "live",
55+
"default_strategy_profile": "tqqq_growth_income",
56+
"reserved_cash_ratio": "0.03",
57+
"min_reserved_cash_usd": ""
5558
},
5659
"deployment": {
5760
"default_execution_mode": "live",
@@ -90,7 +93,10 @@
9093
"hk_equity"
9194
],
9295
"cash_currency": "USD",
93-
"default_execution_mode": "live"
96+
"default_execution_mode": "live",
97+
"default_strategy_profile": "soxl_soxx_trend_income",
98+
"reserved_cash_ratio": "0.03",
99+
"min_reserved_cash_usd": ""
94100
},
95101
"deployment": {
96102
"default_execution_mode": "live",
@@ -117,7 +123,7 @@
117123
"reserved_cash": true,
118124
"income_layer": true,
119125
"option_overlay": true,
120-
"dca": false
126+
"dca": true
121127
},
122128
"default_account": {
123129
"key": "preview",
@@ -127,7 +133,10 @@
127133
"us_equity"
128134
],
129135
"cash_currency": "USD",
130-
"default_execution_mode": "live"
136+
"default_execution_mode": "live",
137+
"default_strategy_profile": "soxl_soxx_trend_income",
138+
"reserved_cash_ratio": "0.03",
139+
"min_reserved_cash_usd": "150"
131140
},
132141
"deployment": {
133142
"default_execution_mode": "live",
@@ -164,7 +173,10 @@
164173
"us_equity"
165174
],
166175
"cash_currency": "USD",
167-
"default_execution_mode": "live"
176+
"default_execution_mode": "live",
177+
"default_strategy_profile": "ibit_smart_dca",
178+
"reserved_cash_ratio": "0.03",
179+
"min_reserved_cash_usd": ""
168180
},
169181
"deployment": {
170182
"default_execution_mode": "live",

scripts/build_platform_config.py

Lines changed: 249 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
#!/usr/bin/env python3
2+
"""Generate config.js from platform-config.json — single source of truth.
3+
4+
Reads platform-config.json and produces:
5+
web/strategy-switch-console/config.js
6+
7+
This file is imported by BOTH index.html (frontend) and worker.js (backend),
8+
replacing the previously hardcoded platformConfig, defaultAccountOptions,
9+
fallbackIncomeLayerDefaults, fallbackOptionOverlayDefaults, and DCA_SUPPORTED_PLATFORMS.
10+
"""
11+
12+
from __future__ import annotations
13+
14+
import json
15+
from pathlib import Path
16+
17+
ROOT = Path(__file__).resolve().parents[1]
18+
SOURCE = ROOT / "platform-config.json"
19+
TARGET = ROOT / "web" / "strategy-switch-console" / "config.js"
20+
STRATEGY_TARGET = ROOT / "web" / "strategy-switch-console" / "strategy_profiles_asset.js"
21+
STRATEGY_SOURCE = ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json"
22+
23+
24+
def build_config_module(config: dict) -> str:
25+
platforms = config["platforms"]
26+
strategies = config["strategies"]
27+
domains = config.get("domains", {})
28+
29+
# ── platformConfig (replaces hardcoded in index.html) ──
30+
platform_config = {}
31+
default_accounts = {}
32+
repositories = {}
33+
dcaplat = []
34+
variable_scopes = {}
35+
36+
for pid, pdata in platforms.items():
37+
caps = pdata["capabilities"]
38+
depl = pdata["deployment"]
39+
platform_config[pid] = {
40+
"dry_run_only": depl.get("dry_run_only", False),
41+
"margin_policy": caps.get("margin_policy", False),
42+
"reserved_cash": caps.get("reserved_cash", False),
43+
"income_layer": caps.get("income_layer", False),
44+
"option_overlay": caps.get("option_overlay", False),
45+
"dca": caps.get("dca", False),
46+
"execution_mode": depl.get("default_execution_mode", "live"),
47+
"service_name": depl.get("service_name", ""),
48+
"default_execution_mode": depl.get("default_execution_mode", "live"),
49+
}
50+
if caps.get("dca"):
51+
dcaplat.append(pid)
52+
repositories[pid] = pdata["repository"]
53+
variable_scopes[pid] = pdata.get("variable_scope", "repository")
54+
55+
# default account options
56+
acct = pdata.get("default_account", {})
57+
entry = {
58+
"key": acct.get("key", pid),
59+
"label": acct.get("label", pdata.get("label", pid)),
60+
"target_name": acct.get("target_name", acct.get("key", pid)),
61+
"supported_domains": acct.get("supported_domains", pdata.get("supported_domains", [])),
62+
"cash_currency": acct.get("cash_currency", "USD"),
63+
}
64+
for fld in ("default_strategy_profile", "service_name", "account_scope",
65+
"deployment_selector", "account_selector", "default_execution_mode",
66+
"min_reserved_cash_usd", "reserved_cash_ratio",
67+
"cash_only_execution_mode", "dca_mode", "dca_base_investment_usd"):
68+
if fld in acct:
69+
entry[fld] = acct[fld]
70+
if "service_name" not in entry:
71+
entry["service_name"] = depl.get("service_name", "")
72+
if "default_execution_mode" not in entry:
73+
entry["default_execution_mode"] = depl.get("default_execution_mode", "live")
74+
default_accounts[pid] = [entry]
75+
76+
# ── strategy features ──
77+
income_layer_defaults = {}
78+
option_overlay_defaults = {}
79+
strategy_features = {}
80+
dca_profile_defaults = {}
81+
82+
for sid, sdata in strategies.items():
83+
feat = sdata.get("features", {})
84+
strategy_features[sid] = {
85+
"income_layer": feat.get("income_layer", False),
86+
"option_overlay": feat.get("option_overlay", False),
87+
"dca": feat.get("dca", False),
88+
"combo": feat.get("combo", False),
89+
}
90+
91+
inc = sdata.get("income_layer_defaults")
92+
if inc:
93+
income_layer_defaults[sid] = {
94+
"startUsd": int(inc.get("start_usd", 0)),
95+
"maxRatio": str(inc.get("max_ratio", "")),
96+
"allocations": inc.get("allocations", {}),
97+
}
98+
99+
opt = sdata.get("option_overlay_defaults")
100+
if opt:
101+
families = []
102+
if opt.get("growth_enabled"):
103+
families.append({
104+
"family": "growth",
105+
"recipe": opt["growth_recipe"],
106+
"startUsd": opt["growth_start_usd"],
107+
"ratio": str(opt.get("nav_budget_ratio", "")),
108+
"ratioKind": "budget",
109+
})
110+
if opt.get("income_enabled"):
111+
families.append({
112+
"family": "income",
113+
"recipe": opt["income_recipe"],
114+
"startUsd": opt["income_start_usd"],
115+
"ratio": str(opt.get("nav_risk_ratio", "")),
116+
"ratioKind": "risk",
117+
})
118+
option_overlay_defaults[sid] = {
119+
"liveGate": opt.get("live_gate", ""),
120+
"liveStatus": opt.get("live_status", ""),
121+
"families": families,
122+
}
123+
124+
dca = sdata.get("dca_defaults")
125+
if dca:
126+
dca_profile_defaults[sid] = {
127+
"defaultMode": dca.get("default_mode", "fixed"),
128+
"defaultBaseInvestmentUsd": str(dca.get("default_base_investment_usd", "1000")),
129+
}
130+
131+
# ── domain labels ──
132+
domain_labels = {}
133+
for did, ddata in domains.items():
134+
domain_labels[did] = {
135+
"zh": ddata.get("label_zh", did),
136+
"en": ddata.get("label_en", did),
137+
}
138+
139+
# ── reserved cash variable names ──
140+
min_cash_vars = {}
141+
ratio_vars = {}
142+
var_prefixes = {
143+
"longbridge": "LONGBRIDGE",
144+
"ibkr": "IBKR",
145+
"schwab": "SCHWAB",
146+
"firstrade": "FIRSTRADE",
147+
}
148+
for pid, prefix in var_prefixes.items():
149+
min_cash_vars[pid] = f"{prefix}_MIN_RESERVED_CASH_USD"
150+
ratio_vars[pid] = f"{prefix}_RESERVED_CASH_RATIO"
151+
152+
# ── Generate JS module ──
153+
lines = [
154+
"// Generated by scripts/build_platform_config.py; single source of truth.",
155+
f"// Source: platform-config.json",
156+
"",
157+
f"export const PLATFORM_CONFIG = {json.dumps(platform_config, indent=2, ensure_ascii=False)};",
158+
"",
159+
f"export const DEFAULT_ACCOUNT_OPTIONS = {json.dumps(default_accounts, indent=2, ensure_ascii=False)};",
160+
"",
161+
f"export const PLATFORM_REPOSITORIES = {json.dumps(repositories, indent=2, ensure_ascii=False)};",
162+
"",
163+
f"export const DCA_SUPPORTED_PLATFORMS = new Set({json.dumps(dcaplat)});",
164+
"",
165+
f"export const DEFAULT_VARIABLE_SCOPES = {json.dumps(variable_scopes, indent=2, ensure_ascii=False)};",
166+
"",
167+
f"export const DOMAIN_LABELS = {json.dumps(domain_labels, indent=2, ensure_ascii=False)};",
168+
"",
169+
f"export const FALLBACK_INCOME_LAYER_DEFAULTS = {json.dumps(income_layer_defaults, indent=2, ensure_ascii=False)};",
170+
"",
171+
f"export const FALLBACK_OPTION_OVERLAY_DEFAULTS = {json.dumps(option_overlay_defaults, indent=2, ensure_ascii=False)};",
172+
"",
173+
f"export const DCA_PROFILE_DEFAULTS = {json.dumps(dca_profile_defaults, indent=2, ensure_ascii=False)};",
174+
"",
175+
f"export const STRATEGY_FEATURES = {json.dumps(strategy_features, indent=2, ensure_ascii=False)};",
176+
"",
177+
f"export const PLATFORM_MIN_RESERVED_CASH_VARIABLES = {json.dumps(min_cash_vars, indent=2, ensure_ascii=False)};",
178+
"",
179+
f"export const PLATFORM_RESERVED_CASH_RATIO_VARIABLES = {json.dumps(ratio_vars, indent=2, ensure_ascii=False)};",
180+
"",
181+
]
182+
return "\n".join(lines)
183+
184+
185+
def build_strategy_profiles(config: dict) -> str:
186+
"""Generate strategy_profiles_asset.js — the strategy catalog."""
187+
strategies = config["strategies"]
188+
profiles = []
189+
for sid, sdata in strategies.items():
190+
feat = sdata.get("features", {})
191+
entry = {
192+
"profile": sid,
193+
"label": sdata.get("label", sid),
194+
"label_en": sdata.get("label_en", sid),
195+
"label_zh": sdata.get("label", sid),
196+
"domain": sdata.get("domain", ""),
197+
"runtime_enabled": sdata.get("runtime_enabled", False),
198+
"income_layer_enabled": feat.get("income_layer", False),
199+
"option_overlay_enabled": feat.get("option_overlay", False),
200+
"combo_enabled": feat.get("combo", False),
201+
}
202+
if feat.get("combo"):
203+
entry["combo_mode"] = feat.get("combo_mode", "dynamic")
204+
inc = sdata.get("income_layer_defaults")
205+
if inc:
206+
entry["income_layer_start_usd"] = str(inc.get("start_usd", ""))
207+
entry["income_layer_max_ratio"] = str(inc.get("max_ratio", ""))
208+
entry["income_layer_allocations"] = inc.get("allocations", {})
209+
opt = sdata.get("option_overlay_defaults")
210+
if opt:
211+
entry["option_overlay_live_gate"] = opt.get("live_gate", "")
212+
entry["option_overlay_live_status"] = opt.get("live_status", "")
213+
if opt.get("growth_enabled"):
214+
entry["option_growth_overlay_enabled"] = True
215+
entry["option_growth_overlay_recipe"] = opt["growth_recipe"]
216+
entry["option_growth_overlay_start_usd"] = opt["growth_start_usd"]
217+
entry["option_growth_overlay_nav_budget_ratio"] = str(opt.get("nav_budget_ratio", ""))
218+
dca = sdata.get("dca_defaults")
219+
if dca or feat.get("dca"):
220+
entry["dca_enabled"] = True
221+
entry["dca_default_mode"] = (dca or {}).get("default_mode", "fixed")
222+
entry["dca_default_base_investment_usd"] = str((dca or {}).get("default_base_investment_usd", "1000"))
223+
profiles.append(entry)
224+
225+
payload = (
226+
"// Generated by scripts/build_platform_config.py from platform-config.json\n"
227+
f"export const DEFAULT_STRATEGY_PROFILES = {json.dumps(profiles, indent=2, ensure_ascii=False)};\n"
228+
)
229+
return payload
230+
231+
232+
def main() -> int:
233+
config = json.loads(SOURCE.read_text(encoding="utf-8"))
234+
235+
# Generate config.js
236+
module = build_config_module(config)
237+
TARGET.write_text(module, encoding="utf-8")
238+
239+
# Generate strategy_profiles_asset.js
240+
profiles = build_strategy_profiles(config)
241+
STRATEGY_TARGET.write_text(profiles, encoding="utf-8")
242+
243+
print(f"Generated: {TARGET}")
244+
print(f"Generated: {STRATEGY_TARGET}")
245+
return 0
246+
247+
248+
if __name__ == "__main__":
249+
raise SystemExit(main())

0 commit comments

Comments
 (0)