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"[^`]*