Skip to content

Commit c00c64f

Browse files
committed
Read current strategies in switch console
1 parent 0050419 commit c00c64f

6 files changed

Lines changed: 357 additions & 22 deletions

File tree

web/strategy-switch-console/README.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,21 @@ Each account item supports:
108108

109109
The Worker validates dispatch inputs against this config. Keep only routing metadata here. Do not store broker passwords, tokens, or API keys in this config.
110110

111+
For signed-in users, `/api/config` also reads the target repositories' current GitHub Variables. It prefers account-specific `CLOUD_RUN_SERVICE_TARGETS_JSON`, then matching `RUNTIME_TARGET_JSON.strategy_profile`, then `STRATEGY_PROFILE`; if none can be read safely, the page falls back to `default_strategy_profile`.
112+
113+
## Strategy Profile Alignment
114+
115+
Treat `strategy_profile` as the canonical strategy id across the switch console, runtime settings, and platform repositories.
116+
117+
When adding or renaming a strategy profile:
118+
119+
- Add the profile id and display label to `web/strategy-switch-console/index.html`.
120+
- Set each affected account's `default_strategy_profile` in `account-options.example.json` and the deployed KV account config.
121+
- Make sure the platform repository's current `RUNTIME_TARGET_JSON.strategy_profile` or account-specific `CLOUD_RUN_SERVICE_TARGETS_JSON` uses the same id.
122+
- Use lower-case ids with letters, numbers, dot, underscore, dash, or equals only. Do not encode account names or secrets in profile ids.
123+
124+
The console can display a dynamically read unknown profile, but the profile should still be added to the catalog so the UI and docs stay aligned.
125+
111126
## GitHub OAuth App
112127

113128
Create a GitHub OAuth App:

web/strategy-switch-console/README.zh-CN.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ wrangler secret put STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON < /tmp/strategy-switch-
115115

116116
Worker 会校验 dispatch 参数必须匹配这里的某个账号项。只放路由信息,不放 broker 密码、token、API key。
117117

118+
登录用户访问 `/api/config` 时,Worker 还会读取目标平台仓库的当前 GitHub Variables。读取优先级是账号匹配的 `CLOUD_RUN_SERVICE_TARGETS_JSON`、匹配的 `RUNTIME_TARGET_JSON.strategy_profile``STRATEGY_PROFILE`;都读不到时,页面才回退到 `default_strategy_profile`
119+
120+
## 策略 Profile 对齐规范
121+
122+
`strategy_profile` 是切换页、runtime settings 和各平台仓库之间的统一策略 ID。
123+
124+
新增或重命名策略 profile 时,需要同时做这些事:
125+
126+
-`web/strategy-switch-console/index.html` 增加 profile id 和显示名称。
127+
-`account-options.example.json` 和已部署的 KV 账号配置里更新对应账号的 `default_strategy_profile`
128+
- 确认平台仓库当前的 `RUNTIME_TARGET_JSON.strategy_profile` 或账号级 `CLOUD_RUN_SERVICE_TARGETS_JSON` 使用同一个 id。
129+
- profile id 只使用小写字母、数字、点、下划线、短横线或等号。不要把账号名、密码、token、密钥信息写进 profile id。
130+
131+
切换页可以临时显示动态读取到但未登记的 profile,但后续仍应补进策略目录,保持 UI 和文档一致。
132+
118133
## GitHub OAuth App
119134

120135
创建 GitHub OAuth App:

web/strategy-switch-console/account-options.example.json

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"label": "hk",
66
"target_name": "hk",
77
"account_selector": "HK",
8-
"default_strategy_profile": "tqqq_growth_income"
8+
"default_strategy_profile": "hk_low_vol_dividend_quality_snapshot"
99
},
1010
{
1111
"key": "sg",
@@ -19,7 +19,7 @@
1919
"label": "paper",
2020
"target_name": "paper",
2121
"account_selector": "PAPER",
22-
"default_strategy_profile": "tqqq_growth_income"
22+
"default_strategy_profile": "mega_cap_leader_rotation_top50_balanced"
2323
}
2424
],
2525
"ibkr": [
@@ -59,15 +59,15 @@
5959
"key": "default",
6060
"label": "default",
6161
"target_name": "default",
62-
"default_strategy_profile": "tqqq_growth_income"
62+
"default_strategy_profile": "soxl_soxx_trend_income"
6363
}
6464
],
6565
"firstrade": [
6666
{
6767
"key": "default",
6868
"label": "default",
6969
"target_name": "default",
70-
"default_strategy_profile": "tqqq_growth_income"
70+
"default_strategy_profile": "mega_cap_leader_rotation_top50_balanced"
7171
}
7272
]
7373
}

web/strategy-switch-console/index.html

Lines changed: 87 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -612,6 +612,7 @@ <h2 data-i18n="summary">切换摘要</h2>
612612
"tqqq_growth_income",
613613
"soxl_soxx_trend_income",
614614
"nasdaq_sp500_smart_dca",
615+
"hk_low_vol_dividend_quality_snapshot",
615616
"global_etf_rotation",
616617
"russell_1000_multi_factor_defensive",
617618
"mega_cap_leader_rotation_top50_balanced",
@@ -621,16 +622,17 @@ <h2 data-i18n="summary">切换摘要</h2>
621622
tqqq_growth_income: "TQQQ Growth Income",
622623
soxl_soxx_trend_income: "SOXL/SOXX Trend Income",
623624
nasdaq_sp500_smart_dca: "Nasdaq/S&P 500 Smart DCA",
625+
hk_low_vol_dividend_quality_snapshot: "HK Low Vol Dividend Quality",
624626
global_etf_rotation: "Global ETF Rotation",
625627
russell_1000_multi_factor_defensive: "Russell 1000 Defensive",
626628
mega_cap_leader_rotation_top50_balanced: "Mega Cap Top 50",
627629
};
628630

629631
const defaultAccountOptions = {
630632
longbridge: [
631-
{ key: "hk", label: "hk", target_name: "hk", account_selector: "HK", default_strategy_profile: "tqqq_growth_income" },
633+
{ key: "hk", label: "hk", target_name: "hk", account_selector: "HK", default_strategy_profile: "hk_low_vol_dividend_quality_snapshot" },
632634
{ key: "sg", label: "sg", target_name: "sg", account_selector: "SG", default_strategy_profile: "tqqq_growth_income" },
633-
{ key: "paper", label: "paper", target_name: "paper", account_selector: "PAPER", default_strategy_profile: "tqqq_growth_income" },
635+
{ key: "paper", label: "paper", target_name: "paper", account_selector: "PAPER", default_strategy_profile: "mega_cap_leader_rotation_top50_balanced" },
634636
],
635637
ibkr: [
636638
{
@@ -665,10 +667,10 @@ <h2 data-i18n="summary">切换摘要</h2>
665667
},
666668
],
667669
schwab: [
668-
{ key: "default", label: "default", target_name: "default", default_strategy_profile: "tqqq_growth_income" },
670+
{ key: "default", label: "default", target_name: "default", default_strategy_profile: "soxl_soxx_trend_income" },
669671
],
670672
firstrade: [
671-
{ key: "default", label: "default", target_name: "default", default_strategy_profile: "tqqq_growth_income" },
673+
{ key: "default", label: "default", target_name: "default", default_strategy_profile: "mega_cap_leader_rotation_top50_balanced" },
672674
],
673675
};
674676

@@ -760,6 +762,7 @@ <h2 data-i18n="summary">切换摘要</h2>
760762
lang: initialLang,
761763
auth: { available: false, allowed: false, admin: false, login: null },
762764
accountOptions: clone(defaultAccountOptions),
765+
currentStrategies: {},
763766
configSource: "default",
764767
forms: {
765768
longbridge: { accountKey: "hk", strategy: "tqqq_growth_income", executionMode: "live" },
@@ -784,9 +787,50 @@ <h2 data-i18n="summary">切换摘要</h2>
784787
return options.find((option) => option.key === form.accountKey) || options[0];
785788
}
786789

787-
function defaultStrategyForAccount(account, fallback = "tqqq_growth_income") {
788-
const profile = String(account?.default_strategy_profile || account?.strategy_profile || "").trim();
789-
if (strategyOptions.includes(profile)) return profile;
790+
function cleanStrategyProfile(value) {
791+
const profile = String(value || "").trim();
792+
return /^[a-z0-9._=-]{1,120}$/.test(profile) ? profile : "";
793+
}
794+
795+
function strategyChoices() {
796+
const choices = [...strategyOptions];
797+
const addChoice = (value) => {
798+
const profile = cleanStrategyProfile(value);
799+
if (profile && !choices.includes(profile)) choices.push(profile);
800+
};
801+
for (const platform of Object.keys(platformMeta)) {
802+
for (const account of optionsFor(platform)) {
803+
addChoice(account.default_strategy_profile || account.strategy_profile);
804+
}
805+
for (const entry of Object.values(state.currentStrategies[platform] || {})) {
806+
addChoice(entry.strategy_profile);
807+
}
808+
addChoice(state.forms[platform].strategy);
809+
}
810+
return choices;
811+
}
812+
813+
function strategyLabel(profile) {
814+
return strategyLabels[profile] || profile;
815+
}
816+
817+
function currentStrategyForAccount(platform, account) {
818+
const byPlatform = state.currentStrategies[platform] || {};
819+
const keys = [account?.key, account?.target_name, account?.label]
820+
.filter(Boolean)
821+
.map((value) => String(value));
822+
for (const key of keys) {
823+
const profile = cleanStrategyProfile(byPlatform[key]?.strategy_profile);
824+
if (profile) return profile;
825+
}
826+
return "";
827+
}
828+
829+
function defaultStrategyForAccount(platform, account, fallback = "tqqq_growth_income") {
830+
const currentProfile = currentStrategyForAccount(platform, account);
831+
if (currentProfile) return currentProfile;
832+
const profile = cleanStrategyProfile(account?.default_strategy_profile || account?.strategy_profile);
833+
if (profile) return profile;
790834
const hint = [
791835
account?.key,
792836
account?.label,
@@ -801,12 +845,18 @@ <h2 data-i18n="summary">切换摘要</h2>
801845
return fallback;
802846
}
803847

848+
function syncStrategyForAccount(platform) {
849+
const account = selectedAccount(platform);
850+
if (!account) return;
851+
state.forms[platform].strategy = defaultStrategyForAccount(platform, account, state.forms[platform].strategy);
852+
}
853+
804854
function ensureAccountSelection(platform) {
805855
const options = optionsFor(platform);
806856
if (!options.length) return;
807857
if (!options.some((option) => option.key === state.forms[platform].accountKey)) {
808858
state.forms[platform].accountKey = options[0].key;
809-
state.forms[platform].strategy = defaultStrategyForAccount(options[0], state.forms[platform].strategy);
859+
state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0], state.forms[platform].strategy);
810860
}
811861
}
812862

@@ -859,7 +909,7 @@ <h2 data-i18n="summary">切换摘要</h2>
859909
return [
860910
[t("repository"), repositories[state.selected]],
861911
[t("selectedAccount"), account.label],
862-
[t("selectedStrategy"), strategyLabels[inputs.strategy_profile] || inputs.strategy_profile],
912+
[t("selectedStrategy"), strategyLabel(inputs.strategy_profile)],
863913
[t("selectedMode"), inputs.execution_mode],
864914
[t("target"), inputs.target_name],
865915
[t("accountSelector"), inputs.account_selector || "auto"],
@@ -894,7 +944,7 @@ <h2 data-i18n="summary">切换摘要</h2>
894944
<span class="platform-copy">
895945
<strong>${meta.label}</strong>
896946
<span>${escapeHtml(account.label)}</span>
897-
<small>${escapeHtml(strategyLabels[form.strategy] || form.strategy)}</small>
947+
<small>${escapeHtml(strategyLabel(form.strategy))}</small>
898948
</span>
899949
`;
900950
strip.appendChild(button);
@@ -919,8 +969,8 @@ <h2 data-i18n="summary">切换摘要</h2>
919969
: `<option value="">${t("noAccount")}</option>`;
920970
el("account-meta").textContent = accounts.length ? accountMetaText(platform) : "";
921971

922-
strategySelect.innerHTML = strategyOptions.map((strategy) => (
923-
`<option value="${strategy}" ${strategy === form.strategy ? "selected" : ""}>${escapeHtml(strategyLabels[strategy] || strategy)}</option>`
972+
strategySelect.innerHTML = strategyChoices().map((strategy) => (
973+
`<option value="${strategy}" ${strategy === form.strategy ? "selected" : ""}>${escapeHtml(strategyLabel(strategy))}</option>`
924974
)).join("");
925975

926976
document.querySelectorAll("[data-mode]").forEach((button) => {
@@ -1002,12 +1052,17 @@ <h2 data-i18n="summary">切换摘要</h2>
10021052
const payload = await response.json();
10031053
if (payload.accountOptions) {
10041054
state.accountOptions = normalizeAccountOptions(payload.accountOptions);
1055+
state.currentStrategies = normalizeCurrentStrategies(payload.currentStrategies || {});
10051056
state.configSource = "private";
1006-
for (const platform of Object.keys(platformMeta)) ensureAccountSelection(platform);
1057+
for (const platform of Object.keys(platformMeta)) {
1058+
ensureAccountSelection(platform);
1059+
syncStrategyForAccount(platform);
1060+
}
10071061
render();
10081062
}
10091063
} catch {
10101064
state.configSource = "default";
1065+
state.currentStrategies = {};
10111066
}
10121067
}
10131068

@@ -1034,6 +1089,24 @@ <h2 data-i18n="summary">切换摘要</h2>
10341089
return normalized;
10351090
}
10361091

1092+
function normalizeCurrentStrategies(raw) {
1093+
const normalized = {};
1094+
for (const platform of Object.keys(platformMeta)) {
1095+
if (!raw[platform] || typeof raw[platform] !== "object" || Array.isArray(raw[platform])) continue;
1096+
normalized[platform] = {};
1097+
for (const [key, entry] of Object.entries(raw[platform])) {
1098+
const profile = cleanStrategyProfile(entry?.strategy_profile);
1099+
if (!profile) continue;
1100+
normalized[platform][String(key)] = {
1101+
strategy_profile: profile,
1102+
source: entry?.source ? String(entry.source) : "",
1103+
};
1104+
}
1105+
if (!Object.keys(normalized[platform]).length) delete normalized[platform];
1106+
}
1107+
return normalized;
1108+
}
1109+
10371110
async function dispatchSwitch() {
10381111
if (!state.auth.allowed) return;
10391112
el("toast").textContent = t("dispatching");
@@ -1083,10 +1156,7 @@ <h2 data-i18n="summary">切换摘要</h2>
10831156

10841157
el("account-select").addEventListener("change", () => {
10851158
state.forms[state.selected].accountKey = el("account-select").value;
1086-
state.forms[state.selected].strategy = defaultStrategyForAccount(
1087-
selectedAccount(),
1088-
state.forms[state.selected].strategy,
1089-
);
1159+
syncStrategyForAccount(state.selected);
10901160
render();
10911161
});
10921162

web/strategy-switch-console/page_asset.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)