Skip to content

Commit 8252da1

Browse files
committed
fix: harden runtime switch config and deployment validation
1 parent 338e99e commit 8252da1

20 files changed

Lines changed: 372 additions & 259 deletions

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ jobs:
5959
python3 python/scripts/sync_strategy_switch_page_asset.py
6060
6161
- name: Validate Worker assets
62-
continue-on-error: true
6362
run: |
6463
set -euo pipefail
6564
jq empty web/strategy-switch-console/strategy-profiles.example.json
@@ -117,7 +116,7 @@ jobs:
117116
118117
- name: Deploy Worker
119118
working-directory: web/strategy-switch-console
120-
run: npx wrangler@latest deploy --config wrangler.toml
119+
run: npx wrangler@4.106.0 deploy --config wrangler.toml
121120

122121
- name: Sync bundled strategy profiles to KV
123122
if: github.event_name != 'workflow_dispatch' || inputs.sync_strategy_profiles

.github/workflows/manual-strategy-switch.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ on:
1313
- schwab
1414
- firstrade
1515
- qmt
16+
- binance
1617
target_name:
1718
description: "Target name, e.g. sg, live, live-u1599-tqqq."
1819
required: true
@@ -172,6 +173,8 @@ jobs:
172173
RUNTIME_SETTINGS_IBKR_REPO: ${{ vars.RUNTIME_SETTINGS_IBKR_REPO }}
173174
RUNTIME_SETTINGS_SCHWAB_REPO: ${{ vars.RUNTIME_SETTINGS_SCHWAB_REPO }}
174175
RUNTIME_SETTINGS_FIRSTRADE_REPO: ${{ vars.RUNTIME_SETTINGS_FIRSTRADE_REPO }}
176+
RUNTIME_SETTINGS_QMT_REPO: ${{ vars.RUNTIME_SETTINGS_QMT_REPO }}
177+
RUNTIME_SETTINGS_BINANCE_REPO: ${{ vars.RUNTIME_SETTINGS_BINANCE_REPO }}
175178
steps:
176179
- name: Checkout
177180
uses: actions/checkout@v6
@@ -352,8 +355,9 @@ jobs:
352355
schwab|firstrade)
353356
gh workflow run "${workflow}" --repo "${TARGET_REPOSITORY}" --ref main
354357
;;
355-
qmt)
356-
echo "QMT platform Cloud Run sync workflow is not configured yet; skipped."
358+
qmt|binance)
359+
echo "${PLATFORM} platform Cloud Run sync workflow is not configured yet; skipped."
360+
exit 0
357361
;;
358362
*)
359363
echo "No platform sync dispatch rule for ${PLATFORM}" >&2

docs/architecture_config_driven_20260629.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
|------|------|------|
1111
| `index.html` | `platformMeta`(6), `defaultRepositories`(6), `defaultAccountOptions`(6), `state.forms`(6), `defaultStrategyProfiles`(18), `strategyDomains`(4), `platformSupportsMargin`(1), `platformSupportsReservedCash`(1), `platformSupportsDca`(1), `platformDryRunOnly`(1), `dcaProfileDefaults`(2), i18n domain labels(3) | 数据+逻辑 |
1212
| `worker.js` | `SUPPORTED_PLATFORMS`(6), `SUPPORTED_STRATEGY_DOMAINS`(4), `PLATFORM_META`(6), `DEFAULT_PLATFORM_REPOSITORIES`(6), `PLATFORM_REPOSITORY_ENV`(6), `DEFAULT_VARIABLE_SCOPE`(6), `PLATFORM_RESERVED_CASH_*`(4) | 数据 |
13-
| `strategy-profiles.example.json` | 18个策略完整定义 | 数据 |
13+
| `strategy-profiles.example.json` | 19个策略完整定义 | 数据 |
1414
| `examples/targets/*/` | 每个组合独立的 target JSON | 数据 |
1515

1616
### 核心问题

platform-config.json

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@
247247
"label": "Binance",
248248
"target_name": "default",
249249
"cash_currency": "USD",
250-
"default_strategy_profile": "crypto_live_pool_rotation",
250+
"default_strategy_profile": "crypto_equity_combo",
251251
"supported_domains": [
252252
"crypto"
253253
]
@@ -504,6 +504,18 @@
504504
"combo_mode": "dynamic"
505505
}
506506
},
507+
"cn_industry_etf_rotation": {
508+
"label": "A股行业ETF轮动",
509+
"label_en": "CN Industry ETF Rotation",
510+
"domain": "cn_equity",
511+
"runtime_enabled": true,
512+
"features": {
513+
"income_layer": false,
514+
"option_overlay": false,
515+
"dca": false,
516+
"combo": false
517+
}
518+
},
507519
"cn_industry_etf_rotation_aggressive": {
508520
"label": "A股ETF轮动",
509521
"label_en": "CN ETF Rotation",
@@ -595,4 +607,4 @@
595607
}
596608
}
597609
}
598-
}
610+
}

python/scripts/build_config.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,9 @@ def build_platform_meta_js(config: dict) -> str:
117117
lines = []
118118
lines.append(" let platformMeta = {")
119119
for pid, pdata in sorted(platforms.items()):
120-
lines.append(f' {pid}: {{ label: "{pdata["label"]}", code: "{pdata["code"]}", accent: "{pdata["accent_color"]}" }},')
120+
lines.append(
121+
f' {pid}: {{ label: "{pdata["label"]}", code: "{pdata["code"]}", accent: "{pdata["accent_color"]}" }},'
122+
)
121123
lines.append(" };")
122124
lines.append("")
123125
lines.append(" const platformRepositories = {")
@@ -148,13 +150,13 @@ def build_platform_meta_js(config: dict) -> str:
148150
for pid, pdata in sorted(platforms.items()):
149151
caps = pdata.get("capabilities", {})
150152
dep = pdata.get("deployment", {})
151-
lines.append(f' {pid}: {{')
152-
lines.append(f' dry_run_only: {"true" if dep.get("dry_run_only") else "false"},')
153-
lines.append(f' margin_policy: {"true" if caps.get("margin_policy") else "false"},')
154-
lines.append(f' reserved_cash: {"true" if caps.get("reserved_cash") else "false"},')
155-
lines.append(f' income_layer: {"true" if caps.get("income_layer") else "false"},')
156-
lines.append(f' option_overlay: {"true" if caps.get("option_overlay") else "false"},')
157-
lines.append(f' dca: {"true" if caps.get("dca") else "false"},')
153+
lines.append(f" {pid}: {{")
154+
lines.append(f" dry_run_only: {'true' if dep.get('dry_run_only') else 'false'},")
155+
lines.append(f" margin_policy: {'true' if caps.get('margin_policy') else 'false'},")
156+
lines.append(f" reserved_cash: {'true' if caps.get('reserved_cash') else 'false'},")
157+
lines.append(f" income_layer: {'true' if caps.get('income_layer') else 'false'},")
158+
lines.append(f" option_overlay: {'true' if caps.get('option_overlay') else 'false'},")
159+
lines.append(f" dca: {'true' if caps.get('dca') else 'false'},")
158160
lines.append(f' execution_mode: "{dep.get("default_execution_mode", "live")}",')
159161
lines.append(f' service_name: "{dep.get("service_name", "")}",')
160162
lines.append(f' default_execution_mode: "{dep.get("default_execution_mode", "live")}"')

python/scripts/build_platform_config.py

Lines changed: 32 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -61,10 +61,19 @@ def build_config_module(config: dict) -> str:
6161
"supported_domains": acct.get("supported_domains", pdata.get("supported_domains", [])),
6262
"cash_currency": acct.get("cash_currency", "USD"),
6363
}
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"):
64+
for fld in (
65+
"default_strategy_profile",
66+
"service_name",
67+
"account_scope",
68+
"deployment_selector",
69+
"account_selector",
70+
"default_execution_mode",
71+
"min_reserved_cash_usd",
72+
"reserved_cash_ratio",
73+
"cash_only_execution_mode",
74+
"dca_mode",
75+
"dca_base_investment_usd",
76+
):
6877
if fld in acct:
6978
entry[fld] = acct[fld]
7079
if "service_name" not in entry:
@@ -100,21 +109,25 @@ def build_config_module(config: dict) -> str:
100109
if opt:
101110
families = []
102111
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-
})
112+
families.append(
113+
{
114+
"family": "growth",
115+
"recipe": opt["growth_recipe"],
116+
"startUsd": opt["growth_start_usd"],
117+
"ratio": str(opt.get("nav_budget_ratio", "")),
118+
"ratioKind": "budget",
119+
}
120+
)
110121
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-
})
122+
families.append(
123+
{
124+
"family": "income",
125+
"recipe": opt["income_recipe"],
126+
"startUsd": opt["income_start_usd"],
127+
"ratio": str(opt.get("nav_risk_ratio", "")),
128+
"ratioKind": "risk",
129+
}
130+
)
118131
option_overlay_defaults[sid] = {
119132
"liveGate": opt.get("live_gate", ""),
120133
"liveStatus": opt.get("live_status", ""),
@@ -152,7 +165,7 @@ def build_config_module(config: dict) -> str:
152165
# ── Generate JS module ──
153166
lines = [
154167
"// Generated by python/scripts/build_platform_config.py; single source of truth.",
155-
f"// Source: platform-config.json",
168+
"// Source: platform-config.json",
156169
"",
157170
f"export const PLATFORM_CONFIG = {json.dumps(platform_config, indent=2, ensure_ascii=False)};",
158171
"",

python/scripts/build_runtime_switch.py

Lines changed: 8 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"ibkr": "IBKR_DRY_RUN_ONLY",
9595
"firstrade": "FIRSTRADE_DRY_RUN_ONLY",
9696
"qmt": "QMT_DRY_RUN_ONLY",
97+
"binance": "BINANCE_DRY_RUN",
9798
}
9899
PLATFORM_RESERVED_CASH_RATIO_VARIABLES = {
99100
"schwab": "SCHWAB_RESERVED_CASH_RATIO",
@@ -168,9 +169,7 @@
168169
OPTION_OVERLAY_VARIABLES = tuple(field.upper() for field in OPTION_OVERLAY_CONTROL_FIELDS)
169170
OPTION_OVERLAY_MODES = frozenset({"current", "enabled", "disabled"})
170171
OPTION_OVERLAY_PROFILE_PATH = ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json"
171-
RUNTIME_TARGET_VARIABLES = (
172-
"RUNTIME_TARGET_ENABLED",
173-
)
172+
RUNTIME_TARGET_VARIABLES = ("RUNTIME_TARGET_ENABLED",)
174173
DCA_PROFILES = frozenset(
175174
{
176175
"nasdaq_sp500_smart_dca",
@@ -214,11 +213,13 @@
214213
"schwab": "repository",
215214
"firstrade": "repository",
216215
"qmt": "repository",
216+
"binance": "repository",
217217
}
218218
DEFAULT_SERVICE_NAME = {
219219
"schwab": "charles-schwab-quant-service",
220220
"firstrade": "firstrade-quant-service",
221221
"qmt": "qmt-quant-service",
222+
"binance": "",
222223
}
223224
PLATFORM_ALIASES = {
224225
"firsttrade": "firstrade",
@@ -536,9 +537,7 @@ def _option_overlay_extra_variables(args: argparse.Namespace, strategy_profile:
536537

537538
defaults = _load_option_overlay_profile_defaults().get(strategy_profile)
538539
if not defaults:
539-
raise ValueError(
540-
"option_overlay_mode enabled is only supported for strategies with option overlay defaults"
541-
)
540+
raise ValueError("option_overlay_mode enabled is only supported for strategies with option overlay defaults")
542541
return dict(defaults)
543542

544543

@@ -594,10 +593,7 @@ def _reject_direct_ibit_zscore_exit_extra_variables(extra_variables: dict[str, A
594593
]
595594
if provided:
596595
names = ", ".join(provided)
597-
raise ValueError(
598-
"use ibit_zscore_exit_* control fields instead of extra_variables_json "
599-
f"for {names}"
600-
)
596+
raise ValueError(f"use ibit_zscore_exit_* control fields instead of extra_variables_json for {names}")
601597

602598

603599
def _reject_research_only_extra_variables(extra_variables: dict[str, Any]) -> None:
@@ -813,9 +809,7 @@ def _build_runtime_target(args: argparse.Namespace) -> dict[str, Any]:
813809
else _deployment_selector_default(platform, target_name)
814810
)
815811
account_scope = (
816-
args.account_scope.strip()
817-
if args.account_scope
818-
else _account_scope_default(platform, deployment_selector)
812+
args.account_scope.strip() if args.account_scope else _account_scope_default(platform, deployment_selector)
819813
)
820814
account_selector = _split_csv(args.account_selector) or _account_selector_default(platform, account_scope)
821815
service_name = args.service_name.strip() if args.service_name else _default_service_name(platform, target_name)
@@ -948,9 +942,7 @@ def build_switch_target(args: argparse.Namespace) -> dict[str, Any]:
948942
dca_controls = _extract_dca_control_fields(extra_variables)
949943
ibit_zscore_exit_controls = _extract_ibit_zscore_exit_control_fields(extra_variables)
950944
if cash_only_controls.get(CASH_ONLY_EXECUTION_CONTROL_FIELD):
951-
args.cash_only_execution_mode = str(
952-
cash_only_controls[CASH_ONLY_EXECUTION_CONTROL_FIELD]
953-
).strip().lower()
945+
args.cash_only_execution_mode = str(cash_only_controls[CASH_ONLY_EXECUTION_CONTROL_FIELD]).strip().lower()
954946
_reject_direct_dca_extra_variables(extra_variables)
955947
_reject_direct_ibit_zscore_exit_extra_variables(extra_variables)
956948
_reject_research_only_extra_variables(extra_variables)

0 commit comments

Comments
 (0)