diff --git a/docs/binance-orderbook-trade-development.md b/docs/binance-orderbook-trade-development.md
index b370766..b300218 100644
--- a/docs/binance-orderbook-trade-development.md
+++ b/docs/binance-orderbook-trade-development.md
@@ -157,6 +157,8 @@ 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.
+
When selecting account-order tabs, scope to the bottom account-orders tab group. Do not globally match `当前委托` or `Open Orders`.
When a pane is found through `aria-controls`, confirm it contains current-orders controls such as `隐藏其他合约` or `全撤`. Binance may reuse pane ids in unrelated tab systems.
@@ -196,6 +198,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
- 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
- cancel the native cancel-all dialog through its secondary button, Escape, and backdrop; verify chart and account-order UI state restores immediately without waiting for order clearing
diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js
index cbf60f7..9b40322 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.156
+// @version 2.7.157
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -865,52 +865,6 @@
});
}
- // src/binance-orderbook-trade/core/continuous-ladder.js
- var CONTINUOUS_LADDER_COOLDOWN_MS = 1e3;
- var CONTINUOUS_LADDER_READY_CHECK_MS = 50;
- function assertReadinessState(state) {
- if (!["ready", "waiting", "stopped"].includes(state?.status)) {
- throw new Error("Invalid continuous ladder readiness state");
- }
- return state;
- }
- async function waitUntilReadyOrStopped({
- readReadiness,
- delay,
- signal,
- readyCheckMs
- }) {
- while (true) {
- throwIfAborted(signal);
- const state = assertReadinessState(readReadiness());
- if (state.status !== "waiting") return state;
- await waitForPromiseOrAbort(delay(readyCheckMs), signal);
- }
- }
- async function waitForContinuousLadderNextRound({
- readReadiness,
- delay,
- signal = null,
- cooldownMs = CONTINUOUS_LADDER_COOLDOWN_MS,
- readyCheckMs = CONTINUOUS_LADDER_READY_CHECK_MS
- }) {
- if (!(cooldownMs >= 0)) throw new Error("Invalid continuous ladder cooldown");
- if (!(readyCheckMs > 0)) throw new Error("Invalid continuous ladder readiness interval");
- while (true) {
- const readyState = await waitUntilReadyOrStopped({
- readReadiness,
- delay,
- signal,
- readyCheckMs
- });
- if (readyState.status === "stopped") return readyState;
- await waitForPromiseOrAbort(delay(cooldownMs), signal);
- throwIfAborted(signal);
- const afterCooldown = assertReadinessState(readReadiness());
- if (afterCooldown.status !== "waiting") return afterCooldown;
- }
- }
-
// src/binance-orderbook-trade/core/ladder-progress.js
function assertLadderProgress(progress) {
if (!progress || !Number.isInteger(progress.submittedOrders) || progress.submittedOrders < 0 || !Number.isInteger(progress.cancelledOrders) || progress.cancelledOrders < 0 || !Number.isInteger(progress.currentPlanSubmittedOrders) || progress.currentPlanSubmittedOrders < 0 || (progress.plannedOrders === null ? progress.currentPlanSubmittedOrders !== 0 : !(Number.isInteger(progress.plannedOrders) && progress.plannedOrders > 0 && progress.currentPlanSubmittedOrders <= progress.plannedOrders))) {
@@ -950,6 +904,15 @@
currentPlanSubmittedOrders: 0
};
}
+ function snapshotLadderProgress(progress) {
+ assertLadderProgress(progress);
+ return {
+ submittedOrders: progress.submittedOrders,
+ cancelledOrders: progress.cancelledOrders,
+ plannedOrders: progress.plannedOrders,
+ currentPlanSubmittedOrders: progress.currentPlanSubmittedOrders
+ };
+ }
function setLadderPlannedOrders(progress, plannedOrders) {
assertLadderProgress(progress);
if (!Number.isInteger(plannedOrders) || plannedOrders <= 0) {
@@ -1002,6 +965,122 @@
return `${label}已完成 · 已挂 ${completedOrders}/${totalOrders} 笔${cancelledText}`;
}
+ // src/binance-orderbook-trade/core/continuous-ladder.js
+ var CONTINUOUS_LADDER_COOLDOWN_MS = 1e3;
+ var CONTINUOUS_LADDER_READY_CHECK_MS = 50;
+ var recordedRoundOutcomes = /* @__PURE__ */ new WeakSet();
+ var CONTINUOUS_LADDER_PHASE_TEXT = Object.freeze({
+ running: "连续中",
+ 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)) {
+ throw new Error("Invalid continuous ladder progress");
+ }
+ if (progress.lastRound !== null) snapshotLadderProgress(progress.lastRound);
+ }
+ function createContinuousLadderProgress() {
+ return {
+ startedRounds: 0,
+ completedRounds: 0,
+ submittedOrders: 0,
+ cancelledOrders: 0,
+ lastRound: null
+ };
+ }
+ function recordContinuousLadderRound(progress, outcome) {
+ assertContinuousLadderProgress(progress);
+ if (!outcome || typeof outcome !== "object") {
+ throw new Error("Invalid continuous ladder round outcome");
+ }
+ if (recordedRoundOutcomes.has(outcome)) {
+ throw new Error("Continuous ladder round was already recorded");
+ }
+ if (!["completed", "stopped", "failed", "interrupted"].includes(outcome.status)) {
+ throw new Error("Invalid continuous ladder round outcome");
+ }
+ const roundProgress = snapshotLadderProgress(outcome.progress);
+ progress.startedRounds += 1;
+ if (outcome.status === "completed") progress.completedRounds += 1;
+ progress.submittedOrders += roundProgress.submittedOrders;
+ progress.cancelledOrders += roundProgress.cancelledOrders;
+ progress.lastRound = {
+ status: outcome.status,
+ ...roundProgress
+ };
+ recordedRoundOutcomes.add(outcome);
+ assertContinuousLadderProgress(progress);
+ }
+ function formatContinuousLadderProgress(label, phase, progress, reason = null) {
+ if (typeof label !== "string" || label.trim() === "") {
+ throw new Error("Invalid continuous ladder label");
+ }
+ const phaseText = CONTINUOUS_LADDER_PHASE_TEXT[phase];
+ if (!phaseText) throw new Error("Invalid continuous ladder phase");
+ if (reason !== null && (typeof reason !== "string" || reason.trim() === "")) {
+ throw new Error("Invalid continuous ladder reason");
+ }
+ assertContinuousLadderProgress(progress);
+ const parts = [`${label}${phaseText}`];
+ parts.push(progress.startedRounds === 0 ? "0 轮" : `${progress.completedRounds}/${progress.startedRounds} 轮`);
+ if (progress.lastRound?.plannedOrders !== null) {
+ parts.push(
+ `本轮 ${progress.lastRound.currentPlanSubmittedOrders}/${progress.lastRound.plannedOrders} 笔`
+ );
+ } else if (progress.lastRound?.submittedOrders > 0) {
+ parts.push(`本轮 ${progress.lastRound.submittedOrders} 笔`);
+ }
+ parts.push(`累计 ${progress.submittedOrders} 笔`);
+ if (progress.cancelledOrders > 0) parts.push(`撤 ${progress.cancelledOrders} 笔`);
+ if (reason !== null) parts.push(reason);
+ return parts.join(" · ");
+ }
+ function assertReadinessState(state) {
+ if (!["ready", "waiting", "stopped"].includes(state?.status)) {
+ throw new Error("Invalid continuous ladder readiness state");
+ }
+ return state;
+ }
+ async function waitUntilReadyOrStopped({
+ readReadiness,
+ delay,
+ signal,
+ readyCheckMs
+ }) {
+ while (true) {
+ throwIfAborted(signal);
+ const state = assertReadinessState(readReadiness());
+ if (state.status !== "waiting") return state;
+ await waitForPromiseOrAbort(delay(readyCheckMs), signal);
+ }
+ }
+ async function waitForContinuousLadderNextRound({
+ readReadiness,
+ delay,
+ signal = null,
+ cooldownMs = CONTINUOUS_LADDER_COOLDOWN_MS,
+ readyCheckMs = CONTINUOUS_LADDER_READY_CHECK_MS
+ }) {
+ if (!(cooldownMs >= 0)) throw new Error("Invalid continuous ladder cooldown");
+ if (!(readyCheckMs > 0)) throw new Error("Invalid continuous ladder readiness interval");
+ while (true) {
+ const readyState = await waitUntilReadyOrStopped({
+ readReadiness,
+ delay,
+ signal,
+ readyCheckMs
+ });
+ if (readyState.status === "stopped") return readyState;
+ await waitForPromiseOrAbort(delay(cooldownMs), signal);
+ throwIfAborted(signal);
+ const afterCooldown = assertReadinessState(readReadiness());
+ if (afterCooldown.status !== "waiting") return afterCooldown;
+ }
+ }
+
// src/binance-orderbook-trade/core/status-symbol.js
var STATUS_QUOTE_ASSETS = Object.freeze(["USDT", "USDC"]);
function formatStatusBaseAsset(symbol) {
@@ -2621,6 +2700,7 @@
let continuousLadderAbortController = null;
let activeLadderActionType = null;
let activeContinuousLadderActionType = null;
+ let activeContinuousLadderProgress = null;
let activeLadderPanelContext = null;
let cancelCurrentSymbolOpenOrdersTask = null;
let cancelCurrentSymbolOpenOrdersBlocksLadderActions = false;
@@ -4563,7 +4643,11 @@
"交易对已切换",
progress
));
- return { status: "interrupted" };
+ return {
+ status: "interrupted",
+ reason: "交易对已切换",
+ progress: snapshotLadderProgress(progress)
+ };
}
const diagnostics = formatLadderRepriceDiagnostics(repriceAttempts, lastRepriceApiErrorCode);
setLadderStatus(
@@ -4574,13 +4658,19 @@
progress
)}${diagnostics}`
);
- return { status: wasStopped ? "stopped" : "completed" };
+ return {
+ status: wasStopped ? "stopped" : "completed",
+ progress: snapshotLadderProgress(progress)
+ };
}).catch((e) => {
if (isLadderStoppedError(e)) {
if (isCurrentObservedSymbol(actionSymbol)) {
setLadderStatus(formatStoppedLadderProgress(spec.label, progress));
}
- return { status: "stopped" };
+ return {
+ status: "stopped",
+ progress: snapshotLadderProgress(progress)
+ };
}
err("Maker 阶梯执行失败:", e);
if (!isCurrentObservedSymbol(actionSymbol)) {
@@ -4589,13 +4679,23 @@
"交易对已切换",
progress
));
- return { status: "interrupted" };
+ return {
+ status: "interrupted",
+ reason: "交易对已切换",
+ progress: snapshotLadderProgress(progress)
+ };
}
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);
- return { status: "failed", error: e };
+ return {
+ status: "failed",
+ error: e,
+ reason: failureMessage,
+ titleReason: e?.statusTitle || failureMessage,
+ progress: snapshotLadderProgress(progress)
+ };
}).finally(() => {
if (ladderAbortController === abortController) ladderAbortController = null;
ladderTask = null;
@@ -4610,7 +4710,7 @@
function readContinuousLadderReadiness(actionType, actionSymbol) {
const spec = getLadderActionSpec2(actionType);
if (!spec || spec.mode !== "CLOSE") {
- throw new Error("Continuous ladder trading requires a close action");
+ throw new Error("连续交易仅支持阶梯平仓");
}
if (!isCurrentObservedSymbol(actionSymbol)) {
return { status: "stopped", reason: "symbol_changed" };
@@ -4627,14 +4727,20 @@
}
return { status: "ready" };
}
- function setContinuousLadderStoppedStatus(reason) {
+ function getContinuousLadderStopReason(reason) {
const reasonTextByCode = {
- symbol_changed: "symbol changed",
- mode_changed: "trade mode changed"
+ symbol_changed: "交易对已切换",
+ mode_changed: "开仓/平仓模式已切换"
};
const reasonText = reasonTextByCode[reason];
- if (!reasonText) throw new Error(`Unknown continuous ladder stop reason: ${reason}`);
- setLadderStatus(`Continuous trading stopped: ${reasonText}`);
+ if (!reasonText) throw new Error(`未知连续交易停止原因:${reason}`);
+ return reasonText;
+ }
+ function setContinuousLadderProgressStatus(label, phase, progress, reason = null, titleReason = reason) {
+ setLadderStatus(
+ formatContinuousLadderProgress(label, phase, progress, reason),
+ formatContinuousLadderProgress(label, phase, progress, titleReason)
+ );
}
async function startContinuousLadder(actionType) {
const spec = getLadderActionSpec2(actionType);
@@ -4646,28 +4752,56 @@
return { status: "not_started" };
}
const abortController = new AbortController();
+ const continuousProgress = createContinuousLadderProgress();
continuousLadderAbortController = abortController;
activeContinuousLadderActionType = actionType;
+ activeContinuousLadderProgress = continuousProgress;
const executionTask = (async () => {
while (true) {
throwIfAborted(abortController.signal);
const outcome = await startLadder(actionType, true);
- if (outcome?.status !== "completed") return outcome;
+ if (!outcome?.progress) return outcome;
+ recordContinuousLadderRound(continuousProgress, outcome);
+ if (outcome.status !== "completed") {
+ setContinuousLadderProgressStatus(
+ spec.label,
+ outcome.status,
+ continuousProgress,
+ outcome.reason || null,
+ outcome.titleReason || outcome.reason || null
+ );
+ return outcome;
+ }
+ setContinuousLadderProgressStatus(spec.label, "running", continuousProgress);
const readiness = await waitForContinuousLadderNextRound({
readReadiness: () => readContinuousLadderReadiness(actionType, actionSymbol),
delay,
signal: abortController.signal
});
if (readiness.status === "stopped") {
- setContinuousLadderStoppedStatus(readiness.reason);
+ setContinuousLadderProgressStatus(
+ spec.label,
+ "stopped",
+ continuousProgress,
+ getContinuousLadderStopReason(readiness.reason)
+ );
return readiness;
}
}
})();
continuousLadderTask = executionTask.catch((e) => {
- if (isLadderStoppedError(e)) return { status: "stopped" };
- err("Continuous ladder trading failed:", e);
- setLadderStatus(`Continuous trading failed: ${e?.message || "unknown error"}`);
+ if (isLadderStoppedError(e)) {
+ setContinuousLadderProgressStatus(spec.label, "stopped", continuousProgress);
+ return { status: "stopped" };
+ }
+ err("连续阶梯交易失败:", e);
+ setContinuousLadderProgressStatus(
+ spec.label,
+ "failed",
+ continuousProgress,
+ e?.message || "未知错误",
+ e?.statusTitle || e?.message || "未知错误"
+ );
return { status: "failed", error: e };
}).finally(() => {
if (continuousLadderAbortController === abortController) {
@@ -4675,6 +4809,7 @@
}
continuousLadderTask = null;
activeContinuousLadderActionType = null;
+ activeContinuousLadderProgress = null;
scheduleRenderPanel();
});
scheduleRenderPanel();
@@ -4697,9 +4832,13 @@
if (ladderAbortController && !ladderAbortController.signal.aborted) {
ladderAbortController.abort(stoppedError);
}
- setLadderStatus(`${activeSpec.label}停止中`);
+ setLadderStatus(`${activeSpec.label}${continuousLadderTask ? "连续" : ""}停止中`);
} else {
- setLadderStatus(`${ladderStatusText} · Continuous trading stopped`);
+ setContinuousLadderProgressStatus(
+ activeSpec.label,
+ "stopping",
+ activeContinuousLadderProgress
+ );
}
scheduleRenderPanel();
}
@@ -6868,7 +7007,7 @@
const borderColor = isBuyTone ? "var(--color-Buy)" : "var(--color-Sell)";
const background = isBuyTone ? "var(--color-GreenAlpha01)" : "var(--color-RedAlpha01)";
const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : "";
- const continuousHint = actionType.startsWith("CLOSE_") ? ' title="Option/Alt + click: continuous trading"' : "";
+ const continuousHint = actionType.startsWith("CLOSE_") ? ' title="Option/Alt + 单击:连续交易"' : "";
const cursor = disabled ? "not-allowed" : "pointer";
return ``;
}
diff --git a/src/binance-orderbook-trade/core/continuous-ladder.js b/src/binance-orderbook-trade/core/continuous-ladder.js
index 2eb7541..74068d5 100644
--- a/src/binance-orderbook-trade/core/continuous-ladder.js
+++ b/src/binance-orderbook-trade/core/continuous-ladder.js
@@ -2,10 +2,101 @@ import {
throwIfAborted,
waitForPromiseOrAbort,
} from './abort.js';
+import { snapshotLadderProgress } from './ladder-progress.js';
export const CONTINUOUS_LADDER_COOLDOWN_MS = 1000;
export const CONTINUOUS_LADDER_READY_CHECK_MS = 50;
+const recordedRoundOutcomes = new WeakSet();
+const CONTINUOUS_LADDER_PHASE_TEXT = Object.freeze({
+ running: '连续中',
+ 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)
+ ) {
+ throw new Error('Invalid continuous ladder progress');
+ }
+ if (progress.lastRound !== null) snapshotLadderProgress(progress.lastRound);
+}
+
+export function createContinuousLadderProgress() {
+ return {
+ startedRounds: 0,
+ completedRounds: 0,
+ submittedOrders: 0,
+ cancelledOrders: 0,
+ lastRound: null,
+ };
+}
+
+export function recordContinuousLadderRound(progress, outcome) {
+ assertContinuousLadderProgress(progress);
+ if (!outcome || typeof outcome !== 'object') {
+ throw new Error('Invalid continuous ladder round outcome');
+ }
+ if (recordedRoundOutcomes.has(outcome)) {
+ throw new Error('Continuous ladder round was already recorded');
+ }
+ if (!['completed', 'stopped', 'failed', 'interrupted'].includes(outcome.status)) {
+ throw new Error('Invalid continuous ladder round outcome');
+ }
+ const roundProgress = snapshotLadderProgress(outcome.progress);
+ progress.startedRounds += 1;
+ if (outcome.status === 'completed') progress.completedRounds += 1;
+ progress.submittedOrders += roundProgress.submittedOrders;
+ progress.cancelledOrders += roundProgress.cancelledOrders;
+ progress.lastRound = {
+ status: outcome.status,
+ ...roundProgress,
+ };
+ recordedRoundOutcomes.add(outcome);
+ assertContinuousLadderProgress(progress);
+}
+
+export function formatContinuousLadderProgress(label, phase, progress, reason = null) {
+ if (typeof label !== 'string' || label.trim() === '') {
+ throw new Error('Invalid continuous ladder label');
+ }
+ const phaseText = CONTINUOUS_LADDER_PHASE_TEXT[phase];
+ if (!phaseText) throw new Error('Invalid continuous ladder phase');
+ if (reason !== null && (typeof reason !== 'string' || reason.trim() === '')) {
+ throw new Error('Invalid continuous ladder reason');
+ }
+ assertContinuousLadderProgress(progress);
+
+ const parts = [`${label}${phaseText}`];
+ parts.push(progress.startedRounds === 0
+ ? '0 轮'
+ : `${progress.completedRounds}/${progress.startedRounds} 轮`);
+ if (progress.lastRound?.plannedOrders !== null) {
+ parts.push(
+ `本轮 ${progress.lastRound.currentPlanSubmittedOrders}/${progress.lastRound.plannedOrders} 笔`,
+ );
+ } else if (progress.lastRound?.submittedOrders > 0) {
+ parts.push(`本轮 ${progress.lastRound.submittedOrders} 笔`);
+ }
+ parts.push(`累计 ${progress.submittedOrders} 笔`);
+ if (progress.cancelledOrders > 0) parts.push(`撤 ${progress.cancelledOrders} 笔`);
+ if (reason !== null) parts.push(reason);
+ return parts.join(' · ');
+}
+
function assertReadinessState(state) {
if (!['ready', 'waiting', 'stopped'].includes(state?.status)) {
throw new Error('Invalid continuous ladder readiness state');
diff --git a/src/binance-orderbook-trade/core/ladder-progress.js b/src/binance-orderbook-trade/core/ladder-progress.js
index eba2328..12f906b 100644
--- a/src/binance-orderbook-trade/core/ladder-progress.js
+++ b/src/binance-orderbook-trade/core/ladder-progress.js
@@ -62,6 +62,16 @@ export function createLadderProgress() {
};
}
+export function snapshotLadderProgress(progress) {
+ assertLadderProgress(progress);
+ return {
+ submittedOrders: progress.submittedOrders,
+ cancelledOrders: progress.cancelledOrders,
+ plannedOrders: progress.plannedOrders,
+ currentPlanSubmittedOrders: progress.currentPlanSubmittedOrders,
+ };
+}
+
export function setLadderPlannedOrders(progress, plannedOrders) {
assertLadderProgress(progress);
if (!Number.isInteger(plannedOrders) || plannedOrders <= 0) {
diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js
index a869d5a..f4f6caa 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.156
+// @version 2.7.157
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -78,6 +78,9 @@ import {
} from './core/ladder-plan.js';
import { keepInteractionFeedbackVisible } from './core/interaction-feedback.js';
import {
+ createContinuousLadderProgress,
+ formatContinuousLadderProgress,
+ recordContinuousLadderRound,
waitForContinuousLadderNextRound,
} from './core/continuous-ladder.js';
import {
@@ -89,6 +92,7 @@ import {
recordLadderCancelledOrder,
recordLadderSubmittedOrder,
setLadderPlannedOrders,
+ snapshotLadderProgress,
} from './core/ladder-progress.js';
import {
throwIfAborted,
@@ -377,6 +381,7 @@ import {
let continuousLadderAbortController = null;
let activeLadderActionType = null;
let activeContinuousLadderActionType = null;
+ let activeContinuousLadderProgress = null;
let activeLadderPanelContext = null;
let cancelCurrentSymbolOpenOrdersTask = null;
let cancelCurrentSymbolOpenOrdersBlocksLadderActions = false;
@@ -2759,7 +2764,11 @@ import {
'交易对已切换',
progress,
));
- return { status: 'interrupted' };
+ return {
+ status: 'interrupted',
+ reason: '交易对已切换',
+ progress: snapshotLadderProgress(progress),
+ };
}
const diagnostics = formatLadderRepriceDiagnostics(repriceAttempts, lastRepriceApiErrorCode);
setLadderStatus(
@@ -2772,14 +2781,20 @@ import {
progress,
)}${diagnostics}`,
);
- return { status: wasStopped ? 'stopped' : 'completed' };
+ return {
+ status: wasStopped ? 'stopped' : 'completed',
+ progress: snapshotLadderProgress(progress),
+ };
})
.catch((e) => {
if (isLadderStoppedError(e)) {
if (isCurrentObservedSymbol(actionSymbol)) {
setLadderStatus(formatStoppedLadderProgress(spec.label, progress));
}
- return { status: 'stopped' };
+ return {
+ status: 'stopped',
+ progress: snapshotLadderProgress(progress),
+ };
}
err('Maker 阶梯执行失败:', e);
if (!isCurrentObservedSymbol(actionSymbol)) {
@@ -2788,7 +2803,11 @@ import {
'交易对已切换',
progress,
));
- return { status: 'interrupted' };
+ return {
+ status: 'interrupted',
+ reason: '交易对已切换',
+ progress: snapshotLadderProgress(progress),
+ };
}
const failureMessage = e?.message || '未知错误';
const failureText = formatFailedLadderProgress(spec.label, failureMessage, progress);
@@ -2796,7 +2815,13 @@ import {
? formatFailedLadderProgress(spec.label, e.statusTitle, progress)
: failureText;
setLadderStatus(failureText, failureTitle);
- return { status: 'failed', error: e };
+ return {
+ status: 'failed',
+ error: e,
+ reason: failureMessage,
+ titleReason: e?.statusTitle || failureMessage,
+ progress: snapshotLadderProgress(progress),
+ };
})
.finally(() => {
if (ladderAbortController === abortController) ladderAbortController = null;
@@ -2813,7 +2838,7 @@ import {
function readContinuousLadderReadiness(actionType, actionSymbol) {
const spec = getLadderActionSpec(actionType);
if (!spec || spec.mode !== 'CLOSE') {
- throw new Error('Continuous ladder trading requires a close action');
+ throw new Error('连续交易仅支持阶梯平仓');
}
if (!isCurrentObservedSymbol(actionSymbol)) {
return { status: 'stopped', reason: 'symbol_changed' };
@@ -2843,14 +2868,27 @@ import {
return { status: 'ready' };
}
- function setContinuousLadderStoppedStatus(reason) {
+ function getContinuousLadderStopReason(reason) {
const reasonTextByCode = {
- symbol_changed: 'symbol changed',
- mode_changed: 'trade mode changed',
+ symbol_changed: '交易对已切换',
+ mode_changed: '开仓/平仓模式已切换',
};
const reasonText = reasonTextByCode[reason];
- if (!reasonText) throw new Error(`Unknown continuous ladder stop reason: ${reason}`);
- setLadderStatus(`Continuous trading stopped: ${reasonText}`);
+ if (!reasonText) throw new Error(`未知连续交易停止原因:${reason}`);
+ return reasonText;
+ }
+
+ function setContinuousLadderProgressStatus(
+ label,
+ phase,
+ progress,
+ reason = null,
+ titleReason = reason,
+ ) {
+ setLadderStatus(
+ formatContinuousLadderProgress(label, phase, progress, reason),
+ formatContinuousLadderProgress(label, phase, progress, titleReason),
+ );
}
async function startContinuousLadder(actionType) {
@@ -2865,14 +2903,28 @@ import {
}
const abortController = new AbortController();
+ const continuousProgress = createContinuousLadderProgress();
continuousLadderAbortController = abortController;
activeContinuousLadderActionType = actionType;
+ activeContinuousLadderProgress = continuousProgress;
const executionTask = (async () => {
while (true) {
throwIfAborted(abortController.signal);
const outcome = await startLadder(actionType, true);
- if (outcome?.status !== 'completed') return outcome;
+ if (!outcome?.progress) return outcome;
+ recordContinuousLadderRound(continuousProgress, outcome);
+ if (outcome.status !== 'completed') {
+ setContinuousLadderProgressStatus(
+ spec.label,
+ outcome.status,
+ continuousProgress,
+ outcome.reason || null,
+ outcome.titleReason || outcome.reason || null,
+ );
+ return outcome;
+ }
+ setContinuousLadderProgressStatus(spec.label, 'running', continuousProgress);
const readiness = await waitForContinuousLadderNextRound({
readReadiness: () => readContinuousLadderReadiness(actionType, actionSymbol),
@@ -2880,7 +2932,12 @@ import {
signal: abortController.signal,
});
if (readiness.status === 'stopped') {
- setContinuousLadderStoppedStatus(readiness.reason);
+ setContinuousLadderProgressStatus(
+ spec.label,
+ 'stopped',
+ continuousProgress,
+ getContinuousLadderStopReason(readiness.reason),
+ );
return readiness;
}
}
@@ -2888,9 +2945,18 @@ import {
continuousLadderTask = executionTask
.catch((e) => {
- if (isLadderStoppedError(e)) return { status: 'stopped' };
- err('Continuous ladder trading failed:', e);
- setLadderStatus(`Continuous trading failed: ${e?.message || 'unknown error'}`);
+ if (isLadderStoppedError(e)) {
+ setContinuousLadderProgressStatus(spec.label, 'stopped', continuousProgress);
+ return { status: 'stopped' };
+ }
+ err('连续阶梯交易失败:', e);
+ setContinuousLadderProgressStatus(
+ spec.label,
+ 'failed',
+ continuousProgress,
+ e?.message || '未知错误',
+ e?.statusTitle || e?.message || '未知错误',
+ );
return { status: 'failed', error: e };
})
.finally(() => {
@@ -2899,6 +2965,7 @@ import {
}
continuousLadderTask = null;
activeContinuousLadderActionType = null;
+ activeContinuousLadderProgress = null;
scheduleRenderPanel();
});
scheduleRenderPanel();
@@ -2922,9 +2989,13 @@ import {
if (ladderAbortController && !ladderAbortController.signal.aborted) {
ladderAbortController.abort(stoppedError);
}
- setLadderStatus(`${activeSpec.label}停止中`);
+ setLadderStatus(`${activeSpec.label}${continuousLadderTask ? '连续' : ''}停止中`);
} else {
- setLadderStatus(`${ladderStatusText} · Continuous trading stopped`);
+ setContinuousLadderProgressStatus(
+ activeSpec.label,
+ 'stopping',
+ activeContinuousLadderProgress,
+ );
}
scheduleRenderPanel();
}
@@ -5466,7 +5537,7 @@ import {
const background = isBuyTone ? 'var(--color-GreenAlpha01)' : 'var(--color-RedAlpha01)';
const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : '';
const continuousHint = actionType.startsWith('CLOSE_')
- ? ' title="Option/Alt + click: continuous trading"'
+ ? ' title="Option/Alt + 单击:连续交易"'
: '';
const cursor = disabled ? 'not-allowed' : 'pointer';
return ``;
diff --git a/test/unit/binance-orderbook-trade/continuous-ladder.test.js b/test/unit/binance-orderbook-trade/continuous-ladder.test.js
index 4575075..a3f11bb 100644
--- a/test/unit/binance-orderbook-trade/continuous-ladder.test.js
+++ b/test/unit/binance-orderbook-trade/continuous-ladder.test.js
@@ -4,9 +4,120 @@ import assert from 'node:assert/strict';
import {
CONTINUOUS_LADDER_COOLDOWN_MS,
CONTINUOUS_LADDER_READY_CHECK_MS,
+ createContinuousLadderProgress,
+ formatContinuousLadderProgress,
+ recordContinuousLadderRound,
waitForContinuousLadderNextRound,
} from '../../../src/binance-orderbook-trade/core/continuous-ladder.js';
+function roundProgress({
+ submittedOrders,
+ cancelledOrders = 0,
+ plannedOrders,
+ currentPlanSubmittedOrders,
+}) {
+ return {
+ submittedOrders,
+ cancelledOrders,
+ plannedOrders,
+ currentPlanSubmittedOrders,
+ };
+}
+
+test('continuous ladder status summarizes completed rounds and cumulative submissions', () => {
+ 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.deepEqual(progress, {
+ startedRounds: 2,
+ completedRounds: 2,
+ submittedOrders: 6,
+ cancelledOrders: 0,
+ lastRound: {
+ status: 'completed',
+ submittedOrders: 3,
+ cancelledOrders: 0,
+ plannedOrders: 3,
+ currentPlanSubmittedOrders: 3,
+ },
+ });
+ assert.equal(
+ formatContinuousLadderProgress('阶梯平空', 'running', progress),
+ '阶梯平空连续中 · 2/2 轮 · 本轮 3/3 笔 · 累计 6 笔',
+ );
+});
+
+test('continuous ladder stopped status separates completed rounds from a partial round', () => {
+ const progress = createContinuousLadderProgress();
+ const completedRound = roundProgress({
+ submittedOrders: 3,
+ plannedOrders: 3,
+ currentPlanSubmittedOrders: 3,
+ });
+ recordContinuousLadderRound(progress, { status: 'completed', progress: completedRound });
+ recordContinuousLadderRound(progress, { status: 'completed', progress: completedRound });
+ recordContinuousLadderRound(progress, {
+ status: 'stopped',
+ progress: roundProgress({
+ submittedOrders: 1,
+ plannedOrders: 3,
+ currentPlanSubmittedOrders: 1,
+ }),
+ });
+
+ assert.equal(
+ formatContinuousLadderProgress('阶梯平空', 'stopped', progress),
+ '阶梯平空连续已停止 · 2/3 轮 · 本轮 1/3 笔 · 累计 7 笔',
+ );
+});
+
+test('continuous ladder status shows cancellations only when they occurred', () => {
+ const progress = createContinuousLadderProgress();
+ recordContinuousLadderRound(progress, {
+ status: 'failed',
+ progress: roundProgress({
+ submittedOrders: 2,
+ cancelledOrders: 1,
+ plannedOrders: 3,
+ currentPlanSubmittedOrders: 2,
+ }),
+ });
+
+ assert.equal(
+ formatContinuousLadderProgress('阶梯平多', 'failed', progress, '下单按钮 3 秒内未恢复可点击'),
+ '阶梯平多连续失败 · 0/1 轮 · 本轮 2/3 笔 · 累计 2 笔 · 撤 1 笔 · 下单按钮 3 秒内未恢复可点击',
+ );
+});
+
+test('continuous ladder progress rejects an invalid or duplicate round outcome', () => {
+ const progress = createContinuousLadderProgress();
+ const outcome = {
+ status: 'completed',
+ progress: roundProgress({
+ submittedOrders: 1,
+ plannedOrders: 1,
+ currentPlanSubmittedOrders: 1,
+ }),
+ };
+ recordContinuousLadderRound(progress, outcome);
+
+ assert.throws(
+ () => recordContinuousLadderRound(progress, outcome),
+ /Continuous ladder round was already recorded/,
+ );
+ assert.throws(
+ () => formatContinuousLadderProgress('阶梯平空', 'unknown', progress),
+ /Invalid continuous ladder phase/,
+ );
+});
+
test('continuous ladder starts its cooldown only after the previous round is ready', async () => {
const states = [
{ status: 'waiting' },
diff --git a/test/unit/binance-orderbook-trade/ladder-progress.test.js b/test/unit/binance-orderbook-trade/ladder-progress.test.js
index a6c0cd4..eead34a 100644
--- a/test/unit/binance-orderbook-trade/ladder-progress.test.js
+++ b/test/unit/binance-orderbook-trade/ladder-progress.test.js
@@ -10,8 +10,25 @@ import {
recordLadderCancelledOrder,
recordLadderSubmittedOrder,
setLadderPlannedOrders,
+ snapshotLadderProgress,
} from '../../../src/binance-orderbook-trade/core/ladder-progress.js';
+test('ladder progress snapshot is detached from later mutations', () => {
+ const progress = createLadderProgress();
+ setLadderPlannedOrders(progress, 2);
+ recordLadderSubmittedOrder(progress);
+
+ const snapshot = snapshotLadderProgress(progress);
+ recordLadderSubmittedOrder(progress);
+
+ assert.deepEqual(snapshot, {
+ submittedOrders: 1,
+ cancelledOrders: 0,
+ plannedOrders: 2,
+ currentPlanSubmittedOrders: 1,
+ });
+});
+
test('stopped ladder status reports confirmed submitted and cancelled orders', () => {
const progress = createLadderProgress();
diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js
index 17e72af..e4257c4 100644
--- a/test/unit/binance-orderbook-trade/source-regressions.test.js
+++ b/test/unit/binance-orderbook-trade/source-regressions.test.js
@@ -625,7 +625,8 @@ test('Option or Alt click continuously repeats close ladders only after readines
assert.match(continuousBody, /spec\.mode !== 'CLOSE'\) return startLadder\(actionType\)/);
assert.match(continuousBody, /while \(true\)/);
assert.match(continuousBody, /await startLadder\(actionType, true\)/);
- assert.match(continuousBody, /outcome\?\.status !== 'completed'/);
+ assert.match(continuousBody, /recordContinuousLadderRound\(continuousProgress, outcome\)/);
+ assert.match(continuousBody, /setContinuousLadderProgressStatus\(/);
assert.match(continuousBody, /await waitForContinuousLadderNextRound\(/);
assert.match(continuousBody, /readContinuousLadderReadiness\(actionType, actionSymbol\)/);
@@ -638,7 +639,8 @@ 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(actionButtonBody, /Option\/Alt \+ click: continuous trading/);
+ assert.match(actionButtonBody, /Option\/Alt \+ 单击:连续交易/);
+ assert.doesNotMatch(source, /Continuous trading (?:stopped|failed)/);
assert.match(executionButtonBody, /activeLadderActionType \|\| activeContinuousLadderActionType/);
assert.match(controlSectionsBody, /!!ladderTask \|\| !!continuousLadderTask/);
});
@@ -866,7 +868,7 @@ test('ladder task statuses name the active action and observed outcome', () => {
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\}停止中`/);
+ assert.match(stopBody, /`\$\{activeSpec\.label\}\$\{continuousLadderTask \? '连续' : ''\}停止中`/);
assert.match(cancelPlanBody, /setPlanStepStatus\(`撤销 \$\{rowsToCancel\.length\} 笔当前币挂单`\)/);
assert.match(planStatusBody, /`\$\{plan\.spec\.label\}计划:\$\{plan\.percent\}% \/ \$\{levelText\} \/ 幅\$\{plan\.ladderStep\}`/);
assert.doesNotMatch(planStatusBody, /%\/|\/幅/);