From 0c89386372a3b7e802f5532459cd2d9ed7974192 Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:35:35 +0800 Subject: [PATCH 1/2] fix: clarify continuous ladder wait state --- docs/binance-orderbook-trade-development.md | 3 + scripts/binance-orderbook-trade.user.js | 55 +++++++++++++++++-- .../contracts/panel-copy.js | 4 ++ .../core/continuous-ladder.js | 32 +++++++++++ src/binance-orderbook-trade/index.user.js | 16 +++++- .../continuous-ladder.test.js | 29 ++++++++++ .../panel-copy.test.js | 4 ++ .../source-regressions.test.js | 3 + 8 files changed, 138 insertions(+), 8 deletions(-) diff --git a/docs/binance-orderbook-trade-development.md b/docs/binance-orderbook-trade-development.md index b300218..d0e9cda 100644 --- a/docs/binance-orderbook-trade-development.md +++ b/docs/binance-orderbook-trade-development.md @@ -159,6 +159,8 @@ Continuous ladder trading is available only for close actions through `Option/Al 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. +The active continuous-close control keeps a direction-specific stop action (`停止连续平多` / `停止连续平空`) throughout both execution and inter-round waiting. Before the native submit control is ready, the status appends `等待按钮恢复`. Only after the fixed cooldown actually begins may it append `等待 1s 后继续下一轮`; this is a static duration label, not a countdown. 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`. 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. @@ -199,6 +201,7 @@ Run manual checks when behavior touches trading flow, DOM selectors, account ord - 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 +- confirm continuous close keeps the direction-specific stop button between rounds, distinguishes `等待按钮恢复` from `等待 1s 后继续下一轮`, 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 - 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 9b40322..b631653 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.157 +// @version 2.7.158 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -153,6 +153,10 @@ OPEN_SHORT: "停止开空", CLOSE_LONG: "停止平多", CLOSE_SHORT: "停止平空" + }), + stopContinuousLadderByAction: freezeCopy({ + CLOSE_LONG: "停止连续平多", + CLOSE_SHORT: "停止连续平空" }) }), status: freezeCopy({ @@ -1038,6 +1042,15 @@ if (reason !== null) parts.push(reason); return parts.join(" · "); } + function formatContinuousLadderWaitReason(phase, cooldownMs) { + if (!Number.isFinite(cooldownMs) || cooldownMs < 0) { + throw new Error("Invalid continuous ladder cooldown"); + } + if (phase === "waiting_ready") return "等待按钮恢复"; + if (phase !== "cooldown") throw new Error("Invalid continuous ladder wait phase"); + const duration = cooldownMs % 1e3 === 0 ? `${cooldownMs / 1e3}s` : `${cooldownMs}ms`; + return `等待 ${duration} 后继续下一轮`; + } function assertReadinessState(state) { if (!["ready", "waiting", "stopped"].includes(state?.status)) { throw new Error("Invalid continuous ladder readiness state"); @@ -1048,12 +1061,20 @@ readReadiness, delay, signal, - readyCheckMs + readyCheckMs, + cooldownMs, + onWaitStateChange, + waitingAlreadyReported }) { + let reported = waitingAlreadyReported; while (true) { throwIfAborted(signal); const state = assertReadinessState(readReadiness()); if (state.status !== "waiting") return state; + if (!reported) { + onWaitStateChange({ phase: "waiting_ready", cooldownMs }); + reported = true; + } await waitForPromiseOrAbort(delay(readyCheckMs), signal); } } @@ -1062,22 +1083,35 @@ delay, signal = null, cooldownMs = CONTINUOUS_LADDER_COOLDOWN_MS, - readyCheckMs = CONTINUOUS_LADDER_READY_CHECK_MS + readyCheckMs = CONTINUOUS_LADDER_READY_CHECK_MS, + onWaitStateChange = () => { + } }) { if (!(cooldownMs >= 0)) throw new Error("Invalid continuous ladder cooldown"); if (!(readyCheckMs > 0)) throw new Error("Invalid continuous ladder readiness interval"); + if (typeof onWaitStateChange !== "function") { + throw new Error("Invalid continuous ladder wait-state callback"); + } + let waitingAlreadyReported = false; while (true) { const readyState = await waitUntilReadyOrStopped({ readReadiness, delay, signal, - readyCheckMs + readyCheckMs, + cooldownMs, + onWaitStateChange, + waitingAlreadyReported }); if (readyState.status === "stopped") return readyState; + waitingAlreadyReported = false; + onWaitStateChange({ phase: "cooldown", cooldownMs }); await waitForPromiseOrAbort(delay(cooldownMs), signal); throwIfAborted(signal); const afterCooldown = assertReadinessState(readReadiness()); if (afterCooldown.status !== "waiting") return afterCooldown; + onWaitStateChange({ phase: "waiting_ready", cooldownMs }); + waitingAlreadyReported = true; } } @@ -4776,7 +4810,15 @@ const readiness = await waitForContinuousLadderNextRound({ readReadiness: () => readContinuousLadderReadiness(actionType, actionSymbol), delay, - signal: abortController.signal + signal: abortController.signal, + onWaitStateChange: ({ phase, cooldownMs }) => { + setContinuousLadderProgressStatus( + spec.label, + "running", + continuousProgress, + formatContinuousLadderWaitReason(phase, cooldownMs) + ); + } }); if (readiness.status === "stopped") { setContinuousLadderProgressStatus( @@ -7016,7 +7058,8 @@ if (activeActionType !== actionType) { return ladderActionButton(actionType, label, tone, disabled); } - return ``; + const stopLabel = activeContinuousLadderActionType === actionType ? PANEL_COPY.action.stopContinuousLadderByAction[actionType] : PANEL_COPY.action.stopLadderByAction[actionType]; + return ``; } function getLadderControlSections(tradeMode, closeContext, symbol, precision) { const ladderRunning = !!ladderTask || !!continuousLadderTask; diff --git a/src/binance-orderbook-trade/contracts/panel-copy.js b/src/binance-orderbook-trade/contracts/panel-copy.js index 91a8176..86d1475 100644 --- a/src/binance-orderbook-trade/contracts/panel-copy.js +++ b/src/binance-orderbook-trade/contracts/panel-copy.js @@ -29,6 +29,10 @@ export const PANEL_COPY = Object.freeze({ CLOSE_LONG: '停止平多', CLOSE_SHORT: '停止平空', }), + stopContinuousLadderByAction: freezeCopy({ + CLOSE_LONG: '停止连续平多', + CLOSE_SHORT: '停止连续平空', + }), }), status: freezeCopy({ precisionUpdated: '精度推荐已更新', diff --git a/src/binance-orderbook-trade/core/continuous-ladder.js b/src/binance-orderbook-trade/core/continuous-ladder.js index 74068d5..df87a5f 100644 --- a/src/binance-orderbook-trade/core/continuous-ladder.js +++ b/src/binance-orderbook-trade/core/continuous-ladder.js @@ -97,6 +97,18 @@ export function formatContinuousLadderProgress(label, phase, progress, reason = return parts.join(' · '); } +export function formatContinuousLadderWaitReason(phase, cooldownMs) { + if (!Number.isFinite(cooldownMs) || cooldownMs < 0) { + throw new Error('Invalid continuous ladder cooldown'); + } + if (phase === 'waiting_ready') return '等待按钮恢复'; + if (phase !== 'cooldown') throw new Error('Invalid continuous ladder wait phase'); + const duration = cooldownMs % 1000 === 0 + ? `${cooldownMs / 1000}s` + : `${cooldownMs}ms`; + return `等待 ${duration} 后继续下一轮`; +} + function assertReadinessState(state) { if (!['ready', 'waiting', 'stopped'].includes(state?.status)) { throw new Error('Invalid continuous ladder readiness state'); @@ -109,11 +121,19 @@ async function waitUntilReadyOrStopped({ delay, signal, readyCheckMs, + cooldownMs, + onWaitStateChange, + waitingAlreadyReported, }) { + let reported = waitingAlreadyReported; while (true) { throwIfAborted(signal); const state = assertReadinessState(readReadiness()); if (state.status !== 'waiting') return state; + if (!reported) { + onWaitStateChange({ phase: 'waiting_ready', cooldownMs }); + reported = true; + } await waitForPromiseOrAbort(delay(readyCheckMs), signal); } } @@ -128,22 +148,34 @@ export async function waitForContinuousLadderNextRound({ signal = null, cooldownMs = CONTINUOUS_LADDER_COOLDOWN_MS, readyCheckMs = CONTINUOUS_LADDER_READY_CHECK_MS, + onWaitStateChange = () => {}, }) { if (!(cooldownMs >= 0)) throw new Error('Invalid continuous ladder cooldown'); if (!(readyCheckMs > 0)) throw new Error('Invalid continuous ladder readiness interval'); + if (typeof onWaitStateChange !== 'function') { + throw new Error('Invalid continuous ladder wait-state callback'); + } + let waitingAlreadyReported = false; while (true) { const readyState = await waitUntilReadyOrStopped({ readReadiness, delay, signal, readyCheckMs, + cooldownMs, + onWaitStateChange, + waitingAlreadyReported, }); if (readyState.status === 'stopped') return readyState; + waitingAlreadyReported = false; + onWaitStateChange({ phase: 'cooldown', cooldownMs }); await waitForPromiseOrAbort(delay(cooldownMs), signal); throwIfAborted(signal); const afterCooldown = assertReadinessState(readReadiness()); if (afterCooldown.status !== 'waiting') return afterCooldown; + onWaitStateChange({ phase: 'waiting_ready', cooldownMs }); + waitingAlreadyReported = true; } } diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index f4f6caa..dc6fcc9 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.157 +// @version 2.7.158 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -80,6 +80,7 @@ import { keepInteractionFeedbackVisible } from './core/interaction-feedback.js'; import { createContinuousLadderProgress, formatContinuousLadderProgress, + formatContinuousLadderWaitReason, recordContinuousLadderRound, waitForContinuousLadderNextRound, } from './core/continuous-ladder.js'; @@ -2930,6 +2931,14 @@ import { readReadiness: () => readContinuousLadderReadiness(actionType, actionSymbol), delay, signal: abortController.signal, + onWaitStateChange: ({ phase, cooldownMs }) => { + setContinuousLadderProgressStatus( + spec.label, + 'running', + continuousProgress, + formatContinuousLadderWaitReason(phase, cooldownMs), + ); + }, }); if (readiness.status === 'stopped') { setContinuousLadderProgressStatus( @@ -5548,7 +5557,10 @@ import { if (activeActionType !== actionType) { return ladderActionButton(actionType, label, tone, disabled); } - return ``; + const stopLabel = activeContinuousLadderActionType === actionType + ? PANEL_COPY.action.stopContinuousLadderByAction[actionType] + : PANEL_COPY.action.stopLadderByAction[actionType]; + return ``; } function getLadderControlSections(tradeMode, closeContext, symbol, precision) { diff --git a/test/unit/binance-orderbook-trade/continuous-ladder.test.js b/test/unit/binance-orderbook-trade/continuous-ladder.test.js index a3f11bb..70db50b 100644 --- a/test/unit/binance-orderbook-trade/continuous-ladder.test.js +++ b/test/unit/binance-orderbook-trade/continuous-ladder.test.js @@ -6,6 +6,7 @@ import { CONTINUOUS_LADDER_READY_CHECK_MS, createContinuousLadderProgress, formatContinuousLadderProgress, + formatContinuousLadderWaitReason, recordContinuousLadderRound, waitForContinuousLadderNextRound, } from '../../../src/binance-orderbook-trade/core/continuous-ladder.js'; @@ -126,10 +127,12 @@ test('continuous ladder starts its cooldown only after the previous round is rea { status: 'ready' }, ]; const delays = []; + const waitStates = []; const result = await waitForContinuousLadderNextRound({ readReadiness: () => states.shift(), delay: async (ms) => delays.push(ms), + onWaitStateChange: (state) => waitStates.push(state), }); assert.deepEqual(result, { status: 'ready' }); @@ -138,6 +141,10 @@ test('continuous ladder starts its cooldown only after the previous round is rea CONTINUOUS_LADDER_READY_CHECK_MS, CONTINUOUS_LADDER_COOLDOWN_MS, ]); + assert.deepEqual(waitStates, [ + { phase: 'waiting_ready', cooldownMs: CONTINUOUS_LADDER_COOLDOWN_MS }, + { phase: 'cooldown', cooldownMs: CONTINUOUS_LADDER_COOLDOWN_MS }, + ]); }); test('continuous ladder restarts a full cooldown if readiness is lost', async () => { @@ -149,10 +156,12 @@ test('continuous ladder restarts a full cooldown if readiness is lost', async () { status: 'ready' }, ]; const delays = []; + const waitStates = []; const result = await waitForContinuousLadderNextRound({ readReadiness: () => states.shift(), delay: async (ms) => delays.push(ms), + onWaitStateChange: (state) => waitStates.push(state), }); assert.deepEqual(result, { status: 'ready' }); @@ -161,6 +170,26 @@ test('continuous ladder restarts a full cooldown if readiness is lost', async () CONTINUOUS_LADDER_READY_CHECK_MS, CONTINUOUS_LADDER_COOLDOWN_MS, ]); + assert.deepEqual(waitStates, [ + { phase: 'cooldown', cooldownMs: CONTINUOUS_LADDER_COOLDOWN_MS }, + { phase: 'waiting_ready', cooldownMs: CONTINUOUS_LADDER_COOLDOWN_MS }, + { phase: 'cooldown', cooldownMs: CONTINUOUS_LADDER_COOLDOWN_MS }, + ]); +}); + +test('continuous ladder wait reasons distinguish readiness from the actual cooldown', () => { + assert.equal( + formatContinuousLadderWaitReason('waiting_ready', CONTINUOUS_LADDER_COOLDOWN_MS), + '等待按钮恢复', + ); + assert.equal( + formatContinuousLadderWaitReason('cooldown', CONTINUOUS_LADDER_COOLDOWN_MS), + '等待 1s 后继续下一轮', + ); + assert.throws( + () => formatContinuousLadderWaitReason('unknown', CONTINUOUS_LADDER_COOLDOWN_MS), + /Invalid continuous ladder wait phase/, + ); }); test('continuous ladder returns a terminal readiness state without another cooldown', async () => { diff --git a/test/unit/binance-orderbook-trade/panel-copy.test.js b/test/unit/binance-orderbook-trade/panel-copy.test.js index 58ac4fd..f36f3b9 100644 --- a/test/unit/binance-orderbook-trade/panel-copy.test.js +++ b/test/unit/binance-orderbook-trade/panel-copy.test.js @@ -36,6 +36,10 @@ test('panel copy keeps the approved labels and tooltips in one contract', () => CLOSE_LONG: '停止平多', CLOSE_SHORT: '停止平空', }, + stopContinuousLadderByAction: { + CLOSE_LONG: '停止连续平多', + CLOSE_SHORT: '停止连续平空', + }, }, status: { precisionUpdated: '精度推荐已更新', diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index e4257c4..317ab8e 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -629,6 +629,8 @@ test('Option or Alt click continuously repeats close ladders only after readines assert.match(continuousBody, /setContinuousLadderProgressStatus\(/); assert.match(continuousBody, /await waitForContinuousLadderNextRound\(/); assert.match(continuousBody, /readContinuousLadderReadiness\(actionType, actionSymbol\)/); + assert.match(continuousBody, /onWaitStateChange:/); + assert.match(continuousBody, /formatContinuousLadderWaitReason\(phase, cooldownMs\)/); assert.match(readinessBody, /isCurrentObservedSymbol\(actionSymbol\)/); assert.match(readinessBody, /getActiveTradeMode\(\) !== spec\.mode/); @@ -642,6 +644,7 @@ test('Option or Alt click continuously repeats close ladders only after readines assert.match(actionButtonBody, /Option\/Alt \+ 单击:连续交易/); assert.doesNotMatch(source, /Continuous trading (?:stopped|failed)/); assert.match(executionButtonBody, /activeLadderActionType \|\| activeContinuousLadderActionType/); + assert.match(executionButtonBody, /PANEL_COPY\.action\.stopContinuousLadderByAction\[actionType\]/); assert.match(controlSectionsBody, /!!ladderTask \|\| !!continuousLadderTask/); }); From a3c07f0a1ddc020104bc86a0e069da5505003698 Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Sun, 30 Aug 2026 12:39:01 +0800 Subject: [PATCH 2/2] fix: prioritize continuous wait feedback --- docs/binance-orderbook-trade-development.md | 4 +- scripts/binance-orderbook-trade.user.js | 27 ++++++++---- .../core/continuous-ladder.js | 21 +++++++--- src/binance-orderbook-trade/index.user.js | 10 ++--- .../continuous-ladder.test.js | 42 ++++++++++++++++++- .../source-regressions.test.js | 2 +- 6 files changed, 83 insertions(+), 23 deletions(-) diff --git a/docs/binance-orderbook-trade-development.md b/docs/binance-orderbook-trade-development.md index d0e9cda..c203035 100644 --- a/docs/binance-orderbook-trade-development.md +++ b/docs/binance-orderbook-trade-development.md @@ -159,7 +159,7 @@ Continuous ladder trading is available only for close actions through `Option/Al 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. -The active continuous-close control keeps a direction-specific stop action (`停止连续平多` / `停止连续平空`) throughout both execution and inter-round waiting. Before the native submit control is ready, the status appends `等待按钮恢复`. Only after the fixed cooldown actually begins may it append `等待 1s 后继续下一轮`; 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 direction-specific stop action (`停止连续平多` / `停止连续平空`) 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. When selecting account-order tabs, scope to the bottom account-orders tab group. Do not globally match `当前委托` or `Open Orders`. @@ -201,7 +201,7 @@ Run manual checks when behavior touches trading flow, DOM selectors, account ord - 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 -- confirm continuous close keeps the direction-specific stop button between rounds, distinguishes `等待按钮恢复` from `等待 1s 后继续下一轮`, and never exposes a start button during the cooldown +- 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 - 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 b631653..e2a6acd 100644 --- a/scripts/binance-orderbook-trade.user.js +++ b/scripts/binance-orderbook-trade.user.js @@ -1018,15 +1018,12 @@ recordedRoundOutcomes.add(outcome); assertContinuousLadderProgress(progress); } - function formatContinuousLadderProgress(label, phase, progress, reason = null) { + function buildContinuousLadderProgressParts(label, phase, progress) { 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} 轮`); @@ -1039,6 +1036,13 @@ } parts.push(`累计 ${progress.submittedOrders} 笔`); if (progress.cancelledOrders > 0) parts.push(`撤 ${progress.cancelledOrders} 笔`); + return parts; + } + function formatContinuousLadderProgress(label, phase, progress, reason = null) { + if (reason !== null && (typeof reason !== "string" || reason.trim() === "")) { + throw new Error("Invalid continuous ladder reason"); + } + const parts = buildContinuousLadderProgressParts(label, phase, progress); if (reason !== null) parts.push(reason); return parts.join(" · "); } @@ -1049,7 +1053,12 @@ if (phase === "waiting_ready") return "等待按钮恢复"; if (phase !== "cooldown") throw new Error("Invalid continuous ladder wait phase"); const duration = cooldownMs % 1e3 === 0 ? `${cooldownMs / 1e3}s` : `${cooldownMs}ms`; - return `等待 ${duration} 后继续下一轮`; + return `${duration} 后继续`; + } + function formatContinuousLadderWaitProgress(label, progress, phase, cooldownMs) { + const parts = buildContinuousLadderProgressParts(label, "running", progress); + parts.splice(1, 0, formatContinuousLadderWaitReason(phase, cooldownMs)); + return parts.join(" · "); } function assertReadinessState(state) { if (!["ready", "waiting", "stopped"].includes(state?.status)) { @@ -4812,12 +4821,12 @@ delay, signal: abortController.signal, onWaitStateChange: ({ phase, cooldownMs }) => { - setContinuousLadderProgressStatus( + setLadderStatus(formatContinuousLadderWaitProgress( spec.label, - "running", continuousProgress, - formatContinuousLadderWaitReason(phase, cooldownMs) - ); + phase, + cooldownMs + )); } }); if (readiness.status === "stopped") { diff --git a/src/binance-orderbook-trade/core/continuous-ladder.js b/src/binance-orderbook-trade/core/continuous-ladder.js index df87a5f..3fbaae4 100644 --- a/src/binance-orderbook-trade/core/continuous-ladder.js +++ b/src/binance-orderbook-trade/core/continuous-ladder.js @@ -69,15 +69,12 @@ export function recordContinuousLadderRound(progress, outcome) { assertContinuousLadderProgress(progress); } -export function formatContinuousLadderProgress(label, phase, progress, reason = null) { +function buildContinuousLadderProgressParts(label, phase, progress) { 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}`]; @@ -93,6 +90,14 @@ export function formatContinuousLadderProgress(label, phase, progress, reason = } parts.push(`累计 ${progress.submittedOrders} 笔`); if (progress.cancelledOrders > 0) parts.push(`撤 ${progress.cancelledOrders} 笔`); + return parts; +} + +export function formatContinuousLadderProgress(label, phase, progress, reason = null) { + if (reason !== null && (typeof reason !== 'string' || reason.trim() === '')) { + throw new Error('Invalid continuous ladder reason'); + } + const parts = buildContinuousLadderProgressParts(label, phase, progress); if (reason !== null) parts.push(reason); return parts.join(' · '); } @@ -106,7 +111,13 @@ export function formatContinuousLadderWaitReason(phase, cooldownMs) { const duration = cooldownMs % 1000 === 0 ? `${cooldownMs / 1000}s` : `${cooldownMs}ms`; - return `等待 ${duration} 后继续下一轮`; + return `${duration} 后继续`; +} + +export function formatContinuousLadderWaitProgress(label, progress, phase, cooldownMs) { + const parts = buildContinuousLadderProgressParts(label, 'running', progress); + parts.splice(1, 0, formatContinuousLadderWaitReason(phase, cooldownMs)); + return parts.join(' · '); } function assertReadinessState(state) { diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index dc6fcc9..6887ecb 100644 --- a/src/binance-orderbook-trade/index.user.js +++ b/src/binance-orderbook-trade/index.user.js @@ -80,7 +80,7 @@ import { keepInteractionFeedbackVisible } from './core/interaction-feedback.js'; import { createContinuousLadderProgress, formatContinuousLadderProgress, - formatContinuousLadderWaitReason, + formatContinuousLadderWaitProgress, recordContinuousLadderRound, waitForContinuousLadderNextRound, } from './core/continuous-ladder.js'; @@ -2932,12 +2932,12 @@ import { delay, signal: abortController.signal, onWaitStateChange: ({ phase, cooldownMs }) => { - setContinuousLadderProgressStatus( + setLadderStatus(formatContinuousLadderWaitProgress( spec.label, - 'running', continuousProgress, - formatContinuousLadderWaitReason(phase, cooldownMs), - ); + phase, + cooldownMs, + )); }, }); if (readiness.status === 'stopped') { diff --git a/test/unit/binance-orderbook-trade/continuous-ladder.test.js b/test/unit/binance-orderbook-trade/continuous-ladder.test.js index 70db50b..cca499a 100644 --- a/test/unit/binance-orderbook-trade/continuous-ladder.test.js +++ b/test/unit/binance-orderbook-trade/continuous-ladder.test.js @@ -6,6 +6,7 @@ import { CONTINUOUS_LADDER_READY_CHECK_MS, createContinuousLadderProgress, formatContinuousLadderProgress, + formatContinuousLadderWaitProgress, formatContinuousLadderWaitReason, recordContinuousLadderRound, waitForContinuousLadderNextRound, @@ -184,7 +185,7 @@ test('continuous ladder wait reasons distinguish readiness from the actual coold ); assert.equal( formatContinuousLadderWaitReason('cooldown', CONTINUOUS_LADDER_COOLDOWN_MS), - '等待 1s 后继续下一轮', + '1s 后继续', ); assert.throws( () => formatContinuousLadderWaitReason('unknown', CONTINUOUS_LADDER_COOLDOWN_MS), @@ -192,6 +193,45 @@ test('continuous ladder wait reasons distinguish readiness from the actual coold ); }); +test('continuous ladder wait status puts the current wait before progress counters', () => { + const progress = createContinuousLadderProgress(); + recordContinuousLadderRound(progress, { + status: 'completed', + progress: roundProgress({ + submittedOrders: 3, + plannedOrders: 3, + currentPlanSubmittedOrders: 3, + }), + }); + recordContinuousLadderRound(progress, { + status: 'completed', + progress: roundProgress({ + submittedOrders: 3, + plannedOrders: 3, + currentPlanSubmittedOrders: 3, + }), + }); + + assert.equal( + formatContinuousLadderWaitProgress( + '阶梯平空', + progress, + 'cooldown', + CONTINUOUS_LADDER_COOLDOWN_MS, + ), + '阶梯平空连续中 · 1s 后继续 · 2/2 轮 · 本轮 3/3 笔 · 累计 6 笔', + ); + assert.equal( + formatContinuousLadderWaitProgress( + '阶梯平空', + progress, + 'waiting_ready', + CONTINUOUS_LADDER_COOLDOWN_MS, + ), + '阶梯平空连续中 · 等待按钮恢复 · 2/2 轮 · 本轮 3/3 笔 · 累计 6 笔', + ); +}); + test('continuous ladder returns a terminal readiness state without another cooldown', async () => { const delays = []; const stopped = { status: 'stopped', reason: 'position_flat' }; diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index 317ab8e..1bc8cf5 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -630,7 +630,7 @@ test('Option or Alt click continuously repeats close ladders only after readines assert.match(continuousBody, /await waitForContinuousLadderNextRound\(/); assert.match(continuousBody, /readContinuousLadderReadiness\(actionType, actionSymbol\)/); assert.match(continuousBody, /onWaitStateChange:/); - assert.match(continuousBody, /formatContinuousLadderWaitReason\(phase, cooldownMs\)/); + assert.match(continuousBody, /formatContinuousLadderWaitProgress\(/); assert.match(readinessBody, /isCurrentObservedSymbol\(actionSymbol\)/); assert.match(readinessBody, /getActiveTradeMode\(\) !== spec\.mode/);