From d9bcdfacd5b9a70f77f3271b4efc32647db13636 Mon Sep 17 00:00:00 2001 From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com> Date: Wed, 26 Aug 2026 21:56:16 +0800 Subject: [PATCH] feat: redesign Binance order panel --- .../fixtures/binance-futures.js | 1 - .../helpers/userscript-page.js | 2 +- .../helpers/visual-contract.js | 1 - .../scenarios/cancel-current-symbol.js | 1 - .../specs/cancel-covering-matrix.pw.js | 2 +- .../specs/cancel-current-symbol.pw.js | 28 +- .../specs/control-flows.pw.js | 14 +- .../specs/live-performance-probe.pw.js | 14 +- .../specs/panel-visual-contract.pw.js | 20 +- ...ed-visual.json => close-fixed-visual.json} | 302 ++++++++---------- .../open-collapsed-visual.json | 264 --------------- ...ded-visual.json => open-fixed-visual.json} | 302 ++++++++---------- scripts/binance-orderbook-trade.user.js | 294 ++++++++--------- .../contracts/panel-copy.js | 44 +++ .../core/cancel-orders.js | 7 +- src/binance-orderbook-trade/index.user.js | 277 ++++++---------- .../binance-orderbook-trade/cancel.test.js | 4 +- .../ladder-options.test.js | 14 +- .../panel-copy.test.js | 52 +++ .../source-regressions.test.js | 102 +++--- 20 files changed, 701 insertions(+), 1044 deletions(-) rename e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/{close-expanded-visual.json => close-fixed-visual.json} (84%) delete mode 100644 e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-collapsed-visual.json rename e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/{open-expanded-visual.json => open-fixed-visual.json} (84%) create mode 100644 src/binance-orderbook-trade/contracts/panel-copy.js create mode 100644 test/unit/binance-orderbook-trade/panel-copy.test.js diff --git a/e2e/binance-orderbook/fixtures/binance-futures.js b/e2e/binance-orderbook/fixtures/binance-futures.js index e3cc35e..9c5057c 100644 --- a/e2e/binance-orderbook/fixtures/binance-futures.js +++ b/e2e/binance-orderbook/fixtures/binance-futures.js @@ -205,7 +205,6 @@ export function renderBinanceFuturesFixture(scenario) { }); } - localStorage.setItem('jh_binance_ladder_expanded', scenario.ui.ladderExpanded ? 'true' : 'false'); localStorage.setItem('jh_binance_orderbook_precision_samples_v3:' + scenario.currentSymbol, '["81.0","81.01","81.02","81.03","81.04","81.05"]'); function renderOrdersRows() { diff --git a/e2e/binance-orderbook/helpers/userscript-page.js b/e2e/binance-orderbook/helpers/userscript-page.js index 171f442..9d3ad7e 100644 --- a/e2e/binance-orderbook/helpers/userscript-page.js +++ b/e2e/binance-orderbook/helpers/userscript-page.js @@ -96,7 +96,7 @@ export async function openUserscriptScenario(page, scenario) { await page.goto(`https://www.binance.com/zh-CN/futures/${scenario.currentSymbol}`); await page.locator('#jh-binance-close-qty-multiplier-panel').waitFor({ state: 'visible' }); - await page.locator('#jh-binance-ladder-toggle').waitFor({ state: 'visible' }); + await page.locator('#jh-binance-ladder-body').waitFor({ state: 'visible' }); await page.evaluate(() => new Promise((resolve) => { requestAnimationFrame(() => requestAnimationFrame(resolve)); })); diff --git a/e2e/binance-orderbook/helpers/visual-contract.js b/e2e/binance-orderbook/helpers/visual-contract.js index ae3b25a..3726630 100644 --- a/e2e/binance-orderbook/helpers/visual-contract.js +++ b/e2e/binance-orderbook/helpers/visual-contract.js @@ -39,7 +39,6 @@ export async function readPanelVisualContract(page) { '#jh-binance-close-qty-multiplier-dec', '#jh-binance-close-qty-multiplier-inc', '[data-orderbook-precision-value]', - '#jh-binance-ladder-toggle', '[data-ladder-group]', '[data-ladder-action]', '[data-ladder-stop]', diff --git a/e2e/binance-orderbook/scenarios/cancel-current-symbol.js b/e2e/binance-orderbook/scenarios/cancel-current-symbol.js index 88d9937..631dc3e 100644 --- a/e2e/binance-orderbook/scenarios/cancel-current-symbol.js +++ b/e2e/binance-orderbook/scenarios/cancel-current-symbol.js @@ -44,7 +44,6 @@ export function createCancelScenario(overrides = {}) { openOrdersSubTab: 'basic', hideOtherSymbols: false, showOrders: true, - ladderExpanded: true, tradeMode: 'OPEN', orderbookPrecision: '0.1', leverage: 2, diff --git a/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js b/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js index cff2e65..d5c3a7c 100644 --- a/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js +++ b/e2e/binance-orderbook/specs/cancel-covering-matrix.pw.js @@ -46,7 +46,7 @@ for (const entry of CANCEL_COVERING_SCENARIOS) { const { errors } = await openUserscriptScenario(page, scenario); await installInteractionProbe(page, CANCEL_BUTTON_SELECTOR); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); if (!hasCurrentOrders) { await expect(page.getByRole('button', { name: '无挂单' })).toBeVisible(); } else { diff --git a/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js b/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js index 71642bf..2dc2727 100644 --- a/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js +++ b/e2e/binance-orderbook/specs/cancel-current-symbol.pw.js @@ -45,7 +45,7 @@ test('no position and no orders returns immediate stable no-order feedback', asy const { errors } = await openUserscriptScenario(page, scenario); await installInteractionProbe(page, CANCEL_BUTTON_SELECTOR); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('button', { name: '无挂单' })).toBeVisible(); const state = await readFixtureState(page); @@ -66,7 +66,7 @@ test('other-symbol position and orders never open a current-symbol cancel dialog }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('button', { name: '无挂单' })).toBeVisible(); const state = await readFixtureState(page); @@ -90,7 +90,7 @@ test('cancelling the native dialog preserves current and other orders and restor const { errors } = await openUserscriptScenario(page, scenario); await installInteractionProbe(page, CANCEL_BUTTON_SELECTOR); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await expect.poll(async () => (await readFixtureState(page)).showOrders).toBe(false); await page.getByRole('button', { name: '取消' }).click(); @@ -113,7 +113,7 @@ test('confirming with mixed-symbol orders clears only the current symbol', async }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await page.getByRole('button', { name: '确认' }).click(); await expect(page.getByText('HYPEUSDT 撤单流程结束,已恢复筛选状态')).toBeVisible(); @@ -133,7 +133,7 @@ test('an originally enabled symbol filter remains enabled after confirmation', a }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await page.getByRole('button', { name: '确认' }).click(); await expect(page.getByText('HYPEUSDT 撤单流程结束,已恢复筛选状态')).toBeVisible(); @@ -179,7 +179,7 @@ for (const closeMethod of ['Escape', 'backdrop']) { }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); if (closeMethod === 'Escape') { await page.keyboard.press('Escape'); @@ -204,7 +204,7 @@ test('a BFCache pagehide does not abort the active native dialog', async ({ page }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await page.evaluate(() => window.dispatchEvent(new PageTransitionEvent('pagehide', { persisted: true, @@ -224,7 +224,7 @@ test('a real pagehide aborts dialog tracking without mutating orders', async ({ }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await page.evaluate(() => window.dispatchEvent(new PageTransitionEvent('pagehide', { persisted: false, @@ -248,7 +248,7 @@ test('a missing native dialog stops cleanly and restores temporary UI state', as }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByText('HYPEUSDT 未识别到撤单确认弹窗,未继续撤单流程')).toBeVisible({ timeout: 3_000, }); @@ -269,7 +269,7 @@ for (const dialogMode of ['extraAction', 'missingPrimary']) { }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByText( 'HYPEUSDT 撤单确认弹窗结构异常,图表当前委托保持隐藏', )).toBeVisible(); @@ -291,7 +291,7 @@ test('a delayed confirmation keeps visible progress and clears only the current }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await page.getByRole('button', { name: '确认' }).click(); await expect(page.getByText('HYPEUSDT 已确认撤单,等待当前币挂单清空')).toBeVisible(); await expect(page.getByText('HYPEUSDT 撤单流程结束,已恢复筛选状态')).toBeVisible(); @@ -311,7 +311,7 @@ test('dialog tracking survives React replacing the native dialog subtree', async }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect.poll(async () => ( await readFixtureState(page) ).events.filter((event) => event.type === 'dialog-replaced').length).toBe(1); @@ -334,7 +334,7 @@ test('a confirmed dialog that does not clear current orders reports incomplete c }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await page.getByRole('button', { name: '确认' }).click(); await expect(page.getByText('HYPEUSDT 当前币挂单仍存在,撤单流程未完成')).toBeVisible({ timeout: 10_000, @@ -354,7 +354,7 @@ test('a symbol change before the dialog decision stops the captured-symbol workf }); const { errors } = await openUserscriptScenario(page, scenario); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await page.evaluate((symbol) => { history.pushState({}, '', `/zh-CN/futures/${symbol}`); diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js index c4037d0..10aabf8 100644 --- a/e2e/binance-orderbook/specs/control-flows.pw.js +++ b/e2e/binance-orderbook/specs/control-flows.pw.js @@ -93,7 +93,7 @@ test('a precision shortcut selects the exact native orderbook option once', asyn test('starting a ladder disables every start action and exposes one stop control', async ({ page }) => { const scenario = createCancelScenario({ - ui: { tradeMode: 'OPEN', orderbookPrecision: '0.1', ladderExpanded: true }, + ui: { tradeMode: 'OPEN', orderbookPrecision: '0.1' }, }); const { errors } = await openUserscriptScenario(page, scenario); const panel = page.locator(PANEL_SELECTOR); @@ -103,11 +103,11 @@ test('starting a ladder disables every start action and exposes one stop control await expect(startLong).toBeEnabled(); await expect(startShort).toBeEnabled(); - await expect(stop).toBeDisabled(); + await expect(stop).toHaveCount(0); await installInteractionProbe(page, '[data-ladder-action="OPEN_LONG"]'); await startLong.click(); - await expect(startLong).toBeDisabled(); - await expect(startShort).toBeDisabled(); + await expect(startLong).toHaveCount(0); + await expect(startShort).toHaveCount(0); await expect(stop).toBeEnabled(); const submissionsBeforeStop = (await readFixtureState(page)).events .filter((event) => event.type === 'order-submitted').length; @@ -115,7 +115,7 @@ test('starting a ladder disables every start action and exposes one stop control await expect(panel.locator('#jh-binance-ladder-status')).toContainText('已停止'); await expect(startLong).toBeEnabled(); await expect(startShort).toBeEnabled(); - await expect(stop).toBeDisabled(); + await expect(stop).toHaveCount(0); const probe = await finishInteractionProbe(page); assertResponsiveInteraction(expect, probe); @@ -132,7 +132,7 @@ test('starting a ladder disables every start action and exposes one stop control test('a complete ladder submits the planned five native orders and restores controls', async ({ page }) => { test.setTimeout(15_000); const scenario = createCancelScenario({ - ui: { tradeMode: 'OPEN', orderbookPrecision: '0.1', ladderExpanded: true }, + ui: { tradeMode: 'OPEN', orderbookPrecision: '0.1' }, }); const { errors } = await openUserscriptScenario(page, scenario); const panel = page.locator(PANEL_SELECTOR); @@ -144,7 +144,7 @@ test('a complete ladder submits the planned five native orders and restores cont timeout: 12_000, }); await expect(startLong).toBeEnabled(); - await expect(stop).toBeDisabled(); + await expect(stop).toHaveCount(0); const submissions = (await readFixtureState(page)).events .filter((event) => event.type === 'order-submitted'); diff --git a/e2e/binance-orderbook/specs/live-performance-probe.pw.js b/e2e/binance-orderbook/specs/live-performance-probe.pw.js index e2306d9..b814eb0 100644 --- a/e2e/binance-orderbook/specs/live-performance-probe.pw.js +++ b/e2e/binance-orderbook/specs/live-performance-probe.pw.js @@ -22,8 +22,8 @@ test('live probe captures a no-order run and destroys every listener', async ({ const secondArm = await armLivePerformanceProbe(page, 'cancel-current-symbol-no-orders'); expect(secondArm.sessionId).toBe(firstArm.sessionId); - await page.getByRole('button', { name: '撤本币挂单' }).click(); - await expect(page.getByRole('button', { name: '撤本币挂单' })).toBeEnabled(); + await page.getByRole('button', { name: '撤单' }).click(); + await expect(page.getByRole('button', { name: '撤单' })).toBeEnabled(); const snapshot = await finishLivePerformanceProbe(page); expect(() => validateLivePerformanceProbeSnapshot(snapshot)).not.toThrow(); expect(snapshot.events.map((event) => event.kind)).toContain('first-feedback'); @@ -39,7 +39,7 @@ test('live probe captures a no-order run and destroys every listener', async ({ const beforeDestroy = snapshot.events.length; await destroyLivePerformanceProbe(page); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); expect(await page.evaluate(() => window.__BINANCE_LIVE_PERFORMANCE_PROBE__)).toBeUndefined(); expect(snapshot.events).toHaveLength(beforeDestroy); }); @@ -54,7 +54,7 @@ test('live probe has no user-decision deadline and follows a replaced portal dia await installLivePerformanceProbe(page); await armLivePerformanceProbe(page, 'cancel-dialog-cancel'); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await expect(page.getByRole('dialog')).toBeVisible(); await page.waitForTimeout(1_000); const waiting = await page.evaluate(() => window.__BINANCE_LIVE_PERFORMANCE_PROBE__.snapshot()); @@ -78,7 +78,7 @@ test('live probe serializes uncaught errors and unhandled rejections', async ({ await installLivePerformanceProbe(page); await armLivePerformanceProbe(page, 'serializable-errors'); - await page.getByRole('button', { name: '撤本币挂单' }).click(); + await page.getByRole('button', { name: '撤单' }).click(); await page.evaluate(() => { window.dispatchEvent(new ErrorEvent('error', { message: 'probe test error' })); const rejection = new Event('unhandledrejection'); @@ -131,8 +131,8 @@ test('live probe reports overflow and supports raw Runtime.evaluate injection', await page.evaluate(createLivePerformanceProbeExpression({ eventLimit: 1 })); await page.evaluate(() => window.__BINANCE_LIVE_PERFORMANCE_PROBE__.arm('overflow')); - await page.getByRole('button', { name: '撤本币挂单' }).click(); - await expect(page.getByRole('button', { name: '撤本币挂单' })).toBeEnabled(); + await page.getByRole('button', { name: '撤单' }).click(); + await expect(page.getByRole('button', { name: '撤单' })).toBeEnabled(); const snapshot = await page.evaluate(() => window.__BINANCE_LIVE_PERFORMANCE_PROBE__.finish()); expect(snapshot.dropped.events).toBeGreaterThan(0); expect(() => validateLivePerformanceProbeSnapshot(snapshot)).toThrow(/events overflowed/); diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js index 4d44f03..8b2ca26 100644 --- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js @@ -19,30 +19,22 @@ async function expectVisualContract(page, name) { expect(`${JSON.stringify(contract, null, 2)}\n`).toMatchSnapshot(name); } -test('open expanded panel matches the stable visual contract', async ({ page }) => { +test('open panel matches the fixed visual contract', async ({ page }) => { await openUserscriptScenario(page, createCancelScenario({ - ui: { tradeMode: 'OPEN', ladderExpanded: true }, + ui: { tradeMode: 'OPEN' }, })); await expectPrecisionReady(page); - await expectVisualContract(page, 'open-expanded.visual.json'); + await expectVisualContract(page, 'open-fixed.visual.json'); }); -test('close expanded panel matches the disabled-state visual contract', async ({ page }) => { +test('close panel matches the disabled-state visual contract', async ({ page }) => { await openUserscriptScenario(page, createCancelScenario({ positions: POSITION_SETS.current, - ui: { tradeMode: 'CLOSE', ladderExpanded: true }, + ui: { tradeMode: 'CLOSE' }, })); const panel = page.locator(PANEL_SELECTOR); await expect(panel.getByRole('radio', { name: '平多' })).toBeEnabled(); await expect(panel.getByRole('radio', { name: '平空' })).toBeDisabled(); await expectPrecisionReady(page); - await expectVisualContract(page, 'close-expanded.visual.json'); -}); - -test('collapsed ladder keeps the compact panel visual contract', async ({ page }) => { - await openUserscriptScenario(page, createCancelScenario({ - ui: { tradeMode: 'OPEN', ladderExpanded: false }, - })); - await expectPrecisionReady(page); - await expectVisualContract(page, 'open-collapsed.visual.json'); + await expectVisualContract(page, 'close-fixed.visual.json'); }); diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-expanded-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json similarity index 84% rename from e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-expanded-visual.json rename to e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json index 80ad890..78316ce 100644 --- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-expanded-visual.json +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json @@ -1,14 +1,14 @@ { "panel": { "width": 314, - "height": 467, + "height": 407, "padding": "8px 10px", "borderRadius": "10px" }, "groups": { "direction": { "rect": { - "top": 9, + "top": 29, "width": 292, "height": 32 }, @@ -17,27 +17,27 @@ }, "multiplier": { "rect": { - "top": 53, + "top": 69, "width": 292, "height": 54 }, "display": "block", "visibility": "visible" }, - "precision": { + "ladder": { "rect": { - "top": 119, + "top": 135, "width": 292, - "height": 62 + "height": 263 }, "display": "block", "visibility": "visible" }, - "ladder": { + "precision": { "rect": { - "top": 193, + "top": 298, "width": 292, - "height": 265 + "height": 32 }, "display": "block", "visibility": "visible" @@ -51,7 +51,7 @@ "checked": "true", "pressed": null, "rect": { - "top": 9, + "top": 29, "width": 54, "height": 30 }, @@ -72,7 +72,7 @@ "checked": "false", "pressed": null, "rect": { - "top": 9, + "top": 29, "width": 54, "height": 30 }, @@ -93,7 +93,7 @@ "checked": null, "pressed": null, "rect": { - "top": 53, + "top": 69, "width": 60, "height": 32 }, @@ -114,7 +114,7 @@ "checked": null, "pressed": null, "rect": { - "top": 53, + "top": 69, "width": 32, "height": 32 }, @@ -135,7 +135,7 @@ "checked": null, "pressed": null, "rect": { - "top": 53, + "top": 69, "width": 32, "height": 32 }, @@ -149,111 +149,6 @@ "opacity": "1" } }, - { - "key": "0.001", - "text": "0.001", - "disabled": false, - "checked": null, - "pressed": "false", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "0.01", - "text": "0.01", - "disabled": false, - "checked": null, - "pressed": "false", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "0.1", - "text": "0.1", - "disabled": false, - "checked": null, - "pressed": "true", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(0, 0, 0)", - "backgroundColor": "rgba(0, 0, 0, 0)", - "borderColor": "rgb(0, 0, 0)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "1", - "text": "1", - "disabled": false, - "checked": null, - "pressed": "false", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "jh-binance-ladder-toggle", - "text": "Maker 阶梯 ▾", - "disabled": false, - "checked": null, - "pressed": null, - "rect": { - "top": 206, - "width": 292, - "height": 28 - }, - "style": { - "color": "rgb(0, 0, 0)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "13px", - "fontWeight": "500", - "opacity": "1" - } - }, { "key": "percent:0.3", "text": "0.3%", @@ -261,8 +156,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -282,8 +177,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -303,8 +198,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -324,8 +219,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -345,8 +240,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -366,8 +261,8 @@ "checked": null, "pressed": null, "rect": { - "top": 283, - "width": 48.8, + "top": 217, + "width": 44.8, "height": 28 }, "style": { @@ -387,8 +282,8 @@ "checked": null, "pressed": null, "rect": { - "top": 283, - "width": 48.8, + "top": 217, + "width": 44.8, "height": 28 }, "style": { @@ -408,8 +303,8 @@ "checked": null, "pressed": null, "rect": { - "top": 283, - "width": 48.8, + "top": 217, + "width": 44.8, "height": 28 }, "style": { @@ -429,8 +324,8 @@ "checked": null, "pressed": null, "rect": { - "top": 283, - "width": 48.8, + "top": 217, + "width": 44.8, "height": 28 }, "style": { @@ -450,8 +345,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -471,8 +366,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -492,8 +387,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -513,8 +408,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -534,8 +429,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -549,56 +444,119 @@ } }, { - "key": "CLOSE_LONG", - "text": "阶梯平多", + "key": "0.001", + "text": "0.001", "disabled": false, "checked": null, - "pressed": null, + "pressed": "false", "rect": { - "top": 366, - "width": 144, + "top": 298, + "width": 44.5, "height": 32 }, "style": { - "color": "rgb(30, 35, 41)", + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.01", + "text": "0.01", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 298, + "width": 44.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.1", + "text": "0.1", + "disabled": false, + "checked": null, + "pressed": "true", + "rect": { + "top": 298, + "width": 44.5, + "height": 32 + }, + "style": { + "color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)", - "borderColor": "rgb(30, 35, 41)", + "borderColor": "rgb(0, 0, 0)", "borderRadius": "6px", - "fontSize": "14px", + "fontSize": "12px", "fontWeight": "500", "opacity": "1" } }, { - "key": "CLOSE_SHORT", - "text": "阶梯平空", - "disabled": true, + "key": "1", + "text": "1", + "disabled": false, "checked": null, - "pressed": null, + "pressed": "false", "rect": { - "top": 366, - "width": 144, + "top": 298, + "width": 44.5, "height": 32 }, "style": { - "color": "rgb(183, 189, 198)", - "backgroundColor": "rgb(245, 245, 245)", + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", "borderColor": "rgb(213, 217, 226)", "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "CLOSE_LONG", + "text": "阶梯平多", + "disabled": false, + "checked": null, + "pressed": null, + "rect": { + "top": 342, + "width": 94.7, + "height": 32 + }, + "style": { + "color": "rgb(30, 35, 41)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(30, 35, 41)", + "borderRadius": "6px", "fontSize": "14px", "fontWeight": "500", - "opacity": "0.65" + "opacity": "1" } }, { - "key": "true", - "text": "停止阶梯挂单", + "key": "CLOSE_SHORT", + "text": "阶梯平空", "disabled": true, "checked": null, "pressed": null, "rect": { - "top": 402, - "width": 144, + "top": 342, + "width": 94.7, "height": 32 }, "style": { @@ -613,13 +571,13 @@ }, { "key": "true", - "text": "撤本币挂单", + "text": "撤单", "disabled": false, "checked": null, "pressed": null, "rect": { - "top": 402, - "width": 144, + "top": 342, + "width": 94.7, "height": 32 }, "style": { @@ -636,7 +594,7 @@ "text": { "mode": "单击订单簿时", "formula": "0.01 × 1 =0.01", - "precision": "订单簿缩放0.0010.010.11推荐 0.01更新推荐", + "precision": "价格精度0.0010.010.11", "status": "空闲" } } diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-collapsed-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-collapsed-visual.json deleted file mode 100644 index f758f67..0000000 --- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-collapsed-visual.json +++ /dev/null @@ -1,264 +0,0 @@ -{ - "panel": { - "width": 314, - "height": 267, - "padding": "8px 10px", - "borderRadius": "10px" - }, - "groups": { - "direction": { - "rect": { - "top": 9, - "width": 292, - "height": 32 - }, - "display": "flex", - "visibility": "visible" - }, - "multiplier": { - "rect": { - "top": 53, - "width": 292, - "height": 54 - }, - "display": "block", - "visibility": "visible" - }, - "precision": { - "rect": { - "top": 119, - "width": 292, - "height": 62 - }, - "display": "block", - "visibility": "visible" - }, - "ladder": { - "rect": { - "top": 193, - "width": 292, - "height": 65 - }, - "display": "block", - "visibility": "visible" - } - }, - "controls": [ - { - "key": "jh-binance-close-side-long", - "text": "开多", - "disabled": false, - "checked": "true", - "pressed": null, - "rect": { - "top": 9, - "width": 54, - "height": 30 - }, - "style": { - "color": "rgb(30, 35, 41)", - "backgroundColor": "rgba(0, 0, 0, 0)", - "borderColor": "rgb(30, 35, 41)", - "borderRadius": "5px 0px 0px 5px", - "fontSize": "14px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "jh-binance-close-side-short", - "text": "开空", - "disabled": false, - "checked": "false", - "pressed": null, - "rect": { - "top": 9, - "width": 54, - "height": 30 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(94, 102, 115)", - "borderRadius": "0px 5px 5px 0px", - "fontSize": "14px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "jh-binance-close-qty-multiplier-input", - "text": "", - "disabled": false, - "checked": null, - "pressed": null, - "rect": { - "top": 53, - "width": 60, - "height": 32 - }, - "style": { - "color": "rgb(0, 0, 0)", - "backgroundColor": "rgba(0, 0, 0, 0)", - "borderColor": "rgb(0, 0, 0)", - "borderRadius": "8px", - "fontSize": "15px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "jh-binance-close-qty-multiplier-dec", - "text": "-", - "disabled": true, - "checked": null, - "pressed": null, - "rect": { - "top": 53, - "width": 32, - "height": 32 - }, - "style": { - "color": "rgb(183, 189, 198)", - "backgroundColor": "rgb(245, 245, 245)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "18px", - "fontWeight": "500", - "opacity": "0.65" - } - }, - { - "key": "jh-binance-close-qty-multiplier-inc", - "text": "+", - "disabled": false, - "checked": null, - "pressed": null, - "rect": { - "top": 53, - "width": 32, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "18px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "0.001", - "text": "0.001", - "disabled": false, - "checked": null, - "pressed": "false", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "0.01", - "text": "0.01", - "disabled": false, - "checked": null, - "pressed": "false", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "0.1", - "text": "0.1", - "disabled": false, - "checked": null, - "pressed": "true", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(0, 0, 0)", - "backgroundColor": "rgba(0, 0, 0, 0)", - "borderColor": "rgb(0, 0, 0)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "1", - "text": "1", - "disabled": false, - "checked": null, - "pressed": "false", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "jh-binance-ladder-toggle", - "text": "Maker 阶梯 ▸", - "disabled": false, - "checked": null, - "pressed": null, - "rect": { - "top": 206, - "width": 292, - "height": 28 - }, - "style": { - "color": "rgb(0, 0, 0)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "13px", - "fontWeight": "500", - "opacity": "1" - } - } - ], - "text": { - "mode": "单击订单簿时", - "formula": "0.07 × 1 =0.07≥5U @ 81", - "precision": "订单簿缩放0.0010.010.11推荐 0.01更新推荐", - "status": "空闲" - } -} diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-expanded-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json similarity index 84% rename from e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-expanded-visual.json rename to e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json index 7d13574..5147355 100644 --- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-expanded-visual.json +++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json @@ -1,14 +1,14 @@ { "panel": { "width": 314, - "height": 467, + "height": 407, "padding": "8px 10px", "borderRadius": "10px" }, "groups": { "direction": { "rect": { - "top": 9, + "top": 29, "width": 292, "height": 32 }, @@ -17,27 +17,27 @@ }, "multiplier": { "rect": { - "top": 53, + "top": 69, "width": 292, "height": 54 }, "display": "block", "visibility": "visible" }, - "precision": { + "ladder": { "rect": { - "top": 119, + "top": 135, "width": 292, - "height": 62 + "height": 263 }, "display": "block", "visibility": "visible" }, - "ladder": { + "precision": { "rect": { - "top": 193, + "top": 298, "width": 292, - "height": 265 + "height": 32 }, "display": "block", "visibility": "visible" @@ -51,7 +51,7 @@ "checked": "true", "pressed": null, "rect": { - "top": 9, + "top": 29, "width": 54, "height": 30 }, @@ -72,7 +72,7 @@ "checked": "false", "pressed": null, "rect": { - "top": 9, + "top": 29, "width": 54, "height": 30 }, @@ -93,7 +93,7 @@ "checked": null, "pressed": null, "rect": { - "top": 53, + "top": 69, "width": 60, "height": 32 }, @@ -114,7 +114,7 @@ "checked": null, "pressed": null, "rect": { - "top": 53, + "top": 69, "width": 32, "height": 32 }, @@ -135,7 +135,7 @@ "checked": null, "pressed": null, "rect": { - "top": 53, + "top": 69, "width": 32, "height": 32 }, @@ -149,111 +149,6 @@ "opacity": "1" } }, - { - "key": "0.001", - "text": "0.001", - "disabled": false, - "checked": null, - "pressed": "false", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "0.01", - "text": "0.01", - "disabled": false, - "checked": null, - "pressed": "false", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "0.1", - "text": "0.1", - "disabled": false, - "checked": null, - "pressed": "true", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(0, 0, 0)", - "backgroundColor": "rgba(0, 0, 0, 0)", - "borderColor": "rgb(0, 0, 0)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "1", - "text": "1", - "disabled": false, - "checked": null, - "pressed": "false", - "rect": { - "top": 119, - "width": 49.5, - "height": 32 - }, - "style": { - "color": "rgb(94, 102, 115)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "12px", - "fontWeight": "500", - "opacity": "1" - } - }, - { - "key": "jh-binance-ladder-toggle", - "text": "Maker 阶梯 ▾", - "disabled": false, - "checked": null, - "pressed": null, - "rect": { - "top": 206, - "width": 292, - "height": 28 - }, - "style": { - "color": "rgb(0, 0, 0)", - "backgroundColor": "rgb(255, 255, 255)", - "borderColor": "rgb(213, 217, 226)", - "borderRadius": "6px", - "fontSize": "13px", - "fontWeight": "500", - "opacity": "1" - } - }, { "key": "percent:2", "text": "2%", @@ -261,8 +156,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -282,8 +177,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -303,8 +198,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -324,8 +219,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -345,8 +240,8 @@ "checked": null, "pressed": null, "rect": { - "top": 243, - "width": 48.8, + "top": 177, + "width": 44.8, "height": 28 }, "style": { @@ -366,8 +261,8 @@ "checked": null, "pressed": null, "rect": { - "top": 283, - "width": 48.8, + "top": 217, + "width": 44.8, "height": 28 }, "style": { @@ -387,8 +282,8 @@ "checked": null, "pressed": null, "rect": { - "top": 283, - "width": 48.8, + "top": 217, + "width": 44.8, "height": 28 }, "style": { @@ -408,8 +303,8 @@ "checked": null, "pressed": null, "rect": { - "top": 283, - "width": 48.8, + "top": 217, + "width": 44.8, "height": 28 }, "style": { @@ -429,8 +324,8 @@ "checked": null, "pressed": null, "rect": { - "top": 283, - "width": 48.8, + "top": 217, + "width": 44.8, "height": 28 }, "style": { @@ -450,8 +345,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -471,8 +366,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -492,8 +387,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -513,8 +408,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -534,8 +429,8 @@ "checked": null, "pressed": null, "rect": { - "top": 323, - "width": 48.8, + "top": 257, + "width": 44.8, "height": 28 }, "style": { @@ -549,35 +444,98 @@ } }, { - "key": "OPEN_LONG", - "text": "阶梯开多", + "key": "0.001", + "text": "0.001", "disabled": false, "checked": null, - "pressed": null, + "pressed": "false", "rect": { - "top": 366, - "width": 144, + "top": 298, + "width": 44.5, "height": 32 }, "style": { - "color": "rgb(30, 35, 41)", + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.01", + "text": "0.01", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 298, + "width": 44.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "0.1", + "text": "0.1", + "disabled": false, + "checked": null, + "pressed": "true", + "rect": { + "top": 298, + "width": 44.5, + "height": 32 + }, + "style": { + "color": "rgb(0, 0, 0)", "backgroundColor": "rgba(0, 0, 0, 0)", - "borderColor": "rgb(30, 35, 41)", + "borderColor": "rgb(0, 0, 0)", "borderRadius": "6px", - "fontSize": "14px", + "fontSize": "12px", "fontWeight": "500", "opacity": "1" } }, { - "key": "OPEN_SHORT", - "text": "阶梯开空", + "key": "1", + "text": "1", + "disabled": false, + "checked": null, + "pressed": "false", + "rect": { + "top": 298, + "width": 44.5, + "height": 32 + }, + "style": { + "color": "rgb(94, 102, 115)", + "backgroundColor": "rgb(255, 255, 255)", + "borderColor": "rgb(213, 217, 226)", + "borderRadius": "6px", + "fontSize": "12px", + "fontWeight": "500", + "opacity": "1" + } + }, + { + "key": "OPEN_LONG", + "text": "阶梯开多", "disabled": false, "checked": null, "pressed": null, "rect": { - "top": 366, - "width": 144, + "top": 342, + "width": 94.7, "height": 32 }, "style": { @@ -591,35 +549,35 @@ } }, { - "key": "true", - "text": "停止阶梯挂单", - "disabled": true, + "key": "OPEN_SHORT", + "text": "阶梯开空", + "disabled": false, "checked": null, "pressed": null, "rect": { - "top": 402, - "width": 144, + "top": 342, + "width": 94.7, "height": 32 }, "style": { - "color": "rgb(183, 189, 198)", - "backgroundColor": "rgb(245, 245, 245)", - "borderColor": "rgb(213, 217, 226)", + "color": "rgb(30, 35, 41)", + "backgroundColor": "rgba(0, 0, 0, 0)", + "borderColor": "rgb(30, 35, 41)", "borderRadius": "6px", "fontSize": "14px", "fontWeight": "500", - "opacity": "0.65" + "opacity": "1" } }, { "key": "true", - "text": "撤本币挂单", + "text": "撤单", "disabled": false, "checked": null, "pressed": null, "rect": { - "top": 402, - "width": 144, + "top": 342, + "width": 94.7, "height": 32 }, "style": { @@ -636,7 +594,7 @@ "text": { "mode": "单击订单簿时", "formula": "0.07 × 1 =0.07≥5U @ 81", - "precision": "订单簿缩放0.0010.010.11推荐 0.01更新推荐", + "precision": "价格精度0.0010.010.11", "status": "空闲" } } diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js index d04ebc3..5201afb 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.120 +// @version 2.7.121 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -123,6 +123,50 @@ return matchesBinancePageText(value, BINANCE_PAGE_TEXT.accountOrders.cancelAll); } + // src/binance-orderbook-trade/contracts/panel-copy.js + var freezeCopy = (copy) => Object.freeze(copy); + var PANEL_COPY = Object.freeze({ + section: freezeCopy({ + singleOrder: "单击下单", + ladderMaker: "阶梯下单 · Maker" + }), + field: freezeCopy({ + clickOrderbook: "单击订单簿时", + minimumOrderQuantity: "最小下单量的", + minimumOpenQuantity: "最小开仓量的", + minimumCloseQuantity: "最小平仓量的", + ratio: "比例", + orderCount: "笔数", + interval: "间距", + pricePrecision: "价格精度" + }), + action: freezeCopy({ + openLong: "阶梯开多", + openShort: "阶梯开空", + closeLong: "阶梯平多", + closeShort: "阶梯平空", + cancel: "撤单", + cancelRunning: "撤单处理中", + noOrders: "无挂单", + stopLadder: "停止阶梯挂单" + }), + tooltip: freezeCopy({ + singleOrder: "单击订单簿中的某个价格,按当前方向和数量设置提交一笔订单。", + ladderMaker: "根据当前比例、笔数、间距和价格精度设置,依次提交只做 Maker 的阶梯订单。", + ratio: "本次阶梯下单使用可开/可平数量的百分比。", + orderCount: "计划拆分成多少笔阶梯订单。", + interval: "相邻订单跨越多少个订单簿价格级别。", + pricePrecision: "与订单簿中的价格精度联动。黄点表示推荐值。比例、笔数、间距会随所选精度恢复对应设置。" + }) + }); + function formatPrecisionRefreshTooltip(durationMs) { + const seconds = Number(durationMs) / 1e3; + if (!Number.isInteger(seconds) || seconds <= 0) { + throw new Error(`Invalid precision sample duration: ${durationMs}`); + } + return `根据最近 ${seconds} 秒的成交价变动,重新计算推荐的价格精度。`; + } + // src/binance-orderbook-trade/core/cancel-orders.js var PERPETUAL_LABEL_PATTERN = buildBinanceTextAlternation( BINANCE_PAGE_TEXT.accountOrders.perpetual @@ -260,7 +304,7 @@ }) { return { disabled: Boolean(ladderRunning || cancelRunning), - label: cancelRunning ? "撤单处理中" : noOrdersFeedback && !ladderRunning ? "无挂单" : "撤本币挂单" + label: cancelRunning ? PANEL_COPY.action.cancelRunning : noOrdersFeedback && !ladderRunning ? PANEL_COPY.action.noOrders : PANEL_COPY.action.cancel }; } function hasCurrentSymbolOpenOrdersEvidence({ @@ -1706,7 +1750,6 @@ const LOCAL_QTY_MULTIPLIER_PREFIX = "jh_binance_qty_multiplier_v2"; const LOCAL_CLOSE_SIDE_KEY = "jh_binance_close_side"; const LOCAL_OPEN_SIDE_KEY = "jh_binance_open_side"; - const LOCAL_LADDER_EXPANDED_KEY = "jh_binance_ladder_expanded"; const LOCAL_LADDER_OPEN_PERCENT_KEY = "jh_binance_ladder_open_percent"; const LOCAL_LADDER_CLOSE_PERCENT_KEY = "jh_binance_ladder_close_percent"; const LOCAL_LADDER_OPEN_LEVELS_KEY = "jh_binance_ladder_open_levels"; @@ -1730,7 +1773,6 @@ const INC_ID = "jh-binance-close-qty-multiplier-inc"; const SIDE_LONG_ID = "jh-binance-close-side-long"; const SIDE_SHORT_ID = "jh-binance-close-side-short"; - const LADDER_TOGGLE_ID = "jh-binance-ladder-toggle"; const LADDER_BODY_ID = "jh-binance-ladder-body"; const LADDER_STATUS_ID = "jh-binance-ladder-status"; const ORDERBOOK_PRECISION_RECOMMENDATION_ID = "jh-binance-orderbook-precision-recommendation"; @@ -1790,7 +1832,6 @@ 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_DURATION_MS = ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS; const ORDERBOOK_PRECISION_SAMPLE_POLL_MS = 300; const ORDERBOOK_PRECISION_READY_POLL_MS = 100; const ORDERBOOK_PRECISION_READY_TIMEOUT_MS = 5e3; @@ -1887,7 +1928,6 @@ let orderbookPrecisionObserver = null; let orderbookPrecisionObserverRoot = null; let lastObservedOrderbookPrecision = null; - const orderbookPrecisionInitialSampledSymbols = /* @__PURE__ */ new Set(); let orderbookPrecisionState = { symbol: null, samples: [], @@ -1895,7 +1935,7 @@ current: null, nativeOptions: [], nativeOptionsStatus: null, - status: "采样中", + status: "数据不足", sampleEndsAt: 0 }; const controlledNativeButtons = /* @__PURE__ */ new Set(); @@ -2009,13 +2049,6 @@ window.setTimeout(resolve, ms); }); } - function isLadderExpanded() { - return localStorage.getItem(LOCAL_LADDER_EXPANDED_KEY) === "true"; - } - function setLadderExpanded(expanded) { - localStorage.setItem(LOCAL_LADDER_EXPANDED_KEY, expanded ? "true" : "false"); - scheduleRenderPanel(); - } function getLadderOpenPercent(symbol = getCurrentSymbol(), precision = readCurrentOrderbookPrecisionValue()) { return loadModeSymbolPrecisionNumberOption( localStorage, @@ -2739,22 +2772,14 @@ } return false; } - function formatOrderbookPrecisionBusyStatus(status, sampleEndsAt = orderbookPrecisionState.sampleEndsAt) { - if (status !== "刷新中" && status !== "采样中") return status; - const remainingMs = Number(sampleEndsAt) - Date.now(); - if (!(remainingMs > 0)) return status; - const remainingSeconds = Math.max(1, Math.ceil(remainingMs / 1e3)); - if (status === "刷新中") return `刷新中 ${remainingSeconds}s`; - return `采样中 ${remainingSeconds}s`; - } function renderOrderbookPrecisionShortcut(value, current, recommendation, disabled) { const selected = value === current; const recommended = value === recommendation; const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : ""; - const title = disabled ? "缩放调整暂不可用" : selected && recommended ? `当前且推荐的原生缩放 ${value}` : selected ? `当前原生缩放 ${value}` : recommended ? `推荐原生缩放 ${value}` : `切换到原生缩放 ${value}`; + const title = disabled ? "缩放调整暂不可用" : selected && recommended ? `当前且推荐的价格精度 ${value}` : selected ? `当前价格精度 ${value}` : recommended ? `推荐价格精度 ${value}` : `切换到价格精度 ${value}`; const activeStyle = selected ? `border-color:var(--color-PrimaryYellow);background:var(--color-BadgeBg);color:${PRIMARY_EMPHASIS_COLOR};font-weight:${PRIMARY_EMPHASIS_FONT_WEIGHT};` : NEUTRAL_CONTROL_STYLE; const recommendationMarker = recommended ? '' : ""; - return ``; + return ``; } function renderOrderbookPrecisionShortcutSlots(options, current, recommendation, disabled) { const slots = options.map((value) => renderOrderbookPrecisionShortcut(value, current, recommendation, disabled)); @@ -2795,34 +2820,25 @@ const selectionBusy = Boolean(orderbookPrecisionSelectionTask); const controlsBusy = busy || selectionBusy; const nativeOptions = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.nativeOptions : []; - const nativeOptionsStatus = !nativeOptions.length && orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.nativeOptionsStatus : null; const shortcutOptions = getOrderbookPrecisionShortcutOptions( nativeOptions, ORDERBOOK_PRECISION_SHORTCUT_LIMIT ); if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol); const canRefresh = !controlsBusy; - const buttonBaseStyle = `width:68px;height:24px;padding:0;border-radius:5px;border:1px solid ${CONTROL_BORDER_COLOR};font-size:12px;line-height:22px;`; - const recommendationText = recommendation || "--"; - const precisionMessage = selectionBusy ? nativeOptions.length ? "调整中" : "读取档位" : busy ? formatOrderbookPrecisionBusyStatus(status) : nativeOptionsStatus ? nativeOptionsStatus : status === "ready" ? `推荐 ${recommendationText}${recommendation && !shortcutOptions.includes(recommendation) ? "(原生)" : ""}` : status; + const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS); const recommendationHtml = [ - `
`, - '
', - `订单簿缩放`, + '
', + '
', + `${PANEL_COPY.field.pricePrecision}`, ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy), - "
", - '
', - `${precisionMessage}`, - ``, + ``, "
", "
" ].join(""); if (el.innerHTML !== recommendationHtml) { el.innerHTML = recommendationHtml; } - if (busy && Number(orderbookPrecisionState.sampleEndsAt) > Date.now()) { - scheduleRenderPanel({ followUpMs: 1e3 }); - } } async function clickAndConfirmOrderbookPrecisionOption(options) { const { symbol, startPrecision, targetPrecision, triggerElement } = options; @@ -2998,7 +3014,7 @@ orderbookPrecisionSampling = true; orderbookPrecisionActiveRequest = request; const tradeMoveSamples = []; - const sampleDurationMs = Math.max(0, Number(request.durationMs) || ORDERBOOK_PRECISION_SAMPLE_DURATION_MS); + 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; @@ -3032,7 +3048,6 @@ status: recommendation ? "ready" : "数据不足", sampleEndsAt: 0 }; - if (request.initial) orderbookPrecisionInitialSampledSymbols.add(symbol); refreshOrderbookPrecisionRecommendation(); scheduleRenderPanel(); return true; @@ -3049,19 +3064,14 @@ function scheduleOrderbookPrecisionSampleRound(delayMs = 0, options) { const { force = false, - durationMs = ORDERBOOK_PRECISION_SAMPLE_DURATION_MS, - initial = 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, initial }; + const request = { symbol, durationMs }; if (orderbookPrecisionSampling) { - const sameInitialIsActive = initial && orderbookPrecisionActiveRequest?.initial && orderbookPrecisionActiveRequest?.symbol === symbol; - const sameInitialIsPending = initial && orderbookPrecisionPendingRequest?.initial && orderbookPrecisionPendingRequest?.symbol === symbol; - if (force && !sameInitialIsActive && !sameInitialIsPending) { - orderbookPrecisionPendingRequest = request; - } + if (force) orderbookPrecisionPendingRequest = request; return; } if (orderbookPrecisionSampling || orderbookPrecisionSampleTimer) return; @@ -3075,21 +3085,6 @@ orderbookPrecisionSampleTimer = 0; orderbookPrecisionPendingRequest = null; } - function startInitialOrderbookPrecisionSample() { - const symbol = getCurrentSymbol(); - if (!isCurrentObservedSymbol(symbol) || orderbookPrecisionInitialSampledSymbols.has(symbol)) return; - orderbookPrecisionState = { - ...orderbookPrecisionState, - symbol, - status: "采样中", - sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_SAMPLE_DURATION_MS - }; - scheduleOrderbookPrecisionSampleRound(0, { - force: true, - durationMs: ORDERBOOK_PRECISION_SAMPLE_DURATION_MS, - initial: true - }); - } function refreshOrderbookPrecisionSamplesNow() { const symbol = getCurrentSymbol(); orderbookPrecisionState = { @@ -5574,10 +5569,10 @@ const activeStyle = selected ? `border-color:var(--color-PrimaryYellow);background:var(--color-BadgeBg);color:${PRIMARY_EMPHASIS_COLOR};font-weight:${PRIMARY_EMPHASIS_FONT_WEIGHT};` : NEUTRAL_CONTROL_STYLE; return ``; } - function ladderOptionRow(title, options, selected, group, suffix = "") { + function ladderOptionRow(title, tooltip, options, selected, group, suffix = "") { return [ - '
', - `${title}`, + '
', + `${title}`, ...options.map((value) => ladderOptionButton(`${value}${suffix}`, value, Number(value) === Number(selected), group)), "
" ].join(""); @@ -5589,25 +5584,33 @@ const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : ""; return ``; } - function getLadderActionRows(tradeMode, closeContext, symbol, precision) { + function getLadderControlSections(tradeMode, closeContext, symbol, precision) { const ladderRunning = !!ladderTask; const actionDisabled = ladderRunning || cancelCurrentSymbolOpenOrdersBlocksLadderActions; if (!["OPEN", "CLOSE"].includes(tradeMode)) { - return [`
等待开仓/平仓状态
`]; + return { + optionRows: [`
等待开仓/平仓状态
`], + actionButtons: [] + }; } if (!precision) { - return [`
等待订单簿缩放值
`]; + return { + optionRows: [`
等待价格精度
`], + actionButtons: [] + }; } if (tradeMode === "OPEN") { - return [ - ladderOptionRow("量", LADDER_OPEN_PERCENTS, getLadderOpenPercent(symbol, precision), "percent", "%"), - ladderOptionRow("档", LADDER_LEVEL_OPTIONS, getLadderLevels(tradeMode, symbol, precision), "levels", ""), - ladderOptionRow("幅", LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), "step", ""), - '
', - ladderActionButton("OPEN_LONG", "阶梯开多", "BUY", actionDisabled), - ladderActionButton("OPEN_SHORT", "阶梯开空", "SELL", actionDisabled), - "
" - ]; + return { + optionRows: [ + ladderOptionRow(PANEL_COPY.field.ratio, PANEL_COPY.tooltip.ratio, LADDER_OPEN_PERCENTS, getLadderOpenPercent(symbol, precision), "percent", "%"), + ladderOptionRow(PANEL_COPY.field.orderCount, PANEL_COPY.tooltip.orderCount, LADDER_LEVEL_OPTIONS, getLadderLevels(tradeMode, symbol, precision), "levels", ""), + ladderOptionRow(PANEL_COPY.field.interval, PANEL_COPY.tooltip.interval, LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), "step", "") + ], + actionButtons: [ + ladderActionButton("OPEN_LONG", PANEL_COPY.action.openLong, "BUY", actionDisabled), + ladderActionButton("OPEN_SHORT", PANEL_COPY.action.openShort, "SELL", actionDisabled) + ] + }; } const closeLongDisabled = shouldDisableCloseControl({ actionDisabled, @@ -5619,75 +5622,68 @@ knowsPosition: closeContext?.knowsShort, hasPosition: closeContext?.hasShort }); - return [ - ladderOptionRow("量", LADDER_CLOSE_PERCENTS, getLadderClosePercent(symbol, precision), "percent", "%"), - ladderOptionRow("档", LADDER_LEVEL_OPTIONS, getLadderLevels(tradeMode, symbol, precision), "levels", ""), - ladderOptionRow("幅", LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), "step", ""), - '
', - ladderActionButton("CLOSE_LONG", "阶梯平多", "SELL", closeLongDisabled), - ladderActionButton("CLOSE_SHORT", "阶梯平空", "BUY", closeShortDisabled), - "
" - ]; + return { + optionRows: [ + ladderOptionRow(PANEL_COPY.field.ratio, PANEL_COPY.tooltip.ratio, LADDER_CLOSE_PERCENTS, getLadderClosePercent(symbol, precision), "percent", "%"), + ladderOptionRow(PANEL_COPY.field.orderCount, PANEL_COPY.tooltip.orderCount, LADDER_LEVEL_OPTIONS, getLadderLevels(tradeMode, symbol, precision), "levels", ""), + ladderOptionRow(PANEL_COPY.field.interval, PANEL_COPY.tooltip.interval, LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), "step", "") + ], + actionButtons: [ + ladderActionButton("CLOSE_LONG", PANEL_COPY.action.closeLong, "SELL", closeLongDisabled), + ladderActionButton("CLOSE_SHORT", PANEL_COPY.action.closeShort, "BUY", closeShortDisabled) + ] + }; } function refreshLadderPanel(panel, tradeMode, closeContext) { - const toggle = panel.querySelector(`#${LADDER_TOGGLE_ID}`); const body = panel.querySelector(`#${LADDER_BODY_ID}`); const status = panel.querySelector(`#${LADDER_STATUS_ID}`); - const expanded = isLadderExpanded(); const mode = ["OPEN", "CLOSE"].includes(tradeMode) ? tradeMode : null; const symbol = getCurrentSymbol(); const precision = readCurrentOrderbookPrecisionValue(); - if (toggle) { - const toggleText = `Maker 阶梯 ${expanded ? "▾" : "▸"}`; - if (toggle.textContent !== toggleText) toggle.textContent = toggleText; - } if (body) { - body.style.display = expanded ? "block" : "none"; - if (expanded) { - const stopDisabled = !ladderTask; - const stopDisabledAttrs = stopDisabled ? ' disabled aria-disabled="true"' : ""; - const cancelRunning = !!cancelCurrentSymbolOpenOrdersTask; - const cancelPresentation = resolveCancelSymbolButtonPresentation({ - ladderRunning: !!ladderTask, - cancelRunning, - noOrdersFeedback: cancelNoOrdersFeedbackActive - }); - const bodyHtml = [ - ...getLadderActionRows(mode, closeContext, symbol, precision), - '
', - ``, - ``, - "
" - ].join(""); - if (ladderPanelBodySignature !== bodyHtml) { - body.innerHTML = bodyHtml; - ladderPanelBodySignature = bodyHtml; + const ladderRunning = !!ladderTask; + const cancelRunning = !!cancelCurrentSymbolOpenOrdersTask; + const cancelPresentation = resolveCancelSymbolButtonPresentation({ + ladderRunning, + cancelRunning, + noOrdersFeedback: cancelNoOrdersFeedbackActive + }); + const controlSections = getLadderControlSections(mode, closeContext, symbol, precision); + const primaryActions = ladderRunning ? [``] : controlSections.actionButtons; + const actionColumnCount = Math.max(1, primaryActions.length + 1); + const bodyHtml = [ + ...controlSections.optionRows, + `
`, + `
`, + ...primaryActions, + ``, + "
" + ].join(""); + if (ladderPanelBodySignature !== bodyHtml) { + body.innerHTML = bodyHtml; + ladderPanelBodySignature = bodyHtml; + } + const cancelButton = body.querySelector('[data-ladder-cancel-symbol="true"]'); + if (cancelButton) { + if (cancelButton.textContent !== cancelPresentation.label) { + cancelButton.textContent = cancelPresentation.label; } - const cancelButton = body.querySelector('[data-ladder-cancel-symbol="true"]'); - if (cancelButton) { - if (cancelButton.textContent !== cancelPresentation.label) { - cancelButton.textContent = cancelPresentation.label; - } - if (cancelButton.disabled !== cancelPresentation.disabled) { - cancelButton.disabled = cancelPresentation.disabled; - } - if (cancelPresentation.disabled) { - if (cancelButton.getAttribute("aria-disabled") !== "true") { - cancelButton.setAttribute("aria-disabled", "true"); - } - } else { - if (cancelButton.hasAttribute("aria-disabled")) { - cancelButton.removeAttribute("aria-disabled"); - } + if (cancelButton.disabled !== cancelPresentation.disabled) { + cancelButton.disabled = cancelPresentation.disabled; + } + if (cancelPresentation.disabled) { + if (cancelButton.getAttribute("aria-disabled") !== "true") { + cancelButton.setAttribute("aria-disabled", "true"); } + } else if (cancelButton.hasAttribute("aria-disabled")) { + cancelButton.removeAttribute("aria-disabled"); } } } if (status) { if (status.textContent !== ladderStatusText) status.textContent = ladderStatusText; if (status.title !== ladderStatusTitle) status.title = ladderStatusTitle; - const statusVisibility = expanded || ladderTask || ladderStatusText !== "空闲" ? "visible" : "hidden"; - if (status.style.visibility !== statusVisibility) status.style.visibility = statusVisibility; + if (status.style.visibility !== "visible") status.style.visibility = "visible"; } } function refreshComputedInfo(panel, multiplier, qtyRuleContext) { @@ -5756,18 +5752,18 @@ const calculationTitle = [formulaPrefixText, finalText, constraintText].filter(Boolean).join(" "); if (calculationEl.title !== calculationTitle) calculationEl.title = calculationTitle; } - let multiplierHintText = "最小下单量的"; + let multiplierHintText = PANEL_COPY.field.minimumOrderQuantity; if (multiplierHintEl) { if (tradeMode === "OPEN") { - multiplierHintText = "最小开仓量的"; + multiplierHintText = PANEL_COPY.field.minimumOpenQuantity; } else if (tradeMode === "CLOSE") { - multiplierHintText = "最小平仓量的"; + multiplierHintText = PANEL_COPY.field.minimumCloseQuantity; } if (multiplierHintEl.textContent !== multiplierHintText) { multiplierHintEl.textContent = multiplierHintText; } } - let hintText = "单击订单簿时"; + let hintText = PANEL_COPY.field.clickOrderbook; let hintTitle = ""; if (hintEl) { if (tradeMode === "OPEN") { @@ -5844,8 +5840,8 @@ sideShortBtn.style.color = isActive ? isOpenMode ? "var(--color-Sell)" : "var(--color-Buy)" : CONTROL_TEXT_COLOR; } syncNativeCloseButtons(tradeMode, rawCloseContext); - refreshOrderbookPrecisionRecommendation(panel); refreshLadderPanel(panel, tradeMode, closeContext); + refreshOrderbookPrecisionRecommendation(panel); } function findQtyFormItem(input) { if (!input) return null; @@ -5953,16 +5949,18 @@ panel.style.boxShadow = "none"; panel.style.visibility = "hidden"; panel.innerHTML = [ + '
', + `
${PANEL_COPY.section.singleOrder}
`, '
', - ``, + `${PANEL_COPY.field.clickOrderbook}`, `
`, ``, ``, "
", "
", - '
', + '
', '
', - ``, + ``, ``, `倍`, ``, @@ -5975,11 +5973,11 @@ ``, "
", "
", - `
`, + "
", '
', - ``, - ``, - ``, + `
${PANEL_COPY.section.ladderMaker}
`, + `
`, + `
空闲
`, "
" ].join(""); panelPositionInvalidated = true; @@ -5990,7 +5988,6 @@ const incBtn = panel.querySelector(`#${INC_ID}`); const sideLongBtn = panel.querySelector(`#${SIDE_LONG_ID}`); const sideShortBtn = panel.querySelector(`#${SIDE_SHORT_ID}`); - const ladderToggle = panel.querySelector(`#${LADDER_TOGGLE_ID}`); if (input) { const initialContext = getPanelOptionContext(); input.value = initialContext ? loadMultiplier(initialContext.mode, initialContext.symbol, initialContext.precision) || "" : ""; @@ -6085,11 +6082,6 @@ }; sideLongBtn?.addEventListener("keydown", handleSideSelectorKeydown); sideShortBtn?.addEventListener("keydown", handleSideSelectorKeydown); - if (ladderToggle) { - ladderToggle.addEventListener("click", () => { - setLadderExpanded(!isLadderExpanded()); - }); - } panel.addEventListener("click", (event) => { const target = event.target instanceof Element ? event.target : null; if (!target) return; @@ -6493,7 +6485,7 @@ } }, true); window.addEventListener("storage", (event) => { - if (event.key?.startsWith(`${LOCAL_QTY_MULTIPLIER_PREFIX}:`) || isSymbolScopedSideStorageKey(event.key, [LOCAL_CLOSE_SIDE_KEY, LOCAL_OPEN_SIDE_KEY]) || event.key === LOCAL_LADDER_EXPANDED_KEY || event.key?.startsWith(`${LOCAL_ORDERBOOK_PRECISION_SAMPLES_PREFIX}:`) || isModeSymbolOptionStorageKey(event.key, LADDER_OPTION_STORAGE_KEYS)) scheduleRenderPanel(); + if (event.key?.startsWith(`${LOCAL_QTY_MULTIPLIER_PREFIX}:`) || isSymbolScopedSideStorageKey(event.key, [LOCAL_CLOSE_SIDE_KEY, LOCAL_OPEN_SIDE_KEY]) || event.key?.startsWith(`${LOCAL_ORDERBOOK_PRECISION_SAMPLES_PREFIX}:`) || isModeSymbolOptionStorageKey(event.key, LADDER_OPTION_STORAGE_KEYS)) scheduleRenderPanel(); }); installUiSyncObservers(); function clearSymbolOwnedRuntimeState(symbol) { @@ -6525,7 +6517,6 @@ if (!symbol || symbol === lastObservedSymbol) return; lastObservedSymbol = symbol; clearSymbolOwnedRuntimeState(symbol); - startInitialOrderbookPrecisionSample(); scheduleRenderPanel(); if (getActiveTradeMode() === "OPEN") { queueAutoOpenLeveragePositionCheck("symbol_change"); @@ -6561,7 +6552,6 @@ ensureAccountPositionObserver(); ensureOrderbookPrecisionObserver(); startRenderPanelTimer(); - startInitialOrderbookPrecisionSample(); } function stopTradingTimers() { stopSymbolChangeTimer(); diff --git a/src/binance-orderbook-trade/contracts/panel-copy.js b/src/binance-orderbook-trade/contracts/panel-copy.js new file mode 100644 index 0000000..cfa637d --- /dev/null +++ b/src/binance-orderbook-trade/contracts/panel-copy.js @@ -0,0 +1,44 @@ +const freezeCopy = (copy) => Object.freeze(copy); + +export const PANEL_COPY = Object.freeze({ + section: freezeCopy({ + singleOrder: '单击下单', + ladderMaker: '阶梯下单 · Maker', + }), + field: freezeCopy({ + clickOrderbook: '单击订单簿时', + minimumOrderQuantity: '最小下单量的', + minimumOpenQuantity: '最小开仓量的', + minimumCloseQuantity: '最小平仓量的', + ratio: '比例', + orderCount: '笔数', + interval: '间距', + pricePrecision: '价格精度', + }), + action: freezeCopy({ + openLong: '阶梯开多', + openShort: '阶梯开空', + closeLong: '阶梯平多', + closeShort: '阶梯平空', + cancel: '撤单', + cancelRunning: '撤单处理中', + noOrders: '无挂单', + stopLadder: '停止阶梯挂单', + }), + tooltip: freezeCopy({ + singleOrder: '单击订单簿中的某个价格,按当前方向和数量设置提交一笔订单。', + ladderMaker: '根据当前比例、笔数、间距和价格精度设置,依次提交只做 Maker 的阶梯订单。', + ratio: '本次阶梯下单使用可开/可平数量的百分比。', + orderCount: '计划拆分成多少笔阶梯订单。', + interval: '相邻订单跨越多少个订单簿价格级别。', + pricePrecision: '与订单簿中的价格精度联动。黄点表示推荐值。比例、笔数、间距会随所选精度恢复对应设置。', + }), +}); + +export function formatPrecisionRefreshTooltip(durationMs) { + const seconds = Number(durationMs) / 1000; + if (!Number.isInteger(seconds) || seconds <= 0) { + throw new Error(`Invalid precision sample duration: ${durationMs}`); + } + return `根据最近 ${seconds} 秒的成交价变动,重新计算推荐的价格精度。`; +} diff --git a/src/binance-orderbook-trade/core/cancel-orders.js b/src/binance-orderbook-trade/core/cancel-orders.js index aa2e246..4d57cc2 100644 --- a/src/binance-orderbook-trade/core/cancel-orders.js +++ b/src/binance-orderbook-trade/core/cancel-orders.js @@ -5,6 +5,7 @@ import { parseBinanceTabCount, startsWithBinancePageText, } from '../contracts/binance-page-text.js'; +import { PANEL_COPY } from '../contracts/panel-copy.js'; const PERPETUAL_LABEL_PATTERN = buildBinanceTextAlternation( BINANCE_PAGE_TEXT.accountOrders.perpetual, @@ -186,10 +187,10 @@ export function resolveCancelSymbolButtonPresentation({ return { disabled: Boolean(ladderRunning || cancelRunning), label: cancelRunning - ? '撤单处理中' + ? PANEL_COPY.action.cancelRunning : noOrdersFeedback && !ladderRunning - ? '无挂单' - : '撤本币挂单', + ? PANEL_COPY.action.noOrders + : PANEL_COPY.action.cancel, }; } diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js index d7f0c64..356cec2 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.120 +// @version 2.7.121 // @author jackhai9 // @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板 // @match https://www.binance.com/*/futures/* @@ -39,6 +39,10 @@ import { isBinanceCancelAllText, matchesBinancePageText, } from './contracts/binance-page-text.js'; +import { + PANEL_COPY, + formatPrecisionRefreshTooltip, +} from './contracts/panel-copy.js'; import { resolveCloseDisplayQuantities, resolveConfirmedCloseDirection, @@ -184,7 +188,6 @@ import { const LOCAL_QTY_MULTIPLIER_PREFIX = 'jh_binance_qty_multiplier_v2'; const LOCAL_CLOSE_SIDE_KEY = 'jh_binance_close_side'; const LOCAL_OPEN_SIDE_KEY = 'jh_binance_open_side'; - const LOCAL_LADDER_EXPANDED_KEY = 'jh_binance_ladder_expanded'; const LOCAL_LADDER_OPEN_PERCENT_KEY = 'jh_binance_ladder_open_percent'; const LOCAL_LADDER_CLOSE_PERCENT_KEY = 'jh_binance_ladder_close_percent'; const LOCAL_LADDER_OPEN_LEVELS_KEY = 'jh_binance_ladder_open_levels'; @@ -208,7 +211,6 @@ import { const INC_ID = 'jh-binance-close-qty-multiplier-inc'; const SIDE_LONG_ID = 'jh-binance-close-side-long'; const SIDE_SHORT_ID = 'jh-binance-close-side-short'; - const LADDER_TOGGLE_ID = 'jh-binance-ladder-toggle'; const LADDER_BODY_ID = 'jh-binance-ladder-body'; const LADDER_STATUS_ID = 'jh-binance-ladder-status'; const ORDERBOOK_PRECISION_RECOMMENDATION_ID = 'jh-binance-orderbook-precision-recommendation'; @@ -268,7 +270,6 @@ import { 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_DURATION_MS = ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS; const ORDERBOOK_PRECISION_SAMPLE_POLL_MS = 300; const ORDERBOOK_PRECISION_READY_POLL_MS = 100; const ORDERBOOK_PRECISION_READY_TIMEOUT_MS = 5000; @@ -367,7 +368,6 @@ import { let orderbookPrecisionObserver = null; let orderbookPrecisionObserverRoot = null; let lastObservedOrderbookPrecision = null; - const orderbookPrecisionInitialSampledSymbols = new Set(); let orderbookPrecisionState = { symbol: null, samples: [], @@ -375,7 +375,7 @@ import { current: null, nativeOptions: [], nativeOptionsStatus: null, - status: '采样中', + status: '数据不足', sampleEndsAt: 0, }; const controlledNativeButtons = new Set(); @@ -518,15 +518,6 @@ import { }); } - function isLadderExpanded() { - return localStorage.getItem(LOCAL_LADDER_EXPANDED_KEY) === 'true'; - } - - function setLadderExpanded(expanded) { - localStorage.setItem(LOCAL_LADDER_EXPANDED_KEY, expanded ? 'true' : 'false'); - scheduleRenderPanel(); - } - function getLadderOpenPercent( symbol = getCurrentSymbol(), precision = readCurrentOrderbookPrecisionValue(), @@ -1466,15 +1457,6 @@ import { return false; } - function formatOrderbookPrecisionBusyStatus(status, sampleEndsAt = orderbookPrecisionState.sampleEndsAt) { - if (status !== '刷新中' && status !== '采样中') return status; - const remainingMs = Number(sampleEndsAt) - Date.now(); - if (!(remainingMs > 0)) return status; - const remainingSeconds = Math.max(1, Math.ceil(remainingMs / 1000)); - if (status === '刷新中') return `刷新中 ${remainingSeconds}s`; - return `采样中 ${remainingSeconds}s`; - } - function renderOrderbookPrecisionShortcut(value, current, recommendation, disabled) { const selected = value === current; const recommended = value === recommendation; @@ -1482,19 +1464,19 @@ import { const title = disabled ? '缩放调整暂不可用' : selected && recommended - ? `当前且推荐的原生缩放 ${value}` + ? `当前且推荐的价格精度 ${value}` : selected - ? `当前原生缩放 ${value}` + ? `当前价格精度 ${value}` : recommended - ? `推荐原生缩放 ${value}` - : `切换到原生缩放 ${value}`; + ? `推荐价格精度 ${value}` + : `切换到价格精度 ${value}`; const activeStyle = selected ? `border-color:var(--color-PrimaryYellow);background:var(--color-BadgeBg);color:${PRIMARY_EMPHASIS_COLOR};font-weight:${PRIMARY_EMPHASIS_FONT_WEIGHT};` : NEUTRAL_CONTROL_STYLE; const recommendationMarker = recommended ? '' : ''; - return ``; + return ``; } function renderOrderbookPrecisionShortcutSlots(options, current, recommendation, disabled) { @@ -1544,44 +1526,25 @@ import { const nativeOptions = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.nativeOptions : []; - const nativeOptionsStatus = !nativeOptions.length && orderbookPrecisionState.symbol === symbol - ? orderbookPrecisionState.nativeOptionsStatus - : null; const shortcutOptions = getOrderbookPrecisionShortcutOptions( nativeOptions, ORDERBOOK_PRECISION_SHORTCUT_LIMIT ); if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol); const canRefresh = !controlsBusy; - const buttonBaseStyle = `width:68px;height:24px;padding:0;border-radius:5px;border:1px solid ${CONTROL_BORDER_COLOR};font-size:12px;line-height:22px;`; - const recommendationText = recommendation || '--'; - const precisionMessage = selectionBusy - ? nativeOptions.length ? '调整中' : '读取档位' - : busy - ? formatOrderbookPrecisionBusyStatus(status) - : nativeOptionsStatus - ? nativeOptionsStatus - : status === 'ready' - ? `推荐 ${recommendationText}${recommendation && !shortcutOptions.includes(recommendation) ? '(原生)' : ''}` - : status; + const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS); const recommendationHtml = [ - `
`, - '
', - `订单簿缩放`, + '
', + '
', + `${PANEL_COPY.field.pricePrecision}`, ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy), - '
', - '
', - `${precisionMessage}`, - ``, + ``, '
', '
', ].join(''); if (el.innerHTML !== recommendationHtml) { el.innerHTML = recommendationHtml; } - if (busy && Number(orderbookPrecisionState.sampleEndsAt) > Date.now()) { - scheduleRenderPanel({ followUpMs: 1000 }); - } } async function clickAndConfirmOrderbookPrecisionOption(options) { @@ -1792,7 +1755,7 @@ import { orderbookPrecisionSampling = true; orderbookPrecisionActiveRequest = request; const tradeMoveSamples = []; - const sampleDurationMs = Math.max(0, Number(request.durationMs) || ORDERBOOK_PRECISION_SAMPLE_DURATION_MS); + 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; @@ -1827,7 +1790,6 @@ import { status: recommendation ? 'ready' : '数据不足', sampleEndsAt: 0, }; - if (request.initial) orderbookPrecisionInitialSampledSymbols.add(symbol); refreshOrderbookPrecisionRecommendation(); scheduleRenderPanel(); return true; @@ -1845,23 +1807,14 @@ import { function scheduleOrderbookPrecisionSampleRound(delayMs = 0, options) { const { force = false, - durationMs = ORDERBOOK_PRECISION_SAMPLE_DURATION_MS, - initial = 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, initial }; + const request = { symbol, durationMs }; if (orderbookPrecisionSampling) { - const sameInitialIsActive = initial - && orderbookPrecisionActiveRequest?.initial - && orderbookPrecisionActiveRequest?.symbol === symbol; - const sameInitialIsPending = initial - && orderbookPrecisionPendingRequest?.initial - && orderbookPrecisionPendingRequest?.symbol === symbol; - if (force && !sameInitialIsActive && !sameInitialIsPending) { - orderbookPrecisionPendingRequest = request; - } + if (force) orderbookPrecisionPendingRequest = request; return; } if (orderbookPrecisionSampling || orderbookPrecisionSampleTimer) return; @@ -1877,22 +1830,6 @@ import { orderbookPrecisionPendingRequest = null; } - function startInitialOrderbookPrecisionSample() { - const symbol = getCurrentSymbol(); - if (!isCurrentObservedSymbol(symbol) || orderbookPrecisionInitialSampledSymbols.has(symbol)) return; - orderbookPrecisionState = { - ...orderbookPrecisionState, - symbol, - status: '采样中', - sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_SAMPLE_DURATION_MS, - }; - scheduleOrderbookPrecisionSampleRound(0, { - force: true, - durationMs: ORDERBOOK_PRECISION_SAMPLE_DURATION_MS, - initial: true, - }); - } - function refreshOrderbookPrecisionSamplesNow() { const symbol = getCurrentSymbol(); orderbookPrecisionState = { @@ -4802,10 +4739,10 @@ import { return ``; } - function ladderOptionRow(title, options, selected, group, suffix = '') { + function ladderOptionRow(title, tooltip, options, selected, group, suffix = '') { return [ - '
', - `${title}`, + '
', + `${title}`, ...options.map((value) => ladderOptionButton(`${value}${suffix}`, value, Number(value) === Number(selected), group)), '
', ].join(''); @@ -4819,25 +4756,33 @@ import { return ``; } - function getLadderActionRows(tradeMode, closeContext, symbol, precision) { + function getLadderControlSections(tradeMode, closeContext, symbol, precision) { const ladderRunning = !!ladderTask; const actionDisabled = ladderRunning || cancelCurrentSymbolOpenOrdersBlocksLadderActions; if (!['OPEN', 'CLOSE'].includes(tradeMode)) { - return [`
等待开仓/平仓状态
`]; + return { + optionRows: [`
等待开仓/平仓状态
`], + actionButtons: [], + }; } if (!precision) { - return [`
等待订单簿缩放值
`]; + return { + optionRows: [`
等待价格精度
`], + actionButtons: [], + }; } if (tradeMode === 'OPEN') { - return [ - ladderOptionRow('量', LADDER_OPEN_PERCENTS, getLadderOpenPercent(symbol, precision), 'percent', '%'), - ladderOptionRow('档', LADDER_LEVEL_OPTIONS, getLadderLevels(tradeMode, symbol, precision), 'levels', ''), - ladderOptionRow('幅', LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), 'step', ''), - '
', - ladderActionButton('OPEN_LONG', '阶梯开多', 'BUY', actionDisabled), - ladderActionButton('OPEN_SHORT', '阶梯开空', 'SELL', actionDisabled), - '
', - ]; + return { + optionRows: [ + ladderOptionRow(PANEL_COPY.field.ratio, PANEL_COPY.tooltip.ratio, LADDER_OPEN_PERCENTS, getLadderOpenPercent(symbol, precision), 'percent', '%'), + ladderOptionRow(PANEL_COPY.field.orderCount, PANEL_COPY.tooltip.orderCount, LADDER_LEVEL_OPTIONS, getLadderLevels(tradeMode, symbol, precision), 'levels', ''), + ladderOptionRow(PANEL_COPY.field.interval, PANEL_COPY.tooltip.interval, LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), 'step', ''), + ], + actionButtons: [ + ladderActionButton('OPEN_LONG', PANEL_COPY.action.openLong, 'BUY', actionDisabled), + ladderActionButton('OPEN_SHORT', PANEL_COPY.action.openShort, 'SELL', actionDisabled), + ], + }; } const closeLongDisabled = shouldDisableCloseControl({ @@ -4850,76 +4795,71 @@ import { knowsPosition: closeContext?.knowsShort, hasPosition: closeContext?.hasShort, }); - return [ - ladderOptionRow('量', LADDER_CLOSE_PERCENTS, getLadderClosePercent(symbol, precision), 'percent', '%'), - ladderOptionRow('档', LADDER_LEVEL_OPTIONS, getLadderLevels(tradeMode, symbol, precision), 'levels', ''), - ladderOptionRow('幅', LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), 'step', ''), - '
', - ladderActionButton('CLOSE_LONG', '阶梯平多', 'SELL', closeLongDisabled), - ladderActionButton('CLOSE_SHORT', '阶梯平空', 'BUY', closeShortDisabled), - '
', - ]; + return { + optionRows: [ + ladderOptionRow(PANEL_COPY.field.ratio, PANEL_COPY.tooltip.ratio, LADDER_CLOSE_PERCENTS, getLadderClosePercent(symbol, precision), 'percent', '%'), + ladderOptionRow(PANEL_COPY.field.orderCount, PANEL_COPY.tooltip.orderCount, LADDER_LEVEL_OPTIONS, getLadderLevels(tradeMode, symbol, precision), 'levels', ''), + ladderOptionRow(PANEL_COPY.field.interval, PANEL_COPY.tooltip.interval, LADDER_STEP_OPTIONS, getLadderStep(tradeMode, symbol, precision), 'step', ''), + ], + actionButtons: [ + ladderActionButton('CLOSE_LONG', PANEL_COPY.action.closeLong, 'SELL', closeLongDisabled), + ladderActionButton('CLOSE_SHORT', PANEL_COPY.action.closeShort, 'BUY', closeShortDisabled), + ], + }; } function refreshLadderPanel(panel, tradeMode, closeContext) { - const toggle = panel.querySelector(`#${LADDER_TOGGLE_ID}`); const body = panel.querySelector(`#${LADDER_BODY_ID}`); const status = panel.querySelector(`#${LADDER_STATUS_ID}`); - const expanded = isLadderExpanded(); const mode = ['OPEN', 'CLOSE'].includes(tradeMode) ? tradeMode : null; const symbol = getCurrentSymbol(); const precision = readCurrentOrderbookPrecisionValue(); - if (toggle) { - const toggleText = `Maker 阶梯 ${expanded ? '▾' : '▸'}`; - if (toggle.textContent !== toggleText) toggle.textContent = toggleText; - } if (body) { - body.style.display = expanded ? 'block' : 'none'; - if (expanded) { - const stopDisabled = !ladderTask; - const stopDisabledAttrs = stopDisabled ? ' disabled aria-disabled="true"' : ''; - const cancelRunning = !!cancelCurrentSymbolOpenOrdersTask; - const cancelPresentation = resolveCancelSymbolButtonPresentation({ - ladderRunning: !!ladderTask, - cancelRunning, - noOrdersFeedback: cancelNoOrdersFeedbackActive, - }); - const bodyHtml = [ - ...getLadderActionRows(mode, closeContext, symbol, precision), - '
', - ``, - ``, - '
', - ].join(''); - if (ladderPanelBodySignature !== bodyHtml) { - body.innerHTML = bodyHtml; - ladderPanelBodySignature = bodyHtml; + const ladderRunning = !!ladderTask; + const cancelRunning = !!cancelCurrentSymbolOpenOrdersTask; + const cancelPresentation = resolveCancelSymbolButtonPresentation({ + ladderRunning, + cancelRunning, + noOrdersFeedback: cancelNoOrdersFeedbackActive, + }); + const controlSections = getLadderControlSections(mode, closeContext, symbol, precision); + const primaryActions = ladderRunning + ? [``] + : controlSections.actionButtons; + const actionColumnCount = Math.max(1, primaryActions.length + 1); + const bodyHtml = [ + ...controlSections.optionRows, + `
`, + `
`, + ...primaryActions, + ``, + '
', + ].join(''); + if (ladderPanelBodySignature !== bodyHtml) { + body.innerHTML = bodyHtml; + ladderPanelBodySignature = bodyHtml; + } + const cancelButton = body.querySelector('[data-ladder-cancel-symbol="true"]'); + if (cancelButton) { + if (cancelButton.textContent !== cancelPresentation.label) { + cancelButton.textContent = cancelPresentation.label; } - const cancelButton = body.querySelector('[data-ladder-cancel-symbol="true"]'); - if (cancelButton) { - if (cancelButton.textContent !== cancelPresentation.label) { - cancelButton.textContent = cancelPresentation.label; - } - if (cancelButton.disabled !== cancelPresentation.disabled) { - cancelButton.disabled = cancelPresentation.disabled; - } - if (cancelPresentation.disabled) { - if (cancelButton.getAttribute('aria-disabled') !== 'true') { - cancelButton.setAttribute('aria-disabled', 'true'); - } - } else { - if (cancelButton.hasAttribute('aria-disabled')) { - cancelButton.removeAttribute('aria-disabled'); - } + if (cancelButton.disabled !== cancelPresentation.disabled) { + cancelButton.disabled = cancelPresentation.disabled; + } + if (cancelPresentation.disabled) { + if (cancelButton.getAttribute('aria-disabled') !== 'true') { + cancelButton.setAttribute('aria-disabled', 'true'); } + } else if (cancelButton.hasAttribute('aria-disabled')) { + cancelButton.removeAttribute('aria-disabled'); } } } if (status) { if (status.textContent !== ladderStatusText) status.textContent = ladderStatusText; if (status.title !== ladderStatusTitle) status.title = ladderStatusTitle; - const statusVisibility = expanded || ladderTask || ladderStatusText !== '空闲' ? 'visible' : 'hidden'; - if (status.style.visibility !== statusVisibility) status.style.visibility = statusVisibility; + if (status.style.visibility !== 'visible') status.style.visibility = 'visible'; } } @@ -4992,18 +4932,18 @@ import { const calculationTitle = [formulaPrefixText, finalText, constraintText].filter(Boolean).join(' '); if (calculationEl.title !== calculationTitle) calculationEl.title = calculationTitle; } - let multiplierHintText = '最小下单量的'; + let multiplierHintText = PANEL_COPY.field.minimumOrderQuantity; if (multiplierHintEl) { if (tradeMode === 'OPEN') { - multiplierHintText = '最小开仓量的'; + multiplierHintText = PANEL_COPY.field.minimumOpenQuantity; } else if (tradeMode === 'CLOSE') { - multiplierHintText = '最小平仓量的'; + multiplierHintText = PANEL_COPY.field.minimumCloseQuantity; } if (multiplierHintEl.textContent !== multiplierHintText) { multiplierHintEl.textContent = multiplierHintText; } } - let hintText = '单击订单簿时'; + let hintText = PANEL_COPY.field.clickOrderbook; let hintTitle = ''; if (hintEl) { if (tradeMode === 'OPEN') { @@ -5099,8 +5039,8 @@ import { } syncNativeCloseButtons(tradeMode, rawCloseContext); - refreshOrderbookPrecisionRecommendation(panel); refreshLadderPanel(panel, tradeMode, closeContext); + refreshOrderbookPrecisionRecommendation(panel); } function findQtyFormItem(input) { @@ -5233,16 +5173,18 @@ import { panel.style.boxShadow = 'none'; panel.style.visibility = 'hidden'; panel.innerHTML = [ + '
', + `
${PANEL_COPY.section.singleOrder}
`, '
', - ``, + `${PANEL_COPY.field.clickOrderbook}`, `
`, ``, ``, '
', '
', - '
', + '
', '
', - ``, + ``, ``, `倍`, ``, @@ -5255,11 +5197,11 @@ import { ``, '
', '
', - `
`, + '
', '
', - ``, - ``, - ``, + `
${PANEL_COPY.section.ladderMaker}
`, + `
`, + `
空闲
`, '
', ].join(''); panelPositionInvalidated = true; @@ -5271,7 +5213,6 @@ import { const incBtn = panel.querySelector(`#${INC_ID}`); const sideLongBtn = panel.querySelector(`#${SIDE_LONG_ID}`); const sideShortBtn = panel.querySelector(`#${SIDE_SHORT_ID}`); - const ladderToggle = panel.querySelector(`#${LADDER_TOGGLE_ID}`); if (input) { const initialContext = getPanelOptionContext(); input.value = initialContext @@ -5372,11 +5313,6 @@ import { }; sideLongBtn?.addEventListener('keydown', handleSideSelectorKeydown); sideShortBtn?.addEventListener('keydown', handleSideSelectorKeydown); - if (ladderToggle) { - ladderToggle.addEventListener('click', () => { - setLadderExpanded(!isLadderExpanded()); - }); - } panel.addEventListener('click', (event) => { const target = event.target instanceof Element ? event.target : null; if (!target) return; @@ -5844,7 +5780,6 @@ import { if ( event.key?.startsWith(`${LOCAL_QTY_MULTIPLIER_PREFIX}:`) || isSymbolScopedSideStorageKey(event.key, [LOCAL_CLOSE_SIDE_KEY, LOCAL_OPEN_SIDE_KEY]) || - event.key === LOCAL_LADDER_EXPANDED_KEY || event.key?.startsWith(`${LOCAL_ORDERBOOK_PRECISION_SAMPLES_PREFIX}:`) || isModeSymbolOptionStorageKey(event.key, LADDER_OPTION_STORAGE_KEYS) ) scheduleRenderPanel(); @@ -5883,7 +5818,6 @@ import { if (!symbol || symbol === lastObservedSymbol) return; lastObservedSymbol = symbol; clearSymbolOwnedRuntimeState(symbol); - startInitialOrderbookPrecisionSample(); scheduleRenderPanel(); if (getActiveTradeMode() === 'OPEN') { queueAutoOpenLeveragePositionCheck('symbol_change'); @@ -5926,7 +5860,6 @@ import { ensureAccountPositionObserver(); ensureOrderbookPrecisionObserver(); startRenderPanelTimer(); - startInitialOrderbookPrecisionSample(); } function stopTradingTimers() { diff --git a/test/unit/binance-orderbook-trade/cancel.test.js b/test/unit/binance-orderbook-trade/cancel.test.js index 4ee1673..7d94713 100644 --- a/test/unit/binance-orderbook-trade/cancel.test.js +++ b/test/unit/binance-orderbook-trade/cancel.test.js @@ -27,7 +27,7 @@ test('cancel button exposes no-order completion feedback without disabling new a ladderRunning: false, cancelRunning: false, noOrdersFeedback: false, - }), { disabled: false, label: '撤本币挂单' }); + }), { disabled: false, label: '撤单' }); assert.deepEqual(resolveCancelSymbolButtonPresentation({ ladderRunning: false, cancelRunning: true, @@ -42,7 +42,7 @@ test('cancel button exposes no-order completion feedback without disabling new a ladderRunning: true, cancelRunning: false, noOrdersFeedback: true, - }), { disabled: true, label: '撤本币挂单' }); + }), { disabled: true, label: '撤单' }); }); test('normalizes text and recognizes open-orders tab labels', () => { diff --git a/test/unit/binance-orderbook-trade/ladder-options.test.js b/test/unit/binance-orderbook-trade/ladder-options.test.js index bd8a715..8e1dd03 100644 --- a/test/unit/binance-orderbook-trade/ladder-options.test.js +++ b/test/unit/binance-orderbook-trade/ladder-options.test.js @@ -230,24 +230,24 @@ test('ladder execution and UI updates use one captured mode-symbol-precision con assert.match(source, /plan\.ladderStep === DEFAULT_LADDER_STEP \? '' : `\/幅\$\{plan\.ladderStep\}`/); }); -test('open and close ladder percentage rows share the quantity label', () => { - assert.equal((source.match(/ladderOptionRow\('量',/g) || []).length, 2); - assert.doesNotMatch(source, /ladderOptionRow\('[开平]',/); +test('open and close ladder percentage rows share the centralized ratio label', () => { + assert.equal((source.match(/ladderOptionRow\(PANEL_COPY\.field\.ratio, PANEL_COPY\.tooltip\.ratio,/g) || []).length, 2); }); 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:28px repeat\(5,minmax\(0,1fr\)\)/); + assert.match(optionRow, /grid-template-columns:48px 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\('幅', LADDER_STEP_OPTIONS/g) || []).length, 2); + assert.equal((source.match(/ladderOptionRow\(PANEL_COPY\.field\.orderCount, PANEL_COPY\.tooltip\.orderCount, LADDER_LEVEL_OPTIONS/g) || []).length, 2); + assert.equal((source.match(/ladderOptionRow\(PANEL_COPY\.field\.interval, PANEL_COPY\.tooltip\.interval, LADDER_STEP_OPTIONS/g) || []).length, 2); assert.doesNotMatch(source, /data-ladder-step-action|function ladderStepRow/); }); test('close ladder buttons match the Binance native long-short order', () => { - const closeLongButton = "ladderActionButton('CLOSE_LONG', '阶梯平多', 'SELL', closeLongDisabled)"; - const closeShortButton = "ladderActionButton('CLOSE_SHORT', '阶梯平空', 'BUY', closeShortDisabled)"; + const closeLongButton = "ladderActionButton('CLOSE_LONG', PANEL_COPY.action.closeLong, 'SELL', closeLongDisabled)"; + const closeShortButton = "ladderActionButton('CLOSE_SHORT', PANEL_COPY.action.closeShort, 'BUY', closeShortDisabled)"; assert.notEqual(source.indexOf(closeLongButton), -1); assert.notEqual(source.indexOf(closeShortButton), -1); diff --git a/test/unit/binance-orderbook-trade/panel-copy.test.js b/test/unit/binance-orderbook-trade/panel-copy.test.js new file mode 100644 index 0000000..44b078f --- /dev/null +++ b/test/unit/binance-orderbook-trade/panel-copy.test.js @@ -0,0 +1,52 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +import { + PANEL_COPY, + formatPrecisionRefreshTooltip, +} from '../../../src/binance-orderbook-trade/contracts/panel-copy.js'; + +test('panel copy keeps the approved labels and tooltips in one contract', () => { + assert.deepEqual(PANEL_COPY, { + section: { + singleOrder: '单击下单', + ladderMaker: '阶梯下单 · Maker', + }, + field: { + clickOrderbook: '单击订单簿时', + minimumOrderQuantity: '最小下单量的', + minimumOpenQuantity: '最小开仓量的', + minimumCloseQuantity: '最小平仓量的', + ratio: '比例', + orderCount: '笔数', + interval: '间距', + pricePrecision: '价格精度', + }, + action: { + openLong: '阶梯开多', + openShort: '阶梯开空', + closeLong: '阶梯平多', + closeShort: '阶梯平空', + cancel: '撤单', + cancelRunning: '撤单处理中', + noOrders: '无挂单', + stopLadder: '停止阶梯挂单', + }, + tooltip: { + singleOrder: '单击订单簿中的某个价格,按当前方向和数量设置提交一笔订单。', + ladderMaker: '根据当前比例、笔数、间距和价格精度设置,依次提交只做 Maker 的阶梯订单。', + ratio: '本次阶梯下单使用可开/可平数量的百分比。', + orderCount: '计划拆分成多少笔阶梯订单。', + interval: '相邻订单跨越多少个订单簿价格级别。', + pricePrecision: '与订单簿中的价格精度联动。黄点表示推荐值。比例、笔数、间距会随所选精度恢复对应设置。', + }, + }); +}); + +test('precision refresh tooltip describes the sampling window without calling it a minimum', () => { + assert.equal( + formatPrecisionRefreshTooltip(6000), + '根据最近 6 秒的成交价变动,重新计算推荐的价格精度。', + ); + assert.doesNotMatch(formatPrecisionRefreshTooltip(6000), /最小值/); +}); diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js index 73c21f2..075c62b 100644 --- a/test/unit/binance-orderbook-trade/source-regressions.test.js +++ b/test/unit/binance-orderbook-trade/source-regressions.test.js @@ -74,7 +74,7 @@ test('cancel-symbol flow restores temporary symbol filter through cleanup path', assert.match(cancelBody, /restoreOpenOrdersSymbolFilter\(openOrdersScope,\s*symbolFilterOriginalChecked,\s*symbol\)/); }); -test('expanded ladder panel avoids rebuilding unchanged body markup', () => { +test('fixed ladder panel avoids rebuilding unchanged body markup', () => { const ladderBody = readFunctionBody('refreshLadderPanel'); assert.match(ladderBody, /ladderPanelBodySignature/); assert.match(ladderBody, /body\.innerHTML = bodyHtml/); @@ -96,7 +96,7 @@ test('panel primary values and ladder selections share the Binance emphasis stan const panelBody = readFunctionBody('ensurePanel'); assert.match(panelBody, /id="\$\{INPUT_ID\}"[^`]*color:\$\{PRIMARY_EMPHASIS_COLOR\}[^`]*font-weight:\$\{PRIMARY_EMPHASIS_FONT_WEIGHT\}/); assert.match(panelBody, /id="jh-binance-close-qty-final"[^`]*font-weight:\$\{PRIMARY_EMPHASIS_FONT_WEIGHT\}[^`]*color:\$\{PRIMARY_EMPHASIS_COLOR\}/); - assert.match(panelBody, /id="\$\{LADDER_TOGGLE_ID\}"[^`]*color:\$\{PRIMARY_EMPHASIS_COLOR\}[^`]*font-weight:\$\{PRIMARY_EMPHASIS_FONT_WEIGHT\}/); + assert.match(panelBody, /title="\$\{PANEL_COPY\.tooltip\.ladderMaker\}"[^`]*color:\$\{PRIMARY_EMPHASIS_COLOR\}[^`]*font-weight:\$\{PRIMARY_EMPHASIS_FONT_WEIGHT\}[^`]*\$\{PANEL_COPY\.section\.ladderMaker\}/); }); test('panel buttons inherit one scoped disabled-state contract', () => { @@ -338,41 +338,49 @@ 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:78px repeat\(4,minmax\(0,1fr\)\);align-items:center;gap:4px;height:32px;overflow:hidden/); - assert.match(source, /grid-template-columns:78px repeat\(4,minmax\(0,1fr\)\);align-items:center;gap:4px;height:24px;margin-top:6px;overflow:hidden/); - assert.match(source, /buttonBaseStyle = `width:68px;height:24px;[^`]*font-size:12px;line-height:22px;`/); + assert.match(source, /grid-template-columns:62px 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\)/); assert.doesNotMatch(source, /data-orderbook-precision-status/); const ladderBody = readFunctionBody('refreshLadderPanel'); - assert.match(ladderBody, /const statusVisibility = expanded \|\| ladderTask \|\| ladderStatusText !== '空闲' \? 'visible' : 'hidden'/); - assert.match(ladderBody, /status\.style\.visibility !== statusVisibility/); + assert.match(ladderBody, /status\.style\.visibility !== 'visible'/); assert.doesNotMatch(ladderBody, /status\.style\.display/); - assert.match(source, new RegExp(`id="\\$\\{LADDER_STATUS_ID\\}"[^>]*height:18px;[^>]*visibility:hidden;[^>]*white-space:nowrap;overflow:hidden;text-overflow:ellipsis`)); + assert.match(source, new RegExp(`id="\\$\\{LADDER_STATUS_ID\\}"[^>]*height:18px;[^>]*visibility:visible;[^>]*white-space:nowrap;overflow:hidden;text-overflow:ellipsis`)); }); test('panel keeps controls in cohesive ordered semantic groups', () => { const ensurePanelBody = readFunctionBody('ensurePanel'); const precisionBody = readFunctionBody('refreshOrderbookPrecisionRecommendation'); - const ladderRowsBody = readFunctionBody('getLadderActionRows'); + const ladderRowsBody = readFunctionBody('getLadderControlSections'); + const ladderPanelBody = readFunctionBody('refreshLadderPanel'); + const singleZoneIndex = ensurePanelBody.indexOf('data-panel-zone="single-order"'); const directionIndex = ensurePanelBody.indexOf('data-panel-group="direction"'); const modeHintIndex = ensurePanelBody.indexOf('id="${MODE_HINT_ID}"'); const multiplierIndex = ensurePanelBody.indexOf('data-panel-group="multiplier"'); const quantityMinIndex = ensurePanelBody.indexOf('id="jh-binance-close-qty-min"'); - const precisionIndex = ensurePanelBody.indexOf('data-panel-group="precision"'); const ladderIndex = ensurePanelBody.indexOf('data-panel-group="ladder"'); - assert.ok(directionIndex >= 0); + assert.ok(singleZoneIndex >= 0); + assert.ok(directionIndex > singleZoneIndex); assert.ok(modeHintIndex > directionIndex); assert.ok(multiplierIndex > modeHintIndex); assert.ok(quantityMinIndex > multiplierIndex); - assert.ok(precisionIndex > quantityMinIndex); - assert.ok(ladderIndex > precisionIndex); - assert.match(ensurePanelBody, /data-panel-group="multiplier" style="margin-top:12px;"/); - assert.match(precisionBody, /margin-top:12px;/); + 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.equal((ladderRowsBody.match(/gap:4px;margin-top:12px/g) || []).length, 2); + assert.match(ensurePanelBody, /PANEL_COPY\.section\.singleOrder/); + assert.match(ensurePanelBody, /PANEL_COPY\.section\.ladderMaker/); + assert.match(ladderPanelBody, /ORDERBOOK_PRECISION_RECOMMENDATION_ID/); + assert.ok( + ladderPanelBody.indexOf('...controlSections.optionRows') + < ladderPanelBody.indexOf('ORDERBOOK_PRECISION_RECOMMENDATION_ID'), + ); + assert.match(ladderRowsBody, /PANEL_COPY\.field\.ratio/); + assert.match(ladderRowsBody, /PANEL_COPY\.field\.orderCount/); + assert.match(ladderRowsBody, /PANEL_COPY\.field\.interval/); }); test('multiplier row reads as a labeled value followed by decrement and increment controls', () => { @@ -390,9 +398,9 @@ test('multiplier row reads as a labeled value followed by decrement and incremen assert.ok(decrementIndex > suffixIndex); assert.ok(incrementIndex > decrementIndex); assert.match(ensurePanelBody, /data-multiplier-controls style="display:flex;align-items:center;justify-content:flex-start;gap:6px;height:32px;overflow:hidden/); - assert.match(refreshBody, /multiplierHintText = '最小开仓量的'/); - assert.match(refreshBody, /multiplierHintText = '最小平仓量的'/); - assert.match(refreshBody, /let multiplierHintText = '最小下单量的'/); + assert.match(refreshBody, /multiplierHintText = PANEL_COPY\.field\.minimumOpenQuantity/); + assert.match(refreshBody, /multiplierHintText = PANEL_COPY\.field\.minimumCloseQuantity/); + assert.match(refreshBody, /let multiplierHintText = PANEL_COPY\.field\.minimumOrderQuantity/); }); test('multiplier calculation keeps the formula primary and separates the notional constraint visually', () => { @@ -416,7 +424,7 @@ test('direction selector is a compact mutually exclusive radio group', () => { assert.equal((ensurePanelBody.match(/role="radio" aria-checked="false"/g) || []).length, 2); assert.equal((ensurePanelBody.match(/border:0;/g) || []).length, 2); assert.match(ensurePanelBody, /border-left:1px solid var\(--color-InputLine\)/); - assert.match(refreshBody, /let hintText = '单击订单簿时'/); + assert.match(refreshBody, /let hintText = PANEL_COPY\.field\.clickOrderbook/); assert.doesNotMatch(refreshBody, /hintText = '仓位确认中'/); assert.match(refreshBody, /hintTitle = isUsingCache/); assert.match(refreshBody, /hintText = '暂无可平仓位'/); @@ -848,7 +856,7 @@ test('cancel current-symbol open orders are single-flight and follow the native assert.match(panelBody, /cancelCurrentSymbolOpenOrdersTask/); assert.match(panelBody, /resolveCancelSymbolButtonPresentation\(\{/); assert.match(panelBody, /noOrdersFeedback: cancelNoOrdersFeedbackActive/); - assert.match(panelBody, /data-ladder-cancel-symbol="true"[^`]*>撤本币挂单<\/button>/); + assert.match(panelBody, /data-ladder-cancel-symbol="true"[^`]*>\$\{PANEL_COPY\.action\.cancel\}<\/button>/); assert.match(panelBody, /cancelButton\.textContent = cancelPresentation\.label/); assert.match(source, /const CANCEL_NO_ORDERS_FEEDBACK_MS = 600/); @@ -886,7 +894,7 @@ test('cancel current-symbol open orders are single-flight and follow the native const startBody = readFunctionBody('startLadder'); assert.match(startBody, /if \(cancelCurrentSymbolOpenOrdersTask\)[\s\S]*撤本币挂单处理中,请等待完成/); - const actionRowsBody = readFunctionBody('getLadderActionRows'); + const actionRowsBody = readFunctionBody('getLadderControlSections'); assert.match(actionRowsBody, /actionDisabled = ladderRunning \|\| cancelCurrentSymbolOpenOrdersBlocksLadderActions/); assert.doesNotMatch(actionRowsBody, /!!cancelCurrentSymbolOpenOrdersTask/); assert.match(actionRowsBody, /ladderActionButton\('OPEN_LONG',[\s\S]*actionDisabled\)/); @@ -898,7 +906,7 @@ test('stable panel refreshes avoid writing unchanged text and state attributes', assert.match(statusBody, /statusEl\.title !== statusTitle/); const panelBody = readFunctionBody('refreshLadderPanel'); - assert.match(panelBody, /toggle\.textContent !== toggleText/); + assert.doesNotMatch(panelBody, /toggle|expanded/); assert.match(panelBody, /status\.textContent !== ladderStatusText/); const computedBody = readFunctionBody('refreshComputedInfo'); @@ -918,7 +926,7 @@ 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_SAMPLE_DURATION_MS = ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_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'/); assert.match(source, /ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4/); @@ -934,7 +942,7 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.match(sampleBody, /saveStoredOrderbookPrecisionSamples\(symbol,\s*newSamples\)/); assert.doesNotMatch(sampleBody, /mergePrecisionSamples\(\s*readStoredOrderbookPrecisionSamples/); assert.match(sampleBody, /waitForLatestTradePricesReady/); - assert.match(sampleBody, /ORDERBOOK_PRECISION_SAMPLE_DURATION_MS/); + assert.match(sampleBody, /ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS/); assert.match(sampleBody, /getLatestTradePrices/); assert.doesNotMatch(sampleBody, /getCurrentOrderbookDisplayStep/); assert.doesNotMatch(sampleBody, /fallbackMovement/); @@ -947,12 +955,11 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.match(refreshBody, /recommendOrderbookPrecision/); assert.match(refreshBody, /resolveOrderbookPrecisionSampleState/); assert.match(refreshBody, /scheduled: Boolean\(orderbookPrecisionSampleTimer\)/); - assert.match(refreshBody, /formatOrderbookPrecisionBusyStatus/); assert.match(refreshBody, /data-orderbook-precision-refresh="true"[\s\S]*disabled/); assert.match(refreshBody, /const controlsBusy = busy \|\| selectionBusy/); assert.match(refreshBody, /getOrderbookPrecisionShortcutOptions\([\s\S]*ORDERBOOK_PRECISION_SHORTCUT_LIMIT/); assert.match(refreshBody, /queueOrderbookPrecisionOptionsLoad\(symbol\)/); - assert.match(refreshBody, /shortcutOptions\.includes\(recommendation\)/); + assert.match(refreshBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS\)/); const shortcutBody = readFunctionBody('renderOrderbookPrecisionShortcut'); assert.match(shortcutBody, /const recommended = value === recommendation/); assert.match(shortcutBody, /position:absolute;top:3px;right:3px;width:6px;height:6px/); @@ -965,26 +972,19 @@ test('orderbook precision recommendation marks one shortcut without applying it assert.doesNotMatch(refreshBody, /当前 \$\{currentText\}/); assert.doesNotMatch(refreshBody, /fallbackMovement/); assert.doesNotMatch(refreshBody, /applyRecommendedOrderbookPrecision\(\)/); - assert.match(refreshBody, /buttonBaseStyle = `width:68px;height:24px;[^`]*padding:0;[^`]*font-size:12px;line-height:22px;/); - assert.match(refreshBody, /margin-top:12px;[^`]*font-size:12px;/); - assert.match(refreshBody, /当前缩放 \$\{current \|\| '--'\}[^`]*订单簿缩放/); + assert.doesNotMatch(refreshBody, /buttonBaseStyle|precisionMessage|更新推荐/); + assert.match(refreshBody, /margin-top:10px;/); + 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:78px repeat\(4,minmax\(0,1fr\)\);[^']*height:24px;margin-top:6px/); - assert.match(refreshBody, /grid-column:1;[^`]*>\$\{precisionMessage\}<\/span>/); - assert.match(refreshBody, /data-orderbook-precision-refresh="true"[^`]*grid-column:2;justify-self:start;/); - const messageIndex = refreshBody.indexOf('>\${precisionMessage}'); - const refreshButtonIndex = refreshBody.indexOf('data-orderbook-precision-refresh="true"'); - assert.ok(messageIndex >= 0, 'recommendation or transient status should be rendered'); - assert.ok(messageIndex < refreshButtonIndex, 'precision message should stay before the update button'); - assert.match(refreshBody, />更新推荐<\/button>/); + assert.match(refreshBody, /grid-template-columns:62px repeat\(4,minmax\(0,1fr\)\) 32px/); + assert.match(refreshBody, /data-orderbook-precision-refresh="true"[^`]* { assert.doesNotMatch(refreshBody, /closeContext\.isPending \|\|/); assert.equal((refreshBody.match(/shouldDisableCloseControl\(/g) || []).length, 2); - const ladderRowsBody = readFunctionBody('getLadderActionRows'); + const ladderRowsBody = readFunctionBody('getLadderControlSections'); assert.doesNotMatch(ladderRowsBody, /closePending/); assert.equal((ladderRowsBody.match(/shouldDisableCloseControl\(/g) || []).length, 2); assert.doesNotMatch(source, /applyCachedNativeCloseButtonState/); @@ -1313,12 +1311,10 @@ test('precision shortcut selection and sampling do not commit after a symbol swi const roundBody = readFunctionBody('runOrderbookPrecisionSampleRound'); assert.match(roundBody, /request\.symbol/); - assert.match(roundBody, /orderbookPrecisionInitialSampledSymbols\.add\(symbol\)/); - const initialBody = readFunctionBody('startInitialOrderbookPrecisionSample'); - assert.doesNotMatch(initialBody, /orderbookPrecisionInitialSampledSymbols\.add\(symbol\)/); + assert.doesNotMatch(roundBody, /initial/); + assert.doesNotMatch(source, /orderbookPrecisionInitialSampledSymbols|startInitialOrderbookPrecisionSample/); const scheduleBody = readFunctionBody('scheduleOrderbookPrecisionSampleRound'); - assert.match(scheduleBody, /orderbookPrecisionActiveRequest\?\.symbol === symbol/); - assert.match(scheduleBody, /orderbookPrecisionPendingRequest\?\.symbol === symbol/); + assert.match(scheduleBody, /if \(force\) orderbookPrecisionPendingRequest = request/); const clearBody = readFunctionBody('clearSymbolOwnedRuntimeState'); assert.match(clearBody, /orderbookPrecisionOptionsLoadRequestedSymbol = null/);