diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index 4ed0047..3e35291 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.125 +// @version 2.7.126 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -557,29 +557,6 @@ } return moves; } - function collectPriceMovesWithExpandingWindow(prices, { - initialLimit = 10, - expansionStep = 10, - minSamples = 5 - } = {}) { - if (!Number.isInteger(initialLimit) || initialLimit < 2) { - throw new Error(`Invalid initial precision trade limit: ${initialLimit}`); - } - if (!Number.isInteger(expansionStep) || expansionStep < 1) { - throw new Error(`Invalid precision trade expansion step: ${expansionStep}`); - } - if (!Number.isInteger(minSamples) || minSamples < 1) { - throw new Error(`Invalid minimum precision sample count: ${minSamples}`); - } - const observedPrices = Array.isArray(prices) ? prices : []; - let usedCount = Math.min(initialLimit, observedPrices.length); - let samples = collectNonZeroPriceMoves(observedPrices.slice(0, usedCount)); - while (samples.length < minSamples && usedCount < observedPrices.length) { - usedCount = Math.min(usedCount + expansionStep, observedPrices.length); - samples = collectNonZeroPriceMoves(observedPrices.slice(0, usedCount)); - } - return { samples, usedCount }; - } function sortedPositiveDecimals(values) { return (values || []).map((value) => normalizeDecimalString(value)).filter((value) => value && isPositiveDecimalString(value)).sort((a, b) => compareDecimalStrings(a, b)); } @@ -629,6 +606,42 @@ } return selectedOption; } + function recommendOrderbookPrecisionWithExpandingWindow({ + prices, + options, + initialLimit = 10, + expansionStep = 10, + minSamples = 5, + minBucketShare = 0.25 + }) { + if (!Array.isArray(prices)) { + throw new Error("Precision trade prices must be an array"); + } + if (!Number.isInteger(initialLimit) || initialLimit < 2) { + throw new Error(`Invalid initial precision trade limit: ${initialLimit}`); + } + if (!Number.isInteger(expansionStep) || expansionStep < 1) { + throw new Error(`Invalid precision trade expansion step: ${expansionStep}`); + } + if (!Number.isInteger(minSamples) || minSamples < 1) { + throw new Error(`Invalid minimum precision sample count: ${minSamples}`); + } + let usedCount = Math.min(initialLimit, prices.length); + let samples = []; + let recommendation = null; + while (true) { + samples = collectNonZeroPriceMoves(prices.slice(0, usedCount)); + recommendation = recommendOrderbookPrecision({ + samples, + options, + minSamples, + minBucketShare + }); + if (recommendation || usedCount >= prices.length) break; + usedCount = Math.min(usedCount + expansionStep, prices.length); + } + return { samples, usedCount, recommendation }; + } // src/binance-orderbook-trade/core/quantity.js function pow102(exp) { @@ -3002,16 +3015,17 @@ scheduleRenderPanel(); return; } - const { samples: latestSamples } = collectPriceMovesWithExpandingWindow(getLatestTradePrices(), { + const { + samples: latestSamples, + recommendation + } = recommendOrderbookPrecisionWithExpandingWindow({ + prices: getLatestTradePrices(), + options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS, initialLimit: ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT, expansionStep: ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP, minSamples: ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES }); const samples = saveStoredOrderbookPrecisionSamples(symbol, latestSamples); - const recommendation = recommendOrderbookPrecision({ - samples, - options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS - }); orderbookPrecisionState = { ...orderbookPrecisionState, symbol, diff --git a/src/binance-orderbook-trade/core/precision.js b/src/binance-orderbook-trade/core/precision.js index f065fee..489c41e 100644 --- a/src/binance-orderbook-trade/core/precision.js +++ b/src/binance-orderbook-trade/core/precision.js @@ -65,30 +65,6 @@ export function collectNonZeroPriceMoves(prices) { return moves; } -export function collectPriceMovesWithExpandingWindow(prices, { - initialLimit = 10, - expansionStep = 10, - minSamples = 5, -} = {}) { - if (!Number.isInteger(initialLimit) || initialLimit < 2) { - throw new Error(`Invalid initial precision trade limit: ${initialLimit}`); - } - if (!Number.isInteger(expansionStep) || expansionStep < 1) { - throw new Error(`Invalid precision trade expansion step: ${expansionStep}`); - } - if (!Number.isInteger(minSamples) || minSamples < 1) { - throw new Error(`Invalid minimum precision sample count: ${minSamples}`); - } - const observedPrices = Array.isArray(prices) ? prices : []; - let usedCount = Math.min(initialLimit, observedPrices.length); - let samples = collectNonZeroPriceMoves(observedPrices.slice(0, usedCount)); - while (samples.length < minSamples && usedCount < observedPrices.length) { - usedCount = Math.min(usedCount + expansionStep, observedPrices.length); - samples = collectNonZeroPriceMoves(observedPrices.slice(0, usedCount)); - } - return { samples, usedCount }; -} - function sortedPositiveDecimals(values) { return (values || []) .map((value) => normalizeDecimalString(value)) @@ -149,3 +125,41 @@ export function recommendOrderbookPrecision({ } return selectedOption; } + +export function recommendOrderbookPrecisionWithExpandingWindow({ + prices, + options, + initialLimit = 10, + expansionStep = 10, + minSamples = 5, + minBucketShare = 0.25, +}) { + if (!Array.isArray(prices)) { + throw new Error('Precision trade prices must be an array'); + } + if (!Number.isInteger(initialLimit) || initialLimit < 2) { + throw new Error(`Invalid initial precision trade limit: ${initialLimit}`); + } + if (!Number.isInteger(expansionStep) || expansionStep < 1) { + throw new Error(`Invalid precision trade expansion step: ${expansionStep}`); + } + if (!Number.isInteger(minSamples) || minSamples < 1) { + throw new Error(`Invalid minimum precision sample count: ${minSamples}`); + } + + let usedCount = Math.min(initialLimit, prices.length); + let samples = []; + let recommendation = null; + while (true) { + samples = collectNonZeroPriceMoves(prices.slice(0, usedCount)); + recommendation = recommendOrderbookPrecision({ + samples, + options, + minSamples, + minBucketShare, + }); + if (recommendation || usedCount >= prices.length) break; + usedCount = Math.min(usedCount + expansionStep, prices.length); + } + return { samples, usedCount, recommendation }; +} diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index da8738a..0050377 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.125 +// @version 2.7.126 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -65,10 +65,10 @@ import { isPositiveDecimalString, } from './core/decimal.js'; import { - collectPriceMovesWithExpandingWindow, formatOrderbookPrecisionShortcutLabel, getOrderbookPrecisionShortcutOptions, recommendOrderbookPrecision, + recommendOrderbookPrecisionWithExpandingWindow, } from './core/precision.js'; import { allocateLadderQuantities } from './core/quantity.js'; import { @@ -1742,16 +1742,17 @@ import { scheduleRenderPanel(); return; } - const { samples: latestSamples } = collectPriceMovesWithExpandingWindow(getLatestTradePrices(), { + const { + samples: latestSamples, + recommendation, + } = recommendOrderbookPrecisionWithExpandingWindow({ + prices: getLatestTradePrices(), + options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS, initialLimit: ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT, expansionStep: ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP, minSamples: ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES, }); const samples = saveStoredOrderbookPrecisionSamples(symbol, latestSamples); - const recommendation = recommendOrderbookPrecision({ - samples, - options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS, - }); orderbookPrecisionState = { ...orderbookPrecisionState, symbol, diff --git a/test/unit/binance-orderbook-trade/precision.test.js b/test/unit/binance-orderbook-trade/precision.test.js index 385cd53..f59c345 100644 --- a/test/unit/binance-orderbook-trade/precision.test.js +++ b/test/unit/binance-orderbook-trade/precision.test.js @@ -3,11 +3,11 @@ import assert from 'node:assert/strict'; import { collectNonZeroPriceMoves, - collectPriceMovesWithExpandingWindow, formatOrderbookPrecisionShortcutLabel, getOrderbookPrecisionDecadeTarget, getOrderbookPrecisionShortcutOptions, recommendOrderbookPrecision, + recommendOrderbookPrecisionWithExpandingWindow, } from '../../../src/binance-orderbook-trade/core/precision.js'; test('keeps only the four smallest exact native precision shortcuts', () => { @@ -117,7 +117,7 @@ test('ten latest trade rows provide enough movement evidence when eight do not', }), '0.001'); }); -test('expands the latest-trade window only until enough effective price moves exist', () => { +test('expands the latest-trade window until it produces a recommendation', () => { const prices = [ '80.757', '80.757', '80.756', '80.756', '80.756', '80.757', '80.756', '80.755', '80.755', '80.755', @@ -126,17 +126,41 @@ test('expands the latest-trade window only until enough effective price moves ex '80.700', ]; - const result = collectPriceMovesWithExpandingWindow(prices); + const result = recommendOrderbookPrecisionWithExpandingWindow({ + prices, + options: ['0.0001', '0.001', '0.01', '0.1', '1'], + }); assert.equal(result.usedCount, 20); assert.equal(result.samples.length, 13); assert.deepEqual([...new Set(result.samples)], ['0.001']); + assert.equal(result.recommendation, '0.001'); }); test('returns the complete visible snapshot when price changes remain insufficient', () => { - assert.deepEqual(collectPriceMovesWithExpandingWindow([ - '80.7', '80.7', '80.7', '80.7', '80.7', '80.7', - ]), { + assert.deepEqual(recommendOrderbookPrecisionWithExpandingWindow({ + prices: ['80.7', '80.7', '80.7', '80.7', '80.7', '80.7'], + options: ['0.001', '0.01', '0.1', '1'], + }), { samples: [], usedCount: 6, + recommendation: null, }); }); + +test('keeps expanding when sample count is sufficient but no precision bucket is decisive', () => { + const prices = [ + '79.748', '79.747', '79.742', '79.742', '79.737', + '79.731', '79.730', '79.730', '79.715', '79.748', + '79.748', '79.746', '79.740', '79.727', '79.727', + '79.746', '79.745', '79.745', '79.767', '79.767', + '79.745', '79.738', '79.737', + ]; + const options = ['0.0001', '0.001', '0.01', '0.1', '1']; + const firstWindowSamples = collectNonZeroPriceMoves(prices.slice(0, 10)); + + assert.equal(firstWindowSamples.length, 7); + assert.equal(recommendOrderbookPrecision({ samples: firstWindowSamples, options }), null); + const result = recommendOrderbookPrecisionWithExpandingWindow({ prices, options }); + assert.equal(result.usedCount, 20); + assert.equal(result.recommendation, '0.01'); +}); diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index bb3f907..341b1d1 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -948,12 +948,14 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.match(source, /orderbookPrecisionOptionsLoadAttemptedSymbol/); const sampleBody = readFunctionBody('refreshOrderbookPrecisionSamplesNow'); - assert.match(sampleBody, /collectPriceMovesWithExpandingWindow\(getLatestTradePrices\(\),/); + assert.match(sampleBody, /recommendOrderbookPrecisionWithExpandingWindow\(\{/); + assert.match(sampleBody, /prices: getLatestTradePrices\(\)/); + assert.match(sampleBody, /options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS/); assert.match(sampleBody, /initialLimit: ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT/); assert.match(sampleBody, /expansionStep: ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP/); assert.match(sampleBody, /minSamples: ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES/); assert.match(sampleBody, /saveStoredOrderbookPrecisionSamples/); - assert.match(sampleBody, /recommendOrderbookPrecision/); + assert.match(sampleBody, /samples: latestSamples,[\s\S]*recommendation,/); assert.match(sampleBody, /status: recommendation \? 'ready' : PANEL_COPY\.status\.precisionInsufficient/); assert.match(sampleBody, /PANEL_COPY\.status\.precisionUpdated/); assert.doesNotMatch(sampleBody, /setTimeout|setInterval|await delay/);