Skip to content

Commit d510739

Browse files
authored
Read current strategies in switch console
Read current GitHub Variables for the switch console, align known strategy profiles, and document profile catalog requirements.
1 parent 0050419 commit d510739

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)