From b59fe2a88fdfed8da68d2e1ddcdeca738c9ea2da Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Wed, 26 Aug 2026 23:24:41 +0800 Subject: [PATCH 1/3] style: match Binance panel divider --- .../specs/panel-visual-contract.pw.js | 23 +++++++++++++++++++ .../close-fixed-visual.json | 2 +- .../open-fixed-visual.json | 2 +- scripts/binance-orderbook-trade.user.js | 5 ++-- src/binance-orderbook-trade/index.user.js | 5 ++-- .../source-regressions.test.js | 3 ++- 6 files changed, 33 insertions(+), 7 deletions(-) diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js index 8b2ca26..b688119 100644 --- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js @@ -19,11 +19,33 @@ async function expectVisualContract(page, name) { expect(`${JSON.stringify(contract, null, 2)}\n`).toMatchSnapshot(name); } +async function expectNativeDivider(page) { + const divider = await page.locator(PANEL_SELECTOR).evaluate((panel) => { + const panelRect = panel.getBoundingClientRect(); + const ladder = panel.querySelector('[data-panel-group="ladder"]'); + const ladderRect = ladder.getBoundingClientRect(); + const style = getComputedStyle(ladder); + return { + leftInset: ladderRect.left - panelRect.left, + rightInset: panelRect.right - ladderRect.right, + borderTopWidth: style.borderTopWidth, + borderTopColor: style.borderTopColor, + }; + }); + expect(divider).toEqual({ + leftInset: 1, + rightInset: 1, + borderTopWidth: '1px', + borderTopColor: 'rgb(237, 237, 237)', + }); +} + test('open panel matches the fixed visual contract', async ({ page }) => { await openUserscriptScenario(page, createCancelScenario({ ui: { tradeMode: 'OPEN' }, })); await expectPrecisionReady(page); + await expectNativeDivider(page); await expectVisualContract(page, 'open-fixed.visual.json'); }); @@ -36,5 +58,6 @@ test('close panel matches the disabled-state visual contract', async ({ page }) await expect(panel.getByRole('radio', { name: '平多' })).toBeEnabled(); await expect(panel.getByRole('radio', { name: '平空' })).toBeDisabled(); await expectPrecisionReady(page); + await expectNativeDivider(page); await expectVisualContract(page, 'close-fixed.visual.json'); }); diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json index 78316ce..0d239f9 100644 --- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json @@ -27,7 +27,7 @@ "ladder": { "rect": { "top": 135, - "width": 292, + "width": 312, "height": 263 }, "display": "block", diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json index 5147355..4457566 100644 --- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json @@ -27,7 +27,7 @@ "ladder": { "rect": { "top": 135, - "width": 292, + "width": 312, "height": 263 }, "display": "block", diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index 70e29dd..bc96ad0 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.122 +// @version 2.7.123 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -1864,6 +1864,7 @@ const PRIMARY_EMPHASIS_COLOR = "#000000"; const PRIMARY_EMPHASIS_FONT_WEIGHT = "500"; const CONTROL_BORDER_COLOR = "#d5d9e2"; + const PANEL_DIVIDER_COLOR = "#ededed"; const CONTROL_BACKGROUND_COLOR = "#ffffff"; const CONTROL_TEXT_COLOR = "#5e6673"; const CONTROL_FONT_WEIGHT = "500"; @@ -5973,7 +5974,7 @@ "", "", "", - '
', + `
`, `
${PANEL_COPY.section.ladderMaker}
`, `
`, `
空闲
`, diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 18a7831..79cf8f7 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.122 +// @version 2.7.123 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -302,6 +302,7 @@ import { const PRIMARY_EMPHASIS_COLOR = '#000000'; const PRIMARY_EMPHASIS_FONT_WEIGHT = '500'; const CONTROL_BORDER_COLOR = '#d5d9e2'; + const PANEL_DIVIDER_COLOR = '#ededed'; const CONTROL_BACKGROUND_COLOR = '#ffffff'; const CONTROL_TEXT_COLOR = '#5e6673'; const CONTROL_FONT_WEIGHT = '500'; @@ -5197,7 +5198,7 @@ import { '
', '
', '', - '
', + `
`, `
${PANEL_COPY.section.ladderMaker}
`, `
`, `
空闲
`, diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index 5343187..9da8d3a 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -370,7 +370,8 @@ test('panel keeps controls in cohesive ordered semantic groups', () => { assert.ok(ladderIndex > quantityMinIndex); assert.match(ensurePanelBody, /data-panel-group="multiplier" style="margin-top:8px;"/); assert.match(precisionBody, /margin-top:10px;/); - assert.match(ensurePanelBody, /data-panel-group="ladder" style="margin-top:12px;padding-top:12px;border-top:/); + assert.match(source, /const PANEL_DIVIDER_COLOR = '#ededed'/); + assert.match(ensurePanelBody, /data-panel-group="ladder" style="margin:12px -10px 0;padding:12px 10px 0;border-top:1px solid \$\{PANEL_DIVIDER_COLOR\}/); assert.match(ensurePanelBody, /PANEL_COPY\.section\.singleOrder/); assert.match(ensurePanelBody, /PANEL_COPY\.section\.ladderMaker/); assert.match(ladderPanelBody, /ORDERBOOK_PRECISION_RECOMMENDATION_ID/); From 7cbe38871a0d4bb6be5c9fde85c3932b2a6aea3c Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Thu, 27 Aug 2026 00:09:17 +0800 Subject: [PATCH 2/3] feat: refresh precision from latest trades --- .../specs/control-flows.pw.js | 22 ++ .../close-fixed-visual.json | 38 ++-- .../open-fixed-visual.json | 38 ++-- scripts/binance-orderbook-trade.user.js | 192 ++++-------------- .../contracts/panel-copy.js | 12 +- src/binance-orderbook-trade/core/precision.js | 29 --- src/binance-orderbook-trade/index.user.js | 167 ++++----------- .../ladder-options.test.js | 2 +- .../panel-copy.test.js | 11 +- .../binance-orderbook-trade/precision.test.js | 67 ++---- .../source-regressions.test.js | 53 ++--- 11 files changed, 190 insertions(+), 441 deletions(-) diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js index cc23c7a..9c97b26 100644 --- a/e2e/binance-orderbook/specs/control-flows.pw.js +++ b/e2e/binance-orderbook/specs/control-flows.pw.js @@ -91,6 +91,28 @@ test('a precision shortcut selects the exact native orderbook option once', asyn expect(errors).toEqual([]); }); +test('precision refresh uses the visible latest trades immediately without a sampling wait', async ({ page }) => { + const scenario = createCancelScenario({ + ui: { orderbookPrecision: '0.1' }, + }); + const { errors } = await openUserscriptScenario(page, scenario); + const panel = page.locator(PANEL_SELECTOR); + const refresh = panel.locator('[data-orderbook-precision-refresh]'); + + await expect(refresh).toBeEnabled({ timeout: 8_000 }); + await installInteractionProbe(page, '[data-orderbook-precision-refresh]'); + await refresh.click(); + await expect(refresh).toBeEnabled(); + await expect(panel.locator('[data-orderbook-precision-value="0.01"]')) + .toHaveAttribute('aria-label', '切换价格精度到 0.01,推荐档位'); + const probe = await finishInteractionProbe(page); + assertResponsiveInteraction(expect, probe); + expect(await page.evaluate((symbol) => JSON.parse( + localStorage.getItem(`jh_binance_orderbook_precision_samples_v3:${symbol}`) + ), scenario.currentSymbol)).toEqual(['0.01', '0.01', '0.01', '0.01', '0.01']); + expect(errors).toEqual([]); +}); + test('starting a ladder disables every start action and exposes one stop control', async ({ page }) => { const scenario = createCancelScenario({ ui: { tradeMode: 'OPEN', orderbookPrecision: '0.1' }, diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json index 0d239f9..79a0134 100644 --- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json @@ -157,7 +157,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -178,7 +178,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -199,7 +199,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -220,7 +220,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -241,7 +241,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -262,7 +262,7 @@ "pressed": null, "rect": { "top": 217, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -283,7 +283,7 @@ "pressed": null, "rect": { "top": 217, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -304,7 +304,7 @@ "pressed": null, "rect": { "top": 217, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -325,7 +325,7 @@ "pressed": null, "rect": { "top": 217, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -346,7 +346,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -367,7 +367,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -388,7 +388,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -409,7 +409,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -430,7 +430,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -451,7 +451,7 @@ "pressed": "false", "rect": { "top": 298, - "width": 44.5, + "width": 51, "height": 32 }, "style": { @@ -472,7 +472,7 @@ "pressed": "false", "rect": { "top": 298, - "width": 44.5, + "width": 51, "height": 32 }, "style": { @@ -493,7 +493,7 @@ "pressed": "true", "rect": { "top": 298, - "width": 44.5, + "width": 51, "height": 32 }, "style": { @@ -514,7 +514,7 @@ "pressed": "false", "rect": { "top": 298, - "width": 44.5, + "width": 51, "height": 32 }, "style": { @@ -594,7 +594,7 @@ "text": { "mode": "单击订单簿时", "formula": "0.01 × 1 =0.01", - "precision": "价格精度0.0010.010.11", + "precision": "精度0.0010.010.11", "status": "空闲" } } diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json index 4457566..4045c0a 100644 --- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json @@ -157,7 +157,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -178,7 +178,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -199,7 +199,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -220,7 +220,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -241,7 +241,7 @@ "pressed": null, "rect": { "top": 177, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -262,7 +262,7 @@ "pressed": null, "rect": { "top": 217, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -283,7 +283,7 @@ "pressed": null, "rect": { "top": 217, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -304,7 +304,7 @@ "pressed": null, "rect": { "top": 217, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -325,7 +325,7 @@ "pressed": null, "rect": { "top": 217, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -346,7 +346,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -367,7 +367,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -388,7 +388,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -409,7 +409,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -430,7 +430,7 @@ "pressed": null, "rect": { "top": 257, - "width": 44.8, + "width": 47.2, "height": 28 }, "style": { @@ -451,7 +451,7 @@ "pressed": "false", "rect": { "top": 298, - "width": 44.5, + "width": 51, "height": 32 }, "style": { @@ -472,7 +472,7 @@ "pressed": "false", "rect": { "top": 298, - "width": 44.5, + "width": 51, "height": 32 }, "style": { @@ -493,7 +493,7 @@ "pressed": "true", "rect": { "top": 298, - "width": 44.5, + "width": 51, "height": 32 }, "style": { @@ -514,7 +514,7 @@ "pressed": "false", "rect": { "top": 298, - "width": 44.5, + "width": 51, "height": 32 }, "style": { @@ -594,7 +594,7 @@ "text": { "mode": "单击订单簿时", "formula": "0.07 × 1 =0.07≥5U @ 81", - "precision": "价格精度0.0010.010.11", + "precision": "精度0.0010.010.11", "status": "空闲" } } diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index bc96ad0..eff8dde 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.123 +// @version 2.7.124 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -138,7 +138,7 @@ ratio: "比例", orderCount: "笔数", interval: "间距", - pricePrecision: "价格精度" + pricePrecision: "精度" }), action: freezeCopy({ openLong: "阶梯开多", @@ -159,12 +159,12 @@ pricePrecision: "与订单簿中的价格精度联动。黄点表示推荐值。比例、笔数、间距会随所选精度恢复对应设置。" }) }); - function formatPrecisionRefreshTooltip(durationMs) { - const seconds = Number(durationMs) / 1e3; - if (!Number.isInteger(seconds) || seconds <= 0) { - throw new Error(`Invalid precision sample duration: ${durationMs}`); + function formatPrecisionRefreshTooltip(tradeCount) { + const count = Number(tradeCount); + if (!Number.isInteger(count) || count <= 1) { + throw new Error(`Invalid precision trade count: ${tradeCount}`); } - return `根据最近 ${seconds} 秒的成交价变动,重新计算推荐的价格精度。`; + return `根据最新 ${count} 条成交价中的有效价格变动,重新计算推荐精度。`; } // src/binance-orderbook-trade/core/cancel-orders.js @@ -553,31 +553,6 @@ } return moves; } - function mergePrecisionSamples(existingSamples, newSamples, maxSamples = 64) { - const merged = [...existingSamples || [], ...newSamples || []].map((sample) => normalizeDecimalString(sample)).filter((sample) => sample && isPositiveDecimalString(sample)); - return merged.slice(Math.max(0, merged.length - maxSamples)); - } - function resolveOrderbookPrecisionSampleState({ - sampling, - scheduled, - status, - recommendation - }) { - const busy = Boolean(sampling || scheduled); - if (busy) { - return { - busy, - status: status === "刷新中" ? "刷新中" : "采样中" - }; - } - if (status && /^(未定位|未找到|数据不足)/.test(status)) { - return { busy, status }; - } - return { - busy, - status: recommendation ? "ready" : "数据不足" - }; - } function sortedPositiveDecimals(values) { return (values || []).map((value) => normalizeDecimalString(value)).filter((value) => value && isPositiveDecimalString(value)).sort((a, b) => compareDecimalStrings(a, b)); } @@ -1831,13 +1806,10 @@ const LADDER_OPEN_QTY_READY_TIMEOUT_MS = 1200; const TRADE_INPUT_SYNC_TIMEOUT_MS = 350; const TRADE_INPUT_SYNC_STABLE_FRAMES = 2; - const ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS = 6e3; - const ORDERBOOK_PRECISION_SAMPLE_POLL_MS = 300; const ORDERBOOK_PRECISION_READY_POLL_MS = 100; const ORDERBOOK_PRECISION_READY_TIMEOUT_MS = 5e3; const ORDERBOOK_PRECISION_OPTION_WAIT_MS = 1200; - const ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS = 6; - const ORDERBOOK_PRECISION_SAMPLE_MAX = 96; + const ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT = 10; const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4; const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [ "0.00000001", @@ -1919,10 +1891,6 @@ let panelResizeObserver = null; let ladderSubmitCaptureSequence = 0; let activeLadderSubmitCapture = null; - let orderbookPrecisionSampling = false; - let orderbookPrecisionSampleTimer = 0; - let orderbookPrecisionActiveRequest = null; - let orderbookPrecisionPendingRequest = null; let orderbookPrecisionSelectionTask = null; let orderbookPrecisionOptionsLoadRequestedSymbol = null; let orderbookPrecisionOptionsLoadAttemptedSymbol = null; @@ -1936,8 +1904,7 @@ current: null, nativeOptions: [], nativeOptionsStatus: null, - status: "数据不足", - sampleEndsAt: 0 + status: "数据不足" }; const controlledNativeButtons = /* @__PURE__ */ new Set(); let lastObservedSymbol = getCurrentSymbol(); @@ -2539,18 +2506,11 @@ function getBestOrderbookPrice(side) { return getOrderbookPrices(side, 1)[0] || null; } - function getLatestTradePrices(limit = 20) { - return Array.from(document.querySelectorAll(".tradew-tradelist .price.emit-price")).filter((node) => isVisibleElement(node)).map((node) => parsePrice(node)).filter(Boolean).slice(0, Math.max(1, Number(limit) || 20)); - } - async function waitForLatestTradePricesReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) { - const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0); - while (!document.hidden && isFuturesTradingPage() && isCurrentObservedSymbol(symbol)) { - const prices = getLatestTradePrices(); - if (prices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) return prices; - if (Date.now() >= deadline) return prices; - await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS); + function getLatestTradePrices(limit = ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT) { + if (!Number.isInteger(limit) || limit < 2) { + throw new Error(`Invalid latest trade price limit: ${limit}`); } - return []; + return Array.from(document.querySelectorAll(".tradew-tradelist .price.emit-price")).filter((node) => isVisibleElement(node)).map((node) => parsePrice(node)).filter(Boolean).slice(0, limit); } async function waitForOrderbookPrecisionBootstrapReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) { const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0); @@ -2577,9 +2537,12 @@ function saveStoredOrderbookPrecisionSamples(symbol, samples) { const key = orderbookPrecisionSamplesKey(symbol); if (!key) return []; - const merged = mergePrecisionSamples([], samples, ORDERBOOK_PRECISION_SAMPLE_MAX); - localStorage.setItem(key, JSON.stringify(merged)); - return merged; + if (!Array.isArray(samples) || samples.length > ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT - 1) { + throw new Error("Invalid latest trade movement snapshot"); + } + const normalizedSamples = samples.map((sample) => normalizeDecimalString(sample)).filter((sample) => sample && isPositiveDecimalString(sample)); + localStorage.setItem(key, JSON.stringify(normalizedSamples)); + return normalizedSamples; } function getOrderbookPrecisionRecommendation(symbol = getCurrentSymbol()) { const samples = readStoredOrderbookPrecisionSamples(symbol); @@ -2804,12 +2767,7 @@ }); const current = readCurrentOrderbookPrecisionValue(); const existingStatus = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.status : null; - const { busy, status } = resolveOrderbookPrecisionSampleState({ - sampling: orderbookPrecisionSampling, - scheduled: Boolean(orderbookPrecisionSampleTimer), - status: existingStatus, - recommendation - }); + const status = recommendation ? "ready" : existingStatus && /^(未定位|未找到|数据不足)/.test(existingStatus) ? existingStatus : "数据不足"; orderbookPrecisionState = { ...orderbookPrecisionState, symbol, @@ -2819,7 +2777,7 @@ status }; const selectionBusy = Boolean(orderbookPrecisionSelectionTask); - const controlsBusy = busy || selectionBusy; + const controlsBusy = selectionBusy; const nativeOptions = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.nativeOptions : []; const shortcutOptions = getOrderbookPrecisionShortcutOptions( nativeOptions, @@ -2827,10 +2785,10 @@ ); if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol); const canRefresh = !controlsBusy; - const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS); + const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT); const recommendationHtml = [ '
', - '
', + '
', `${PANEL_COPY.field.pricePrecision}`, ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy), ``, @@ -3007,101 +2965,37 @@ } }, 0); } - async function runOrderbookPrecisionSampleRound(request) { - orderbookPrecisionSampleTimer = 0; - if (orderbookPrecisionSampling || document.hidden || !isFuturesTradingPage()) return; - const symbol = request.symbol; - if (!isCurrentObservedSymbol(symbol)) return false; - orderbookPrecisionSampling = true; - orderbookPrecisionActiveRequest = request; - const tradeMoveSamples = []; - const sampleDurationMs = Math.max(0, Number(request.durationMs) || ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS); - try { - const readyPrices = await waitForLatestTradePricesReady(symbol); - if (!isCurrentObservedSymbol(symbol)) return false; - if (readyPrices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) { - tradeMoveSamples.push(...collectNonZeroPriceMoves(readyPrices)); - } - const deadline = Date.now() + sampleDurationMs; - orderbookPrecisionState = { - ...orderbookPrecisionState, - symbol, - sampleEndsAt: deadline - }; - scheduleRenderPanel({ followUpMs: 1e3 }); - while (Date.now() < deadline && !document.hidden && isFuturesTradingPage() && isCurrentObservedSymbol(symbol)) { - tradeMoveSamples.push(...collectNonZeroPriceMoves(getLatestTradePrices())); - await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS); - } - if (!isCurrentObservedSymbol(symbol)) return false; - const newSamples = tradeMoveSamples; - const samples = saveStoredOrderbookPrecisionSamples(symbol, newSamples); - const recommendation = recommendOrderbookPrecision({ - samples, - options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS - }); + function refreshOrderbookPrecisionSamplesNow() { + const symbol = getCurrentSymbol(); + if (!symbol || !isCurrentObservedSymbol(symbol)) { orderbookPrecisionState = { ...orderbookPrecisionState, symbol, - samples, - recommendation, - current: readCurrentOrderbookPrecisionValue(), - status: recommendation ? "ready" : "数据不足", - sampleEndsAt: 0 + status: "未定位当前交易对" }; - refreshOrderbookPrecisionRecommendation(); scheduleRenderPanel(); - return true; - } finally { - orderbookPrecisionSampling = false; - orderbookPrecisionActiveRequest = null; - const pending = orderbookPrecisionPendingRequest; - orderbookPrecisionPendingRequest = null; - if (pending && isCurrentObservedSymbol(pending.symbol)) { - scheduleOrderbookPrecisionSampleRound(0, { ...pending, force: true }); - } - } - } - function scheduleOrderbookPrecisionSampleRound(delayMs = 0, options) { - const { - force = false, - durationMs = ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS - } = options || {}; - if (document.hidden || !isFuturesTradingPage()) return; - const symbol = getCurrentSymbol(); - if (!isCurrentObservedSymbol(symbol)) return; - const request = { symbol, durationMs }; - if (orderbookPrecisionSampling) { - if (force) orderbookPrecisionPendingRequest = request; return; } - if (orderbookPrecisionSampling || orderbookPrecisionSampleTimer) return; - orderbookPrecisionSampleTimer = window.setTimeout( - () => runOrderbookPrecisionSampleRound(request), - Math.max(0, Number(delayMs) || 0) + const samples = saveStoredOrderbookPrecisionSamples( + symbol, + collectNonZeroPriceMoves(getLatestTradePrices(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT)) ); - } - function stopOrderbookPrecisionSampler() { - window.clearTimeout(orderbookPrecisionSampleTimer); - orderbookPrecisionSampleTimer = 0; - orderbookPrecisionPendingRequest = null; - } - function refreshOrderbookPrecisionSamplesNow() { - const symbol = getCurrentSymbol(); + const recommendation = recommendOrderbookPrecision({ + samples, + options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS + }); orderbookPrecisionState = { ...orderbookPrecisionState, symbol, - status: "刷新中", - sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS + samples, + recommendation, + current: readCurrentOrderbookPrecisionValue(), + status: recommendation ? "ready" : "数据不足" }; - stopOrderbookPrecisionSampler(); if (!orderbookPrecisionState.nativeOptions.length) { queueOrderbookPrecisionOptionsLoad(symbol, true); } - scheduleOrderbookPrecisionSampleRound(0, { - force: true, - durationMs: ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS - }); + refreshOrderbookPrecisionRecommendation(); scheduleRenderPanel(); } function getBufferedMakerPrices(side, levels, ladderStep = DEFAULT_LADDER_STEP) { @@ -5572,7 +5466,7 @@ } function ladderOptionRow(title, tooltip, options, selected, group, suffix = "") { return [ - '
', + '
', `${title}`, ...options.map((value) => ladderOptionButton(`${value}${suffix}`, value, Number(value) === Number(selected), group)), "
" @@ -6153,7 +6047,6 @@ stopTradingTimers(); invalidateTradeButtonCache(); lastDisplayCloseState = null; - stopOrderbookPrecisionSampler(); } function renderPanel() { if (!isFuturesTradingPage()) { @@ -6497,7 +6390,6 @@ lastObservedAccountPositionState = null; closeGuard = null; invalidateTradeButtonCache(); - stopOrderbookPrecisionSampler(); orderbookPrecisionOptionsLoadRequestedSymbol = null; orderbookPrecisionOptionsLoadAttemptedSymbol = null; const recommendation = getOrderbookPrecisionRecommendation(symbol); @@ -6508,8 +6400,7 @@ current: readCurrentOrderbookPrecisionValue(), nativeOptions: [], nativeOptionsStatus: null, - status: recommendation ? "ready" : "数据不足", - sampleEndsAt: 0 + status: recommendation ? "ready" : "数据不足" }; } function checkSymbolChangeForLeverage() { @@ -6560,7 +6451,6 @@ stopOrderbookPrecisionObserver(); clearTradeUiMutationWait(); stopRenderPanelTimer(); - stopOrderbookPrecisionSampler(); } function syncRouteState() { if (document.hidden) return; diff --git a/src/binance-orderbook-trade/contracts/panel-copy.js b/src/binance-orderbook-trade/contracts/panel-copy.js index cfa637d..4e408ea 100644 --- a/src/binance-orderbook-trade/contracts/panel-copy.js +++ b/src/binance-orderbook-trade/contracts/panel-copy.js @@ -13,7 +13,7 @@ export const PANEL_COPY = Object.freeze({ ratio: '比例', orderCount: '笔数', interval: '间距', - pricePrecision: '价格精度', + pricePrecision: '精度', }), action: freezeCopy({ openLong: '阶梯开多', @@ -35,10 +35,10 @@ export const PANEL_COPY = Object.freeze({ }), }); -export function formatPrecisionRefreshTooltip(durationMs) { - const seconds = Number(durationMs) / 1000; - if (!Number.isInteger(seconds) || seconds <= 0) { - throw new Error(`Invalid precision sample duration: ${durationMs}`); +export function formatPrecisionRefreshTooltip(tradeCount) { + const count = Number(tradeCount); + if (!Number.isInteger(count) || count <= 1) { + throw new Error(`Invalid precision trade count: ${tradeCount}`); } - return `根据最近 ${seconds} 秒的成交价变动,重新计算推荐的价格精度。`; + return `根据最新 ${count} 条成交价中的有效价格变动,重新计算推荐精度。`; } diff --git a/src/binance-orderbook-trade/core/precision.js b/src/binance-orderbook-trade/core/precision.js index 4ba1f8a..fb377de 100644 --- a/src/binance-orderbook-trade/core/precision.js +++ b/src/binance-orderbook-trade/core/precision.js @@ -65,35 +65,6 @@ export function collectNonZeroPriceMoves(prices) { return moves; } -export function mergePrecisionSamples(existingSamples, newSamples, maxSamples = 64) { - const merged = [...(existingSamples || []), ...(newSamples || [])] - .map((sample) => normalizeDecimalString(sample)) - .filter((sample) => sample && isPositiveDecimalString(sample)); - return merged.slice(Math.max(0, merged.length - maxSamples)); -} - -export function resolveOrderbookPrecisionSampleState({ - sampling, - scheduled, - status, - recommendation, -}) { - const busy = Boolean(sampling || scheduled); - if (busy) { - return { - busy, - status: status === '刷新中' ? '刷新中' : '采样中', - }; - } - if (status && /^(未定位|未找到|数据不足)/.test(status)) { - return { busy, status }; - } - return { - busy, - status: recommendation ? 'ready' : '数据不足', - }; -} - function sortedPositiveDecimals(values) { return (values || []) .map((value) => normalizeDecimalString(value)) diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 79cf8f7..3e171ff 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.123 +// @version 2.7.124 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -68,9 +68,7 @@ import { collectNonZeroPriceMoves, formatOrderbookPrecisionShortcutLabel, getOrderbookPrecisionShortcutOptions, - mergePrecisionSamples, recommendOrderbookPrecision, - resolveOrderbookPrecisionSampleState, } from './core/precision.js'; import { allocateLadderQuantities } from './core/quantity.js'; import { @@ -269,13 +267,10 @@ import { const LADDER_OPEN_QTY_READY_TIMEOUT_MS = 1200; const TRADE_INPUT_SYNC_TIMEOUT_MS = 350; const TRADE_INPUT_SYNC_STABLE_FRAMES = 2; - const ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS = 6000; - const ORDERBOOK_PRECISION_SAMPLE_POLL_MS = 300; const ORDERBOOK_PRECISION_READY_POLL_MS = 100; const ORDERBOOK_PRECISION_READY_TIMEOUT_MS = 5000; const ORDERBOOK_PRECISION_OPTION_WAIT_MS = 1200; - const ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS = 6; - const ORDERBOOK_PRECISION_SAMPLE_MAX = 96; + const ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT = 10; const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4; const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [ '0.00000001', @@ -359,10 +354,6 @@ import { let panelResizeObserver = null; let ladderSubmitCaptureSequence = 0; let activeLadderSubmitCapture = null; - let orderbookPrecisionSampling = false; - let orderbookPrecisionSampleTimer = 0; - let orderbookPrecisionActiveRequest = null; - let orderbookPrecisionPendingRequest = null; let orderbookPrecisionSelectionTask = null; let orderbookPrecisionOptionsLoadRequestedSymbol = null; let orderbookPrecisionOptionsLoadAttemptedSymbol = null; @@ -377,7 +368,6 @@ import { nativeOptions: [], nativeOptionsStatus: null, status: '数据不足', - sampleEndsAt: 0, }; const controlledNativeButtons = new Set(); let lastObservedSymbol = getCurrentSymbol(); @@ -1170,23 +1160,15 @@ import { return getOrderbookPrices(side, 1)[0] || null; } - function getLatestTradePrices(limit = 20) { + function getLatestTradePrices(limit = ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT) { + if (!Number.isInteger(limit) || limit < 2) { + throw new Error(`Invalid latest trade price limit: ${limit}`); + } return Array.from(document.querySelectorAll('.tradew-tradelist .price.emit-price')) .filter((node) => isVisibleElement(node)) .map((node) => parsePrice(node)) .filter(Boolean) - .slice(0, Math.max(1, Number(limit) || 20)); - } - - async function waitForLatestTradePricesReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) { - const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0); - while (!document.hidden && isFuturesTradingPage() && isCurrentObservedSymbol(symbol)) { - const prices = getLatestTradePrices(); - if (prices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) return prices; - if (Date.now() >= deadline) return prices; - await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS); - } - return []; + .slice(0, limit); } async function waitForOrderbookPrecisionBootstrapReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) { @@ -1219,9 +1201,14 @@ import { function saveStoredOrderbookPrecisionSamples(symbol, samples) { const key = orderbookPrecisionSamplesKey(symbol); if (!key) return []; - const merged = mergePrecisionSamples([], samples, ORDERBOOK_PRECISION_SAMPLE_MAX); - localStorage.setItem(key, JSON.stringify(merged)); - return merged; + if (!Array.isArray(samples) || samples.length > ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT - 1) { + throw new Error('Invalid latest trade movement snapshot'); + } + const normalizedSamples = samples + .map((sample) => normalizeDecimalString(sample)) + .filter((sample) => sample && isPositiveDecimalString(sample)); + localStorage.setItem(key, JSON.stringify(normalizedSamples)); + return normalizedSamples; } function getOrderbookPrecisionRecommendation(symbol = getCurrentSymbol()) { @@ -1507,12 +1494,11 @@ import { }); const current = readCurrentOrderbookPrecisionValue(); const existingStatus = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.status : null; - const { busy, status } = resolveOrderbookPrecisionSampleState({ - sampling: orderbookPrecisionSampling, - scheduled: Boolean(orderbookPrecisionSampleTimer), - status: existingStatus, - recommendation, - }); + const status = recommendation + ? 'ready' + : existingStatus && /^(未定位|未找到|数据不足)/.test(existingStatus) + ? existingStatus + : '数据不足'; orderbookPrecisionState = { ...orderbookPrecisionState, symbol, @@ -1523,7 +1509,7 @@ import { }; const selectionBusy = Boolean(orderbookPrecisionSelectionTask); - const controlsBusy = busy || selectionBusy; + const controlsBusy = selectionBusy; const nativeOptions = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.nativeOptions : []; @@ -1533,10 +1519,10 @@ import { ); if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol); const canRefresh = !controlsBusy; - const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS); + const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT); const recommendationHtml = [ '
', - '
', + '
', `${PANEL_COPY.field.pricePrecision}`, ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy), ``, @@ -1747,106 +1733,37 @@ import { }, 0); } - async function runOrderbookPrecisionSampleRound(request) { - orderbookPrecisionSampleTimer = 0; - if (orderbookPrecisionSampling || document.hidden || !isFuturesTradingPage()) return; - const symbol = request.symbol; - if (!isCurrentObservedSymbol(symbol)) return false; - - orderbookPrecisionSampling = true; - orderbookPrecisionActiveRequest = request; - const tradeMoveSamples = []; - const sampleDurationMs = Math.max(0, Number(request.durationMs) || ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS); - try { - const readyPrices = await waitForLatestTradePricesReady(symbol); - if (!isCurrentObservedSymbol(symbol)) return false; - if (readyPrices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) { - tradeMoveSamples.push(...collectNonZeroPriceMoves(readyPrices)); - } - const deadline = Date.now() + sampleDurationMs; - orderbookPrecisionState = { - ...orderbookPrecisionState, - symbol, - sampleEndsAt: deadline, - }; - scheduleRenderPanel({ followUpMs: 1000 }); - while (Date.now() < deadline && !document.hidden && isFuturesTradingPage() && isCurrentObservedSymbol(symbol)) { - tradeMoveSamples.push(...collectNonZeroPriceMoves(getLatestTradePrices())); - await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS); - } - - if (!isCurrentObservedSymbol(symbol)) return false; - const newSamples = tradeMoveSamples; - const samples = saveStoredOrderbookPrecisionSamples(symbol, newSamples); - const recommendation = recommendOrderbookPrecision({ - samples, - options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS, - }); + function refreshOrderbookPrecisionSamplesNow() { + const symbol = getCurrentSymbol(); + if (!symbol || !isCurrentObservedSymbol(symbol)) { orderbookPrecisionState = { ...orderbookPrecisionState, symbol, - samples, - recommendation, - current: readCurrentOrderbookPrecisionValue(), - status: recommendation ? 'ready' : '数据不足', - sampleEndsAt: 0, + status: '未定位当前交易对', }; - refreshOrderbookPrecisionRecommendation(); scheduleRenderPanel(); - return true; - } finally { - orderbookPrecisionSampling = false; - orderbookPrecisionActiveRequest = null; - const pending = orderbookPrecisionPendingRequest; - orderbookPrecisionPendingRequest = null; - if (pending && isCurrentObservedSymbol(pending.symbol)) { - scheduleOrderbookPrecisionSampleRound(0, { ...pending, force: true }); - } - } - } - - function scheduleOrderbookPrecisionSampleRound(delayMs = 0, options) { - const { - force = false, - durationMs = ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS, - } = options || {}; - if (document.hidden || !isFuturesTradingPage()) return; - const symbol = getCurrentSymbol(); - if (!isCurrentObservedSymbol(symbol)) return; - const request = { symbol, durationMs }; - if (orderbookPrecisionSampling) { - if (force) orderbookPrecisionPendingRequest = request; return; } - if (orderbookPrecisionSampling || orderbookPrecisionSampleTimer) return; - orderbookPrecisionSampleTimer = window.setTimeout( - () => runOrderbookPrecisionSampleRound(request), - Math.max(0, Number(delayMs) || 0) + const samples = saveStoredOrderbookPrecisionSamples( + symbol, + collectNonZeroPriceMoves(getLatestTradePrices(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT)) ); - } - - function stopOrderbookPrecisionSampler() { - window.clearTimeout(orderbookPrecisionSampleTimer); - orderbookPrecisionSampleTimer = 0; - orderbookPrecisionPendingRequest = null; - } - - function refreshOrderbookPrecisionSamplesNow() { - const symbol = getCurrentSymbol(); + const recommendation = recommendOrderbookPrecision({ + samples, + options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS, + }); orderbookPrecisionState = { ...orderbookPrecisionState, symbol, - status: '刷新中', - sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS, + samples, + recommendation, + current: readCurrentOrderbookPrecisionValue(), + status: recommendation ? 'ready' : '数据不足', }; - stopOrderbookPrecisionSampler(); if (!orderbookPrecisionState.nativeOptions.length) { queueOrderbookPrecisionOptionsLoad(symbol, true); } - scheduleOrderbookPrecisionSampleRound(0, { - force: true, - durationMs: ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS, - }); + refreshOrderbookPrecisionRecommendation(); scheduleRenderPanel(); } @@ -4742,7 +4659,7 @@ import { function ladderOptionRow(title, tooltip, options, selected, group, suffix = '') { return [ - '
', + '
', `${title}`, ...options.map((value) => ladderOptionButton(`${value}${suffix}`, value, Number(value) === Number(selected), group)), '
', @@ -5387,7 +5304,6 @@ import { stopTradingTimers(); invalidateTradeButtonCache(); lastDisplayCloseState = null; - stopOrderbookPrecisionSampler(); } function renderPanel() { @@ -5797,7 +5713,6 @@ import { lastObservedAccountPositionState = null; closeGuard = null; invalidateTradeButtonCache(); - stopOrderbookPrecisionSampler(); orderbookPrecisionOptionsLoadRequestedSymbol = null; orderbookPrecisionOptionsLoadAttemptedSymbol = null; const recommendation = getOrderbookPrecisionRecommendation(symbol); @@ -5809,7 +5724,6 @@ import { nativeOptions: [], nativeOptionsStatus: null, status: recommendation ? 'ready' : '数据不足', - sampleEndsAt: 0, }; } @@ -5869,7 +5783,6 @@ import { stopOrderbookPrecisionObserver(); clearTradeUiMutationWait(); stopRenderPanelTimer(); - stopOrderbookPrecisionSampler(); } function syncRouteState() { diff --git a/test/unit/binance-orderbook-trade/ladder-options.test.js b/test/unit/binance-orderbook-trade/ladder-options.test.js index 8e1dd03..b7f5b43 100644 --- a/test/unit/binance-orderbook-trade/ladder-options.test.js +++ b/test/unit/binance-orderbook-trade/ladder-options.test.js @@ -237,7 +237,7 @@ test('open and close ladder percentage rows share the centralized ratio label', test('ladder quantity levels and step options share one stable five-slot grid', () => { const optionRow = source.match(/function ladderOptionRow[\s\S]*?\n }/)?.[0] || ''; - assert.match(optionRow, /grid-template-columns:48px repeat\(5,minmax\(0,1fr\)\)/); + assert.match(optionRow, /grid-template-columns:36px repeat\(5,minmax\(0,1fr\)\)/); assert.doesNotMatch(optionRow, /flex-wrap/); assert.match(source, /data-ladder-value="\$\{value\}" style="box-sizing:border-box;width:100%;min-width:0;height:28px/); assert.equal((source.match(/ladderOptionRow\(PANEL_COPY\.field\.orderCount, PANEL_COPY\.tooltip\.orderCount, LADDER_LEVEL_OPTIONS/g) || []).length, 2); diff --git a/test/unit/binance-orderbook-trade/panel-copy.test.js b/test/unit/binance-orderbook-trade/panel-copy.test.js index 44b078f..4dc3320 100644 --- a/test/unit/binance-orderbook-trade/panel-copy.test.js +++ b/test/unit/binance-orderbook-trade/panel-copy.test.js @@ -20,7 +20,7 @@ test('panel copy keeps the approved labels and tooltips in one contract', () => ratio: '比例', orderCount: '笔数', interval: '间距', - pricePrecision: '价格精度', + pricePrecision: '精度', }, action: { openLong: '阶梯开多', @@ -43,10 +43,11 @@ test('panel copy keeps the approved labels and tooltips in one contract', () => }); }); -test('precision refresh tooltip describes the sampling window without calling it a minimum', () => { +test('precision refresh tooltip describes the latest trade snapshot without calling it a minimum', () => { assert.equal( - formatPrecisionRefreshTooltip(6000), - '根据最近 6 秒的成交价变动,重新计算推荐的价格精度。', + formatPrecisionRefreshTooltip(10), + '根据最新 10 条成交价中的有效价格变动,重新计算推荐精度。', ); - assert.doesNotMatch(formatPrecisionRefreshTooltip(6000), /最小值/); + assert.doesNotMatch(formatPrecisionRefreshTooltip(10), /最小值/); + assert.throws(() => formatPrecisionRefreshTooltip(1), /Invalid precision trade count/); }); diff --git a/test/unit/binance-orderbook-trade/precision.test.js b/test/unit/binance-orderbook-trade/precision.test.js index adf6257..1bd2788 100644 --- a/test/unit/binance-orderbook-trade/precision.test.js +++ b/test/unit/binance-orderbook-trade/precision.test.js @@ -6,9 +6,7 @@ import { formatOrderbookPrecisionShortcutLabel, getOrderbookPrecisionDecadeTarget, getOrderbookPrecisionShortcutOptions, - mergePrecisionSamples, recommendOrderbookPrecision, - resolveOrderbookPrecisionSampleState, } from '../../../src/binance-orderbook-trade/core/precision.js'; test('keeps only the four smallest exact native precision shortcuts', () => { @@ -57,14 +55,7 @@ test('collects only non-zero price moves from consecutive observations', () => { ); }); -test('keeps multiple sampling rounds bounded and newest samples last', () => { - assert.deepEqual( - mergePrecisionSamples(['0.001', '0.002', '0.003'], ['0.004', '0.005'], 4), - ['0.002', '0.003', '0.004', '0.005'] - ); -}); - -test('recommends precision from accumulated effective price movement instead of tick size', () => { +test('recommends precision from the latest effective price movement instead of tick size', () => { assert.equal(recommendOrderbookPrecision({ samples: ['0.0001', '0.0061', '0.0107', '0.0089', '0.0112', '0.0075'], options: ['0.0001', '0.001', '0.01', '0.1', '1'], @@ -107,46 +98,20 @@ test('does not treat display precision fallback as a recommendation', () => { }), null); }); -test('does not keep a stale sampling label busy after the sampler has stopped', () => { - assert.deepEqual(resolveOrderbookPrecisionSampleState({ - sampling: false, - scheduled: false, - status: '采样中', - recommendation: '0.00001', - }), { - busy: false, - status: 'ready', - }); - - assert.deepEqual(resolveOrderbookPrecisionSampleState({ - sampling: false, - scheduled: false, - status: '采样中', - recommendation: null, - }), { - busy: false, - status: '数据不足', - }); -}); - -test('keeps precision controls busy only for an active or scheduled sample', () => { - assert.deepEqual(resolveOrderbookPrecisionSampleState({ - sampling: true, - scheduled: false, - status: '采样中', - recommendation: null, - }), { - busy: true, - status: '采样中', - }); +test('ten latest trade rows provide enough movement evidence when eight do not', () => { + const prices = [ + '80.757', '80.757', '80.756', '80.756', '80.756', + '80.757', '80.756', '80.755', '80.783', '80.781', + ]; + const options = ['0.0001', '0.001', '0.01', '0.1', '1']; - assert.deepEqual(resolveOrderbookPrecisionSampleState({ - sampling: false, - scheduled: true, - status: '刷新中', - recommendation: '0.00001', - }), { - busy: true, - status: '刷新中', - }); + assert.equal(collectNonZeroPriceMoves(prices.slice(0, 8)).length, 4); + assert.equal(recommendOrderbookPrecision({ + samples: collectNonZeroPriceMoves(prices.slice(0, 8)), + options, + }), null); + assert.equal(recommendOrderbookPrecision({ + samples: collectNonZeroPriceMoves(prices), + options, + }), '0.001'); }); diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index 9da8d3a..136d8c3 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -338,7 +338,7 @@ test('dynamic panel text keeps fixed single-line slots', () => { assert.match(source, /data-panel-group="direction" style="display:flex;align-items:center;justify-content:flex-start;gap:6px;height:32px;overflow:hidden/); assert.match(source, /data-side-selector role="radiogroup"[^>]*display:grid;grid-template-columns:54px 54px;[^>]*border-radius:6px;[^>]*overflow:hidden/); assert.match(source, new RegExp(`id="\\$\\{MODE_HINT_ID\\}" style="width:78px;flex:0 0 78px;[^\"]*white-space:nowrap;overflow:hidden;text-overflow:ellipsis`)); - assert.match(source, /grid-template-columns:62px repeat\(4,minmax\(0,1fr\)\) 32px;align-items:center;gap:4px;height:32px;overflow:hidden/); + assert.match(source, /grid-template-columns:36px repeat\(4,minmax\(0,1fr\)\) 32px;align-items:center;gap:4px;height:32px;overflow:hidden/); assert.doesNotMatch(source, /buttonBaseStyle = `width:68px;height:24px/); assert.match(readFunctionBody('renderOrderbookPrecisionShortcut'), /height:32px[^`]*font-size:12px;line-height:30px/); assert.match(readFunctionBody('renderOrderbookPrecisionShortcutSlots'), /while \(slots\.length < ORDERBOOK_PRECISION_SHORTCUT_LIMIT\)/); @@ -929,7 +929,9 @@ test('stable panel refreshes avoid writing unchanged text and state attributes', }); test('orderbook precision recommendation marks one shortcut without applying it automatically', () => { - assert.match(source, /ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS = 6000/); + assert.match(source, /ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT = 10/); + assert.doesNotMatch(source, /ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS/); + assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_POLL_MS/); assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_DURATION_MS/); assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_PAUSE_MS/); assert.match(source, /LOCAL_ORDERBOOK_PRECISION_SAMPLES_PREFIX = 'jh_binance_orderbook_precision_samples_v3'/); @@ -938,32 +940,27 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.match(source, /data-orderbook-precision-value/); assert.doesNotMatch(source, /data-orderbook-precision-adjust/); assert.match(source, /data-orderbook-precision-refresh/); - assert.match(source, /orderbookPrecisionPendingRequest/); + assert.doesNotMatch(source, /orderbookPrecisionPendingRequest/); + assert.doesNotMatch(source, /orderbookPrecisionSampleTimer/); + assert.doesNotMatch(source, /mergePrecisionSamples/); assert.match(source, /orderbookPrecisionOptionsLoadAttemptedSymbol/); - const sampleBody = readFunctionBody('runOrderbookPrecisionSampleRound'); - assert.match(sampleBody, /collectNonZeroPriceMoves/); - assert.match(sampleBody, /saveStoredOrderbookPrecisionSamples\(symbol,\s*newSamples\)/); - assert.doesNotMatch(sampleBody, /mergePrecisionSamples\(\s*readStoredOrderbookPrecisionSamples/); - assert.match(sampleBody, /waitForLatestTradePricesReady/); - assert.match(sampleBody, /ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS/); - assert.match(sampleBody, /getLatestTradePrices/); - assert.doesNotMatch(sampleBody, /getCurrentOrderbookDisplayStep/); - assert.doesNotMatch(sampleBody, /fallbackMovement/); - assert.match(sampleBody, /orderbookPrecisionPendingRequest/); - assert.doesNotMatch(sampleBody, /ORDERBOOK_PRECISION_SAMPLE_PAUSE_MS/); - assert.match(sampleBody, /orderbookPrecisionState[\s\S]*sampleEndsAt: deadline/); - assert.match(sampleBody, /scheduleRenderPanel\(\{ followUpMs: 1000 \}\)/); + const sampleBody = readFunctionBody('refreshOrderbookPrecisionSamplesNow'); + assert.match(sampleBody, /collectNonZeroPriceMoves\(getLatestTradePrices\(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT\)\)/); + assert.match(sampleBody, /saveStoredOrderbookPrecisionSamples/); + assert.match(sampleBody, /recommendOrderbookPrecision/); + assert.match(sampleBody, /status: recommendation \? 'ready' : '数据不足'/); + assert.doesNotMatch(sampleBody, /setTimeout|setInterval|await delay/); + assert.doesNotMatch(sampleBody, /getCurrentOrderbookDisplayStep|fallbackMovement/); const refreshBody = readFunctionBody('refreshOrderbookPrecisionRecommendation'); assert.match(refreshBody, /recommendOrderbookPrecision/); - assert.match(refreshBody, /resolveOrderbookPrecisionSampleState/); - assert.match(refreshBody, /scheduled: Boolean\(orderbookPrecisionSampleTimer\)/); + assert.doesNotMatch(refreshBody, /resolveOrderbookPrecisionSampleState|orderbookPrecisionSampleTimer/); assert.match(refreshBody, /data-orderbook-precision-refresh="true"[\s\S]*disabled/); - assert.match(refreshBody, /const controlsBusy = busy \|\| selectionBusy/); + assert.match(refreshBody, /const controlsBusy = selectionBusy/); assert.match(refreshBody, /getOrderbookPrecisionShortcutOptions\([\s\S]*ORDERBOOK_PRECISION_SHORTCUT_LIMIT/); assert.match(refreshBody, /queueOrderbookPrecisionOptionsLoad\(symbol\)/); - assert.match(refreshBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS\)/); + assert.match(refreshBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT\)/); const shortcutBody = readFunctionBody('renderOrderbookPrecisionShortcut'); assert.match(shortcutBody, /const recommended = value === recommendation/); assert.match(shortcutBody, /position:absolute;top:3px;right:3px;width:6px;height:6px/); @@ -981,7 +978,7 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.match(refreshBody, /PANEL_COPY\.field\.pricePrecision/); assert.match(refreshBody, /PANEL_COPY\.tooltip\.pricePrecision/); assert.match(refreshBody, /renderOrderbookPrecisionShortcutSlots\(shortcutOptions, current, recommendation, controlsBusy\)/); - assert.match(refreshBody, /grid-template-columns:62px repeat\(4,minmax\(0,1fr\)\) 32px/); + assert.match(refreshBody, /grid-template-columns:36px repeat\(4,minmax\(0,1fr\)\) 32px/); assert.match(refreshBody, /data-orderbook-precision-refresh="true"[^`]* { +test('precision shortcut selection and refresh do not commit after a symbol switch', () => { const confirmBody = readFunctionBody('clickAndConfirmOrderbookPrecisionOption'); assert.match(confirmBody, /!isCurrentObservedSymbol\(symbol\)/); assert.match(confirmBody, /readCurrentOrderbookPrecisionValue\(\) !== startPrecision/); @@ -1313,12 +1304,8 @@ test('precision shortcut selection and sampling do not commit after a symbol swi assert.match(stableLoadBody, /isCurrentObservedSymbol\(symbol\)/); assert.match(stableLoadBody, /currentTrigger\.value === startPrecision/); - const roundBody = readFunctionBody('runOrderbookPrecisionSampleRound'); - assert.match(roundBody, /request\.symbol/); - assert.doesNotMatch(roundBody, /initial/); + assert.doesNotMatch(source, /runOrderbookPrecisionSampleRound|scheduleOrderbookPrecisionSampleRound/); assert.doesNotMatch(source, /orderbookPrecisionInitialSampledSymbols|startInitialOrderbookPrecisionSample/); - const scheduleBody = readFunctionBody('scheduleOrderbookPrecisionSampleRound'); - assert.match(scheduleBody, /if \(force\) orderbookPrecisionPendingRequest = request/); const clearBody = readFunctionBody('clearSymbolOwnedRuntimeState'); assert.match(clearBody, /orderbookPrecisionOptionsLoadRequestedSymbol = null/); From c66d7f8268582566e152e7afa721a03e4b15ee93 Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Thu, 27 Aug 2026 00:31:40 +0800 Subject: [PATCH 3/3] fix: expand precision trade sampling --- .../specs/control-flows.pw.js | 20 ++++++ scripts/binance-orderbook-trade.user.js | 67 ++++++++++++++----- .../contracts/panel-copy.js | 6 +- src/binance-orderbook-trade/core/precision.js | 24 +++++++ src/binance-orderbook-trade/index.user.js | 45 +++++++------ .../panel-copy.test.js | 6 +- .../binance-orderbook-trade/precision.test.js | 25 +++++++ .../source-regressions.test.js | 16 +++-- 8 files changed, 164 insertions(+), 45 deletions(-) diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js index 9c97b26..19262b2 100644 --- a/e2e/binance-orderbook/specs/control-flows.pw.js +++ b/e2e/binance-orderbook/specs/control-flows.pw.js @@ -113,6 +113,26 @@ test('precision refresh uses the visible latest trades immediately without a sam expect(errors).toEqual([]); }); +test('precision refresh explains when the complete visible trade list still lacks movement', async ({ page }) => { + const scenario = createCancelScenario({ + ui: { orderbookPrecision: '0.1' }, + }); + const { errors } = await openUserscriptScenario(page, scenario); + const panel = page.locator(PANEL_SELECTOR); + await page.locator('.tradew-tradelist .price.emit-price').evaluateAll((nodes) => { + nodes.forEach((node) => { node.textContent = '81.00'; }); + }); + + await panel.locator('[data-orderbook-precision-refresh]').click(); + await expect(panel.locator('#jh-binance-ladder-status')) + .toHaveText('近期价格变化不足,请稍后重试'); + await expect(panel.locator('[aria-label*="推荐档位"]')).toHaveCount(0); + expect(await page.evaluate((symbol) => JSON.parse( + localStorage.getItem(`jh_binance_orderbook_precision_samples_v3:${symbol}`) + ), scenario.currentSymbol)).toEqual([]); + expect(errors).toEqual([]); +}); + test('starting a ladder disables every start action and exposes one stop control', async ({ page }) => { const scenario = createCancelScenario({ ui: { tradeMode: 'OPEN', orderbookPrecision: '0.1' }, diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index eff8dde..4ed0047 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.124 +// @version 2.7.125 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -150,6 +150,10 @@ noOrders: "无挂单", stopLadder: "停止阶梯挂单" }), + status: freezeCopy({ + precisionUpdated: "精度推荐已更新", + precisionInsufficient: "近期价格变化不足,请稍后重试" + }), tooltip: freezeCopy({ singleOrder: "单击订单簿中的某个价格,按当前方向和数量设置提交一笔订单。", ladderMaker: "根据当前比例、笔数、间距和价格精度设置,依次提交只做 Maker 的阶梯订单。", @@ -164,7 +168,7 @@ if (!Number.isInteger(count) || count <= 1) { throw new Error(`Invalid precision trade count: ${tradeCount}`); } - return `根据最新 ${count} 条成交价中的有效价格变动,重新计算推荐精度。`; + return `优先根据最新 ${count} 条成交价;价格变化不足时自动扩大范围,重新计算推荐精度。`; } // src/binance-orderbook-trade/core/cancel-orders.js @@ -553,6 +557,29 @@ } 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)); } @@ -1809,7 +1836,9 @@ const ORDERBOOK_PRECISION_READY_POLL_MS = 100; const ORDERBOOK_PRECISION_READY_TIMEOUT_MS = 5e3; const ORDERBOOK_PRECISION_OPTION_WAIT_MS = 1200; - const ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT = 10; + const ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT = 10; + const ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10; + const ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5; const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4; const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [ "0.00000001", @@ -1904,7 +1933,7 @@ current: null, nativeOptions: [], nativeOptionsStatus: null, - status: "数据不足" + status: PANEL_COPY.status.precisionInsufficient }; const controlledNativeButtons = /* @__PURE__ */ new Set(); let lastObservedSymbol = getCurrentSymbol(); @@ -2506,11 +2535,8 @@ function getBestOrderbookPrice(side) { return getOrderbookPrices(side, 1)[0] || null; } - function getLatestTradePrices(limit = ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT) { - if (!Number.isInteger(limit) || limit < 2) { - throw new Error(`Invalid latest trade price limit: ${limit}`); - } - return Array.from(document.querySelectorAll(".tradew-tradelist .price.emit-price")).filter((node) => isVisibleElement(node)).map((node) => parsePrice(node)).filter(Boolean).slice(0, limit); + function getLatestTradePrices() { + return Array.from(document.querySelectorAll(".tradew-tradelist .price.emit-price")).filter((node) => isVisibleElement(node)).map((node) => parsePrice(node)).filter(Boolean); } async function waitForOrderbookPrecisionBootstrapReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) { const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0); @@ -2537,7 +2563,7 @@ function saveStoredOrderbookPrecisionSamples(symbol, samples) { const key = orderbookPrecisionSamplesKey(symbol); if (!key) return []; - if (!Array.isArray(samples) || samples.length > ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT - 1) { + if (!Array.isArray(samples)) { throw new Error("Invalid latest trade movement snapshot"); } const normalizedSamples = samples.map((sample) => normalizeDecimalString(sample)).filter((sample) => sample && isPositiveDecimalString(sample)); @@ -2767,7 +2793,7 @@ }); const current = readCurrentOrderbookPrecisionValue(); const existingStatus = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.status : null; - const status = recommendation ? "ready" : existingStatus && /^(未定位|未找到|数据不足)/.test(existingStatus) ? existingStatus : "数据不足"; + const status = recommendation ? "ready" : existingStatus && existingStatus !== "ready" ? existingStatus : PANEL_COPY.status.precisionInsufficient; orderbookPrecisionState = { ...orderbookPrecisionState, symbol, @@ -2785,7 +2811,7 @@ ); if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol); const canRefresh = !controlsBusy; - const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT); + const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT); const recommendationHtml = [ '
', '
', @@ -2976,10 +3002,12 @@ scheduleRenderPanel(); return; } - const samples = saveStoredOrderbookPrecisionSamples( - symbol, - collectNonZeroPriceMoves(getLatestTradePrices(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT)) - ); + const { samples: latestSamples } = collectPriceMovesWithExpandingWindow(getLatestTradePrices(), { + 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 @@ -2990,12 +3018,15 @@ samples, recommendation, current: readCurrentOrderbookPrecisionValue(), - status: recommendation ? "ready" : "数据不足" + status: recommendation ? "ready" : PANEL_COPY.status.precisionInsufficient }; if (!orderbookPrecisionState.nativeOptions.length) { queueOrderbookPrecisionOptionsLoad(symbol, true); } refreshOrderbookPrecisionRecommendation(); + setLadderStatus( + recommendation ? PANEL_COPY.status.precisionUpdated : PANEL_COPY.status.precisionInsufficient + ); scheduleRenderPanel(); } function getBufferedMakerPrices(side, levels, ladderStep = DEFAULT_LADDER_STEP) { @@ -6400,7 +6431,7 @@ current: readCurrentOrderbookPrecisionValue(), nativeOptions: [], nativeOptionsStatus: null, - status: recommendation ? "ready" : "数据不足" + status: recommendation ? "ready" : PANEL_COPY.status.precisionInsufficient }; } function checkSymbolChangeForLeverage() { diff --git a/src/binance-orderbook-trade/contracts/panel-copy.js b/src/binance-orderbook-trade/contracts/panel-copy.js index 4e408ea..95b3613 100644 --- a/src/binance-orderbook-trade/contracts/panel-copy.js +++ b/src/binance-orderbook-trade/contracts/panel-copy.js @@ -25,6 +25,10 @@ export const PANEL_COPY = Object.freeze({ noOrders: '无挂单', stopLadder: '停止阶梯挂单', }), + status: freezeCopy({ + precisionUpdated: '精度推荐已更新', + precisionInsufficient: '近期价格变化不足,请稍后重试', + }), tooltip: freezeCopy({ singleOrder: '单击订单簿中的某个价格,按当前方向和数量设置提交一笔订单。', ladderMaker: '根据当前比例、笔数、间距和价格精度设置,依次提交只做 Maker 的阶梯订单。', @@ -40,5 +44,5 @@ export function formatPrecisionRefreshTooltip(tradeCount) { if (!Number.isInteger(count) || count <= 1) { throw new Error(`Invalid precision trade count: ${tradeCount}`); } - return `根据最新 ${count} 条成交价中的有效价格变动,重新计算推荐精度。`; + return `优先根据最新 ${count} 条成交价;价格变化不足时自动扩大范围,重新计算推荐精度。`; } diff --git a/src/binance-orderbook-trade/core/precision.js b/src/binance-orderbook-trade/core/precision.js index fb377de..f065fee 100644 --- a/src/binance-orderbook-trade/core/precision.js +++ b/src/binance-orderbook-trade/core/precision.js @@ -65,6 +65,30 @@ 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)) diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index 3e171ff..da8738a 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.124 +// @version 2.7.125 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -65,7 +65,7 @@ import { isPositiveDecimalString, } from './core/decimal.js'; import { - collectNonZeroPriceMoves, + collectPriceMovesWithExpandingWindow, formatOrderbookPrecisionShortcutLabel, getOrderbookPrecisionShortcutOptions, recommendOrderbookPrecision, @@ -270,7 +270,9 @@ import { const ORDERBOOK_PRECISION_READY_POLL_MS = 100; const ORDERBOOK_PRECISION_READY_TIMEOUT_MS = 5000; const ORDERBOOK_PRECISION_OPTION_WAIT_MS = 1200; - const ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT = 10; + const ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT = 10; + const ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10; + const ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5; const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4; const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [ '0.00000001', @@ -367,7 +369,7 @@ import { current: null, nativeOptions: [], nativeOptionsStatus: null, - status: '数据不足', + status: PANEL_COPY.status.precisionInsufficient, }; const controlledNativeButtons = new Set(); let lastObservedSymbol = getCurrentSymbol(); @@ -1160,15 +1162,11 @@ import { return getOrderbookPrices(side, 1)[0] || null; } - function getLatestTradePrices(limit = ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT) { - if (!Number.isInteger(limit) || limit < 2) { - throw new Error(`Invalid latest trade price limit: ${limit}`); - } + function getLatestTradePrices() { return Array.from(document.querySelectorAll('.tradew-tradelist .price.emit-price')) .filter((node) => isVisibleElement(node)) .map((node) => parsePrice(node)) - .filter(Boolean) - .slice(0, limit); + .filter(Boolean); } async function waitForOrderbookPrecisionBootstrapReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) { @@ -1201,7 +1199,7 @@ import { function saveStoredOrderbookPrecisionSamples(symbol, samples) { const key = orderbookPrecisionSamplesKey(symbol); if (!key) return []; - if (!Array.isArray(samples) || samples.length > ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT - 1) { + if (!Array.isArray(samples)) { throw new Error('Invalid latest trade movement snapshot'); } const normalizedSamples = samples @@ -1496,9 +1494,9 @@ import { const existingStatus = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.status : null; const status = recommendation ? 'ready' - : existingStatus && /^(未定位|未找到|数据不足)/.test(existingStatus) + : existingStatus && existingStatus !== 'ready' ? existingStatus - : '数据不足'; + : PANEL_COPY.status.precisionInsufficient; orderbookPrecisionState = { ...orderbookPrecisionState, symbol, @@ -1519,7 +1517,7 @@ import { ); if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol); const canRefresh = !controlsBusy; - const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT); + const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT); const recommendationHtml = [ '
', '
', @@ -1744,10 +1742,12 @@ import { scheduleRenderPanel(); return; } - const samples = saveStoredOrderbookPrecisionSamples( - symbol, - collectNonZeroPriceMoves(getLatestTradePrices(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT)) - ); + const { samples: latestSamples } = collectPriceMovesWithExpandingWindow(getLatestTradePrices(), { + 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, @@ -1758,12 +1758,17 @@ import { samples, recommendation, current: readCurrentOrderbookPrecisionValue(), - status: recommendation ? 'ready' : '数据不足', + status: recommendation ? 'ready' : PANEL_COPY.status.precisionInsufficient, }; if (!orderbookPrecisionState.nativeOptions.length) { queueOrderbookPrecisionOptionsLoad(symbol, true); } refreshOrderbookPrecisionRecommendation(); + setLadderStatus( + recommendation + ? PANEL_COPY.status.precisionUpdated + : PANEL_COPY.status.precisionInsufficient + ); scheduleRenderPanel(); } @@ -5723,7 +5728,7 @@ import { current: readCurrentOrderbookPrecisionValue(), nativeOptions: [], nativeOptionsStatus: null, - status: recommendation ? 'ready' : '数据不足', + status: recommendation ? 'ready' : PANEL_COPY.status.precisionInsufficient, }; } diff --git a/test/unit/binance-orderbook-trade/panel-copy.test.js b/test/unit/binance-orderbook-trade/panel-copy.test.js index 4dc3320..b436252 100644 --- a/test/unit/binance-orderbook-trade/panel-copy.test.js +++ b/test/unit/binance-orderbook-trade/panel-copy.test.js @@ -32,6 +32,10 @@ test('panel copy keeps the approved labels and tooltips in one contract', () => noOrders: '无挂单', stopLadder: '停止阶梯挂单', }, + status: { + precisionUpdated: '精度推荐已更新', + precisionInsufficient: '近期价格变化不足,请稍后重试', + }, tooltip: { singleOrder: '单击订单簿中的某个价格,按当前方向和数量设置提交一笔订单。', ladderMaker: '根据当前比例、笔数、间距和价格精度设置,依次提交只做 Maker 的阶梯订单。', @@ -46,7 +50,7 @@ test('panel copy keeps the approved labels and tooltips in one contract', () => test('precision refresh tooltip describes the latest trade snapshot without calling it a minimum', () => { assert.equal( formatPrecisionRefreshTooltip(10), - '根据最新 10 条成交价中的有效价格变动,重新计算推荐精度。', + '优先根据最新 10 条成交价;价格变化不足时自动扩大范围,重新计算推荐精度。', ); assert.doesNotMatch(formatPrecisionRefreshTooltip(10), /最小值/); assert.throws(() => formatPrecisionRefreshTooltip(1), /Invalid precision trade count/); diff --git a/test/unit/binance-orderbook-trade/precision.test.js b/test/unit/binance-orderbook-trade/precision.test.js index 1bd2788..385cd53 100644 --- a/test/unit/binance-orderbook-trade/precision.test.js +++ b/test/unit/binance-orderbook-trade/precision.test.js @@ -3,6 +3,7 @@ import assert from 'node:assert/strict'; import { collectNonZeroPriceMoves, + collectPriceMovesWithExpandingWindow, formatOrderbookPrecisionShortcutLabel, getOrderbookPrecisionDecadeTarget, getOrderbookPrecisionShortcutOptions, @@ -115,3 +116,27 @@ test('ten latest trade rows provide enough movement evidence when eight do not', options, }), '0.001'); }); + +test('expands the latest-trade window only until enough effective price moves exist', () => { + const prices = [ + '80.757', '80.757', '80.756', '80.756', '80.756', + '80.757', '80.756', '80.755', '80.755', '80.755', + '80.755', '80.754', '80.753', '80.752', '80.751', + '80.750', '80.749', '80.748', '80.747', '80.746', + '80.700', + ]; + + const result = collectPriceMovesWithExpandingWindow(prices); + assert.equal(result.usedCount, 20); + assert.equal(result.samples.length, 13); + assert.deepEqual([...new Set(result.samples)], ['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', + ]), { + samples: [], + usedCount: 6, + }); +}); diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index 136d8c3..bb3f907 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -929,7 +929,9 @@ test('stable panel refreshes avoid writing unchanged text and state attributes', }); test('orderbook precision recommendation marks one shortcut without applying it automatically', () => { - assert.match(source, /ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT = 10/); + assert.match(source, /ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT = 10/); + assert.match(source, /ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10/); + assert.match(source, /ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5/); assert.doesNotMatch(source, /ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS/); assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_POLL_MS/); assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_DURATION_MS/); @@ -946,10 +948,14 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.match(source, /orderbookPrecisionOptionsLoadAttemptedSymbol/); const sampleBody = readFunctionBody('refreshOrderbookPrecisionSamplesNow'); - assert.match(sampleBody, /collectNonZeroPriceMoves\(getLatestTradePrices\(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT\)\)/); + assert.match(sampleBody, /collectPriceMovesWithExpandingWindow\(getLatestTradePrices\(\),/); + 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, /status: recommendation \? 'ready' : '数据不足'/); + assert.match(sampleBody, /status: recommendation \? 'ready' : PANEL_COPY\.status\.precisionInsufficient/); + assert.match(sampleBody, /PANEL_COPY\.status\.precisionUpdated/); assert.doesNotMatch(sampleBody, /setTimeout|setInterval|await delay/); assert.doesNotMatch(sampleBody, /getCurrentOrderbookDisplayStep|fallbackMovement/); @@ -960,7 +966,7 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.match(refreshBody, /const controlsBusy = selectionBusy/); assert.match(refreshBody, /getOrderbookPrecisionShortcutOptions\([\s\S]*ORDERBOOK_PRECISION_SHORTCUT_LIMIT/); assert.match(refreshBody, /queueOrderbookPrecisionOptionsLoad\(symbol\)/); - assert.match(refreshBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT\)/); + assert.match(refreshBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT\)/); const shortcutBody = readFunctionBody('renderOrderbookPrecisionShortcut'); assert.match(shortcutBody, /const recommended = value === recommendation/); assert.match(shortcutBody, /position:absolute;top:3px;right:3px;width:6px;height:6px/); @@ -1309,7 +1315,7 @@ test('precision shortcut selection and refresh do not commit after a symbol swit const clearBody = readFunctionBody('clearSymbolOwnedRuntimeState'); assert.match(clearBody, /orderbookPrecisionOptionsLoadRequestedSymbol = null/); - assert.match(clearBody, /status: recommendation \? 'ready' : '数据不足'/); + assert.match(clearBody, /status: recommendation \? 'ready' : PANEL_COPY\.status\.precisionInsufficient/); assert.doesNotMatch(clearBody, /status: '采样中'/); });