Skip to content

Commit b71060b

Browse files
authored
Read current reserved cash in switch console (#42)
1 parent a91298c commit b71060b

4 files changed

Lines changed: 210 additions & 9 deletions

File tree

tests/strategy_switch_worker_validation.mjs

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ assert.ok(indexHtml.includes('el("quick-form").hidden = !showPrivateControls'));
2222
assert.ok(indexHtml.includes('id="min-reserved-cash-input"'));
2323
assert.ok(indexHtml.includes('id="reserved-cash-ratio-input"'));
2424
assert.ok(indexHtml.includes('function selectedCashCurrency('));
25+
assert.ok(indexHtml.includes('function currentReservedCashPolicyText('));
26+
assert.ok(indexHtml.includes('reservedCashTouched: false'));
27+
assert.ok(indexHtml.includes('.reserve-ratio-block'));
2528
assert.equal(indexHtml.includes("ibkr-primary"), false);
2629
assert.equal(indexHtml.includes("longbridge-quant-sg-service"), false);
2730
assert.equal(indexHtml.includes('account_selector: "SG"'), false);
@@ -257,6 +260,18 @@ globalThis.fetch = async (url) => {
257260
if (requestUrl.endsWith("/CLOUD_RUN_SERVICE_TARGETS_JSON")) {
258261
return new Response("", { status: 404 });
259262
}
263+
if (requestUrl.endsWith("/SCHWAB_MIN_RESERVED_CASH_USD")) {
264+
return new Response(JSON.stringify({ value: "150" }), {
265+
status: 200,
266+
headers: { "Content-Type": "application/json" },
267+
});
268+
}
269+
if (requestUrl.endsWith("/SCHWAB_RESERVED_CASH_RATIO")) {
270+
return new Response(JSON.stringify({ value: "0.03" }), {
271+
status: 200,
272+
headers: { "Content-Type": "application/json" },
273+
});
274+
}
260275
if (requestUrl.endsWith("/RUNTIME_TARGET_JSON")) {
261276
return new Response(JSON.stringify({
262277
value: JSON.stringify({
@@ -278,11 +293,52 @@ try {
278293
);
279294
assert.equal(currentStrategies.schwab.default.strategy_profile, "soxl_soxx_trend_income");
280295
assert.equal(currentStrategies.schwab.default.execution_mode, "live");
296+
assert.equal(currentStrategies.schwab.default.min_reserved_cash_usd, "150");
297+
assert.equal(currentStrategies.schwab.default.reserved_cash_ratio, "0.03");
281298
assert.equal(currentStrategies.schwab.default.source, "RUNTIME_TARGET_JSON");
282299
} finally {
283300
globalThis.fetch = originalFetch;
284301
}
285302

303+
globalThis.fetch = async (url) => {
304+
const requestUrl = String(url);
305+
if (requestUrl.endsWith("/CLOUD_RUN_SERVICE_TARGETS_JSON")) {
306+
return new Response(JSON.stringify({
307+
value: JSON.stringify({
308+
targets: [
309+
{
310+
service: "interactive-brokers-demo-ibkr-tqqq-service",
311+
ACCOUNT_GROUP: "demo-ibkr-tqqq",
312+
IBKR_MIN_RESERVED_CASH_USD: "150",
313+
IBKR_RESERVED_CASH_RATIO: "0.03",
314+
runtime_target: {
315+
platform_id: "ibkr",
316+
strategy_profile: "tqqq_growth_income",
317+
dry_run_only: false,
318+
account_scope: "demo-ibkr-tqqq",
319+
service_name: "interactive-brokers-demo-ibkr-tqqq-service",
320+
execution_mode: "live",
321+
},
322+
},
323+
],
324+
}),
325+
}), { status: 200, headers: { "Content-Type": "application/json" } });
326+
}
327+
return new Response("", { status: 404 });
328+
};
329+
try {
330+
const currentStrategies = await __test.loadCurrentStrategies(
331+
{ ibkr: accountOptions.ibkr },
332+
{ RUNTIME_SETTINGS_DISPATCH_TOKEN: "test-token" },
333+
);
334+
assert.equal(currentStrategies.ibkr["ibkr-primary"].strategy_profile, "tqqq_growth_income");
335+
assert.equal(currentStrategies.ibkr["ibkr-primary"].min_reserved_cash_usd, "150");
336+
assert.equal(currentStrategies.ibkr["ibkr-primary"].reserved_cash_ratio, "0.03");
337+
assert.equal(currentStrategies.ibkr["ibkr-primary"].source, "CLOUD_RUN_SERVICE_TARGETS_JSON");
338+
} finally {
339+
globalThis.fetch = originalFetch;
340+
}
341+
286342
const longbridgeHk = __test.assertConfiguredAccount(
287343
{
288344
platform: "longbridge",

web/strategy-switch-console/index.html

Lines changed: 86 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -407,18 +407,30 @@
407407
.quick-form {
408408
display: grid;
409409
grid-template-columns: minmax(0, 1.2fr) minmax(0, 1fr);
410+
align-items: start;
410411
gap: 14px;
411412
}
412413

413414
.control-block {
414415
display: grid;
416+
grid-template-rows: auto 48px minmax(20px, auto);
417+
align-content: start;
415418
gap: 8px;
416419
min-width: 0;
417420
color: var(--muted);
418421
font-size: 12px;
419422
font-weight: 760;
420423
}
421424

425+
.mode-block {
426+
grid-column: 1;
427+
}
428+
429+
.min-reserve-block,
430+
.reserve-ratio-block {
431+
grid-column: 2;
432+
}
433+
422434
select,
423435
input {
424436
width: 100%;
@@ -635,6 +647,12 @@
635647
grid-template-columns: 1fr;
636648
}
637649

650+
.mode-block,
651+
.min-reserve-block,
652+
.reserve-ratio-block {
653+
grid-column: auto;
654+
}
655+
638656
.switch-panel {
639657
padding: 18px;
640658
}
@@ -736,21 +754,21 @@ <h2 id="platform-title">LongBridge</h2>
736754
<span class="selection-meta" id="strategy-meta"></span>
737755
</label>
738756

739-
<div class="control-block">
757+
<div class="control-block mode-block">
740758
<span data-i18n="mode">模式</span>
741759
<div class="mode" id="mode-control">
742760
<button type="button" data-mode="live" class="active" data-i18n="live">实盘</button>
743761
<button type="button" data-mode="paper" data-i18n="paper">模拟</button>
744762
</div>
745763
</div>
746764

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

753-
<label class="control-block">
771+
<label class="control-block reserve-ratio-block">
754772
<span data-i18n="reservedCashRatio">预留现金比例</span>
755773
<input id="reserved-cash-ratio-input" type="number" inputmode="decimal" min="0" max="1" step="0.01" placeholder="0.03">
756774
<span class="selection-meta" data-i18n="reservedCashRatioMeta">例如 0.03 表示 3%。</span>
@@ -940,7 +958,11 @@ <h2 data-i18n="summary">切换摘要</h2>
940958
? storedLang
941959
: ((navigator.language || "").toLowerCase().startsWith("zh") ? "zh" : "en");
942960
const clone = (value) => JSON.parse(JSON.stringify(value));
943-
const defaultReserveForm = () => ({ minReservedCashUsd: "", reservedCashRatio: "" });
961+
const defaultReserveForm = () => ({
962+
minReservedCashUsd: "",
963+
reservedCashRatio: "",
964+
reservedCashTouched: false,
965+
});
944966

945967
const state = {
946968
selected: "longbridge",
@@ -1103,6 +1125,33 @@ <h2 data-i18n="summary">切换摘要</h2>
11031125
return profile || "";
11041126
}
11051127

1128+
function currentReservePolicyForAccount(platform, account) {
1129+
const entry = currentEntryForAccount(platform, account);
1130+
return reservePolicyFromEntry(entry);
1131+
}
1132+
1133+
function reservePolicyFromEntry(entry) {
1134+
return {
1135+
minReservedCashUsd: cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd),
1136+
reservedCashRatio: cleanDisplayRatio(entry?.reserved_cash_ratio),
1137+
};
1138+
}
1139+
1140+
function cleanDisplayNumber(value) {
1141+
const text = String(value ?? "").trim();
1142+
if (!text || text.length > 32 || !/^(?:\d+|\d*\.\d+)$/.test(text)) return "";
1143+
const numeric = Number(text);
1144+
if (!Number.isFinite(numeric) || numeric < 0) return "";
1145+
return text;
1146+
}
1147+
1148+
function cleanDisplayRatio(value) {
1149+
const text = cleanDisplayNumber(value);
1150+
if (!text) return "";
1151+
const numeric = Number(text);
1152+
return numeric >= 0 && numeric <= 1 ? text : "";
1153+
}
1154+
11061155
function normalizeExecutionMode(value, dryRunOnly) {
11071156
const mode = String(value || "").trim().toLowerCase();
11081157
if (mode === "live" || mode === "paper") return mode;
@@ -1160,14 +1209,25 @@ <h2 data-i18n="summary">切换摘要</h2>
11601209
platform,
11611210
account,
11621211
);
1212+
syncReservePolicyForAccount(platform);
1213+
}
1214+
1215+
function syncReservePolicyForAccount(platform) {
1216+
const form = state.forms[platform];
1217+
if (!form || form.reservedCashTouched) return;
1218+
const policy = currentReservePolicyForAccount(platform, selectedAccount(platform));
1219+
form.minReservedCashUsd = policy.minReservedCashUsd;
1220+
form.reservedCashRatio = policy.reservedCashRatio;
11631221
}
11641222

11651223
function ensureAccountSelection(platform) {
11661224
const options = optionsFor(platform);
11671225
if (!options.length) return;
11681226
if (!options.some((option) => option.key === state.forms[platform].accountKey)) {
11691227
state.forms[platform].accountKey = options[0].key;
1228+
state.forms[platform].reservedCashTouched = false;
11701229
state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0], state.forms[platform].strategy);
1230+
syncReservePolicyForAccount(platform);
11711231
}
11721232
}
11731233

@@ -1226,16 +1286,29 @@ <h2 data-i18n="summary">切换摘要</h2>
12261286
return inputs;
12271287
}
12281288

1229-
function reservedCashPolicyText(inputs) {
1289+
function reservedCashPolicyText(inputs, platform = state.selected, account = selectedAccount(platform), fallback = t("unchanged")) {
12301290
const floor = inputs.min_reserved_cash_usd;
12311291
const ratio = inputs.reserved_cash_ratio;
1232-
const currency = selectedCashCurrency();
1233-
if (!floor && !ratio) return t("unchanged");
1292+
const currency = selectedCashCurrency(platform, account);
1293+
if (!floor && !ratio) return fallback;
12341294
if (floor && ratio) return `max(${floor} ${currency}, ${formatRatioPercent(ratio)})`;
12351295
if (floor) return `${floor} ${currency}`;
12361296
return formatRatioPercent(ratio);
12371297
}
12381298

1299+
function currentReservedCashPolicyText(platform = state.selected, account = selectedAccount(platform)) {
1300+
const policy = currentReservePolicyForAccount(platform, account);
1301+
return reservedCashPolicyText(
1302+
{
1303+
min_reserved_cash_usd: policy.minReservedCashUsd,
1304+
reserved_cash_ratio: policy.reservedCashRatio,
1305+
},
1306+
platform,
1307+
account,
1308+
t("notRead"),
1309+
);
1310+
}
1311+
12391312
function formatRatioPercent(value) {
12401313
const numeric = Number(value);
12411314
if (!Number.isFinite(numeric)) return String(value);
@@ -1249,7 +1322,7 @@ <h2 data-i18n="summary">切换摘要</h2>
12491322
[t("repository"), state.repositories[state.selected] || defaultRepositories[state.selected]],
12501323
[t("selectedAccount"), account.label],
12511324
[t("selectedMarket"), supportedDomainLabel(state.selected, account)],
1252-
[t("reservedCashPolicy"), reservedCashPolicyText(inputs)],
1325+
[t("reservedCashPolicy"), currentReservedCashPolicyText(state.selected, account)],
12531326
[t("nextStrategy"), strategyLabel(inputs.strategy_profile)],
12541327
];
12551328
if (state.auth.allowed) {
@@ -1562,6 +1635,8 @@ <h2 data-i18n="summary">切换摘要</h2>
15621635
strategy_profile: profile,
15631636
execution_mode: normalizeExecutionMode(entry?.execution_mode, entry?.dry_run_only),
15641637
dry_run_only: entry?.dry_run_only === true || entry?.dry_run_only === "true" || entry?.dry_run_only === "1",
1638+
min_reserved_cash_usd: cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd),
1639+
reserved_cash_ratio: cleanDisplayRatio(entry?.reserved_cash_ratio),
15651640
source: entry?.source ? String(entry.source) : "",
15661641
};
15671642
}
@@ -1619,6 +1694,7 @@ <h2 data-i18n="summary">切换摘要</h2>
16191694

16201695
el("account-select").addEventListener("change", () => {
16211696
state.forms[state.selected].accountKey = el("account-select").value;
1697+
state.forms[state.selected].reservedCashTouched = false;
16221698
syncStrategyForAccount(state.selected);
16231699
render();
16241700
});
@@ -1636,11 +1712,13 @@ <h2 data-i18n="summary">切换摘要</h2>
16361712
});
16371713

16381714
el("min-reserved-cash-input").addEventListener("input", () => {
1715+
state.forms[state.selected].reservedCashTouched = true;
16391716
state.forms[state.selected].minReservedCashUsd = el("min-reserved-cash-input").value.trim();
16401717
render();
16411718
});
16421719

16431720
el("reserved-cash-ratio-input").addEventListener("input", () => {
1721+
state.forms[state.selected].reservedCashTouched = true;
16441722
state.forms[state.selected].reservedCashRatio = el("reserved-cash-ratio-input").value.trim();
16451723
render();
16461724
});

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)