From 81e94ff293f0eb796a1dadd65d38c89ee6e4ed89 Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Thu, 27 Aug 2026 12:38:16 +0800 Subject: [PATCH] fix: allow full ladder auto-fit capacity --- scripts/binance-orderbook-trade.user.js | 16 ++---- .../core/ladder-plan.js | 8 ++- src/binance-orderbook-trade/index.user.js | 9 +-- .../binance-orderbook-trade/ladder.test.js | 57 ++++++++++++++++--- .../source-regressions.test.js | 9 +-- 5 files changed, 67 insertions(+), 32 deletions(-) diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index 0b46195..6177104 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.130 +// @version 2.7.131 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -708,6 +708,7 @@ } // src/binance-orderbook-trade/core/ladder-plan.js + var MAX_AUTO_FIT_LADDER_PERCENT = "100"; var LADDER_ACTION_SPECS = { OPEN_LONG: { mode: "OPEN", @@ -771,10 +772,11 @@ return candidateMinRequiredQty || minRequiredQty; } function fitLadderPlanForMinimumQty(options) { - const { baseQty, minRequiredQty, minRequiredQtyByLevel, percent, levels, stepSize, maxPercent } = options; + const { baseQty, minRequiredQty, minRequiredQtyByLevel, percent, levels, stepSize } = options; + const maxPercent = MAX_AUTO_FIT_LADDER_PERCENT; const requestedLevels = Number(levels); let minimumPercent = null; - if (!maxPercent || !Number.isInteger(requestedLevels) || requestedLevels <= 0) { + if (!Number.isInteger(requestedLevels) || requestedLevels <= 0) { return { allocation: null, minimumPercent, maxPercent }; } for (let candidateLevels = requestedLevels; candidateLevels >= 1; candidateLevels -= 1) { @@ -3255,8 +3257,7 @@ minRequiredQtyByLevel, percent, levels, - stepSize: ruleContext.stepSize, - maxPercent: getMaxAutoFitLadderPercent(spec.mode) + stepSize: ruleContext.stepSize }); if (autoFit.allocation) { allocation = autoFit.allocation; @@ -3299,11 +3300,6 @@ orders: orderPrices.map((price, index) => ({ price, qty: allocation.quantities[index] })) }; } - function getMaxAutoFitLadderPercent(mode) { - if (mode === "OPEN") return String(Math.max(...LADDER_OPEN_PERCENTS)); - if (mode === "CLOSE") return "100"; - return null; - } function createLadderMinimumQtyFailure(options) { const { spec, diff --git a/src/binance-orderbook-trade/core/ladder-plan.js b/src/binance-orderbook-trade/core/ladder-plan.js index 8fe955c..4cc0768 100644 --- a/src/binance-orderbook-trade/core/ladder-plan.js +++ b/src/binance-orderbook-trade/core/ladder-plan.js @@ -7,6 +7,9 @@ import { } from './decimal.js'; import { allocateLadderQuantities, decimalToStepCount, formatStepCount } from './quantity.js'; +/** Auto-fit may use the full available quantity without changing the user's saved panel preset. */ +export const MAX_AUTO_FIT_LADDER_PERCENT = '100'; + const LADDER_ACTION_SPECS = { OPEN_LONG: { mode: 'OPEN', @@ -79,10 +82,11 @@ function getMinRequiredQtyForLevels(minRequiredQty, minRequiredQtyByLevel, level } export function fitLadderPlanForMinimumQty(options) { - const { baseQty, minRequiredQty, minRequiredQtyByLevel, percent, levels, stepSize, maxPercent } = options; + const { baseQty, minRequiredQty, minRequiredQtyByLevel, percent, levels, stepSize } = options; + const maxPercent = MAX_AUTO_FIT_LADDER_PERCENT; const requestedLevels = Number(levels); let minimumPercent = null; - if (!maxPercent || !Number.isInteger(requestedLevels) || requestedLevels <= 0) { + if (!Number.isInteger(requestedLevels) || requestedLevels <= 0) { return { allocation: null, minimumPercent, maxPercent }; } diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index b4101c7..1e02dad 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.130 +// @version 2.7.131 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -2026,7 +2026,6 @@ import { percent, levels, stepSize: ruleContext.stepSize, - maxPercent: getMaxAutoFitLadderPercent(spec.mode), }); if (autoFit.allocation) { allocation = autoFit.allocation; @@ -2071,12 +2070,6 @@ import { }; } - function getMaxAutoFitLadderPercent(mode) { - if (mode === 'OPEN') return String(Math.max(...LADDER_OPEN_PERCENTS)); - if (mode === 'CLOSE') return '100'; - return null; - } - function createLadderMinimumQtyFailure(options) { const { spec, diff --git a/test/unit/binance-orderbook-trade/ladder.test.js b/test/unit/binance-orderbook-trade/ladder.test.js index 06c4730..f5d163e 100644 --- a/test/unit/binance-orderbook-trade/ladder.test.js +++ b/test/unit/binance-orderbook-trade/ladder.test.js @@ -2,6 +2,7 @@ import test from 'node:test'; import assert from 'node:assert/strict'; import { + MAX_AUTO_FIT_LADDER_PERCENT, fitLadderPlanForMinimumQty, getLadderActionSpec, getLadderPercentForMode, @@ -52,7 +53,6 @@ test('auto-fits ladder percent before reducing requested levels', () => { percent: 30, levels: 5, stepSize: '0.1', - maxPercent: '70', }); assert.equal(fit.percent, '50'); @@ -60,19 +60,61 @@ test('auto-fits ladder percent before reducing requested levels', () => { assert.deepEqual(fit.allocation.quantities, ['1', '1', '1', '1', '1']); }); -test('auto-fits ladder levels when keeping requested levels needs too much percent', () => { +test('auto-fits up to 100 percent without depending on panel percent presets', () => { + const fit = fitLadderPlanForMinimumQty({ + baseQty: '9', + minRequiredQty: '0.8', + percent: 30, + levels: 9, + stepSize: '0.01', + }); + + assert.equal(MAX_AUTO_FIT_LADDER_PERCENT, '100'); + assert.equal(fit.maxPercent, '100'); + assert.equal(fit.percent, '80'); + assert.equal(fit.levels, 9); + assert.deepEqual(fit.allocation.quantities, Array(9).fill('0.8')); +}); + +test('reduces requested levels only when they need more than 100 percent', () => { const fit = fitLadderPlanForMinimumQty({ baseQty: '10', - minRequiredQty: '1', + minRequiredQty: '2.1', percent: 30, levels: 9, stepSize: '0.1', - maxPercent: '70', }); - assert.equal(fit.percent, '70'); - assert.equal(fit.levels, 7); - assert.deepEqual(fit.allocation.quantities, ['1', '1', '1', '1', '1', '1', '1']); + assert.equal(fit.percent, '84'); + assert.equal(fit.levels, 4); + assert.deepEqual(fit.allocation.quantities, ['2.1', '2.1', '2.1', '2.1']); +}); + +test('never lowers the saved percent while auto-reducing levels', () => { + const fit = fitLadderPlanForMinimumQty({ + baseQty: '10', + minRequiredQty: '2.1', + percent: 90, + levels: 9, + stepSize: '0.1', + }); + + assert.equal(fit.percent, '90'); + assert.equal(fit.levels, 4); + assert.deepEqual(fit.allocation.quantities, ['2.2', '2.2', '2.2', '2.4']); +}); + +test('rejects the ladder only when even one order needs more than 100 percent', () => { + const fit = fitLadderPlanForMinimumQty({ + baseQty: '10', + minRequiredQty: '10.1', + percent: 30, + levels: 3, + stepSize: '0.1', + }); + + assert.equal(fit.allocation, null); + assert.equal(fit.maxPercent, '100'); }); test('auto-fit recomputes the minimum quantity for retained open ladder levels', () => { @@ -83,7 +125,6 @@ test('auto-fit recomputes the minimum quantity for retained open ladder levels', percent: 30, levels: 5, stepSize: '1', - maxPercent: '70', }); assert.equal(fit.percent, '60'); diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index 9aa96b6..6f9e681 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -464,7 +464,8 @@ test('ladder minimum quantity failure explains safe manual options', () => { const buildBody = readFunctionBody('buildLadderPlan'); assert.match(buildBody, /const minRequiredQtyByLevel = spec\.mode === 'OPEN'/); assert.match(buildBody, /getQtyRuleContext\(startSymbol,\s*spec\.mode,\s*price\)\.effectiveMinQty \|\| ruleContext\.baseMinQty/); - assert.match(buildBody, /fitLadderPlanForMinimumQty\(\{\s*baseQty,\s*minRequiredQty,\s*minRequiredQtyByLevel,\s*percent,\s*levels,\s*stepSize: ruleContext\.stepSize,\s*maxPercent: getMaxAutoFitLadderPercent\(spec\.mode\),\s*\}\)/); + assert.match(buildBody, /fitLadderPlanForMinimumQty\(\{\s*baseQty,\s*minRequiredQty,\s*minRequiredQtyByLevel,\s*percent,\s*levels,\s*stepSize: ruleContext\.stepSize,\s*\}\)/); + assert.doesNotMatch(buildBody, /setLadderOpenPercent|setLadderLevels/); assert.match(buildBody, /allocation = autoFit\.allocation/); assert.match(buildBody, /percent = autoFit\.percent/); assert.match(buildBody, /minRequiredQty = autoFit\.minRequiredQty \|\| minRequiredQty/); @@ -498,6 +499,8 @@ test('ladder minimum quantity failure explains safe manual options', () => { assert.match(percentBody, /formatDecimalParts\(scaledPercent,\s*2\)/); const fitBody = readFunctionBody('fitLadderPlanForMinimumQty', ladderPlanSource); + assert.match(ladderPlanSource, /export const MAX_AUTO_FIT_LADDER_PERCENT = '100'/); + assert.match(fitBody, /const maxPercent = MAX_AUTO_FIT_LADDER_PERCENT/); assert.match(fitBody, /getMinRequiredQtyForLevels\(minRequiredQty,\s*minRequiredQtyByLevel,\s*candidateLevels\)/); assert.match(fitBody, /for \(let candidateLevels = requestedLevels; candidateLevels >= 1; candidateLevels -= 1\)/); assert.match(fitBody, /computeMinimumLadderPercent\(baseQty,\s*candidateMinRequiredQty,\s*candidateLevels,\s*stepSize\)/); @@ -506,9 +509,7 @@ test('ladder minimum quantity failure explains safe manual options', () => { assert.match(fitBody, /minRequiredQty: candidateMinRequiredQty/); assert.match(fitBody, /levels: candidateLevels/); - const maxBody = readFunctionBody('getMaxAutoFitLadderPercent'); - assert.match(maxBody, /Math\.max\(\.\.\.LADDER_OPEN_PERCENTS\)/); - assert.match(maxBody, /100/); + assert.doesNotMatch(source, /getMaxAutoFitLadderPercent|Math\.max\(\.\.\.LADDER_OPEN_PERCENTS\)/); const statusBody = readFunctionBody('setLadderStatus'); assert.match(statusBody, /statusEl\.title =/);