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
36 changes: 36 additions & 0 deletions tests/strategy_switch_worker_validation.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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",
Expand Down
75 changes: 55 additions & 20 deletions web/strategy-switch-console/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -764,13 +776,13 @@ <h2 id="platform-title">LongBridge</h2>

<label class="control-block min-reserve-block">
<span id="min-reserved-cash-label">最小预留现金</span>
<input id="min-reserved-cash-input" type="number" inputmode="decimal" min="0" step="1" placeholder="150">
<input id="min-reserved-cash-input" type="number" inputmode="decimal" min="0" step="1">
<span class="selection-meta" data-i18n="reservedCashMeta">留空则沿用当前平台默认值。</span>
</label>

<label class="control-block reserve-ratio-block">
<span data-i18n="reservedCashRatio">预留现金比例</span>
<input id="reserved-cash-ratio-input" type="number" inputmode="decimal" min="0" max="1" step="0.01" placeholder="0.03">
<input id="reserved-cash-ratio-input" type="number" inputmode="decimal" min="0" max="1" step="0.01">
<span class="selection-meta" data-i18n="reservedCashRatioMeta">例如 0.03 表示 3%。</span>
</label>
</div>
Expand Down Expand Up @@ -864,9 +876,9 @@ <h2 data-i18n="summary">切换摘要</h2>
paper: "Dry run",
minReservedCash: "最小预留现金 ({currency})",
reservedCashRatio: "预留现金比例",
reservedCashMeta: "留空则沿用当前平台默认值。",
reservedCashMeta: "例如 150;留空则不覆盖当前值。",
reservedCashRatioMeta: "例如 0.03 表示 3%。",
summary: "当前配置状态",
summary: "当前 / 待提交",
copySummary: "复制状态",
loginToRun: "登录后切换",
loadingConfig: "读取配置中",
Expand All @@ -883,7 +895,8 @@ <h2 data-i18n="summary">切换摘要</h2>
repository: "平台仓库",
selectedAccount: "账号",
selectedMarket: "市场",
reservedCashPolicy: "预留现金",
reservedCashPolicy: "当前预留现金",
pendingReservedCashPolicy: "待提交预留现金",
unchanged: "不变",
copied: "已复制状态",
dispatching: "正在触发 workflow...",
Expand All @@ -894,7 +907,7 @@ <h2 data-i18n="summary">切换摘要</h2>
usEquity: "美股",
hkEquity: "港股",
currentStrategy: "当前策略",
nextStrategy: "选择策略",
nextStrategy: "切换策略",
notRead: "未读取",
},
en: {
Expand All @@ -918,9 +931,9 @@ <h2 data-i18n="summary">切换摘要</h2>
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",
Expand All @@ -937,7 +950,8 @@ <h2 data-i18n="summary">切换摘要</h2>
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...",
Expand All @@ -948,7 +962,7 @@ <h2 data-i18n="summary">切换摘要</h2>
usEquity: "US equity",
hkEquity: "HK equity",
currentStrategy: "Current strategy",
nextStrategy: "Selected strategy",
nextStrategy: "Switch strategy",
notRead: "Not read",
},
};
Expand Down Expand Up @@ -1113,12 +1127,21 @@ <h2 data-i18n="summary">切换摘要</h2>
.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);
Expand Down Expand Up @@ -1309,6 +1332,10 @@ <h2 data-i18n="summary">切换摘要</h2>
);
}

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);
Expand All @@ -1318,15 +1345,19 @@ <h2 data-i18n="summary">切换摘要</h2>
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;
}
Expand Down Expand Up @@ -1452,9 +1483,10 @@ <h2 data-i18n="summary">切换摘要</h2>
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");
Expand Down Expand Up @@ -1630,13 +1662,16 @@ <h2 data-i18n="summary">切换摘要</h2>
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) : "",
};
}
Expand Down
2 changes: 1 addition & 1 deletion web/strategy-switch-console/page_asset.js

Large diffs are not rendered by default.

20 changes: 19 additions & 1 deletion web/strategy-switch-console/worker.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {

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 Continue resolving the profile when service targets only carry cash

When a matching CLOUD_RUN_SERVICE_TARGETS_JSON entry has reserved-cash fields but no strategy_profile, this early return stops the existing fallback reads from RUNTIME_TARGET_JSON and STRATEGY_PROFILE. In that mixed configuration the UI now reports the current strategy as not read (and may fall back to the account default) even though a lower-priority profile is still available; the reserve-only result should be merged only after those profile sources have been checked.

Useful? React with 👍 / 👎.

return {
...runtimeModePayload(serviceTarget),
...serviceTargetReservedCashPayload,
source: "CLOUD_RUN_SERVICE_TARGETS_JSON",
variable_scope: "repository",
};
Expand Down Expand Up @@ -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;
}

Expand Down