diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js
index 19262b2..7a537aa 100644
--- a/e2e/binance-orderbook/specs/control-flows.pw.js
+++ b/e2e/binance-orderbook/specs/control-flows.pw.js
@@ -103,6 +103,8 @@ test('precision refresh uses the visible latest trades immediately without a sam
await installInteractionProbe(page, '[data-orderbook-precision-refresh]');
await refresh.click();
await expect(refresh).toBeEnabled();
+ await expect(refresh).toHaveAttribute('data-orderbook-precision-refresh-state', 'success');
+ await expect(refresh).toHaveAttribute('aria-label', '精度推荐已更新');
await expect(panel.locator('[data-orderbook-precision-value="0.01"]'))
.toHaveAttribute('aria-label', '切换价格精度到 0.01,推荐档位');
const probe = await finishInteractionProbe(page);
@@ -110,6 +112,7 @@ test('precision refresh uses the visible latest trades immediately without a sam
expect(await page.evaluate((symbol) => JSON.parse(
localStorage.getItem(`jh_binance_orderbook_precision_samples_v3:${symbol}`)
), scenario.currentSymbol)).toEqual(['0.01', '0.01', '0.01', '0.01', '0.01']);
+ await expect(refresh).toHaveAttribute('data-orderbook-precision-refresh-state', 'idle', { timeout: 2_000 });
expect(errors).toEqual([]);
});
@@ -123,7 +126,10 @@ test('precision refresh explains when the complete visible trade list still lack
nodes.forEach((node) => { node.textContent = '81.00'; });
});
- await panel.locator('[data-orderbook-precision-refresh]').click();
+ const refresh = panel.locator('[data-orderbook-precision-refresh]');
+ await refresh.click();
+ await expect(refresh).toHaveAttribute('data-orderbook-precision-refresh-state', 'retry');
+ await expect(refresh).toHaveAttribute('aria-label', '近期价格变化不足,请稍后重试');
await expect(panel.locator('#jh-binance-ladder-status'))
.toHaveText('近期价格变化不足,请稍后重试');
await expect(panel.locator('[aria-label*="推荐档位"]')).toHaveCount(0);
diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js
index 084be15..bf3a150 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.127
+// @version 2.7.128
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -1853,6 +1853,7 @@
const ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10;
const ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5;
const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4;
+ const ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS = 900;
const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [
"0.00000001",
"0.0000001",
@@ -1939,6 +1940,8 @@
let orderbookPrecisionObserver = null;
let orderbookPrecisionObserverRoot = null;
let lastObservedOrderbookPrecision = null;
+ let orderbookPrecisionRefreshFeedback = { symbol: null, state: "idle" };
+ let orderbookPrecisionRefreshFeedbackTimer = null;
let orderbookPrecisionState = {
symbol: null,
samples: [],
@@ -2791,6 +2794,40 @@
}
return slots;
}
+ function renderOrderbookPrecisionRefreshButton(symbol, disabled) {
+ const feedbackState = orderbookPrecisionRefreshFeedback.symbol === symbol ? orderbookPrecisionRefreshFeedback.state : "idle";
+ const feedback = feedbackState === "success" ? {
+ label: PANEL_COPY.status.precisionUpdated,
+ color: "var(--color-Buy)",
+ icon: ''
+ } : feedbackState === "retry" ? {
+ label: PANEL_COPY.status.precisionInsufficient,
+ color: "var(--color-PrimaryYellow)",
+ icon: ''
+ } : {
+ label: formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT),
+ color: CONTROL_TEXT_COLOR,
+ icon: ''
+ };
+ const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : "";
+ return ``;
+ }
+ function showOrderbookPrecisionRefreshFeedback(symbol, state) {
+ if (!["success", "retry"].includes(state)) {
+ throw new Error(`Invalid orderbook precision refresh feedback state: ${state}`);
+ }
+ if (orderbookPrecisionRefreshFeedbackTimer !== null) {
+ window.clearTimeout(orderbookPrecisionRefreshFeedbackTimer);
+ }
+ orderbookPrecisionRefreshFeedback = { symbol, state };
+ refreshOrderbookPrecisionRecommendation();
+ orderbookPrecisionRefreshFeedbackTimer = window.setTimeout(() => {
+ orderbookPrecisionRefreshFeedbackTimer = null;
+ if (orderbookPrecisionRefreshFeedback.symbol !== symbol || orderbookPrecisionRefreshFeedback.state !== state) return;
+ orderbookPrecisionRefreshFeedback = { symbol, state: "idle" };
+ refreshOrderbookPrecisionRecommendation();
+ }, ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS);
+ }
function refreshOrderbookPrecisionRecommendation(panel = document.getElementById(PANEL_ID)) {
const el = panel?.querySelector?.(`#${ORDERBOOK_PRECISION_RECOMMENDATION_ID}`);
if (!el) return;
@@ -2824,13 +2861,12 @@
);
if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol);
const canRefresh = !controlsBusy;
- const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT);
const recommendationHtml = [
'
',
'
',
`
${PANEL_COPY.field.pricePrecision}`,
...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy),
- `
`,
+ renderOrderbookPrecisionRefreshButton(symbol, !canRefresh),
"
",
"
"
].join("");
@@ -3041,6 +3077,7 @@
setLadderStatus(
recommendation ? PANEL_COPY.status.precisionUpdated : PANEL_COPY.status.precisionInsufficient
);
+ showOrderbookPrecisionRefreshFeedback(symbol, recommendation ? "success" : "retry");
scheduleRenderPanel();
}
function getBufferedMakerPrices(side, levels, ladderStep = DEFAULT_LADDER_STEP) {
diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js
index 05398b9..d94baf6 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.127
+// @version 2.7.128
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -274,6 +274,7 @@ import {
const ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10;
const ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5;
const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4;
+ const ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS = 900;
const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [
'0.00000001',
'0.0000001',
@@ -362,6 +363,8 @@ import {
let orderbookPrecisionObserver = null;
let orderbookPrecisionObserverRoot = null;
let lastObservedOrderbookPrecision = null;
+ let orderbookPrecisionRefreshFeedback = { symbol: null, state: 'idle' };
+ let orderbookPrecisionRefreshFeedbackTimer = null;
let orderbookPrecisionState = {
symbol: null,
samples: [],
@@ -1475,6 +1478,51 @@ import {
return slots;
}
+ function renderOrderbookPrecisionRefreshButton(symbol, disabled) {
+ const feedbackState = orderbookPrecisionRefreshFeedback.symbol === symbol
+ ? orderbookPrecisionRefreshFeedback.state
+ : 'idle';
+ const feedback = feedbackState === 'success'
+ ? {
+ label: PANEL_COPY.status.precisionUpdated,
+ color: 'var(--color-Buy)',
+ icon: '',
+ }
+ : feedbackState === 'retry'
+ ? {
+ label: PANEL_COPY.status.precisionInsufficient,
+ color: 'var(--color-PrimaryYellow)',
+ icon: '',
+ }
+ : {
+ label: formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT),
+ color: CONTROL_TEXT_COLOR,
+ icon: '',
+ };
+ const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : '';
+ return ``;
+ }
+
+ function showOrderbookPrecisionRefreshFeedback(symbol, state) {
+ if (!['success', 'retry'].includes(state)) {
+ throw new Error(`Invalid orderbook precision refresh feedback state: ${state}`);
+ }
+ if (orderbookPrecisionRefreshFeedbackTimer !== null) {
+ window.clearTimeout(orderbookPrecisionRefreshFeedbackTimer);
+ }
+ orderbookPrecisionRefreshFeedback = { symbol, state };
+ refreshOrderbookPrecisionRecommendation();
+ orderbookPrecisionRefreshFeedbackTimer = window.setTimeout(() => {
+ orderbookPrecisionRefreshFeedbackTimer = null;
+ if (
+ orderbookPrecisionRefreshFeedback.symbol !== symbol
+ || orderbookPrecisionRefreshFeedback.state !== state
+ ) return;
+ orderbookPrecisionRefreshFeedback = { symbol, state: 'idle' };
+ refreshOrderbookPrecisionRecommendation();
+ }, ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS);
+ }
+
function refreshOrderbookPrecisionRecommendation(panel = document.getElementById(PANEL_ID)) {
const el = panel?.querySelector?.(`#${ORDERBOOK_PRECISION_RECOMMENDATION_ID}`);
if (!el) return;
@@ -1517,13 +1565,12 @@ import {
);
if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol);
const canRefresh = !controlsBusy;
- const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT);
const recommendationHtml = [
'',
'
',
`
${PANEL_COPY.field.pricePrecision}`,
...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy),
- `
`,
+ renderOrderbookPrecisionRefreshButton(symbol, !canRefresh),
'
',
'
',
].join('');
@@ -1770,6 +1817,7 @@ import {
? PANEL_COPY.status.precisionUpdated
: PANEL_COPY.status.precisionInsufficient
);
+ showOrderbookPrecisionRefreshFeedback(symbol, recommendation ? 'success' : 'retry');
scheduleRenderPanel();
}
diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js
index b9d3ae7..fafc825 100644
--- a/test/unit/binance-orderbook-trade/source-regressions.test.js
+++ b/test/unit/binance-orderbook-trade/source-regressions.test.js
@@ -962,13 +962,20 @@ test('orderbook precision recommendation marks one shortcut without applying it
assert.doesNotMatch(sampleBody, /getCurrentOrderbookDisplayStep|fallbackMovement/);
const refreshBody = readFunctionBody('refreshOrderbookPrecisionRecommendation');
+ const refreshButtonBody = readFunctionBody('renderOrderbookPrecisionRefreshButton');
+ const refreshFeedbackBody = readFunctionBody('showOrderbookPrecisionRefreshFeedback');
assert.match(refreshBody, /recommendOrderbookPrecision/);
assert.doesNotMatch(refreshBody, /resolveOrderbookPrecisionSampleState|orderbookPrecisionSampleTimer/);
- assert.match(refreshBody, /data-orderbook-precision-refresh="true"[\s\S]*disabled/);
+ assert.match(refreshButtonBody, /data-orderbook-precision-refresh="true"[\s\S]*disabled/);
+ assert.match(refreshButtonBody, /data-orderbook-precision-refresh-state="\$\{feedbackState\}"/);
+ assert.match(refreshButtonBody, /PANEL_COPY\.status\.precisionUpdated/);
+ assert.match(refreshButtonBody, /PANEL_COPY\.status\.precisionInsufficient/);
+ assert.match(refreshFeedbackBody, /ORDERBOOK_PRECISION_REFRESH_FEEDBACK_MS/);
+ assert.match(sampleBody, /showOrderbookPrecisionRefreshFeedback\(symbol, recommendation \? 'success' : 'retry'\)/);
assert.match(refreshBody, /const controlsBusy = selectionBusy/);
assert.match(refreshBody, /getOrderbookPrecisionShortcutOptions\([\s\S]*ORDERBOOK_PRECISION_SHORTCUT_LIMIT/);
assert.match(refreshBody, /queueOrderbookPrecisionOptionsLoad\(symbol\)/);
- assert.match(refreshBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT\)/);
+ assert.match(refreshButtonBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT\)/);
const shortcutBody = readFunctionBody('renderOrderbookPrecisionShortcut');
assert.match(shortcutBody, /const recommended = value === recommendation/);
assert.match(shortcutBody, /position:absolute;top:3px;right:3px;width:6px;height:6px/);
@@ -987,7 +994,8 @@ test('orderbook precision recommendation marks one shortcut without applying it
assert.match(refreshBody, /PANEL_COPY\.tooltip\.pricePrecision/);
assert.match(refreshBody, /renderOrderbookPrecisionShortcutSlots\(shortcutOptions, current, recommendation, controlsBusy\)/);
assert.match(refreshBody, /grid-template-columns:36px repeat\(4,minmax\(0,1fr\)\) 32px/);
- assert.match(refreshBody, /data-orderbook-precision-refresh="true"[^`]*