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: 3 additions & 0 deletions tests/strategy_switch_worker_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ assert.ok(indexHtml.includes("function hasPrivateConfig()"));
assert.ok(indexHtml.includes('el("quick-form").hidden = !showPrivateControls'));
assert.ok(indexHtml.includes('id="min-reserved-cash-input"'));
assert.ok(indexHtml.includes('id="reserved-cash-ratio-input"'));
assert.ok(indexHtml.includes('function selectedCashCurrency('));
assert.equal(indexHtml.includes("ibkr-primary"), false);
assert.equal(indexHtml.includes("longbridge-quant-sg-service"), false);
assert.equal(indexHtml.includes('account_selector: "SG"'), false);
Expand Down Expand Up @@ -135,6 +136,7 @@ const accountOptions = __test.normalizeAccountOptionsPayload(
target_name: "hk",
account_selector: "HK",
default_strategy_profile: "hk_low_vol_dividend_quality_snapshot",
cash_currency: "HKD",
},
{
key: "sg",
Expand Down Expand Up @@ -178,6 +180,7 @@ const accountOptions = __test.normalizeAccountOptionsPayload(
assert.deepEqual(accountOptions.longbridge[0].supported_domains, ["us_equity", "hk_equity"]);
assert.deepEqual(accountOptions.longbridge[1].supported_domains, ["us_equity", "hk_equity"]);
assert.deepEqual(accountOptions.ibkr[0].supported_domains, ["us_equity", "hk_equity"]);
assert.equal(accountOptions.longbridge[0].cash_currency, "HKD");

const normalizedReservedCashInputs = __test.normalizeSwitchInputs({
platform: "ibkr",
Expand Down
3 changes: 2 additions & 1 deletion web/strategy-switch-console/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,7 @@ Each account item supports:
"deployment_selector": "demo-ibkr-tqqq",
"account_scope": "demo-ibkr-tqqq",
"service_name": "interactive-brokers-demo-ibkr-tqqq-service",
"cash_currency": "USD",
"default_strategy_profile": "tqqq_growth_income",
"supported_domains": ["us_equity", "hk_equity"]
}
Expand All @@ -117,7 +118,7 @@ The Worker validates dispatch inputs against this config, including whether the

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`.

The switch form also accepts optional reserved-cash overrides: minimum reserved cash in USD and reserved-cash ratio. Leave them blank to keep the platform's existing defaults. When set, the Worker passes them to `manual-strategy-switch.yml`, which writes the platform-specific variables such as `IBKR_MIN_RESERVED_CASH_USD` and `IBKR_RESERVED_CASH_RATIO`.
The switch form also accepts optional reserved-cash overrides: minimum reserved cash in the selected account currency and reserved-cash ratio. Set `cash_currency` to `USD` or `HKD` in account config when the account has a fixed cash currency; otherwise the page infers HKD for HK-equity strategy selections and USD for US-equity selections. Leave reserved-cash fields blank to keep the platform's existing defaults. When set, the Worker passes them to `manual-strategy-switch.yml`, which writes the platform-specific variables such as `IBKR_MIN_RESERVED_CASH_USD` and `IBKR_RESERVED_CASH_RATIO`.

Successful strategy switches also sync the selected account's `default_strategy_profile` back to the KV `account_options` key. The web endpoint does this immediately after dispatching the workflow, and the manual GitHub workflow calls the Worker's internal sync endpoint after applying platform variables when the `runtime-strategy-switch` environment variable `STRATEGY_SWITCH_CONSOLE_URL` is set. For that workflow callback, set the GitHub environment secret `STRATEGY_SWITCH_SYNC_TOKEN` to the same value as the Worker secret with that name.

Expand Down
3 changes: 2 additions & 1 deletion web/strategy-switch-console/README.zh-CN.md
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ wrangler secret put STRATEGY_SWITCH_ACCOUNT_OPTIONS_JSON < /tmp/strategy-switch-
"deployment_selector": "demo-ibkr-tqqq",
"account_scope": "demo-ibkr-tqqq",
"service_name": "interactive-brokers-demo-ibkr-tqqq-service",
"cash_currency": "USD",
"default_strategy_profile": "tqqq_growth_income",
"supported_domains": ["us_equity", "hk_equity"]
}
Expand All @@ -124,7 +125,7 @@ Worker 会校验 dispatch 参数必须匹配这里的某个账号项,也会校

登录用户访问 `/api/config` 时,Worker 还会读取目标平台仓库的当前 GitHub Variables。读取优先级是账号匹配的 `CLOUD_RUN_SERVICE_TARGETS_JSON`、匹配的 `RUNTIME_TARGET_JSON.strategy_profile`、`STRATEGY_PROFILE`;都读不到时,页面才回退到 `default_strategy_profile`。

切换表单也支持可选的预留现金覆盖项:最小预留现金 USD 和预留现金比例。留空表示沿用平台现有默认值。填写后,Worker 会把它们传给 `manual-strategy-switch.yml`,由 workflow 写入平台对应变量,例如 `IBKR_MIN_RESERVED_CASH_USD` 和 `IBKR_RESERVED_CASH_RATIO`。
切换表单也支持可选的预留现金覆盖项:所选账号币种下的最小预留现金和预留现金比例。如果账号现金币种固定,可以在账号配置里把 `cash_currency` 设为 `USD` 或 `HKD`;否则页面会按所选策略推断,港股策略显示 HKD,美股策略显示 USD。预留现金字段留空表示沿用平台现有默认值。填写后,Worker 会把它们传给 `manual-strategy-switch.yml`,由 workflow 写入平台对应变量,例如 `IBKR_MIN_RESERVED_CASH_USD` 和 `IBKR_RESERVED_CASH_RATIO`。

策略切换成功后也会把当前账号的 `default_strategy_profile` 同步回 KV 的 `account_options` key。网页接口会在触发 workflow 成功后立即同步;如果 `runtime-strategy-switch` 环境变量里配置了 `STRATEGY_SWITCH_CONSOLE_URL`,手动 GitHub workflow 在写入平台变量后也会回调 Worker 内部接口同步。这个 workflow 回调需要 GitHub 环境 secret `STRATEGY_SWITCH_SYNC_TOKEN`,值要和 Worker 里同名 secret 保持一致。

Expand Down
6 changes: 6 additions & 0 deletions web/strategy-switch-console/account-options.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
"label": "hk",
"target_name": "hk",
"account_selector": "HK",
"cash_currency": "HKD",
"default_strategy_profile": "hk_low_vol_dividend_quality_snapshot",
"supported_domains": ["us_equity", "hk_equity"]
},
Expand Down Expand Up @@ -34,6 +35,7 @@
"deployment_selector": "demo-ibkr-tqqq",
"account_scope": "demo-ibkr-tqqq",
"service_name": "interactive-brokers-demo-ibkr-tqqq-service",
"cash_currency": "USD",
"default_strategy_profile": "tqqq_growth_income",
"supported_domains": ["us_equity", "hk_equity"]
},
Expand All @@ -45,6 +47,7 @@
"deployment_selector": "demo-ibkr-soxl",
"account_scope": "demo-ibkr-soxl",
"service_name": "interactive-brokers-demo-ibkr-soxl-service",
"cash_currency": "USD",
"default_strategy_profile": "soxl_soxx_trend_income",
"supported_domains": ["us_equity", "hk_equity"]
},
Expand All @@ -56,6 +59,7 @@
"deployment_selector": "demo-ibkr-dca",
"account_scope": "demo-ibkr-dca",
"service_name": "interactive-brokers-demo-ibkr-dca-service",
"cash_currency": "USD",
"default_strategy_profile": "nasdaq_sp500_smart_dca",
"supported_domains": ["us_equity", "hk_equity"]
}
Expand All @@ -65,6 +69,7 @@
"key": "default",
"label": "default",
"target_name": "default",
"cash_currency": "USD",
"default_strategy_profile": "soxl_soxx_trend_income",
"supported_domains": ["us_equity"]
}
Expand All @@ -74,6 +79,7 @@
"key": "default",
"label": "default",
"target_name": "default",
"cash_currency": "USD",
"default_strategy_profile": "mega_cap_leader_rotation_top50_balanced",
"supported_domains": ["us_equity"]
}
Expand Down
32 changes: 27 additions & 5 deletions web/strategy-switch-console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -745,7 +745,7 @@ <h2 id="platform-title">LongBridge</h2>
</div>

<label class="control-block">
<span data-i18n="minReservedCash">最小预留现金</span>
<span id="min-reserved-cash-label">最小预留现金</span>
<input id="min-reserved-cash-input" type="number" inputmode="decimal" min="0" step="1" placeholder="150">
<span class="selection-meta" data-i18n="reservedCashMeta">留空则沿用当前平台默认值。</span>
</label>
Expand Down Expand Up @@ -844,7 +844,7 @@ <h2 data-i18n="summary">切换摘要</h2>
mode: "模式",
live: "实盘",
paper: "Dry run",
minReservedCash: "最小预留现金",
minReservedCash: "最小预留现金 ({currency})",
reservedCashRatio: "预留现金比例",
reservedCashMeta: "留空则沿用当前平台默认值。",
reservedCashRatioMeta: "例如 0.03 表示 3%。",
Expand Down Expand Up @@ -898,7 +898,7 @@ <h2 data-i18n="summary">切换摘要</h2>
mode: "Mode",
live: "Live",
paper: "Dry run",
minReservedCash: "Minimum reserved cash",
minReservedCash: "Minimum reserved cash ({currency})",
reservedCashRatio: "Reserved cash ratio",
reservedCashMeta: "Leave blank to keep the platform default.",
reservedCashRatioMeta: "Use 0.03 for 3%.",
Expand Down Expand Up @@ -994,6 +994,20 @@ <h2 data-i18n="summary">切换摘要</h2>
return t("usEquity");
}

function strategyDomain(profile) {
return strategyCatalog[profile]?.domain || "";
}

function selectedCashCurrency(platform = state.selected, account = selectedAccount(platform)) {
const configured = String(account?.cash_currency || "").trim().toUpperCase();
if (configured === "USD" || configured === "HKD") return configured;
const domain = strategyDomain(state.forms[platform]?.strategy);
if (domain === "hk_equity") return "HKD";
const supported = supportedDomainsForAccount(platform, account);
if (supported.length === 1 && supported[0] === "hk_equity") return "HKD";
return "USD";
}

function applyStrategyProfiles(rawProfiles) {
const profiles = Array.isArray(rawProfiles) && rawProfiles.length
? rawProfiles
Expand Down Expand Up @@ -1215,9 +1229,10 @@ <h2 data-i18n="summary">切换摘要</h2>
function reservedCashPolicyText(inputs) {
const floor = inputs.min_reserved_cash_usd;
const ratio = inputs.reserved_cash_ratio;
const currency = selectedCashCurrency();
if (!floor && !ratio) return t("unchanged");
if (floor && ratio) return `max($${floor}, ${formatRatioPercent(ratio)})`;
if (floor) return `$${floor}`;
if (floor && ratio) return `max(${floor} ${currency}, ${formatRatioPercent(ratio)})`;
if (floor) return `${floor} ${currency}`;
return formatRatioPercent(ratio);
}

Expand Down Expand Up @@ -1340,6 +1355,10 @@ <h2 data-i18n="summary">切换摘要</h2>
el("strategy-meta").textContent = account
? t("strategyMeta").replace("{domains}", supportedDomainLabel(platform, account))
: "";
el("min-reserved-cash-label").textContent = t("minReservedCash").replace(
"{currency}",
selectedCashCurrency(platform, account),
);
minReservedCashInput.value = form.minReservedCashUsd;
reservedCashRatioInput.value = form.reservedCashRatio;

Expand Down Expand Up @@ -1507,6 +1526,9 @@ <h2 data-i18n="summary">切换摘要</h2>
deployment_selector: item.deployment_selector ? String(item.deployment_selector) : "",
account_scope: item.account_scope ? String(item.account_scope) : "",
service_name: item.service_name ? String(item.service_name) : "",
cash_currency: item.cash_currency || item.market_currency || item.trading_currency
? String(item.cash_currency || item.market_currency || item.trading_currency).trim().toUpperCase()
: "",
supported_domains: normalizeSupportedDomains(platform, item),
default_strategy_profile: item.default_strategy_profile || item.strategy_profile
? String(item.default_strategy_profile || item.strategy_profile)
Expand Down
2 changes: 1 addition & 1 deletion web/strategy-switch-console/page_asset.js

Large diffs are not rendered by default.

10 changes: 10 additions & 0 deletions web/strategy-switch-console/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,12 @@ function cleanAccountOption(item, platform, index) {
addConfigOptional(option, "deployment_selector", item.deployment_selector, cleanSlug);
addConfigOptional(option, "account_scope", item.account_scope, cleanSlug);
addConfigOptional(option, "service_name", item.service_name, cleanSlug);
addConfigOptional(
option,
"cash_currency",
item.cash_currency || item.market_currency || item.trading_currency,
cleanCashCurrency,
);
addConfigOptional(option, "default_strategy_profile", item.default_strategy_profile || item.strategy_profile, cleanSlug);
addConfigOptional(option, "github_environment", item.github_environment, cleanSlug);
addConfigOptional(option, "variable_scope", item.variable_scope, (value, field) =>
Expand Down Expand Up @@ -1018,6 +1024,10 @@ function cleanStrategyDomain(value, fieldName) {
return cleanChoice(value, SUPPORTED_STRATEGY_DOMAINS, fieldName);
}

function cleanCashCurrency(value, fieldName) {
return cleanChoice(String(value || "").trim().toUpperCase(), ["USD", "HKD"], fieldName);
}

function addConfigOptional(target, key, value, cleaner) {
if (value === undefined || value === null || String(value).trim() === "") return;
target[key] = cleaner(value, key);
Expand Down