From 78702dbd38e37edf036ebace0e8ac7628cc55b8c Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Fri, 28 Aug 2026 13:57:27 +0800 Subject: [PATCH] Show planned totals in ladder progress --- .../specs/control-flows.pw.js | 4 +- scripts/binance-orderbook-trade.user.js | 30 ++++++-- .../core/ladder-progress.js | 41 ++++++++++- src/binance-orderbook-trade/index.user.js | 4 +- .../ladder-progress.test.js | 69 ++++++++++++++++--- .../source-regressions.test.js | 1 + 6 files changed, 132 insertions(+), 17 deletions(-) diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js index 8af00fb..3573e05 100644 --- a/e2e/binance-orderbook/specs/control-flows.pw.js +++ b/e2e/binance-orderbook/specs/control-flows.pw.js @@ -190,9 +190,7 @@ test('starting a ladder preserves action slots and turns only the active action const submissionsAfterStop = state.events .filter((event) => event.type === 'order-submitted').length; await expect(status).toHaveText( - submissionsAfterStop > 0 - ? `阶梯开多已停止 · 已挂 ${submissionsAfterStop} 笔` - : '阶梯开多已停止', + `阶梯开多已停止 · 已挂 ${submissionsAfterStop}/5 笔`, ); expect(submissionsAfterStop).toBeLessThanOrEqual(submissionsBeforeStop + 1); await page.waitForTimeout(700); diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index 32fc174..670d7a7 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.149 +// @version 2.7.150 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -837,7 +837,7 @@ // 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) { + 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))) { throw new Error("Invalid ladder progress"); } } @@ -854,7 +854,11 @@ function formatLadderProgressCounts(progress) { assertLadderProgress(progress); const counts = []; - if (progress.submittedOrders > 0) counts.push(`已挂 ${progress.submittedOrders} 笔`); + if (progress.plannedOrders !== null) { + counts.push(`已挂 ${progress.currentPlanSubmittedOrders}/${progress.plannedOrders} 笔`); + } else if (progress.submittedOrders > 0) { + counts.push(`已挂 ${progress.submittedOrders} 笔`); + } if (progress.cancelledOrders > 0) counts.push(`已撤 ${progress.cancelledOrders} 笔`); return counts; } @@ -865,12 +869,26 @@ function createLadderProgress() { return { submittedOrders: 0, - cancelledOrders: 0 + cancelledOrders: 0, + plannedOrders: null, + currentPlanSubmittedOrders: 0 }; } + function setLadderPlannedOrders(progress, plannedOrders) { + assertLadderProgress(progress); + if (!Number.isInteger(plannedOrders) || plannedOrders <= 0) { + throw new Error("Invalid ladder planned orders"); + } + progress.plannedOrders = plannedOrders; + progress.currentPlanSubmittedOrders = 0; + } function recordLadderSubmittedOrder(progress) { assertLadderProgress(progress); + if (progress.plannedOrders !== null && progress.currentPlanSubmittedOrders >= progress.plannedOrders) { + throw new Error("Ladder submitted orders exceed plan"); + } progress.submittedOrders += 1; + if (progress.plannedOrders !== null) progress.currentPlanSubmittedOrders += 1; } function recordLadderCancelledOrder(progress) { assertLadderProgress(progress); @@ -899,6 +917,9 @@ if (completedOrders !== totalOrders) { throw new Error("Completed ladder progress mismatch"); } + if (progress.plannedOrders !== totalOrders || progress.currentPlanSubmittedOrders !== completedOrders) { + throw new Error("Completed ladder progress mismatch"); + } const cancelledText = progress.cancelledOrders > 0 ? ` · 已撤 ${progress.cancelledOrders} 笔` : ""; return `${label}已完成 · 已挂 ${completedOrders}/${totalOrders} 笔${cancelledText}`; } @@ -5958,6 +5979,7 @@ let plan = null; try { plan = await buildLadderPlan(actionType, replacementContext); + setLadderPlannedOrders(progress, plan.orders.length); throwIfAborted(abortSignal); setLadderStatus(formatLadderPlanStatus(plan)); const execution = await executeLadderPlan(plan, progress, abortSignal); diff --git a/src/binance-orderbook-trade/core/ladder-progress.js b/src/binance-orderbook-trade/core/ladder-progress.js index ceb2ef4..018dfbb 100644 --- a/src/binance-orderbook-trade/core/ladder-progress.js +++ b/src/binance-orderbook-trade/core/ladder-progress.js @@ -5,6 +5,17 @@ function assertLadderProgress(progress) { || 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 + ) + ) ) { throw new Error('Invalid ladder progress'); } @@ -25,7 +36,11 @@ function assertLadderMessage(message) { function formatLadderProgressCounts(progress) { assertLadderProgress(progress); const counts = []; - if (progress.submittedOrders > 0) counts.push(`已挂 ${progress.submittedOrders} 笔`); + if (progress.plannedOrders !== null) { + counts.push(`已挂 ${progress.currentPlanSubmittedOrders}/${progress.plannedOrders} 笔`); + } else if (progress.submittedOrders > 0) { + counts.push(`已挂 ${progress.submittedOrders} 笔`); + } if (progress.cancelledOrders > 0) counts.push(`已撤 ${progress.cancelledOrders} 笔`); return counts; } @@ -42,12 +57,30 @@ export function createLadderProgress() { return { submittedOrders: 0, cancelledOrders: 0, + plannedOrders: null, + currentPlanSubmittedOrders: 0, }; } +export function setLadderPlannedOrders(progress, plannedOrders) { + assertLadderProgress(progress); + if (!Number.isInteger(plannedOrders) || plannedOrders <= 0) { + throw new Error('Invalid ladder planned orders'); + } + progress.plannedOrders = plannedOrders; + progress.currentPlanSubmittedOrders = 0; +} + export function recordLadderSubmittedOrder(progress) { assertLadderProgress(progress); + if ( + progress.plannedOrders !== null + && progress.currentPlanSubmittedOrders >= progress.plannedOrders + ) { + throw new Error('Ladder submitted orders exceed plan'); + } progress.submittedOrders += 1; + if (progress.plannedOrders !== null) progress.currentPlanSubmittedOrders += 1; } export function recordLadderCancelledOrder(progress) { @@ -86,6 +119,12 @@ export function formatCompletedLadderProgress(label, completedOrders, totalOrder if (completedOrders !== totalOrders) { throw new Error('Completed ladder progress mismatch'); } + if ( + progress.plannedOrders !== totalOrders + || progress.currentPlanSubmittedOrders !== completedOrders + ) { + throw new Error('Completed ladder progress mismatch'); + } const cancelledText = progress.cancelledOrders > 0 ? ` · 已撤 ${progress.cancelledOrders} 笔` : ''; diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 1c21fbd..09f2090 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.149 +// @version 2.7.150 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -85,6 +85,7 @@ import { formatStoppedLadderProgress, recordLadderCancelledOrder, recordLadderSubmittedOrder, + setLadderPlannedOrders, } from './core/ladder-progress.js'; import { throwIfAborted, @@ -4503,6 +4504,7 @@ import { let plan = null; try { plan = await buildLadderPlan(actionType, replacementContext); + setLadderPlannedOrders(progress, plan.orders.length); throwIfAborted(abortSignal); setLadderStatus(formatLadderPlanStatus(plan)); const execution = await executeLadderPlan(plan, progress, abortSignal); diff --git a/test/unit/binance-orderbook-trade/ladder-progress.test.js b/test/unit/binance-orderbook-trade/ladder-progress.test.js index fce8eac..5326f2e 100644 --- a/test/unit/binance-orderbook-trade/ladder-progress.test.js +++ b/test/unit/binance-orderbook-trade/ladder-progress.test.js @@ -9,11 +9,13 @@ import { formatStoppedLadderProgress, recordLadderCancelledOrder, recordLadderSubmittedOrder, + setLadderPlannedOrders, } from '../../../src/binance-orderbook-trade/core/ladder-progress.js'; test('stopped ladder status reports confirmed submitted and cancelled orders', () => { const progress = createLadderProgress(); + setLadderPlannedOrders(progress, 5); recordLadderSubmittedOrder(progress); recordLadderSubmittedOrder(progress); recordLadderCancelledOrder(progress); @@ -21,10 +23,12 @@ test('stopped ladder status reports confirmed submitted and cancelled orders', ( assert.deepEqual(progress, { submittedOrders: 2, cancelledOrders: 1, + plannedOrders: 5, + currentPlanSubmittedOrders: 2, }); assert.equal( formatStoppedLadderProgress('阶梯开空', progress), - '阶梯开空已停止 · 已挂 2 笔 · 已撤 1 笔', + '阶梯开空已停止 · 已挂 2/5 笔 · 已撤 1 笔', ); }); @@ -35,10 +39,18 @@ test('stopped ladder status omits counters for actions that did not happen', () ); const submittedOnly = createLadderProgress(); + setLadderPlannedOrders(submittedOnly, 3); recordLadderSubmittedOrder(submittedOnly); assert.equal( formatStoppedLadderProgress('阶梯平空', submittedOnly), - '阶梯平空已停止 · 已挂 1 笔', + '阶梯平空已停止 · 已挂 1/3 笔', + ); + + const stoppedBeforeFirstOrder = createLadderProgress(); + setLadderPlannedOrders(stoppedBeforeFirstOrder, 5); + assert.equal( + formatStoppedLadderProgress('阶梯开空', stoppedBeforeFirstOrder), + '阶梯开空已停止 · 已挂 0/5 笔', ); const cancelledOnly = createLadderProgress(); @@ -51,6 +63,7 @@ test('stopped ladder status omits counters for actions that did not happen', () test('completed ladder status names the action and confirmed result', () => { const progress = createLadderProgress(); + setLadderPlannedOrders(progress, 5); for (let index = 0; index < 5; index += 1) recordLadderSubmittedOrder(progress); assert.equal( @@ -58,27 +71,29 @@ test('completed ladder status names the action and confirmed result', () => { '阶梯平空已完成 · 已挂 5/5 笔', ); - recordLadderSubmittedOrder(progress); - recordLadderSubmittedOrder(progress); - recordLadderCancelledOrder(progress); + const progressWithCancellation = createLadderProgress(); + setLadderPlannedOrders(progressWithCancellation, 5); + for (let index = 0; index < 5; index += 1) recordLadderSubmittedOrder(progressWithCancellation); + recordLadderCancelledOrder(progressWithCancellation); assert.equal( - formatCompletedLadderProgress('阶梯平空', 5, 5, progress), + formatCompletedLadderProgress('阶梯平空', 5, 5, progressWithCancellation), '阶梯平空已完成 · 已挂 5/5 笔 · 已撤 1 笔', ); }); test('failed and interrupted ladder statuses retain confirmed progress', () => { const progress = createLadderProgress(); + setLadderPlannedOrders(progress, 5); recordLadderSubmittedOrder(progress); recordLadderCancelledOrder(progress); assert.equal( formatFailedLadderProgress('阶梯开多', '数量框状态未稳定', progress), - '阶梯开多失败:数量框状态未稳定 · 已挂 1 笔 · 已撤 1 笔', + '阶梯开多失败:数量框状态未稳定 · 已挂 1/5 笔 · 已撤 1 笔', ); assert.equal( formatInterruptedLadderProgress('阶梯开多', '交易对已切换', progress), - '阶梯开多已中止:交易对已切换 · 已挂 1 笔 · 已撤 1 笔', + '阶梯开多已中止:交易对已切换 · 已挂 1/5 笔 · 已撤 1 笔', ); assert.equal( @@ -104,7 +119,45 @@ test('ladder progress rejects invalid counters instead of masking them', () => { () => formatCompletedLadderProgress('阶梯开多', 1, 2, { submittedOrders: 1, cancelledOrders: 0, + plannedOrders: 2, + currentPlanSubmittedOrders: 1, }), /Completed ladder progress mismatch/, ); + + assert.throws( + () => setLadderPlannedOrders(createLadderProgress(), 0), + /Invalid ladder planned orders/, + ); + + const completedProgress = createLadderProgress(); + setLadderPlannedOrders(completedProgress, 1); + recordLadderSubmittedOrder(completedProgress); + assert.throws( + () => recordLadderSubmittedOrder(completedProgress), + /Ladder submitted orders exceed plan/, + ); +}); + +test('a replacement plan resets only the ratio numerator and retains cumulative activity', () => { + const progress = createLadderProgress(); + setLadderPlannedOrders(progress, 5); + recordLadderSubmittedOrder(progress); + recordLadderSubmittedOrder(progress); + recordLadderCancelledOrder(progress); + recordLadderCancelledOrder(progress); + + setLadderPlannedOrders(progress, 3); + recordLadderSubmittedOrder(progress); + + assert.equal( + formatStoppedLadderProgress('阶梯平空', progress), + '阶梯平空已停止 · 已挂 1/3 笔 · 已撤 2 笔', + ); + assert.deepEqual(progress, { + submittedOrders: 3, + cancelledOrders: 2, + plannedOrders: 3, + currentPlanSubmittedOrders: 1, + }); }); diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index 5937afa..798f5f4 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -782,6 +782,7 @@ test('stopping a ladder preserves confirmed submit and cancel progress', () => { assert.match(startBody, /runLadderPlanWithOpenOrderReplacement\(\s*actionType,\s*progress,\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(executeBody, /waitForOrderSubmitAcknowledgement\([\s\S]*recordLadderSubmittedOrder\(progress\)/);