Skip to content
Closed
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
100 changes: 90 additions & 10 deletions scripts/sync_strategy_switch_page_asset.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env python3
"""Embed the strategy switch Worker HTML as a module asset."""
"""Sync page_asset.js from index.html AND inject platform-config into index.html."""

from __future__ import annotations

Expand All @@ -10,24 +10,104 @@
ROOT = Path(__file__).resolve().parents[1]
SOURCE = ROOT / "web" / "strategy-switch-console" / "index.html"
TARGET = ROOT / "web" / "strategy-switch-console" / "page_asset.js"
PROFILE_SOURCE = ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json"
PROFILE_TARGET = ROOT / "web" / "strategy-switch-console" / "strategy_profiles_asset.js"
CONFIG_SOURCE = ROOT / "platform-config.json"


def inject_platform_config():
"""Inject <script id="platform-config"> block into index.html before </head>."""
config = json.loads(CONFIG_SOURCE.read_text(encoding="utf-8"))
platforms = config["platforms"]
strategies = config["strategies"]

# Build platformConfig
pc = {}
dao = {}
dca = {}
inc_layer = {}
opt_overlay = {}
for pid, pdata in platforms.items():
caps = pdata["capabilities"]
depl = pdata["deployment"]
pc[pid] = {"dry_run_only": depl.get("dry_run_only", False),
"margin_policy": caps.get("margin_policy", False),
"reserved_cash": caps.get("reserved_cash", False),
"income_layer": caps.get("income_layer", False),
"option_overlay": caps.get("option_overlay", False),
"dca": caps.get("dca", False),
"execution_mode": depl.get("default_execution_mode", "live"),
"service_name": depl.get("service_name", ""),
"default_execution_mode": depl.get("default_execution_mode", "live")}
acct = pdata.get("default_account", {})
entry = {"key": acct.get("key", pid), "label": acct.get("label", pdata.get("label", pid)),
"target_name": acct.get("target_name", acct.get("key", pid)),
"supported_domains": acct.get("supported_domains", pdata.get("supported_domains", [])),
"cash_currency": acct.get("cash_currency", "USD")}
for fld in ("default_strategy_profile", "service_name", "account_scope",
"deployment_selector", "account_selector", "default_execution_mode",
"min_reserved_cash_usd", "reserved_cash_ratio",
"cash_only_execution_mode", "dca_mode", "dca_base_investment_usd"):
val = acct.get(fld)
if val: entry[fld] = val
if "service_name" not in entry: entry["service_name"] = depl.get("service_name", "")
if "default_execution_mode" not in entry: entry["default_execution_mode"] = depl.get("default_execution_mode", "live")
dao[pid] = [entry]
for sid, sdata in strategies.items():
feat = sdata.get("features", {})
dd = sdata.get("dca_defaults")
if dd: dca[sid] = {"defaultMode": dd.get("default_mode", "fixed"),
"defaultBaseInvestmentUsd": str(dd.get("default_base_investment_usd", "1000"))}
if feat.get("income_layer"):
idl = sdata.get("income_layer_defaults", {})
inc_layer[sid] = {"startUsd": int(idl.get("start_usd", 0)),
"maxRatio": str(idl.get("max_ratio", "")),
"allocations": idl.get("allocations", {})}
if feat.get("option_overlay"):
odl = sdata.get("option_overlay_defaults", {})
families = []
if odl.get("growth_enabled"):
families.append({"family": "growth", "recipe": odl["growth_recipe"],
"startUsd": odl["growth_start_usd"], "ratio": str(odl.get("nav_budget_ratio", "")), "ratioKind": "budget"})
if odl.get("income_enabled"):
families.append({"family": "income", "recipe": odl["income_recipe"],
"startUsd": odl["income_start_usd"], "ratio": str(odl.get("nav_risk_ratio", "")), "ratioKind": "risk"})
opt_overlay[sid] = {"liveGate": odl.get("live_gate", ""), "liveStatus": odl.get("live_status", ""), "families": families}

block = "\n".join([
"<!-- Generated by sync script from platform-config.json -->",
'<script id="platform-config">',
f"window.__PLATFORM_CONFIG__ = {json.dumps(pc, ensure_ascii=False)};",
f"window.__DEFAULT_ACCOUNT_OPTIONS__ = {json.dumps(dao, ensure_ascii=False)};",
f"window.__DCA_PROFILE_DEFAULTS__ = {json.dumps(dca, ensure_ascii=False)};",
f"window.__INCOME_LAYER_DEFAULTS__ = {json.dumps(inc_layer, ensure_ascii=False)};",
f"window.__OPTION_OVERLAY_DEFAULTS__ = {json.dumps(opt_overlay, ensure_ascii=False)};",
"</script>",
])

html = SOURCE.read_text(encoding="utf-8")
existing_start = html.find('<script id="platform-config">')
if existing_start >= 0:
existing_end = html.find('</script>', existing_start) + 9
html = html[:existing_start] + block + html[existing_end:]
else:
head_end = html.find('</head>')
html = html[:head_end] + "\n" + block + "\n" + html[head_end:]

SOURCE.write_text(html, encoding="utf-8")
print(f"Injected platform-config into index.html")


def main() -> int:
# Step 1: Inject config into index.html
inject_platform_config()

# Step 2: Generate page_asset.js from (updated) index.html
html = SOURCE.read_text(encoding="utf-8")
page_payload = (
"// Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand.\n"
f"export const PAGE_HTML = {json.dumps(html, ensure_ascii=False)};\n"
)
TARGET.write_text(page_payload, encoding="utf-8")

profiles = json.loads(PROFILE_SOURCE.read_text(encoding="utf-8"))
profile_payload = (
"// Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand.\n"
f"export const DEFAULT_STRATEGY_PROFILES = {json.dumps(profiles, ensure_ascii=False)};\n"
)
PROFILE_TARGET.write_text(profile_payload, encoding="utf-8")
print(f"Generated: {TARGET}")
return 0


Expand Down
41 changes: 36 additions & 5 deletions web/strategy-switch-console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -1317,6 +1317,16 @@
}
}
</style>

<!-- Generated by sync script from platform-config.json -->
<!-- Generated by sync script from platform-config.json -->
<script id="platform-config">
window.__PLATFORM_CONFIG__ = {"longbridge": {"dry_run_only": false, "margin_policy": true, "reserved_cash": true, "income_layer": true, "option_overlay": true, "dca": true, "execution_mode": "live", "service_name": "", "default_execution_mode": "live"}, "ibkr": {"dry_run_only": false, "margin_policy": true, "reserved_cash": true, "income_layer": true, "option_overlay": true, "dca": false, "execution_mode": "live", "service_name": "", "default_execution_mode": "live"}, "schwab": {"dry_run_only": false, "margin_policy": true, "reserved_cash": true, "income_layer": true, "option_overlay": true, "dca": true, "execution_mode": "live", "service_name": "charles-schwab-quant-service", "default_execution_mode": "live"}, "firstrade": {"dry_run_only": false, "margin_policy": true, "reserved_cash": true, "income_layer": true, "option_overlay": true, "dca": true, "execution_mode": "live", "service_name": "firstrade-quant-service", "default_execution_mode": "live"}, "qmt": {"dry_run_only": true, "margin_policy": false, "reserved_cash": false, "income_layer": false, "option_overlay": false, "dca": false, "execution_mode": "paper", "service_name": "qmt-quant-service", "default_execution_mode": "paper"}, "binance": {"dry_run_only": false, "margin_policy": false, "reserved_cash": false, "income_layer": false, "option_overlay": false, "dca": true, "execution_mode": "live", "service_name": "", "default_execution_mode": "live"}};
window.__DEFAULT_ACCOUNT_OPTIONS__ = {"longbridge": [{"key": "preview", "label": "LongBridge", "target_name": "preview", "supported_domains": ["us_equity", "hk_equity"], "cash_currency": "USD", "default_strategy_profile": "tqqq_growth_income", "default_execution_mode": "live", "reserved_cash_ratio": "0.03", "service_name": ""}], "ibkr": [{"key": "preview", "label": "IBKR", "target_name": "preview", "supported_domains": ["us_equity", "hk_equity"], "cash_currency": "USD", "default_strategy_profile": "soxl_soxx_trend_income", "default_execution_mode": "live", "reserved_cash_ratio": "0.03", "service_name": ""}], "schwab": [{"key": "preview", "label": "Schwab", "target_name": "preview", "supported_domains": ["us_equity"], "cash_currency": "USD", "default_strategy_profile": "soxl_soxx_trend_income", "default_execution_mode": "live", "min_reserved_cash_usd": "150", "reserved_cash_ratio": "0.03", "service_name": "charles-schwab-quant-service"}], "firstrade": [{"key": "preview", "label": "Firstrade", "target_name": "preview", "supported_domains": ["us_equity"], "cash_currency": "USD", "default_strategy_profile": "ibit_smart_dca", "default_execution_mode": "live", "reserved_cash_ratio": "0.03", "service_name": "firstrade-quant-service"}], "qmt": [{"key": "default", "label": "QMT", "target_name": "default", "supported_domains": ["cn_equity"], "cash_currency": "CNY", "default_strategy_profile": "cn_industry_etf_rotation", "service_name": "qmt-quant-service", "default_execution_mode": "paper"}], "binance": [{"key": "default", "label": "Binance", "target_name": "default", "supported_domains": ["crypto"], "cash_currency": "USD", "default_strategy_profile": "crypto_live_pool_rotation", "service_name": "", "default_execution_mode": "live"}]};
window.__DCA_PROFILE_DEFAULTS__ = {"nasdaq_sp500_smart_dca": {"defaultMode": "fixed", "defaultBaseInvestmentUsd": "1000"}, "ibit_smart_dca": {"defaultMode": "fixed", "defaultBaseInvestmentUsd": "1000"}, "crypto_btc_dca": {"defaultMode": "fixed", "defaultBaseInvestmentUsd": "100"}};
window.__INCOME_LAYER_DEFAULTS__ = {"tqqq_growth_income": {"startUsd": 250000, "maxRatio": "0.55", "allocations": {"SCHD": 0.3, "DGRO": 0.2, "SGOV": 0.4, "SPYI": 0.08, "QQQI": 0.02}}, "soxl_soxx_trend_income": {"startUsd": 150000, "maxRatio": "0.95", "allocations": {"SCHD": 0.15, "DGRO": 0.1, "SGOV": 0.7, "SPYI": 0.04, "QQQI": 0.01}}, "global_etf_rotation": {"startUsd": 500000, "maxRatio": "0.15", "allocations": {"SCHD": 0.4, "DGRO": 0.25, "SGOV": 0.3, "SPYI": 0.05}}, "russell_top50_leader_rotation": {"startUsd": 300000, "maxRatio": "0.25", "allocations": {"SCHD": 0.45, "DGRO": 0.3, "SGOV": 0.25}}, "us_equity_combo": {"startUsd": 300000, "maxRatio": "0.25", "allocations": {"SCHD": 0.25, "DGRO": 0.25, "SGOV": 0.2, "SPYI": 0.15, "QQQI": 0.15}}};
window.__OPTION_OVERLAY_DEFAULTS__ = {"tqqq_growth_income": {"liveGate": "promotion_required", "liveStatus": "research_only", "families": [{"family": "growth", "recipe": "tqqq_leaps_growth_v1", "startUsd": "250000", "ratio": "0.03", "ratioKind": "budget"}]}, "soxl_soxx_trend_income": {"liveGate": "promotion_required", "liveStatus": "research_only", "families": [{"family": "income", "recipe": "soxx_put_credit_spread_income_v1", "startUsd": "150000", "ratio": "0.01", "ratioKind": "risk"}]}, "global_etf_rotation": {"liveGate": "promotion_required", "liveStatus": "research_only", "families": [{"family": "growth", "recipe": "spy_leaps_growth_v1", "startUsd": "500000", "ratio": "0.015", "ratioKind": "budget"}]}, "russell_top50_leader_rotation": {"liveGate": "promotion_required", "liveStatus": "research_only", "families": [{"family": "growth", "recipe": "spy_leaps_growth_v1", "startUsd": "300000", "ratio": "0.015", "ratioKind": "budget"}]}, "us_equity_combo": {"liveGate": "promotion_required", "liveStatus": "research_only", "families": [{"family": "growth", "recipe": "spy_leaps_growth_v1", "startUsd": "300000", "ratio": "0.015", "ratioKind": "budget"}]}};
</script>
</head>
<body class="app-loading">
<header class="topbar">
Expand Down Expand Up @@ -2769,7 +2779,25 @@ <h2 data-i18n="summary">切换摘要</h2>
const entry = byPlatform[key];
if (currentEntryHasState(entry)) return entry;
}
return null;
// Build synthetic entry from global defaults when Worker has no data
const globalDefaults = window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0] || {};
const merged = { ...globalDefaults, ...(account || {}) };
const profile = cleanStrategyProfile(merged.default_strategy_profile || merged.strategy_profile || "");
const synth = {
strategy_profile: profile || merged.default_strategy_profile || "",
source: "account_defaults",
};
const cashMode = merged.cash_only_execution_mode;
if (cashMode === "enabled") synth.cash_only_execution = true;
else if (cashMode === "disabled") synth.cash_only_execution = false;
else if (platformSupportsMarginPolicy(platform)) synth.cash_only_execution = true;
if (merged.min_reserved_cash_usd) synth.min_reserved_cash_usd = merged.min_reserved_cash_usd;
if (merged.reserved_cash_ratio) synth.reserved_cash_ratio = merged.reserved_cash_ratio;
synth.runtime_target_enabled = merged.runtime_target_enabled !== false;
const execMode = merged.default_execution_mode || platformConfig[platform]?.default_execution_mode || "live";
synth.execution_mode = execMode;
synth.dry_run_only = execMode === "paper";
return synth;
}

function currentEntryHasState(entry) {
Expand All @@ -2793,10 +2821,13 @@ <h2 data-i18n="summary">切换摘要</h2>

function currentStrategyForAccount(platform, account) {
const entry = currentEntryForAccount(platform, account);
const profile = cleanStrategyProfile(entry?.strategy_profile);
if (profile) return profile;
const fallback = account?.default_strategy_profile || account?.strategy_profile || "";
return cleanStrategyProfile(fallback);
const p = cleanStrategyProfile(entry?.strategy_profile);
if (p) return p;
const fb = account?.default_strategy_profile
|| account?.strategy_profile
|| (window.__DEFAULT_ACCOUNT_OPTIONS__?.[platform]?.[0]?.default_strategy_profile)
|| "";
return cleanStrategyProfile(fb);
}

function currentReservePolicyForAccount(platform, account) {
Expand Down
2 changes: 1 addition & 1 deletion web/strategy-switch-console/page_asset.js

Large diffs are not rendered by default.

Loading