Skip to content

Commit d20210a

Browse files
committed
Add plugin mode to strategy switch
1 parent 1ead6fa commit d20210a

4 files changed

Lines changed: 109 additions & 20 deletions

File tree

tests/strategy_switch_worker_validation.mjs

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ assert.equal(indexHtml.includes("loginLink.hidden = !state.auth.available || sig
2525
assert.ok(indexHtml.includes('id="min-reserved-cash-input"'));
2626
assert.ok(indexHtml.includes('id="reserved-cash-ratio-input"'));
2727
assert.ok(indexHtml.includes('id="reserve-policy-mode-select"'));
28+
assert.ok(indexHtml.includes('id="plugin-mode-select"'));
2829
assert.ok(indexHtml.includes("account-block"));
2930
assert.ok(indexHtml.includes("strategy-block"));
3031
assert.ok(indexHtml.includes("grid-template-columns: repeat(4, minmax(0, 1fr));"));
@@ -34,6 +35,8 @@ assert.ok(indexHtml.includes('reservePolicyNone'));
3435
assert.ok(indexHtml.includes('reservePolicyRatio'));
3536
assert.ok(indexHtml.includes('reservePolicyFloor'));
3637
assert.ok(indexHtml.includes('reservePolicyMax'));
38+
assert.ok(indexHtml.includes('pluginModeAuto'));
39+
assert.ok(indexHtml.includes('pluginModeNone'));
3740
assert.ok(indexHtml.includes('reservedCashDefault'));
3841
assert.ok(indexHtml.includes('paper: "模拟"'));
3942
assert.ok(indexHtml.includes('paper: "Dry run"'));
@@ -53,6 +56,7 @@ assert.ok(indexHtml.includes('.reserve-ratio-block'));
5356
assert.ok(indexHtml.includes('.summary-row.pending'));
5457
assert.ok(indexHtml.includes('function currentEntryHasState('));
5558
assert.ok(indexHtml.includes('changes.reserveCashChanged'));
59+
assert.ok(indexHtml.includes('changes.pluginModeChanged'));
5660
assert.ok(indexHtml.includes('!hasPendingChange'));
5761
assert.ok(indexHtml.includes('hasPendingChange ? t("readyNote") : ""'));
5862
assert.equal(indexHtml.includes('hasPendingChange ? t("readyNote") : t("noChangesNote")'), false);
@@ -300,6 +304,14 @@ const normalizedReservedCashInputs = __test.normalizeSwitchInputs({
300304
});
301305
assert.equal(normalizedReservedCashInputs.reserved_cash_ratio, "0.03");
302306
assert.equal(normalizedReservedCashInputs.min_reserved_cash_usd, "150");
307+
const normalizedPluginInputs = __test.normalizeSwitchInputs({
308+
platform: "ibkr",
309+
target_name: "ibkr-primary",
310+
strategy_profile: "tqqq_growth_income",
311+
execution_mode: "live",
312+
plugin_mode: "none",
313+
});
314+
assert.equal(normalizedPluginInputs.plugin_mode, "none");
303315
const normalizedReserveClearInputs = __test.normalizeSwitchInputs({
304316
platform: "ibkr",
305317
target_name: "ibkr-primary",
@@ -346,6 +358,21 @@ const updatedAccountOptions = __test.updateAccountOptionsDefaultStrategy(
346358
assert.equal(updatedAccountOptions.changed, true);
347359
assert.equal(updatedAccountOptions.options.longbridge[1].default_strategy_profile, "soxl_soxx_trend_income");
348360

361+
const updatedPluginModeOptions = __test.updateAccountOptionsDefaultStrategy(
362+
accountOptions,
363+
{
364+
platform: "longbridge",
365+
target_name: "sg",
366+
account_selector: "SG",
367+
strategy_profile: "tqqq_growth_income",
368+
execution_mode: "live",
369+
variable_scope: "default",
370+
plugin_mode: "none",
371+
},
372+
);
373+
assert.equal(updatedPluginModeOptions.changed, true);
374+
assert.equal(updatedPluginModeOptions.options.longbridge[1].plugin_mode, "none");
375+
349376
const kvWrites = new Map();
350377
const syncResult = await __test.syncDefaultStrategyForAccount(
351378
{

web/strategy-switch-console/index.html

Lines changed: 65 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -423,20 +423,15 @@
423423
}
424424

425425
.account-block,
426-
.mode-block {
426+
.mode-block,
427+
.reserve-policy-block,
428+
.reserve-ratio-block {
427429
grid-column: 1 / span 2;
428430
}
429431

430432
.strategy-block,
431-
.reserve-policy-block {
432-
grid-column: 3 / span 2;
433-
}
434-
433+
.plugin-mode-block,
435434
.min-reserve-block {
436-
grid-column: 1 / span 2;
437-
}
438-
439-
.reserve-ratio-block {
440435
grid-column: 3 / span 2;
441436
}
442437

@@ -671,6 +666,7 @@
671666
.account-block,
672667
.strategy-block,
673668
.mode-block,
669+
.plugin-mode-block,
674670
.reserve-policy-block,
675671
.min-reserve-block,
676672
.reserve-ratio-block {
@@ -786,6 +782,12 @@ <h2 id="platform-title">LongBridge</h2>
786782
</div>
787783
</div>
788784

785+
<label class="control-block plugin-mode-block">
786+
<span data-i18n="pluginMode">策略插件</span>
787+
<select id="plugin-mode-select"></select>
788+
<span class="selection-meta" data-i18n="pluginModeMeta">控制本次切换是否启用策略插件。</span>
789+
</label>
790+
789791
<label class="control-block reserve-policy-block">
790792
<span data-i18n="reservedCashMode">预留现金策略</span>
791793
<select id="reserve-policy-mode-select"></select>
@@ -841,6 +843,7 @@ <h2 data-i18n="summary">切换摘要</h2>
841843
};
842844

843845
const reservePolicyModes = ["current", "none", "ratio", "floor", "max"];
846+
const pluginModes = ["auto", "none"];
844847
const platformMinReservedCashVariables = {
845848
longbridge: "LONGBRIDGE_MIN_RESERVED_CASH_USD",
846849
ibkr: "IBKR_MIN_RESERVED_CASH_USD",
@@ -906,6 +909,10 @@ <h2 data-i18n="summary">切换摘要</h2>
906909
mode: "模式",
907910
live: "实盘",
908911
paper: "模拟",
912+
pluginMode: "策略插件",
913+
pluginModeAuto: "按策略配置启用",
914+
pluginModeNone: "禁用插件",
915+
pluginModeMeta: "控制本次切换是否启用策略插件。",
909916
minReservedCash: "最小预留现金 ({currency})",
910917
reservedCashRatio: "预留现金比例",
911918
reservedCashMode: "预留现金策略",
@@ -942,6 +949,8 @@ <h2 data-i18n="summary">切换摘要</h2>
942949
reservedCashPolicy: "当前预留现金",
943950
pendingReservedCashPolicy: "待提交预留现金",
944951
pendingMode: "待提交模式",
952+
currentPluginMode: "当前插件",
953+
pendingPluginMode: "待提交插件",
945954
unchanged: "不变",
946955
copied: "已复制状态",
947956
dispatching: "正在触发 workflow...",
@@ -974,6 +983,10 @@ <h2 data-i18n="summary">切换摘要</h2>
974983
mode: "Mode",
975984
live: "Live",
976985
paper: "Dry run",
986+
pluginMode: "Strategy plugins",
987+
pluginModeAuto: "Use strategy default",
988+
pluginModeNone: "Disable plugins",
989+
pluginModeMeta: "Controls whether this switch enables strategy plugins.",
977990
minReservedCash: "Minimum reserved cash ({currency})",
978991
reservedCashRatio: "Reserved cash ratio",
979992
reservedCashMode: "Reserved cash policy",
@@ -1010,6 +1023,8 @@ <h2 data-i18n="summary">切换摘要</h2>
10101023
reservedCashPolicy: "Current reserved cash",
10111024
pendingReservedCashPolicy: "Pending reserved cash",
10121025
pendingMode: "Pending mode",
1026+
currentPluginMode: "Current plugins",
1027+
pendingPluginMode: "Pending plugins",
10131028
unchanged: "Unchanged",
10141029
copied: "State copied",
10151030
dispatching: "Dispatching workflow...",
@@ -1048,10 +1063,10 @@ <h2 data-i18n="summary">切换摘要</h2>
10481063
configSource: "default",
10491064
repositories: clone(defaultRepositories),
10501065
forms: {
1051-
longbridge: { accountKey: "preview", strategy: "", executionMode: "live", ...defaultReserveForm() },
1052-
ibkr: { accountKey: "preview", strategy: "", executionMode: "live", ...defaultReserveForm() },
1053-
schwab: { accountKey: "preview", strategy: "", executionMode: "live", ...defaultReserveForm() },
1054-
firstrade: { accountKey: "preview", strategy: "", executionMode: "live", ...defaultReserveForm() },
1066+
longbridge: { accountKey: "preview", strategy: "", executionMode: "live", pluginMode: "auto", ...defaultReserveForm() },
1067+
ibkr: { accountKey: "preview", strategy: "", executionMode: "live", pluginMode: "auto", ...defaultReserveForm() },
1068+
schwab: { accountKey: "preview", strategy: "", executionMode: "live", pluginMode: "auto", ...defaultReserveForm() },
1069+
firstrade: { accountKey: "preview", strategy: "", executionMode: "live", pluginMode: "auto", ...defaultReserveForm() },
10551070
},
10561071
};
10571072

@@ -1179,6 +1194,14 @@ <h2 data-i18n="summary">切换摘要</h2>
11791194
return mode === "paper" ? t("paper") : t("live");
11801195
}
11811196

1197+
function normalizePluginMode(value) {
1198+
return pluginModes.includes(value) ? value : "auto";
1199+
}
1200+
1201+
function pluginModeLabel(mode) {
1202+
return mode === "none" ? t("pluginModeNone") : t("pluginModeAuto");
1203+
}
1204+
11821205
function currentEntryForAccount(platform, account) {
11831206
const byPlatform = state.currentStrategies[platform] || {};
11841207
const keys = [account?.key, account?.target_name, account?.label]
@@ -1212,6 +1235,11 @@ <h2 data-i18n="summary">切换摘要</h2>
12121235
return reservePolicyFromEntry(entry);
12131236
}
12141237

1238+
function currentPluginModeForAccount(platform, account) {
1239+
void platform;
1240+
return normalizePluginMode(account?.plugin_mode);
1241+
}
1242+
12151243
function reservePolicyFromEntry(entry) {
12161244
return {
12171245
minReservedCashUsd: cleanDisplayNumber(entry?.min_reserved_cash_usd ?? entry?.reserved_cash_floor_usd),
@@ -1291,6 +1319,7 @@ <h2 data-i18n="summary">切换摘要</h2>
12911319
platform,
12921320
account,
12931321
);
1322+
state.forms[platform].pluginMode = currentPluginModeForAccount(platform, account);
12941323
syncReservePolicyForAccount(platform);
12951324
}
12961325

@@ -1310,6 +1339,7 @@ <h2 data-i18n="summary">切换摘要</h2>
13101339
state.forms[platform].accountKey = options[0].key;
13111340
state.forms[platform].reservedCashTouched = false;
13121341
state.forms[platform].strategy = defaultStrategyForAccount(platform, options[0], state.forms[platform].strategy);
1342+
state.forms[platform].pluginMode = currentPluginModeForAccount(platform, options[0]);
13131343
syncReservePolicyForAccount(platform);
13141344
}
13151345
}
@@ -1391,7 +1421,7 @@ <h2 data-i18n="summary">切换摘要</h2>
13911421
strategy_profile: form.strategy,
13921422
execution_mode: form.executionMode,
13931423
variable_scope: account.variable_scope || "default",
1394-
plugin_mode: account.plugin_mode || "auto",
1424+
plugin_mode: normalizePluginMode(form.pluginMode),
13951425
service_targets_mode: "auto",
13961426
apply: "true",
13971427
trigger_platform_sync: "true",
@@ -1488,21 +1518,26 @@ <h2 data-i18n="summary">切换摘要</h2>
14881518
const nextProfile = cleanStrategyProfile(inputs.strategy_profile);
14891519
const currentEntry = currentEntryForAccount(platform, account);
14901520
const currentMode = normalizeExecutionMode(currentEntry?.execution_mode, currentEntry?.dry_run_only);
1521+
const currentPluginMode = currentPluginModeForAccount(platform, account);
1522+
const nextPluginMode = normalizePluginMode(inputs.plugin_mode);
14911523
const reserve = pendingReservePolicy(inputs, platform, account);
14921524
return {
14931525
currentProfile,
14941526
nextProfile,
14951527
currentMode,
1528+
currentPluginMode,
1529+
nextPluginMode,
14961530
strategyChanged: Boolean(nextProfile && (!currentProfile || currentProfile !== nextProfile)),
14971531
modeChanged: Boolean(currentMode && inputs.execution_mode && currentMode !== inputs.execution_mode),
1532+
pluginModeChanged: Boolean(currentPluginMode && nextPluginMode && currentPluginMode !== nextPluginMode),
14981533
reserveCashChanged: reserve.changed,
14991534
reserve,
15001535
};
15011536
}
15021537

15031538
function hasPendingChanges(inputs, platform = state.selected, account = selectedAccount(platform)) {
15041539
const changes = pendingChangeState(inputs, platform, account);
1505-
return Boolean(changes.strategyChanged || changes.modeChanged || changes.reserveCashChanged);
1540+
return Boolean(changes.strategyChanged || changes.modeChanged || changes.pluginModeChanged || changes.reserveCashChanged);
15061541
}
15071542

15081543
function formatRatioPercent(value) {
@@ -1520,6 +1555,7 @@ <h2 data-i18n="summary">切换摘要</h2>
15201555
[t("selectedAccount"), account.label],
15211556
[t("currentStrategy"), currentStrategyText],
15221557
[t("selectedMarket"), supportedDomainLabel(state.selected, account)],
1558+
[t("currentPluginMode"), pluginModeLabel(changes.currentPluginMode)],
15231559
[t("reservedCashPolicy"), currentReservedCashPolicyText(state.selected, account)],
15241560
];
15251561
if (changes.reserveCashChanged) {
@@ -1528,6 +1564,9 @@ <h2 data-i18n="summary">切换摘要</h2>
15281564
if (changes.modeChanged) {
15291565
rows.push([t("pendingMode"), modeLabel(inputs.execution_mode), "pending"]);
15301566
}
1567+
if (changes.pluginModeChanged) {
1568+
rows.push([t("pendingPluginMode"), pluginModeLabel(changes.nextPluginMode), "pending"]);
1569+
}
15311570
if (changes.strategyChanged && changes.nextProfile) {
15321571
rows.push([t("nextStrategy"), strategyLabel(changes.nextProfile), "pending"]);
15331572
}
@@ -1585,6 +1624,7 @@ <h2 data-i18n="summary">切换摘要</h2>
15851624
const choices = strategyChoicesForAccount(platform, account);
15861625
const accountSelect = el("account-select");
15871626
const strategySelect = el("strategy-select");
1627+
const pluginModeSelect = el("plugin-mode-select");
15881628
const reservePolicyModeSelect = el("reserve-policy-mode-select");
15891629
const minReservedCashInput = el("min-reserved-cash-input");
15901630
const reservedCashRatioInput = el("reserved-cash-ratio-input");
@@ -1600,6 +1640,7 @@ <h2 data-i18n="summary">切换摘要</h2>
16001640
if (!showPrivateControls) {
16011641
accountSelect.replaceChildren();
16021642
strategySelect.replaceChildren();
1643+
pluginModeSelect.replaceChildren();
16031644
reservePolicyModeSelect.replaceChildren();
16041645
minReservedCashInput.value = "";
16051646
reservedCashRatioInput.value = "";
@@ -1633,6 +1674,10 @@ <h2 data-i18n="summary">切换摘要</h2>
16331674
el("strategy-meta").textContent = account
16341675
? t("strategyMeta").replace("{domains}", supportedDomainLabel(platform, account))
16351676
: "";
1677+
pluginModeSelect.replaceChildren();
1678+
for (const mode of pluginModes) {
1679+
pluginModeSelect.append(new Option(pluginModeLabel(mode), mode, false, mode === normalizePluginMode(form.pluginMode)));
1680+
}
16361681
reservePolicyModeSelect.replaceChildren();
16371682
for (const mode of reservePolicyModes) {
16381683
reservePolicyModeSelect.append(new Option(t(`reservePolicy${mode[0].toUpperCase()}${mode.slice(1)}`), mode, false, mode === normalizeReservePolicyMode(form.reservePolicyMode)));
@@ -1937,6 +1982,11 @@ <h2 data-i18n="summary">切换摘要</h2>
19371982
render();
19381983
});
19391984

1985+
el("plugin-mode-select").addEventListener("change", () => {
1986+
state.forms[state.selected].pluginMode = normalizePluginMode(el("plugin-mode-select").value);
1987+
render();
1988+
});
1989+
19401990
el("reserve-policy-mode-select").addEventListener("change", () => {
19411991
const form = state.forms[state.selected];
19421992
form.reservePolicyMode = normalizeReservePolicyMode(el("reserve-policy-mode-select").value);

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.

web/strategy-switch-console/worker.js

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -764,9 +764,22 @@ function updateAccountOptionsDefaultStrategy(accountOptions, inputs) {
764764
const updatedPlatformOptions = platformOptions.map((option) => {
765765
if (!accountOptionMatchesInputs(option, inputs)) return option;
766766
matched = true;
767-
if (option.default_strategy_profile === inputs.strategy_profile) return option;
768-
changed = true;
769-
return { ...option, default_strategy_profile: inputs.strategy_profile };
767+
const nextOption = { ...option };
768+
let optionChanged = false;
769+
if (nextOption.default_strategy_profile !== inputs.strategy_profile) {
770+
nextOption.default_strategy_profile = inputs.strategy_profile;
771+
optionChanged = true;
772+
}
773+
if (inputs.plugin_mode === "auto" || inputs.plugin_mode === "none") {
774+
const currentPluginMode = nextOption.plugin_mode || "auto";
775+
if (currentPluginMode !== inputs.plugin_mode) {
776+
nextOption.plugin_mode = inputs.plugin_mode;
777+
optionChanged = true;
778+
}
779+
}
780+
changed = changed || optionChanged;
781+
if (!optionChanged) return option;
782+
return nextOption;
770783
});
771784
if (!matched) throw new Error("switch inputs do not match configured account options");
772785
return {
@@ -857,7 +870,6 @@ function accountOptionMatchesInputs(option, inputs) {
857870
"service_name",
858871
"github_environment",
859872
"variable_scope",
860-
"plugin_mode",
861873
];
862874
for (const field of fields) {
863875
const expected = option[field] || "";

0 commit comments

Comments
 (0)