diff --git a/tests/strategy_switch_worker_validation.mjs b/tests/strategy_switch_worker_validation.mjs
index 1106932..e974bd4 100644
--- a/tests/strategy_switch_worker_validation.mjs
+++ b/tests/strategy_switch_worker_validation.mjs
@@ -25,6 +25,10 @@ assert.ok(indexHtml.includes('function selectedCashCurrency('));
assert.ok(indexHtml.includes('function currentReservedCashPolicyText('));
assert.ok(indexHtml.includes('reservedCashTouched: false'));
assert.ok(indexHtml.includes('.reserve-ratio-block'));
+assert.ok(indexHtml.includes('.summary-row.pending'));
+assert.ok(indexHtml.includes('function currentEntryHasState('));
+assert.equal(indexHtml.includes('placeholder="150"'), false);
+assert.equal(indexHtml.includes('placeholder="0.03"'), false);
assert.equal(indexHtml.includes("ibkr-primary"), false);
assert.equal(indexHtml.includes("longbridge-quant-sg-service"), false);
assert.equal(indexHtml.includes('account_selector: "SG"'), false);
@@ -339,6 +343,38 @@ try {
globalThis.fetch = originalFetch;
}
+globalThis.fetch = async (url) => {
+ const requestUrl = String(url);
+ if (requestUrl.endsWith("/CLOUD_RUN_SERVICE_TARGETS_JSON")) {
+ return new Response("", { status: 404 });
+ }
+ if (requestUrl.endsWith("/LONGBRIDGE_MIN_RESERVED_CASH_USD")) {
+ return new Response(JSON.stringify({ value: "150" }), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+ if (requestUrl.endsWith("/LONGBRIDGE_RESERVED_CASH_RATIO")) {
+ return new Response(JSON.stringify({ value: "0.03" }), {
+ status: 200,
+ headers: { "Content-Type": "application/json" },
+ });
+ }
+ return new Response("", { status: 404 });
+};
+try {
+ const currentStrategies = await __test.loadCurrentStrategies(
+ { longbridge: [accountOptions.longbridge[0]] },
+ { RUNTIME_SETTINGS_DISPATCH_TOKEN: "test-token" },
+ );
+ assert.equal(currentStrategies.longbridge.hk.strategy_profile, undefined);
+ assert.equal(currentStrategies.longbridge.hk.min_reserved_cash_usd, "150");
+ assert.equal(currentStrategies.longbridge.hk.reserved_cash_ratio, "0.03");
+ assert.equal(currentStrategies.longbridge.hk.source, "RESERVED_CASH_VARIABLES");
+} finally {
+ globalThis.fetch = originalFetch;
+}
+
const longbridgeHk = __test.assertConfiguredAccount(
{
platform: "longbridge",
diff --git a/web/strategy-switch-console/index.html b/web/strategy-switch-console/index.html
index fdda3cf..0646515 100644
--- a/web/strategy-switch-console/index.html
+++ b/web/strategy-switch-console/index.html
@@ -571,6 +571,13 @@
border-bottom: 0;
}
+ .summary-row.pending {
+ margin: 0 -16px;
+ padding-inline: 13px 16px;
+ border-left: 3px solid var(--platform-color, var(--accent));
+ background: color-mix(in srgb, var(--platform-color, var(--accent)) 6%, #ffffff);
+ }
+
.summary-row dt {
color: var(--muted);
font-size: 12px;
@@ -588,6 +595,11 @@
overflow-wrap: anywhere;
}
+ .summary-row.pending dd {
+ color: var(--platform-color, var(--accent));
+ font-weight: 800;
+ }
+
.summary-actions {
display: flex;
gap: 10px;
@@ -764,13 +776,13 @@
@@ -864,9 +876,9 @@ 切换摘要
paper: "Dry run",
minReservedCash: "最小预留现金 ({currency})",
reservedCashRatio: "预留现金比例",
- reservedCashMeta: "留空则沿用当前平台默认值。",
+ reservedCashMeta: "例如 150;留空则不覆盖当前值。",
reservedCashRatioMeta: "例如 0.03 表示 3%。",
- summary: "当前配置状态",
+ summary: "当前 / 待提交",
copySummary: "复制状态",
loginToRun: "登录后切换",
loadingConfig: "读取配置中",
@@ -883,7 +895,8 @@ 切换摘要
repository: "平台仓库",
selectedAccount: "账号",
selectedMarket: "市场",
- reservedCashPolicy: "预留现金",
+ reservedCashPolicy: "当前预留现金",
+ pendingReservedCashPolicy: "待提交预留现金",
unchanged: "不变",
copied: "已复制状态",
dispatching: "正在触发 workflow...",
@@ -894,7 +907,7 @@ 切换摘要
usEquity: "美股",
hkEquity: "港股",
currentStrategy: "当前策略",
- nextStrategy: "选择策略",
+ nextStrategy: "切换策略",
notRead: "未读取",
},
en: {
@@ -918,9 +931,9 @@ 切换摘要
paper: "Dry run",
minReservedCash: "Minimum reserved cash ({currency})",
reservedCashRatio: "Reserved cash ratio",
- reservedCashMeta: "Leave blank to keep the platform default.",
+ reservedCashMeta: "Example: 150. Leave blank to avoid overriding the current value.",
reservedCashRatioMeta: "Use 0.03 for 3%.",
- summary: "Current Config",
+ summary: "Current / Pending",
copySummary: "Copy state",
loginToRun: "Sign in to switch",
loadingConfig: "Loading config",
@@ -937,7 +950,8 @@ 切换摘要
repository: "Repository",
selectedAccount: "Account",
selectedMarket: "Market",
- reservedCashPolicy: "Reserved cash",
+ reservedCashPolicy: "Current reserved cash",
+ pendingReservedCashPolicy: "Pending reserved cash",
unchanged: "Unchanged",
copied: "State copied",
dispatching: "Dispatching workflow...",
@@ -948,7 +962,7 @@ 切换摘要
usEquity: "US equity",
hkEquity: "HK equity",
currentStrategy: "Current strategy",
- nextStrategy: "Selected strategy",
+ nextStrategy: "Switch strategy",
notRead: "Not read",
},
};
@@ -1113,12 +1127,21 @@ 切换摘要
.map((value) => String(value));
for (const key of keys) {
const entry = byPlatform[key];
- const profile = cleanStrategyProfile(entry?.strategy_profile);
- if (profile) return entry;
+ if (currentEntryHasState(entry)) return entry;
}
return null;
}
+ function currentEntryHasState(entry) {
+ if (!entry || typeof entry !== "object" || Array.isArray(entry)) return false;
+ return Boolean(
+ cleanStrategyProfile(entry.strategy_profile) ||
+ cleanDisplayNumber(entry.min_reserved_cash_usd ?? entry.reserved_cash_floor_usd) ||
+ cleanDisplayRatio(entry.reserved_cash_ratio) ||
+ normalizeExecutionMode(entry.execution_mode, entry.dry_run_only),
+ );
+ }
+
function currentStrategyForAccount(platform, account) {
const entry = currentEntryForAccount(platform, account);
const profile = cleanStrategyProfile(entry?.strategy_profile);
@@ -1309,6 +1332,10 @@ 切换摘要
);
}
+ function pendingReservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform)) {
+ return reservedCashPolicyText(inputs, platform, account, t("unchanged"));
+ }
+
function formatRatioPercent(value) {
const numeric = Number(value);
if (!Number.isFinite(numeric)) return String(value);
@@ -1318,15 +1345,19 @@ 切换摘要
function summaryRows(inputs) {
const account = selectedAccount();
const currentProfile = currentStrategyForAccount(state.selected, account);
+ const nextProfile = cleanStrategyProfile(inputs.strategy_profile);
+ const currentStrategyText = currentProfile ? strategyLabel(currentProfile) : t("notRead");
+ const nextStrategyChanged = !currentProfile || currentProfile !== nextProfile;
const rows = [
[t("repository"), state.repositories[state.selected] || defaultRepositories[state.selected]],
[t("selectedAccount"), account.label],
+ [t("currentStrategy"), currentStrategyText],
[t("selectedMarket"), supportedDomainLabel(state.selected, account)],
[t("reservedCashPolicy"), currentReservedCashPolicyText(state.selected, account)],
- [t("nextStrategy"), strategyLabel(inputs.strategy_profile)],
+ [t("pendingReservedCashPolicy"), pendingReservedCashPolicyText(inputs, state.selected, account), "pending"],
];
- if (state.auth.allowed) {
- rows.splice(2, 0, [t("currentStrategy"), currentProfile ? strategyLabel(currentProfile) : t("notRead")]);
+ if (nextStrategyChanged && nextProfile) {
+ rows.push([t("nextStrategy"), strategyLabel(nextProfile), "pending"]);
}
return rows;
}
@@ -1452,9 +1483,10 @@ 切换摘要
const list = el("summary-list");
list.replaceChildren();
document.querySelector(".summary-head h2").textContent = t("summary");
- for (const [label, value] of summaryRows(inputs)) {
+ for (const [label, value, rowClass] of summaryRows(inputs)) {
const row = document.createElement("div");
row.className = "summary-row";
+ if (rowClass) row.classList.add(rowClass);
const labelNode = document.createElement("dt");
labelNode.textContent = label;
const valueNode = document.createElement("dd");
@@ -1630,13 +1662,16 @@ 切换摘要
normalized[platform] = {};
for (const [key, entry] of Object.entries(raw[platform])) {
const profile = cleanStrategyProfile(entry?.strategy_profile);
- if (!profile) continue;
+ const minReservedCashUsd = cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd);
+ const reservedCashRatio = cleanDisplayRatio(entry?.reserved_cash_ratio);
+ const executionMode = normalizeExecutionMode(entry?.execution_mode, entry?.dry_run_only);
+ if (!profile && !minReservedCashUsd && !reservedCashRatio && !executionMode) continue;
normalized[platform][String(key)] = {
strategy_profile: profile,
- execution_mode: normalizeExecutionMode(entry?.execution_mode, entry?.dry_run_only),
+ execution_mode: executionMode,
dry_run_only: entry?.dry_run_only === true || entry?.dry_run_only === "true" || entry?.dry_run_only === "1",
- min_reserved_cash_usd: cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd),
- reserved_cash_ratio: cleanDisplayRatio(entry?.reserved_cash_ratio),
+ min_reserved_cash_usd: minReservedCashUsd,
+ reserved_cash_ratio: reservedCashRatio,
source: entry?.source ? String(entry.source) : "",
};
}
diff --git a/web/strategy-switch-console/page_asset.js b/web/strategy-switch-console/page_asset.js
index 365752d..6a6e8d6 100644
--- a/web/strategy-switch-console/page_asset.js
+++ b/web/strategy-switch-console/page_asset.js
@@ -1,2 +1,2 @@
// Generated by scripts/sync_strategy_switch_page_asset.py; do not edit by hand.
-export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch\n \n\n\n \n \n
策略切换
\n
选平台、目标账号和策略,一次执行完成切换。
\n
\n \n \n\n \n \n
初始化控制台\n
读取策略配置
\n
正在读取登录状态、账号配置和当前状态。
\n
\n
\n \n\n \n \n\n \n \n
\n 当前平台\n \n
\n\n
登录后查看账号配置。
\n\n
\n\n
\n
\n
登录后才可执行切换。
\n
\n
\n
\n\n \n \n \n\n \n\n\n";
+export const PAGE_HTML = "\n\n\n \n \n \n QuantRuntimeSettings Strategy Switch\n \n\n\n \n \n
策略切换
\n
选平台、目标账号和策略,一次执行完成切换。
\n
\n \n \n\n \n \n
初始化控制台\n
读取策略配置
\n
正在读取登录状态、账号配置和当前状态。
\n
\n
\n \n\n \n \n\n \n \n
\n 当前平台\n \n
\n\n
登录后查看账号配置。
\n\n
\n\n
\n
\n
登录后才可执行切换。
\n
\n
\n
\n\n \n \n \n\n \n\n\n";
diff --git a/web/strategy-switch-console/worker.js b/web/strategy-switch-console/worker.js
index 616e632..143b441 100644
--- a/web/strategy-switch-console/worker.js
+++ b/web/strategy-switch-console/worker.js
@@ -559,11 +559,20 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount
const serviceTargetsValue = await readVariable(repository, "repository", "", "CLOUD_RUN_SERVICE_TARGETS_JSON");
const serviceTarget = runtimeTargetFromServiceTargets(serviceTargetsValue, platform, option);
const serviceTargetProfile = cleanCurrentStrategy(serviceTarget?.strategy_profile);
+ const serviceTargetReservedCashPayload = reservedCashPayloadFromObject(platform, serviceTarget);
if (serviceTargetProfile) {
return {
strategy_profile: serviceTargetProfile,
...runtimeModePayload(serviceTarget),
- ...reservedCashPayloadFromObject(platform, serviceTarget),
+ ...serviceTargetReservedCashPayload,
+ source: "CLOUD_RUN_SERVICE_TARGETS_JSON",
+ variable_scope: "repository",
+ };
+ }
+ if (Object.keys(serviceTargetReservedCashPayload).length) {
+ return {
+ ...runtimeModePayload(serviceTarget),
+ ...serviceTargetReservedCashPayload,
source: "CLOUD_RUN_SERVICE_TARGETS_JSON",
variable_scope: "repository",
};
@@ -611,6 +620,15 @@ async function resolveCurrentStrategyForAccount({ platform, option, optionsCount
}
}
+ if (Object.keys(reservedCashPayload).length) {
+ return {
+ ...reservedCashPayload,
+ source: "RESERVED_CASH_VARIABLES",
+ variable_scope: variableScope,
+ github_environment: githubEnvironment || "",
+ };
+ }
+
return null;
}