From 887153e88b0faa64c12d464d46ba9154cb1038ae Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Sun, 30 Aug 2026 13:19:32 +0800 Subject: [PATCH] fix: unify continuous ladder status --- docs/binance-orderbook-trade-development.md | 6 +- scripts/binance-orderbook-trade.user.js | 212 +++++++++++++----- .../core/continuous-ladder.js | 46 +++- src/binance-orderbook-trade/index.user.js | 198 ++++++++++++---- .../continuous-ladder.test.js | 37 ++- .../ladder-options.test.js | 3 +- .../source-regressions.test.js | 43 ++-- 7 files changed, 409 insertions(+), 136 deletions(-) diff --git a/docs/binance-orderbook-trade-development.md b/docs/binance-orderbook-trade-development.md index 84256e4..ff60690 100644 --- a/docs/binance-orderbook-trade-development.md +++ b/docs/binance-orderbook-trade-development.md @@ -157,9 +157,9 @@ Ladder replacement must stay scoped and direction-aware. Automatic replacement m Continuous ladder trading is available only for close actions through `Option/Alt + click`; an ordinary click remains one round. A round is the complete existing ladder-close workflow, including any scoped same-direction replacement and cleanup. After a completed round, the runner must observe the same symbol, close mode, current precision, and native close button as ready before starting a full one-second cooldown. It must validate readiness again after the cooldown; losing readiness restarts the wait and a new full cooldown. Every new round must rebuild its plan from the latest panel profile and live trading context. A failed, stopped, or interrupted round ends the continuous session. -Continuous-session feedback stays in the shared ladder status row and uses compact Chinese counters. `2/3 轮` means two rounds completed out of three started, `本轮 1/3 笔` reports the latest partial plan, and `累计 7 笔` reports all confirmed submissions across the session. Confirmed cancellations are appended only when greater than zero. Round outcomes must expose a detached progress snapshot so a terminal continuous summary cannot be overwritten by the latest single-round message. +Continuous-session feedback stays in the shared ladder status row and uses `连续阶梯平多` / `连续阶梯平空` as the stable action name. The action, phase, and counters are separated with ` · ` instead of concatenating `连续` after the ordinary ladder label. `2/3 轮` means two rounds completed out of three started, `本轮 1/3 笔` reports the active or latest partial plan, and `累计 7 笔` reports all confirmed submissions across the session. Confirmed cancellations are appended only when greater than zero. The active round must combine its live progress with the completed-round aggregate; ordinary single-round status text must never overwrite the continuous-session identity. Round outcomes must expose a detached progress snapshot so a terminal continuous summary cannot be overwritten by the latest single-round message. -The active continuous-close control keeps a compact direction-specific stop action (`停止平多` / `停止平空`) on one line throughout both execution and inter-round waiting. Before the native submit control is ready, the status places `等待按钮恢复` immediately after the continuous-state label. Only after the fixed cooldown actually begins may it show `1s 后继续` in that same priority position; this is a static duration label, not a countdown. The button must not temporarily revert to a ladder-start action between rounds. +The active continuous-close control keeps a compact direction-specific stop action (`停止平多` / `停止平空`) on one line throughout both execution and inter-round waiting. Before the native submit control is ready, the status places `等待按钮恢复` immediately after the continuous action name. Only after the fixed cooldown actually begins may it show `1s 后继续` in that same priority position; this is a static duration label, not a countdown. `停止中`, `已停止`, `失败`, and `已中止` use the same phase slot. The button must not temporarily revert to a ladder-start action between rounds. When selecting account-order tabs, scope to the bottom account-orders tab group. Do not globally match `当前委托` or `Open Orders`. @@ -200,7 +200,7 @@ Run manual checks when behavior touches trading flow, DOM selectors, account ord - start ladder order, confirm start buttons are disabled while running - Option/Alt-click a close ladder button, confirm the next round starts only after the prior round is complete, the native close action is ready, and one full second has elapsed; change ratio, levels, row span, and precision during the wait and confirm the next plan uses the updated profile - during a continuous close cooldown, make the native close action temporarily unavailable and confirm the cooldown restarts only after readiness returns; confirm Stop also aborts the cooldown immediately -- complete multiple continuous close rounds, then stop both during a round and during cooldown; confirm the Chinese status shows completed/started rounds, latest-round order ratio, cumulative confirmed submissions, and no zero-cancellation segment +- complete multiple continuous close rounds, then stop both during a round and during cooldown; confirm every active-round status starts with `连续阶梯平多` or `连续阶梯平空` and shows completed/started rounds, live-round order ratio, cumulative confirmed submissions, and no zero-cancellation segment - confirm continuous close keeps the direction-specific stop button between rounds, places `等待按钮恢复` or `1s 后继续` before the progress counters, and never exposes a start button during the cooldown - cancel current-symbol orders, verify only Binance native confirmation opens - keep the native cancel-all dialog open for longer than the former decision deadline, verify the script remains in the dialog-tracking state, then cancel and confirm the original order count and temporary page state are restored diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index 9e3f3fc..3425361 100644 --- a/scripts/binance-orderbook-trade.user.js +++ b/scripts/binance-orderbook-trade.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.159 +// @version 2.7.160 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -970,11 +970,11 @@ var CONTINUOUS_LADDER_READY_CHECK_MS = 50; var recordedRoundOutcomes = /* @__PURE__ */ new WeakSet(); var CONTINUOUS_LADDER_PHASE_TEXT = Object.freeze({ - running: "连续中", - stopping: "连续停止中", - stopped: "连续已停止", - failed: "连续失败", - interrupted: "连续已中止" + running: null, + stopping: "停止中", + stopped: "已停止", + failed: "失败", + interrupted: "已中止" }); function assertContinuousLadderProgress(progress) { if (!progress || !Number.isInteger(progress.startedRounds) || progress.startedRounds < 0 || !Number.isInteger(progress.completedRounds) || progress.completedRounds < 0 || progress.completedRounds > progress.startedRounds || !Number.isInteger(progress.submittedOrders) || progress.submittedOrders < 0 || !Number.isInteger(progress.cancelledOrders) || progress.cancelledOrders < 0 || progress.startedRounds === 0 !== (progress.lastRound === null)) { @@ -1019,9 +1019,12 @@ throw new Error("Invalid continuous ladder label"); } const phaseText = CONTINUOUS_LADDER_PHASE_TEXT[phase]; - if (!phaseText) throw new Error("Invalid continuous ladder phase"); + if (!Object.hasOwn(CONTINUOUS_LADDER_PHASE_TEXT, phase)) { + throw new Error("Invalid continuous ladder phase"); + } assertContinuousLadderProgress(progress); - const parts = [`${label}${phaseText}`]; + const parts = [`连续${label}`]; + if (phaseText !== null) parts.push(phaseText); parts.push(progress.startedRounds === 0 ? "0 轮" : `${progress.completedRounds}/${progress.startedRounds} 轮`); if (progress.lastRound?.plannedOrders !== null) { parts.push( @@ -1034,6 +1037,28 @@ if (progress.cancelledOrders > 0) parts.push(`撤 ${progress.cancelledOrders} 笔`); return parts; } + function formatActiveContinuousLadderProgress(label, detail, progress, roundProgress) { + if (typeof label !== "string" || label.trim() === "") { + throw new Error("Invalid continuous ladder label"); + } + if (detail !== null && (typeof detail !== "string" || detail.trim() === "")) { + throw new Error("Invalid active continuous ladder detail"); + } + assertContinuousLadderProgress(progress); + const round = snapshotLadderProgress(roundProgress); + const parts = [`连续${label}`]; + if (detail !== null) parts.push(detail); + parts.push(`${progress.completedRounds}/${progress.startedRounds + 1} 轮`); + if (round.plannedOrders !== null) { + parts.push(`本轮 ${round.currentPlanSubmittedOrders}/${round.plannedOrders} 笔`); + } else if (round.submittedOrders > 0) { + parts.push(`本轮 ${round.submittedOrders} 笔`); + } + parts.push(`累计 ${progress.submittedOrders + round.submittedOrders} 笔`); + const cancelledOrders = progress.cancelledOrders + round.cancelledOrders; + if (cancelledOrders > 0) parts.push(`撤 ${cancelledOrders} 笔`); + return parts.join(" · "); + } function formatContinuousLadderProgress(label, phase, progress, reason = null) { if (reason !== null && (typeof reason !== "string" || reason.trim() === "")) { throw new Error("Invalid continuous ladder reason"); @@ -2740,6 +2765,7 @@ let activeLadderActionType = null; let activeContinuousLadderActionType = null; let activeContinuousLadderProgress = null; + let activeContinuousLadderRoundProgress = null; let activeLadderPanelContext = null; let cancelCurrentSymbolOpenOrdersTask = null; let cancelCurrentSymbolOpenOrdersBlocksLadderActions = false; @@ -4509,7 +4535,7 @@ const settleHint = sawBusy ? "按钮已恢复但未捕获当前下单 API 成功响应" : "未捕获当前下单 API 成功响应"; throw new Error(`未收到明确${label}成功反馈(${settleHint}),已停止;请核对当前委托/历史成交`); } - async function executeLadderPlan(plan, progress, abortSignal = null) { + async function executeLadderPlan(plan, progress, setExecutionStatus, abortSignal = null) { const priceInput = findPriceInput(); const qtyInput = findQtyInput(); if (!priceInput || !qtyInput) throw new Error("未找到价格或数量输入框"); @@ -4562,7 +4588,10 @@ try { throwIfAborted(abortSignal); button.click(); - setLadderStatus(`${plan.spec.label}挂单 ${done + 1}/${plan.orders.length} 确认中`); + setExecutionStatus( + `${plan.spec.label}挂单 ${done + 1}/${plan.orders.length} 确认中`, + `第 ${done + 1} 笔确认中` + ); waitForTradeUiMutation({ timeoutMs: 500 }); await waitForOrderSubmitAcknowledgement( button, @@ -4589,7 +4618,8 @@ throw new Error(`盘口连续移动,已自动刷新 ${repriceAttempts} 次${codeText};已挂 ${done}/${plan.orders.length} 笔后停止`); } repriceAttempts += 1; - setLadderStatus(`${plan.spec.label}:盘口已移动,刷新剩余 ${plan.orders.length - done} 档 (${repriceAttempts}/${LADDER_REPRICE_MAX_ATTEMPTS})`); + const repriceDetail = `盘口已移动,刷新剩余 ${plan.orders.length - done} 档 (${repriceAttempts}/${LADDER_REPRICE_MAX_ATTEMPTS})`; + setExecutionStatus(`${plan.spec.label}:${repriceDetail}`, repriceDetail); await delay(LADDER_REPRICE_DELAY_MS); throwIfAborted(abortSignal); if (ladderStopRequested) break; @@ -4598,31 +4628,46 @@ } done++; recordLadderSubmittedOrder(progress); - setLadderStatus(`${plan.spec.label}已挂 ${done}/${plan.orders.length} 笔`); + setExecutionStatus(`${plan.spec.label}已挂 ${done}/${plan.orders.length} 笔`, null); throwIfAborted(abortSignal); } return { done, repriceAttempts, lastRepriceApiErrorCode }; } - async function startLadder(actionType, continuousSession = false) { + async function startLadder(actionType, continuousProgress = null) { const spec = getLadderActionSpec2(actionType); if (!spec) { setLadderStatus("未知阶梯动作"); return { status: "not_started" }; } + const continuousSession = continuousProgress !== null; + const setStartStatus = (singleStatus, continuousDetail) => { + setLadderStatus( + continuousSession ? `连续${spec.label} · ${continuousDetail}` : singleStatus + ); + }; const actionSymbol = getCurrentSymbol(); if (!isCurrentObservedSymbol(actionSymbol)) { - setLadderStatus(`${spec.label}尚未开始:交易对正在切换`); + setStartStatus( + `${spec.label}尚未开始:交易对正在切换`, + "尚未开始 · 交易对正在切换" + ); return { status: "not_started" }; } if (cancelCurrentSymbolOpenOrdersTask) { - setLadderStatus(`${spec.label}尚未开始:撤本币挂单处理中`); + setStartStatus( + `${spec.label}尚未开始:撤本币挂单处理中`, + "尚未开始 · 撤本币挂单处理中" + ); return { status: "not_started" }; } if (singleOrderTask) { return { status: "not_started" }; } if (ladderTask) { - setLadderStatus(`${spec.label}尚未开始:已有阶梯任务正在执行,请先停止`); + setStartStatus( + `${spec.label}尚未开始:已有阶梯任务正在执行,请先停止`, + "尚未开始 · 已有阶梯任务正在执行,请先停止" + ); return { status: "not_started" }; } if (continuousLadderTask && !continuousSession) { @@ -4630,21 +4675,34 @@ return { status: "not_started" }; } if (spec?.mode === "CLOSE" && !isCloseSnapshotReady(actionSymbol)) { - setLadderStatus(`${spec.label}尚未开始:仓位确认中`); + setStartStatus(`${spec.label}尚未开始:仓位确认中`, "尚未开始 · 仓位确认中"); return { status: "not_started" }; } ladderStopRequested = false; const abortController = new AbortController(); const progress = createLadderProgress(); + const setExecutionStatus = (singleStatus, continuousDetail) => { + if (continuousSession) { + setLadderStatus(formatActiveContinuousLadderProgress( + spec.label, + continuousDetail, + continuousProgress, + progress + )); + return; + } + setLadderStatus(singleStatus); + }; ladderAbortController = abortController; activeLadderActionType = actionType; + if (continuousSession) activeContinuousLadderRoundProgress = progress; activeLadderPanelContext = { mode: spec.mode, symbol: actionSymbol, precision: readCurrentOrderbookPrecisionValue() }; const feedbackStartedAt = performance.now(); - setLadderStatus(`${spec.label}准备中`); + setExecutionStatus(`${spec.label}准备中`, "准备中"); const executionTask = (async () => { const { plan, @@ -4654,6 +4712,7 @@ } = await runLadderPlanWithOpenOrderReplacement( actionType, progress, + setExecutionStatus, abortController.signal ); return { @@ -4677,11 +4736,13 @@ wasStopped }) => { if (!isCurrentObservedSymbol(actionSymbol)) { - setLadderStatus(formatInterruptedLadderProgress( - spec.label, - "交易对已切换", - progress - )); + if (!continuousSession) { + setLadderStatus(formatInterruptedLadderProgress( + spec.label, + "交易对已切换", + progress + )); + } return { status: "interrupted", reason: "交易对已切换", @@ -4689,21 +4750,23 @@ }; } const diagnostics = formatLadderRepriceDiagnostics(repriceAttempts, lastRepriceApiErrorCode); - setLadderStatus( - wasStopped ? `${formatStoppedLadderProgress(spec.label, progress)}${diagnostics}` : `${formatCompletedLadderProgress( - spec.label, - done, - plan.orders.length, - progress - )}${diagnostics}` - ); + if (!continuousSession) { + setLadderStatus( + wasStopped ? `${formatStoppedLadderProgress(spec.label, progress)}${diagnostics}` : `${formatCompletedLadderProgress( + spec.label, + done, + plan.orders.length, + progress + )}${diagnostics}` + ); + } return { status: wasStopped ? "stopped" : "completed", progress: snapshotLadderProgress(progress) }; }).catch((e) => { if (isLadderStoppedError(e)) { - if (isCurrentObservedSymbol(actionSymbol)) { + if (!continuousSession && isCurrentObservedSymbol(actionSymbol)) { setLadderStatus(formatStoppedLadderProgress(spec.label, progress)); } return { @@ -4713,11 +4776,13 @@ } err("Maker 阶梯执行失败:", e); if (!isCurrentObservedSymbol(actionSymbol)) { - setLadderStatus(formatInterruptedLadderProgress( - spec.label, - "交易对已切换", - progress - )); + if (!continuousSession) { + setLadderStatus(formatInterruptedLadderProgress( + spec.label, + "交易对已切换", + progress + )); + } return { status: "interrupted", reason: "交易对已切换", @@ -4727,7 +4792,7 @@ const failureMessage = e?.message || "未知错误"; const failureText = formatFailedLadderProgress(spec.label, failureMessage, progress); const failureTitle = e?.statusTitle ? formatFailedLadderProgress(spec.label, e.statusTitle, progress) : failureText; - setLadderStatus(failureText, failureTitle); + if (!continuousSession) setLadderStatus(failureText, failureTitle); return { status: "failed", error: e, @@ -4737,6 +4802,9 @@ }; }).finally(() => { if (ladderAbortController === abortController) ladderAbortController = null; + if (activeContinuousLadderRoundProgress === progress) { + activeContinuousLadderRoundProgress = null; + } ladderTask = null; activeLadderActionType = null; activeLadderPanelContext = null; @@ -4798,7 +4866,7 @@ const executionTask = (async () => { while (true) { throwIfAborted(abortController.signal); - const outcome = await startLadder(actionType, true); + const outcome = await startLadder(actionType, continuousProgress); if (!outcome?.progress) return outcome; recordContinuousLadderRound(continuousProgress, outcome); if (outcome.status !== "completed") { @@ -4879,7 +4947,19 @@ if (ladderAbortController && !ladderAbortController.signal.aborted) { ladderAbortController.abort(stoppedError); } - setLadderStatus(`${activeSpec.label}${continuousLadderTask ? "连续" : ""}停止中`); + if (continuousLadderTask) { + if (!activeContinuousLadderProgress || !activeContinuousLadderRoundProgress) { + throw new Error("运行中的连续阶梯任务缺少轮次进度"); + } + setLadderStatus(formatActiveContinuousLadderProgress( + activeSpec.label, + "停止中", + activeContinuousLadderProgress, + activeContinuousLadderRoundProgress + )); + } else { + setLadderStatus(`${activeSpec.label}停止中`); + } } else { setContinuousLadderProgressStatus( activeSpec.label, @@ -5424,7 +5504,7 @@ const activeRoot = getActiveOpenOrdersScope2(); return Boolean(activeRoot && countOpenOrderRowsByKey(activeRoot, symbol, key) < previousCount); } - async function cancelOpenOrderRowsForPlan(root, plan, progress, abortSignal = null) { + async function cancelOpenOrderRowsForPlan(root, plan, progress, setExecutionStatus, abortSignal = null) { let cancelQty = "0"; let currentRoot = root; while (compareDecimalStrings(cancelQty, plan.totalQty) < 0) { @@ -5469,7 +5549,10 @@ } if (outcome.status === "dialog_open") { const { dialog } = outcome; - setLadderStatus(`${plan.spec.label}:单行撤单确认弹窗已打开`); + setExecutionStatus( + `${plan.spec.label}:单行撤单确认弹窗已打开`, + "单行撤单确认弹窗已打开" + ); const dialogClosed = await waitForDialogToClose( dialog, ROW_CANCEL_DIALOG_CLOSE_TIMEOUT_MS, @@ -6166,10 +6249,10 @@ }, CANCEL_NO_ORDERS_FEEDBACK_MS); scheduleRenderPanel(); } - async function cancelCurrentSymbolOpenOrdersForPlan(plan, progress, abortSignal = null) { + async function cancelCurrentSymbolOpenOrdersForPlan(plan, progress, setExecutionStatus, abortSignal = null) { throwIfAborted(abortSignal); const setPlanStepStatus = (message) => { - setLadderStatus(`${plan.spec.label}:${message}`); + setExecutionStatus(`${plan.spec.label}:${message}`, message); }; const symbol = getCurrentSymbol(); if (!isCurrentObservedSymbol(symbol) || symbol !== plan?.symbol) { @@ -6259,7 +6342,13 @@ return { ok: false, status: "rows_not_selected", message }; } setPlanStepStatus(`撤销 ${rowsToCancel.length} 笔当前币挂单`); - await cancelOpenOrderRowsForPlan(openOrdersScope, plan, progress, abortSignal); + await cancelOpenOrderRowsForPlan( + openOrdersScope, + plan, + progress, + setExecutionStatus, + abortSignal + ); throwIfAborted(abortSignal); setPlanStepStatus("当前币挂单已替换,继续重挂"); return { ok: true, status: "rows_cleared" }; @@ -6286,9 +6375,12 @@ } } } - function formatLadderPlanStatus(plan) { + function formatLadderPlanDetail(plan) { const levelText = plan.levels === plan.requestedLevels ? `${plan.levels}档` : `${plan.levels}/${plan.requestedLevels}档`; - return `${plan.spec.label}计划:${plan.percent}% / ${levelText} / 幅${plan.ladderStep}`; + return `${plan.percent}% / ${levelText} / 幅${plan.ladderStep}`; + } + function formatLadderPlanStatus(plan) { + return `${plan.spec.label}计划:${formatLadderPlanDetail(plan)}`; } function isReplaceableCloseLadderOpenOrdersFailure(plan, error) { if (plan?.spec?.mode !== "CLOSE") return false; @@ -6310,11 +6402,14 @@ if (isReplaceableOpenLadderOpenOrdersFailure(plan, error)) return plan; return getOpenLadderMinimumQtyReplacementPlan(error); } - function formatOpenOrdersReplacementStatus(plan) { + function formatOpenOrdersReplacementDetail(plan) { if (plan?.spec?.mode === "OPEN") { - return `${plan.spec.label}:同向开仓挂单可能占用可开数量,准备替换`; + return "同向开仓挂单可能占用可开数量,准备替换"; } - return `${plan.spec.label}:当前挂单占用可平数量,准备替换`; + return "当前挂单占用可平数量,准备替换"; + } + function formatOpenOrdersReplacementStatus(plan) { + return `${plan.spec.label}:${formatOpenOrdersReplacementDetail(plan)}`; } function createLadderExpectedContext(plan) { return { @@ -6323,7 +6418,7 @@ precision: plan.precision }; } - async function runLadderPlanWithOpenOrderReplacement(actionType, progress, abortSignal = null) { + async function runLadderPlanWithOpenOrderReplacement(actionType, progress, setExecutionStatus, abortSignal = null) { let replacementContext = null; for (let attempt = 0; attempt < 2; attempt += 1) { throwIfAborted(abortSignal); @@ -6332,18 +6427,27 @@ plan = await buildLadderPlan(actionType, replacementContext); setLadderPlannedOrders(progress, plan.orders.length); throwIfAborted(abortSignal); - setLadderStatus(formatLadderPlanStatus(plan)); - const execution = await executeLadderPlan(plan, progress, abortSignal); + setExecutionStatus(formatLadderPlanStatus(plan), formatLadderPlanDetail(plan)); + const execution = await executeLadderPlan( + plan, + progress, + setExecutionStatus, + abortSignal + ); return { plan, ...execution }; } catch (e) { const replacementPlan = getReplaceableLadderOpenOrdersPlan(plan, e); if (attempt > 0 || !replacementPlan) throw e; assertLadderExecutionContext(replacementPlan); - setLadderStatus(formatOpenOrdersReplacementStatus(replacementPlan)); + setExecutionStatus( + formatOpenOrdersReplacementStatus(replacementPlan), + formatOpenOrdersReplacementDetail(replacementPlan) + ); replacementContext = createLadderExpectedContext(replacementPlan); const result = await cancelCurrentSymbolOpenOrdersForPlan( replacementPlan, progress, + setExecutionStatus, abortSignal ); throwIfAborted(abortSignal); diff --git a/src/binance-orderbook-trade/core/continuous-ladder.js b/src/binance-orderbook-trade/core/continuous-ladder.js index 3fbaae4..f6914bd 100644 --- a/src/binance-orderbook-trade/core/continuous-ladder.js +++ b/src/binance-orderbook-trade/core/continuous-ladder.js @@ -9,11 +9,11 @@ export const CONTINUOUS_LADDER_READY_CHECK_MS = 50; const recordedRoundOutcomes = new WeakSet(); const CONTINUOUS_LADDER_PHASE_TEXT = Object.freeze({ - running: '连续中', - stopping: '连续停止中', - stopped: '连续已停止', - failed: '连续失败', - interrupted: '连续已中止', + running: null, + stopping: '停止中', + stopped: '已停止', + failed: '失败', + interrupted: '已中止', }); function assertContinuousLadderProgress(progress) { @@ -74,10 +74,13 @@ function buildContinuousLadderProgressParts(label, phase, progress) { throw new Error('Invalid continuous ladder label'); } const phaseText = CONTINUOUS_LADDER_PHASE_TEXT[phase]; - if (!phaseText) throw new Error('Invalid continuous ladder phase'); + if (!Object.hasOwn(CONTINUOUS_LADDER_PHASE_TEXT, phase)) { + throw new Error('Invalid continuous ladder phase'); + } assertContinuousLadderProgress(progress); - const parts = [`${label}${phaseText}`]; + const parts = [`连续${label}`]; + if (phaseText !== null) parts.push(phaseText); parts.push(progress.startedRounds === 0 ? '0 轮' : `${progress.completedRounds}/${progress.startedRounds} 轮`); @@ -93,6 +96,35 @@ function buildContinuousLadderProgressParts(label, phase, progress) { return parts; } +export function formatActiveContinuousLadderProgress( + label, + detail, + progress, + roundProgress, +) { + if (typeof label !== 'string' || label.trim() === '') { + throw new Error('Invalid continuous ladder label'); + } + if (detail !== null && (typeof detail !== 'string' || detail.trim() === '')) { + throw new Error('Invalid active continuous ladder detail'); + } + assertContinuousLadderProgress(progress); + const round = snapshotLadderProgress(roundProgress); + + const parts = [`连续${label}`]; + if (detail !== null) parts.push(detail); + parts.push(`${progress.completedRounds}/${progress.startedRounds + 1} 轮`); + if (round.plannedOrders !== null) { + parts.push(`本轮 ${round.currentPlanSubmittedOrders}/${round.plannedOrders} 笔`); + } else if (round.submittedOrders > 0) { + parts.push(`本轮 ${round.submittedOrders} 笔`); + } + parts.push(`累计 ${progress.submittedOrders + round.submittedOrders} 笔`); + const cancelledOrders = progress.cancelledOrders + round.cancelledOrders; + if (cancelledOrders > 0) parts.push(`撤 ${cancelledOrders} 笔`); + return parts.join(' · '); +} + export function formatContinuousLadderProgress(label, phase, progress, reason = null) { if (reason !== null && (typeof reason !== 'string' || reason.trim() === '')) { throw new Error('Invalid continuous ladder reason'); diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index d849e89..58285c7 100644 --- a/src/binance-orderbook-trade/index.user.js +++ b/src/binance-orderbook-trade/index.user.js @@ -3,7 +3,7 @@ // @namespace binance.orderbook.trade // @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E // @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E -// @version 2.7.159 +// @version 2.7.160 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -79,6 +79,7 @@ import { import { keepInteractionFeedbackVisible } from './core/interaction-feedback.js'; import { createContinuousLadderProgress, + formatActiveContinuousLadderProgress, formatContinuousLadderProgress, formatContinuousLadderWaitProgress, recordContinuousLadderRound, @@ -383,6 +384,7 @@ import { let activeLadderActionType = null; let activeContinuousLadderActionType = null; let activeContinuousLadderProgress = null; + let activeContinuousLadderRoundProgress = null; let activeLadderPanelContext = null; let cancelCurrentSymbolOpenOrdersTask = null; let cancelCurrentSymbolOpenOrdersBlocksLadderActions = false; @@ -2584,7 +2586,7 @@ import { throw new Error(`未收到明确${label}成功反馈(${settleHint}),已停止;请核对当前委托/历史成交`); } - async function executeLadderPlan(plan, progress, abortSignal = null) { + async function executeLadderPlan(plan, progress, setExecutionStatus, abortSignal = null) { const priceInput = findPriceInput(); const qtyInput = findQtyInput(); if (!priceInput || !qtyInput) throw new Error('未找到价格或数量输入框'); @@ -2643,7 +2645,10 @@ import { try { throwIfAborted(abortSignal); button.click(); - setLadderStatus(`${plan.spec.label}挂单 ${done + 1}/${plan.orders.length} 确认中`); + setExecutionStatus( + `${plan.spec.label}挂单 ${done + 1}/${plan.orders.length} 确认中`, + `第 ${done + 1} 笔确认中`, + ); waitForTradeUiMutation({ timeoutMs: 500 }); await waitForOrderSubmitAcknowledgement( button, @@ -2670,7 +2675,8 @@ import { throw new Error(`盘口连续移动,已自动刷新 ${repriceAttempts} 次${codeText};已挂 ${done}/${plan.orders.length} 笔后停止`); } repriceAttempts += 1; - setLadderStatus(`${plan.spec.label}:盘口已移动,刷新剩余 ${plan.orders.length - done} 档 (${repriceAttempts}/${LADDER_REPRICE_MAX_ATTEMPTS})`); + const repriceDetail = `盘口已移动,刷新剩余 ${plan.orders.length - done} 档 (${repriceAttempts}/${LADDER_REPRICE_MAX_ATTEMPTS})`; + setExecutionStatus(`${plan.spec.label}:${repriceDetail}`, repriceDetail); await delay(LADDER_REPRICE_DELAY_MS); throwIfAborted(abortSignal); if (ladderStopRequested) break; @@ -2679,32 +2685,49 @@ import { } done++; recordLadderSubmittedOrder(progress); - setLadderStatus(`${plan.spec.label}已挂 ${done}/${plan.orders.length} 笔`); + setExecutionStatus(`${plan.spec.label}已挂 ${done}/${plan.orders.length} 笔`, null); throwIfAborted(abortSignal); } return { done, repriceAttempts, lastRepriceApiErrorCode }; } - async function startLadder(actionType, continuousSession = false) { + async function startLadder(actionType, continuousProgress = null) { const spec = getLadderActionSpec(actionType); if (!spec) { setLadderStatus('未知阶梯动作'); return { status: 'not_started' }; } + const continuousSession = continuousProgress !== null; + const setStartStatus = (singleStatus, continuousDetail) => { + setLadderStatus( + continuousSession + ? `连续${spec.label} · ${continuousDetail}` + : singleStatus, + ); + }; const actionSymbol = getCurrentSymbol(); if (!isCurrentObservedSymbol(actionSymbol)) { - setLadderStatus(`${spec.label}尚未开始:交易对正在切换`); + setStartStatus( + `${spec.label}尚未开始:交易对正在切换`, + '尚未开始 · 交易对正在切换', + ); return { status: 'not_started' }; } if (cancelCurrentSymbolOpenOrdersTask) { - setLadderStatus(`${spec.label}尚未开始:撤本币挂单处理中`); + setStartStatus( + `${spec.label}尚未开始:撤本币挂单处理中`, + '尚未开始 · 撤本币挂单处理中', + ); return { status: 'not_started' }; } if (singleOrderTask) { return { status: 'not_started' }; } if (ladderTask) { - setLadderStatus(`${spec.label}尚未开始:已有阶梯任务正在执行,请先停止`); + setStartStatus( + `${spec.label}尚未开始:已有阶梯任务正在执行,请先停止`, + '尚未开始 · 已有阶梯任务正在执行,请先停止', + ); return { status: 'not_started' }; } if (continuousLadderTask && !continuousSession) { @@ -2712,21 +2735,34 @@ import { return { status: 'not_started' }; } if (spec?.mode === 'CLOSE' && !isCloseSnapshotReady(actionSymbol)) { - setLadderStatus(`${spec.label}尚未开始:仓位确认中`); + setStartStatus(`${spec.label}尚未开始:仓位确认中`, '尚未开始 · 仓位确认中'); return { status: 'not_started' }; } ladderStopRequested = false; const abortController = new AbortController(); const progress = createLadderProgress(); + const setExecutionStatus = (singleStatus, continuousDetail) => { + if (continuousSession) { + setLadderStatus(formatActiveContinuousLadderProgress( + spec.label, + continuousDetail, + continuousProgress, + progress, + )); + return; + } + setLadderStatus(singleStatus); + }; ladderAbortController = abortController; activeLadderActionType = actionType; + if (continuousSession) activeContinuousLadderRoundProgress = progress; activeLadderPanelContext = { mode: spec.mode, symbol: actionSymbol, precision: readCurrentOrderbookPrecisionValue(), }; const feedbackStartedAt = performance.now(); - setLadderStatus(`${spec.label}准备中`); + setExecutionStatus(`${spec.label}准备中`, '准备中'); const executionTask = (async () => { const { plan, @@ -2736,6 +2772,7 @@ import { } = await runLadderPlanWithOpenOrderReplacement( actionType, progress, + setExecutionStatus, abortController.signal, ); return { @@ -2760,11 +2797,13 @@ import { wasStopped, }) => { if (!isCurrentObservedSymbol(actionSymbol)) { - setLadderStatus(formatInterruptedLadderProgress( - spec.label, - '交易对已切换', - progress, - )); + if (!continuousSession) { + setLadderStatus(formatInterruptedLadderProgress( + spec.label, + '交易对已切换', + progress, + )); + } return { status: 'interrupted', reason: '交易对已切换', @@ -2772,16 +2811,18 @@ import { }; } const diagnostics = formatLadderRepriceDiagnostics(repriceAttempts, lastRepriceApiErrorCode); - setLadderStatus( - wasStopped - ? `${formatStoppedLadderProgress(spec.label, progress)}${diagnostics}` - : `${formatCompletedLadderProgress( - spec.label, - done, - plan.orders.length, - progress, - )}${diagnostics}`, - ); + if (!continuousSession) { + setLadderStatus( + wasStopped + ? `${formatStoppedLadderProgress(spec.label, progress)}${diagnostics}` + : `${formatCompletedLadderProgress( + spec.label, + done, + plan.orders.length, + progress, + )}${diagnostics}`, + ); + } return { status: wasStopped ? 'stopped' : 'completed', progress: snapshotLadderProgress(progress), @@ -2789,7 +2830,7 @@ import { }) .catch((e) => { if (isLadderStoppedError(e)) { - if (isCurrentObservedSymbol(actionSymbol)) { + if (!continuousSession && isCurrentObservedSymbol(actionSymbol)) { setLadderStatus(formatStoppedLadderProgress(spec.label, progress)); } return { @@ -2799,11 +2840,13 @@ import { } err('Maker 阶梯执行失败:', e); if (!isCurrentObservedSymbol(actionSymbol)) { - setLadderStatus(formatInterruptedLadderProgress( - spec.label, - '交易对已切换', - progress, - )); + if (!continuousSession) { + setLadderStatus(formatInterruptedLadderProgress( + spec.label, + '交易对已切换', + progress, + )); + } return { status: 'interrupted', reason: '交易对已切换', @@ -2815,7 +2858,7 @@ import { const failureTitle = e?.statusTitle ? formatFailedLadderProgress(spec.label, e.statusTitle, progress) : failureText; - setLadderStatus(failureText, failureTitle); + if (!continuousSession) setLadderStatus(failureText, failureTitle); return { status: 'failed', error: e, @@ -2826,6 +2869,9 @@ import { }) .finally(() => { if (ladderAbortController === abortController) ladderAbortController = null; + if (activeContinuousLadderRoundProgress === progress) { + activeContinuousLadderRoundProgress = null; + } ladderTask = null; activeLadderActionType = null; activeLadderPanelContext = null; @@ -2912,7 +2958,7 @@ import { const executionTask = (async () => { while (true) { throwIfAborted(abortController.signal); - const outcome = await startLadder(actionType, true); + const outcome = await startLadder(actionType, continuousProgress); if (!outcome?.progress) return outcome; recordContinuousLadderRound(continuousProgress, outcome); if (outcome.status !== 'completed') { @@ -2998,7 +3044,19 @@ import { if (ladderAbortController && !ladderAbortController.signal.aborted) { ladderAbortController.abort(stoppedError); } - setLadderStatus(`${activeSpec.label}${continuousLadderTask ? '连续' : ''}停止中`); + if (continuousLadderTask) { + if (!activeContinuousLadderProgress || !activeContinuousLadderRoundProgress) { + throw new Error('运行中的连续阶梯任务缺少轮次进度'); + } + setLadderStatus(formatActiveContinuousLadderProgress( + activeSpec.label, + '停止中', + activeContinuousLadderProgress, + activeContinuousLadderRoundProgress, + )); + } else { + setLadderStatus(`${activeSpec.label}停止中`); + } } else { setContinuousLadderProgressStatus( activeSpec.label, @@ -3656,7 +3714,13 @@ import { return Boolean(activeRoot && countOpenOrderRowsByKey(activeRoot, symbol, key) < previousCount); } - async function cancelOpenOrderRowsForPlan(root, plan, progress, abortSignal = null) { + async function cancelOpenOrderRowsForPlan( + root, + plan, + progress, + setExecutionStatus, + abortSignal = null, + ) { let cancelQty = '0'; let currentRoot = root; while (compareDecimalStrings(cancelQty, plan.totalQty) < 0) { @@ -3701,7 +3765,10 @@ import { } if (outcome.status === 'dialog_open') { const { dialog } = outcome; - setLadderStatus(`${plan.spec.label}:单行撤单确认弹窗已打开`); + setExecutionStatus( + `${plan.spec.label}:单行撤单确认弹窗已打开`, + '单行撤单确认弹窗已打开', + ); const dialogClosed = await waitForDialogToClose( dialog, ROW_CANCEL_DIALOG_CLOSE_TIMEOUT_MS, @@ -4494,10 +4561,15 @@ import { scheduleRenderPanel(); } - async function cancelCurrentSymbolOpenOrdersForPlan(plan, progress, abortSignal = null) { + async function cancelCurrentSymbolOpenOrdersForPlan( + plan, + progress, + setExecutionStatus, + abortSignal = null, + ) { throwIfAborted(abortSignal); const setPlanStepStatus = (message) => { - setLadderStatus(`${plan.spec.label}:${message}`); + setExecutionStatus(`${plan.spec.label}:${message}`, message); }; const symbol = getCurrentSymbol(); if (!isCurrentObservedSymbol(symbol) || symbol !== plan?.symbol) { @@ -4597,7 +4669,13 @@ import { } setPlanStepStatus(`撤销 ${rowsToCancel.length} 笔当前币挂单`); - await cancelOpenOrderRowsForPlan(openOrdersScope, plan, progress, abortSignal); + await cancelOpenOrderRowsForPlan( + openOrdersScope, + plan, + progress, + setExecutionStatus, + abortSignal, + ); throwIfAborted(abortSignal); setPlanStepStatus('当前币挂单已替换,继续重挂'); return { ok: true, status: 'rows_cleared' }; @@ -4627,11 +4705,15 @@ import { } } - function formatLadderPlanStatus(plan) { + function formatLadderPlanDetail(plan) { const levelText = plan.levels === plan.requestedLevels ? `${plan.levels}档` : `${plan.levels}/${plan.requestedLevels}档`; - return `${plan.spec.label}计划:${plan.percent}% / ${levelText} / 幅${plan.ladderStep}`; + return `${plan.percent}% / ${levelText} / 幅${plan.ladderStep}`; + } + + function formatLadderPlanStatus(plan) { + return `${plan.spec.label}计划:${formatLadderPlanDetail(plan)}`; } function isReplaceableCloseLadderOpenOrdersFailure(plan, error) { @@ -4664,11 +4746,15 @@ import { return getOpenLadderMinimumQtyReplacementPlan(error); } - function formatOpenOrdersReplacementStatus(plan) { + function formatOpenOrdersReplacementDetail(plan) { if (plan?.spec?.mode === 'OPEN') { - return `${plan.spec.label}:同向开仓挂单可能占用可开数量,准备替换`; + return '同向开仓挂单可能占用可开数量,准备替换'; } - return `${plan.spec.label}:当前挂单占用可平数量,准备替换`; + return '当前挂单占用可平数量,准备替换'; + } + + function formatOpenOrdersReplacementStatus(plan) { + return `${plan.spec.label}:${formatOpenOrdersReplacementDetail(plan)}`; } function createLadderExpectedContext(plan) { @@ -4679,7 +4765,12 @@ import { }; } - async function runLadderPlanWithOpenOrderReplacement(actionType, progress, abortSignal = null) { + async function runLadderPlanWithOpenOrderReplacement( + actionType, + progress, + setExecutionStatus, + abortSignal = null, + ) { let replacementContext = null; for (let attempt = 0; attempt < 2; attempt += 1) { throwIfAborted(abortSignal); @@ -4688,18 +4779,27 @@ import { plan = await buildLadderPlan(actionType, replacementContext); setLadderPlannedOrders(progress, plan.orders.length); throwIfAborted(abortSignal); - setLadderStatus(formatLadderPlanStatus(plan)); - const execution = await executeLadderPlan(plan, progress, abortSignal); + setExecutionStatus(formatLadderPlanStatus(plan), formatLadderPlanDetail(plan)); + const execution = await executeLadderPlan( + plan, + progress, + setExecutionStatus, + abortSignal, + ); return { plan, ...execution }; } catch (e) { const replacementPlan = getReplaceableLadderOpenOrdersPlan(plan, e); if (attempt > 0 || !replacementPlan) throw e; assertLadderExecutionContext(replacementPlan); - setLadderStatus(formatOpenOrdersReplacementStatus(replacementPlan)); + setExecutionStatus( + formatOpenOrdersReplacementStatus(replacementPlan), + formatOpenOrdersReplacementDetail(replacementPlan), + ); replacementContext = createLadderExpectedContext(replacementPlan); const result = await cancelCurrentSymbolOpenOrdersForPlan( replacementPlan, progress, + setExecutionStatus, abortSignal, ); throwIfAborted(abortSignal); diff --git a/test/unit/binance-orderbook-trade/continuous-ladder.test.js b/test/unit/binance-orderbook-trade/continuous-ladder.test.js index cca499a..e648907 100644 --- a/test/unit/binance-orderbook-trade/continuous-ladder.test.js +++ b/test/unit/binance-orderbook-trade/continuous-ladder.test.js @@ -5,6 +5,7 @@ import { CONTINUOUS_LADDER_COOLDOWN_MS, CONTINUOUS_LADDER_READY_CHECK_MS, createContinuousLadderProgress, + formatActiveContinuousLadderProgress, formatContinuousLadderProgress, formatContinuousLadderWaitProgress, formatContinuousLadderWaitReason, @@ -52,7 +53,7 @@ test('continuous ladder status summarizes completed rounds and cumulative submis }); assert.equal( formatContinuousLadderProgress('阶梯平空', 'running', progress), - '阶梯平空连续中 · 2/2 轮 · 本轮 3/3 笔 · 累计 6 笔', + '连续阶梯平空 · 2/2 轮 · 本轮 3/3 笔 · 累计 6 笔', ); }); @@ -76,7 +77,7 @@ test('continuous ladder stopped status separates completed rounds from a partial assert.equal( formatContinuousLadderProgress('阶梯平空', 'stopped', progress), - '阶梯平空连续已停止 · 2/3 轮 · 本轮 1/3 笔 · 累计 7 笔', + '连续阶梯平空 · 已停止 · 2/3 轮 · 本轮 1/3 笔 · 累计 7 笔', ); }); @@ -94,7 +95,33 @@ test('continuous ladder status shows cancellations only when they occurred', () assert.equal( formatContinuousLadderProgress('阶梯平多', 'failed', progress, '下单按钮 3 秒内未恢复可点击'), - '阶梯平多连续失败 · 0/1 轮 · 本轮 2/3 笔 · 累计 2 笔 · 撤 1 笔 · 下单按钮 3 秒内未恢复可点击', + '连续阶梯平多 · 失败 · 0/1 轮 · 本轮 2/3 笔 · 累计 2 笔 · 撤 1 笔 · 下单按钮 3 秒内未恢复可点击', + ); +}); + +test('active continuous ladder status keeps the continuous action and live round totals', () => { + const progress = createContinuousLadderProgress(); + const completedRound = roundProgress({ + submittedOrders: 3, + plannedOrders: 3, + currentPlanSubmittedOrders: 3, + }); + recordContinuousLadderRound(progress, { status: 'completed', progress: completedRound }); + recordContinuousLadderRound(progress, { status: 'completed', progress: completedRound }); + + assert.equal( + formatActiveContinuousLadderProgress( + '阶梯平空', + '第 2 笔确认中', + progress, + roundProgress({ + submittedOrders: 1, + cancelledOrders: 1, + plannedOrders: 3, + currentPlanSubmittedOrders: 1, + }), + ), + '连续阶梯平空 · 第 2 笔确认中 · 2/3 轮 · 本轮 1/3 笔 · 累计 7 笔 · 撤 1 笔', ); }); @@ -219,7 +246,7 @@ test('continuous ladder wait status puts the current wait before progress counte 'cooldown', CONTINUOUS_LADDER_COOLDOWN_MS, ), - '阶梯平空连续中 · 1s 后继续 · 2/2 轮 · 本轮 3/3 笔 · 累计 6 笔', + '连续阶梯平空 · 1s 后继续 · 2/2 轮 · 本轮 3/3 笔 · 累计 6 笔', ); assert.equal( formatContinuousLadderWaitProgress( @@ -228,7 +255,7 @@ test('continuous ladder wait status puts the current wait before progress counte 'waiting_ready', CONTINUOUS_LADDER_COOLDOWN_MS, ), - '阶梯平空连续中 · 等待按钮恢复 · 2/2 轮 · 本轮 3/3 笔 · 累计 6 笔', + '连续阶梯平空 · 等待按钮恢复 · 2/2 轮 · 本轮 3/3 笔 · 累计 6 笔', ); }); diff --git a/test/unit/binance-orderbook-trade/ladder-options.test.js b/test/unit/binance-orderbook-trade/ladder-options.test.js index 64ff0fc..f9eb3e8 100644 --- a/test/unit/binance-orderbook-trade/ladder-options.test.js +++ b/test/unit/binance-orderbook-trade/ladder-options.test.js @@ -224,7 +224,8 @@ test('ladder execution and UI updates use one captured mode-symbol-precision con assert.match(source, /const optionContext = getPanelOptionContext\(\)/); assert.match(source, /setLadderLevels\(value, optionContext\.mode, optionContext\.symbol, optionContext\.precision\)/); assert.match(source, /setLadderStep\(value, optionContext\.mode, optionContext\.symbol, optionContext\.precision\)/); - assert.match(source, /计划:\$\{plan\.percent\}% \/ \$\{levelText\} \/ 幅\$\{plan\.ladderStep\}/); + assert.match(source, /return `\$\{plan\.percent\}% \/ \$\{levelText\} \/ 幅\$\{plan\.ladderStep\}`/); + assert.match(source, /return `\$\{plan\.spec\.label\}计划:\$\{formatLadderPlanDetail\(plan\)\}`/); }); test('open and close ladder percentage rows share the centralized ratio label', () => { diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index d5610c6..c6fe8e1 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -249,9 +249,9 @@ test('ladder retries with restricted open-order replacement after supported feed assert.match(replacePlanBody, /getOpenLadderMinimumQtyReplacementPlan\(error\)/); const retryBody = readFunctionBody('runLadderPlanWithOpenOrderReplacement'); - assert.match(retryBody, /await executeLadderPlan\(plan,\s*progress,\s*abortSignal\)/); + assert.match(retryBody, /await executeLadderPlan\(\s*plan,\s*progress,\s*setExecutionStatus,\s*abortSignal/); assert.match(retryBody, /getReplaceableLadderOpenOrdersPlan\(plan,\s*e\)/); - assert.match(retryBody, /cancelCurrentSymbolOpenOrdersForPlan\(\s*replacementPlan,\s*progress,\s*abortSignal/); + assert.match(retryBody, /cancelCurrentSymbolOpenOrdersForPlan\(\s*replacementPlan,\s*progress,\s*setExecutionStatus,\s*abortSignal/); assert.doesNotMatch(retryBody, /cancelCurrentSymbolOpenOrders\(\{\s*waitUntilCleared: true\s*\}\)/); assert.match(retryBody, /replacementContext = createLadderExpectedContext\(replacementPlan\)/); assert.match(retryBody, /buildLadderPlan\(actionType,\s*replacementContext\)/); @@ -260,7 +260,7 @@ test('ladder retries with restricted open-order replacement after supported feed const startBody = readFunctionBody('startLadder'); assert.match(startBody, /const spec = getLadderActionSpec\(actionType\)/); assert.doesNotMatch(startBody, /cancelCurrentSymbolOpenOrders\(\{\s*waitUntilCleared: true\s*\}\)/); - assert.match(startBody, /runLadderPlanWithOpenOrderReplacement\(\s*actionType,\s*progress,\s*abortController\.signal/); + assert.match(startBody, /runLadderPlanWithOpenOrderReplacement\(\s*actionType,\s*progress,\s*setExecutionStatus,\s*abortController\.signal/); }); test('open and close ladders reprice only remaining orders after explicit maker conflicts', () => { @@ -614,6 +614,8 @@ test('ladder actions keep only their final UI feedback visible for a minimum win test('Option or Alt click continuously repeats close ladders only after readiness and cooldown', () => { const continuousBody = readFunctionBody('startContinuousLadder'); + const startBody = readFunctionBody('startLadder'); + const executeBody = readFunctionBody('executeLadderPlan'); const readinessBody = readFunctionBody('readContinuousLadderReadiness'); const stopBody = readFunctionBody('stopLadder'); const actionButtonBody = readFunctionBody('ladderActionButton'); @@ -624,13 +626,18 @@ test('Option or Alt click continuously repeats close ladders only after readines assert.match(source, /startContinuousLadder\(actionType\)/); assert.match(continuousBody, /spec\.mode !== 'CLOSE'\) return startLadder\(actionType\)/); assert.match(continuousBody, /while \(true\)/); - assert.match(continuousBody, /await startLadder\(actionType, true\)/); + assert.match(continuousBody, /await startLadder\(actionType, continuousProgress\)/); assert.match(continuousBody, /recordContinuousLadderRound\(continuousProgress, outcome\)/); assert.match(continuousBody, /setContinuousLadderProgressStatus\(/); assert.match(continuousBody, /await waitForContinuousLadderNextRound\(/); assert.match(continuousBody, /readContinuousLadderReadiness\(actionType, actionSymbol\)/); assert.match(continuousBody, /onWaitStateChange:/); assert.match(continuousBody, /formatContinuousLadderWaitProgress\(/); + assert.match(startBody, /formatActiveContinuousLadderProgress\(/); + assert.match(startBody, /activeContinuousLadderRoundProgress = progress/); + assert.match(startBody, /runLadderPlanWithOpenOrderReplacement\([\s\S]*setExecutionStatus/); + assert.match(executeBody, /setExecutionStatus\(/); + assert.doesNotMatch(executeBody, /setLadderStatus\(/); assert.match(readinessBody, /isCurrentObservedSymbol\(actionSymbol\)/); assert.match(readinessBody, /getActiveTradeMode\(\) !== spec\.mode/); @@ -641,6 +648,7 @@ test('Option or Alt click continuously repeats close ladders only after readines assert.match(readinessBody, /isSubmitButtonBusy\(button\)/); assert.match(stopBody, /continuousLadderAbortController\.abort\(stoppedError\)/); + assert.match(stopBody, /formatActiveContinuousLadderProgress\(/); assert.match(actionButtonBody, /Option\/Alt \+ 单击:连续交易/); assert.doesNotMatch(source, /Continuous trading (?:stopped|failed)/); assert.match(executionButtonBody, /activeLadderActionType \|\| activeContinuousLadderActionType/); @@ -806,13 +814,13 @@ test('stopping a ladder aborts replacement waits before another cancel or submit assert.match(source, /let ladderAbortController = null/); assert.match(startBody, /const abortController = new AbortController\(\)/); - assert.match(startBody, /runLadderPlanWithOpenOrderReplacement\(\s*actionType,\s*progress,\s*abortController\.signal/); + assert.match(startBody, /runLadderPlanWithOpenOrderReplacement\(\s*actionType,\s*progress,\s*setExecutionStatus,\s*abortController\.signal/); assert.match(stopBody, /const stoppedError = createLadderStoppedError\(\)/); assert.match(stopBody, /ladderAbortController\.abort\(stoppedError\)/); - assert.match(runBody, /cancelCurrentSymbolOpenOrdersForPlan\(\s*replacementPlan,\s*progress,\s*abortSignal/); + assert.match(runBody, /cancelCurrentSymbolOpenOrdersForPlan\(\s*replacementPlan,\s*progress,\s*setExecutionStatus,\s*abortSignal/); assert.match(runBody, /throwIfAborted\(abortSignal\)[\s\S]*buildLadderPlan/); assert.match(executeBody, /throwIfAborted\(abortSignal\)[\s\S]*button\.click\(\)/); - assert.match(cancelPlanBody, /cancelOpenOrderRowsForPlan\(openOrdersScope,\s*plan,\s*progress,\s*abortSignal\)/); + assert.match(cancelPlanBody, /cancelOpenOrderRowsForPlan\(\s*openOrdersScope,\s*plan,\s*progress,\s*setExecutionStatus,\s*abortSignal/); assert.match(cancelPlanBody, /isLadderStoppedError\(e\)[\s\S]*throw e/); assert.match(cancelPlanBody, /previousOpenOrdersSubTabIdentity = getOpenOrdersSubTabIdentity\([\s\S]*activateOpenOrdersBasicSubTab\(/); assert.match(cancelPlanBody, /symbolFilterOriginalChecked = getCheckboxCheckedState\([\s\S]*ensureOpenOrdersLimitedToCurrentSymbol\(/); @@ -830,14 +838,14 @@ test('stopping a ladder preserves confirmed submit and cancel progress', () => { const cancelRowsBody = readFunctionBody('cancelOpenOrderRowsForPlan'); assert.match(startBody, /const progress = createLadderProgress\(\)/); - assert.match(startBody, /runLadderPlanWithOpenOrderReplacement\(\s*actionType,\s*progress,\s*abortController\.signal/); + assert.match(startBody, /runLadderPlanWithOpenOrderReplacement\(\s*actionType,\s*progress,\s*setExecutionStatus,\s*abortController\.signal/); assert.match(startBody, /isLadderStoppedError\(e\)[\s\S]*formatStoppedLadderProgress\(spec\.label,\s*progress\)/); assert.match(startBody, /formatCompletedLadderProgress\(\s*spec\.label,\s*done,\s*plan\.orders\.length,\s*progress/); assert.match(runBody, /buildLadderPlan\(actionType,\s*replacementContext\)[\s\S]*setLadderPlannedOrders\(progress,\s*plan\.orders\.length\)/); - assert.match(runBody, /executeLadderPlan\(plan,\s*progress,\s*abortSignal\)/); - assert.match(runBody, /cancelCurrentSymbolOpenOrdersForPlan\(\s*replacementPlan,\s*progress,\s*abortSignal/); + assert.match(runBody, /executeLadderPlan\(\s*plan,\s*progress,\s*setExecutionStatus,\s*abortSignal/); + assert.match(runBody, /cancelCurrentSymbolOpenOrdersForPlan\(\s*replacementPlan,\s*progress,\s*setExecutionStatus,\s*abortSignal/); assert.match(executeBody, /waitForOrderSubmitAcknowledgement\([\s\S]*recordLadderSubmittedOrder\(progress\)/); - assert.match(cancelPlanBody, /cancelOpenOrderRowsForPlan\(\s*openOrdersScope,\s*plan,\s*progress,\s*abortSignal/); + assert.match(cancelPlanBody, /cancelOpenOrderRowsForPlan\(\s*openOrdersScope,\s*plan,\s*progress,\s*setExecutionStatus,\s*abortSignal/); assert.match(cancelRowsBody, /confirmOpenOrderRowKeyCountBelow\([\s\S]*recordLadderCancelledOrder\(progress\)/); const acknowledgementIndex = executeBody.indexOf('await waitForOrderSubmitAcknowledgement('); @@ -867,15 +875,16 @@ test('ladder task statuses name the active action and observed outcome', () => { const planStatusBody = readFunctionBody('formatLadderPlanStatus'); const replacementStatusBody = readFunctionBody('formatOpenOrdersReplacementStatus'); - assert.match(executeBody, /setLadderStatus\(`\$\{plan\.spec\.label\}挂单 \$\{done \+ 1\}\/\$\{plan\.orders\.length\} 确认中`\)/); - assert.match(executeBody, /setLadderStatus\(`\$\{plan\.spec\.label\}已挂 \$\{done\}\/\$\{plan\.orders\.length\} 笔`\)/); - assert.match(executeBody, /setLadderStatus\(`\$\{plan\.spec\.label\}:盘口已移动/); + assert.match(executeBody, /setExecutionStatus\(\s*`\$\{plan\.spec\.label\}挂单 \$\{done \+ 1\}\/\$\{plan\.orders\.length\} 确认中`,\s*`第 \$\{done \+ 1\} 笔确认中`,\s*\)/); + assert.match(executeBody, /setExecutionStatus\(`\$\{plan\.spec\.label\}已挂 \$\{done\}\/\$\{plan\.orders\.length\} 笔`, null\)/); + assert.match(executeBody, /setExecutionStatus\(`\$\{plan\.spec\.label\}:\$\{repriceDetail\}`, repriceDetail\)/); assert.match(startBody, /`\$\{spec\.label\}尚未开始:仓位确认中`/); assert.match(startBody, /formatInterruptedLadderProgress\(\s*spec\.label,\s*'交易对已切换',\s*progress/); assert.match(startBody, /formatFailedLadderProgress\(spec\.label,\s*failureMessage,\s*progress\)/); - assert.match(stopBody, /`\$\{activeSpec\.label\}\$\{continuousLadderTask \? '连续' : ''\}停止中`/); + assert.match(stopBody, /formatActiveContinuousLadderProgress\(\s*activeSpec\.label,\s*'停止中'/); + assert.match(stopBody, /setLadderStatus\(`\$\{activeSpec\.label\}停止中`\)/); assert.match(cancelPlanBody, /setPlanStepStatus\(`撤销 \$\{rowsToCancel\.length\} 笔当前币挂单`\)/); - assert.match(planStatusBody, /`\$\{plan\.spec\.label\}计划:\$\{plan\.percent\}% \/ \$\{levelText\} \/ 幅\$\{plan\.ladderStep\}`/); + assert.match(planStatusBody, /`\$\{plan\.spec\.label\}计划:\$\{formatLadderPlanDetail\(plan\)\}`/); assert.doesNotMatch(planStatusBody, /%\/|\/幅/); assert.match(replacementStatusBody, /`\$\{plan\.spec\.label\}:/); assert.match(cancelPlanBody, /const setPlanStepStatus = \(message\) =>/); @@ -1452,7 +1461,7 @@ test('confirmed close-quantity mutations bypass the generic trade UI debounce', test('pending close actions report position confirmation without starting execution', () => { const startBody = readFunctionBody('startLadder'); assert.match(startBody, /spec\?\.mode === 'CLOSE' && !isCloseSnapshotReady\(actionSymbol\)/); - assert.match(startBody, /setLadderStatus\(`\$\{spec\.label\}尚未开始:仓位确认中`\)/); + assert.match(startBody, /setStartStatus\(`\$\{spec\.label\}尚未开始:仓位确认中`, '尚未开始 · 仓位确认中'\)/); assert.match(source, /if \(getActiveTradeMode\(\) === 'CLOSE' && !isCloseSnapshotReady\(clickedSymbol\)\) \{\s*warn\('仓位确认中'\);\s*setLadderStatus\('单击下单未执行:仓位确认中'\);\s*return;/); });