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
3 changes: 1 addition & 2 deletions .github/workflows/deploy-strategy-switch-console.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ jobs:
python3 python/scripts/sync_strategy_switch_page_asset.py

- name: Validate Worker assets
continue-on-error: true
run: |
set -euo pipefail
jq empty web/strategy-switch-console/strategy-profiles.example.json
Expand Down Expand Up @@ -117,7 +116,7 @@ jobs:

- name: Deploy Worker
working-directory: web/strategy-switch-console
run: npx wrangler@latest deploy --config wrangler.toml
run: npx wrangler@4.106.0 deploy --config wrangler.toml

- name: Sync bundled strategy profiles to KV
if: github.event_name != 'workflow_dispatch' || inputs.sync_strategy_profiles
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/manual-strategy-switch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ on:
- schwab
- firstrade
- qmt
- binance

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep Binance DCA dispatch compatible with the builder

Adding binance here makes the console/manual workflow able to dispatch Binance switches, but the UI/backend config already advertises Binance DCA support (DCA_SUPPORTED_PLATFORMS includes binance and crypto_btc_dca has DCA defaults). For a Binance crypto_btc_dca switch with DCA controls, the workflow reaches build_runtime_switch.py, whose DCA_PROFILES still only includes the two US DCA profiles, so it exits with DCA settings are only supported for DCA strategy profiles before applying anything. Please either add the crypto DCA profile to the builder’s DCA support or don’t expose Binance until that path is handled.

Useful? React with 👍 / 👎.

target_name:
description: "Target name, e.g. sg, live, live-u1599-tqqq."
required: true
Expand Down Expand Up @@ -172,6 +173,8 @@ jobs:
RUNTIME_SETTINGS_IBKR_REPO: ${{ vars.RUNTIME_SETTINGS_IBKR_REPO }}
RUNTIME_SETTINGS_SCHWAB_REPO: ${{ vars.RUNTIME_SETTINGS_SCHWAB_REPO }}
RUNTIME_SETTINGS_FIRSTRADE_REPO: ${{ vars.RUNTIME_SETTINGS_FIRSTRADE_REPO }}
RUNTIME_SETTINGS_QMT_REPO: ${{ vars.RUNTIME_SETTINGS_QMT_REPO }}
RUNTIME_SETTINGS_BINANCE_REPO: ${{ vars.RUNTIME_SETTINGS_BINANCE_REPO }}
steps:
- name: Checkout
uses: actions/checkout@v6
Expand Down Expand Up @@ -352,8 +355,9 @@ jobs:
schwab|firstrade)
gh workflow run "${workflow}" --repo "${TARGET_REPOSITORY}" --ref main
;;
qmt)
echo "QMT platform Cloud Run sync workflow is not configured yet; skipped."
qmt|binance)
echo "${PLATFORM} platform Cloud Run sync workflow is not configured yet; skipped."
exit 0
;;
*)
echo "No platform sync dispatch rule for ${PLATFORM}" >&2
Expand Down
2 changes: 1 addition & 1 deletion docs/architecture_config_driven_20260629.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
|------|------|------|
| `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) | 数据+逻辑 |
| `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) | 数据 |
| `strategy-profiles.example.json` | 18个策略完整定义 | 数据 |
| `strategy-profiles.example.json` | 19个策略完整定义 | 数据 |
| `examples/targets/*/` | 每个组合独立的 target JSON | 数据 |

### 核心问题
Expand Down
16 changes: 14 additions & 2 deletions platform-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -247,7 +247,7 @@
"label": "Binance",
"target_name": "default",
"cash_currency": "USD",
"default_strategy_profile": "crypto_live_pool_rotation",
"default_strategy_profile": "crypto_equity_combo",
"supported_domains": [
"crypto"
]
Expand Down Expand Up @@ -504,6 +504,18 @@
"combo_mode": "dynamic"
}
},
"cn_industry_etf_rotation": {
"label": "A股行业ETF轮动",
"label_en": "CN Industry ETF Rotation",
"domain": "cn_equity",
"runtime_enabled": true,
Comment on lines +507 to +511

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Mirror CN profile in frontend fallback catalog

Adding this QMT default profile to the generated catalog still leaves the checked-in browser fallback catalog incomplete: refreshStrategyProfiles() falls back to defaultStrategyProfiles in app.js whenever /api/strategy-profiles is unavailable, and that array still omits cn_industry_etf_rotation. In that API-failure/offline path the QMT default profile remains unavailable and the console falls back to another CN strategy, so please add this profile to the frontend fallback asset as well.

Useful? React with 👍 / 👎.

"features": {
"income_layer": false,
"option_overlay": false,
"dca": false,
"combo": false
}
},
"cn_industry_etf_rotation_aggressive": {
"label": "A股ETF轮动",
"label_en": "CN ETF Rotation",
Expand Down Expand Up @@ -595,4 +607,4 @@
}
}
}
}
}
18 changes: 10 additions & 8 deletions python/scripts/build_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ def build_platform_meta_js(config: dict) -> str:
lines = []
lines.append(" let platformMeta = {")
for pid, pdata in sorted(platforms.items()):
lines.append(f' {pid}: {{ label: "{pdata["label"]}", code: "{pdata["code"]}", accent: "{pdata["accent_color"]}" }},')
lines.append(
f' {pid}: {{ label: "{pdata["label"]}", code: "{pdata["code"]}", accent: "{pdata["accent_color"]}" }},'
)
lines.append(" };")
lines.append("")
lines.append(" const platformRepositories = {")
Expand Down Expand Up @@ -148,13 +150,13 @@ def build_platform_meta_js(config: dict) -> str:
for pid, pdata in sorted(platforms.items()):
caps = pdata.get("capabilities", {})
dep = pdata.get("deployment", {})
lines.append(f' {pid}: {{')
lines.append(f' dry_run_only: {"true" if dep.get("dry_run_only") else "false"},')
lines.append(f' margin_policy: {"true" if caps.get("margin_policy") else "false"},')
lines.append(f' reserved_cash: {"true" if caps.get("reserved_cash") else "false"},')
lines.append(f' income_layer: {"true" if caps.get("income_layer") else "false"},')
lines.append(f' option_overlay: {"true" if caps.get("option_overlay") else "false"},')
lines.append(f' dca: {"true" if caps.get("dca") else "false"},')
lines.append(f" {pid}: {{")
lines.append(f" dry_run_only: {'true' if dep.get('dry_run_only') else 'false'},")
lines.append(f" margin_policy: {'true' if caps.get('margin_policy') else 'false'},")
lines.append(f" reserved_cash: {'true' if caps.get('reserved_cash') else 'false'},")
lines.append(f" income_layer: {'true' if caps.get('income_layer') else 'false'},")
lines.append(f" option_overlay: {'true' if caps.get('option_overlay') else 'false'},")
lines.append(f" dca: {'true' if caps.get('dca') else 'false'},")
lines.append(f' execution_mode: "{dep.get("default_execution_mode", "live")}",')
lines.append(f' service_name: "{dep.get("service_name", "")}",')
lines.append(f' default_execution_mode: "{dep.get("default_execution_mode", "live")}"')
Expand Down
51 changes: 32 additions & 19 deletions python/scripts/build_platform_config.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,19 @@ def build_config_module(config: dict) -> str:
"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"):
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",
):
if fld in acct:
entry[fld] = acct[fld]
if "service_name" not in entry:
Expand Down Expand Up @@ -100,21 +109,25 @@ def build_config_module(config: dict) -> str:
if opt:
families = []
if opt.get("growth_enabled"):
families.append({
"family": "growth",
"recipe": opt["growth_recipe"],
"startUsd": opt["growth_start_usd"],
"ratio": str(opt.get("nav_budget_ratio", "")),
"ratioKind": "budget",
})
families.append(
{
"family": "growth",
"recipe": opt["growth_recipe"],
"startUsd": opt["growth_start_usd"],
"ratio": str(opt.get("nav_budget_ratio", "")),
"ratioKind": "budget",
}
)
if opt.get("income_enabled"):
families.append({
"family": "income",
"recipe": opt["income_recipe"],
"startUsd": opt["income_start_usd"],
"ratio": str(opt.get("nav_risk_ratio", "")),
"ratioKind": "risk",
})
families.append(
{
"family": "income",
"recipe": opt["income_recipe"],
"startUsd": opt["income_start_usd"],
"ratio": str(opt.get("nav_risk_ratio", "")),
"ratioKind": "risk",
}
)
option_overlay_defaults[sid] = {
"liveGate": opt.get("live_gate", ""),
"liveStatus": opt.get("live_status", ""),
Expand Down Expand Up @@ -152,7 +165,7 @@ def build_config_module(config: dict) -> str:
# ── Generate JS module ──
lines = [
"// Generated by python/scripts/build_platform_config.py; single source of truth.",
f"// Source: platform-config.json",
"// Source: platform-config.json",
"",
f"export const PLATFORM_CONFIG = {json.dumps(platform_config, indent=2, ensure_ascii=False)};",
"",
Expand Down
24 changes: 8 additions & 16 deletions python/scripts/build_runtime_switch.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@
"ibkr": "IBKR_DRY_RUN_ONLY",
"firstrade": "FIRSTRADE_DRY_RUN_ONLY",
"qmt": "QMT_DRY_RUN_ONLY",
"binance": "BINANCE_DRY_RUN",
}
PLATFORM_RESERVED_CASH_RATIO_VARIABLES = {
"schwab": "SCHWAB_RESERVED_CASH_RATIO",
Expand Down Expand Up @@ -168,9 +169,7 @@
OPTION_OVERLAY_VARIABLES = tuple(field.upper() for field in OPTION_OVERLAY_CONTROL_FIELDS)
OPTION_OVERLAY_MODES = frozenset({"current", "enabled", "disabled"})
OPTION_OVERLAY_PROFILE_PATH = ROOT / "web" / "strategy-switch-console" / "strategy-profiles.example.json"
RUNTIME_TARGET_VARIABLES = (
"RUNTIME_TARGET_ENABLED",
)
RUNTIME_TARGET_VARIABLES = ("RUNTIME_TARGET_ENABLED",)
DCA_PROFILES = frozenset(
{
"nasdaq_sp500_smart_dca",
Expand Down Expand Up @@ -214,11 +213,13 @@
"schwab": "repository",
"firstrade": "repository",
"qmt": "repository",
"binance": "repository",
}
DEFAULT_SERVICE_NAME = {
"schwab": "charles-schwab-quant-service",
"firstrade": "firstrade-quant-service",
"qmt": "qmt-quant-service",
"binance": "",
}
PLATFORM_ALIASES = {
"firsttrade": "firstrade",
Expand Down Expand Up @@ -536,9 +537,7 @@ def _option_overlay_extra_variables(args: argparse.Namespace, strategy_profile:

defaults = _load_option_overlay_profile_defaults().get(strategy_profile)
if not defaults:
raise ValueError(
"option_overlay_mode enabled is only supported for strategies with option overlay defaults"
)
raise ValueError("option_overlay_mode enabled is only supported for strategies with option overlay defaults")
return dict(defaults)


Expand Down Expand Up @@ -594,10 +593,7 @@ def _reject_direct_ibit_zscore_exit_extra_variables(extra_variables: dict[str, A
]
if provided:
names = ", ".join(provided)
raise ValueError(
"use ibit_zscore_exit_* control fields instead of extra_variables_json "
f"for {names}"
)
raise ValueError(f"use ibit_zscore_exit_* control fields instead of extra_variables_json for {names}")


def _reject_research_only_extra_variables(extra_variables: dict[str, Any]) -> None:
Expand Down Expand Up @@ -813,9 +809,7 @@ def _build_runtime_target(args: argparse.Namespace) -> dict[str, Any]:
else _deployment_selector_default(platform, target_name)
)
account_scope = (
args.account_scope.strip()
if args.account_scope
else _account_scope_default(platform, deployment_selector)
args.account_scope.strip() if args.account_scope else _account_scope_default(platform, deployment_selector)
)
account_selector = _split_csv(args.account_selector) or _account_selector_default(platform, account_scope)
service_name = args.service_name.strip() if args.service_name else _default_service_name(platform, target_name)
Expand Down Expand Up @@ -948,9 +942,7 @@ def build_switch_target(args: argparse.Namespace) -> dict[str, Any]:
dca_controls = _extract_dca_control_fields(extra_variables)
ibit_zscore_exit_controls = _extract_ibit_zscore_exit_control_fields(extra_variables)
if cash_only_controls.get(CASH_ONLY_EXECUTION_CONTROL_FIELD):
args.cash_only_execution_mode = str(
cash_only_controls[CASH_ONLY_EXECUTION_CONTROL_FIELD]
).strip().lower()
args.cash_only_execution_mode = str(cash_only_controls[CASH_ONLY_EXECUTION_CONTROL_FIELD]).strip().lower()
_reject_direct_dca_extra_variables(extra_variables)
_reject_direct_ibit_zscore_exit_extra_variables(extra_variables)
_reject_research_only_extra_variables(extra_variables)
Expand Down
Loading
Loading