`,
`
${PANEL_COPY.section.ladderMaker}
`,
`
`,
`
空闲
`,
diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js
index 5343187..9da8d3a 100644
--- a/test/unit/binance-orderbook-trade/source-regressions.test.js
+++ b/test/unit/binance-orderbook-trade/source-regressions.test.js
@@ -370,7 +370,8 @@ test('panel keeps controls in cohesive ordered semantic groups', () => {
assert.ok(ladderIndex > quantityMinIndex);
assert.match(ensurePanelBody, /data-panel-group="multiplier" style="margin-top:8px;"/);
assert.match(precisionBody, /margin-top:10px;/);
- assert.match(ensurePanelBody, /data-panel-group="ladder" style="margin-top:12px;padding-top:12px;border-top:/);
+ assert.match(source, /const PANEL_DIVIDER_COLOR = '#ededed'/);
+ assert.match(ensurePanelBody, /data-panel-group="ladder" style="margin:12px -10px 0;padding:12px 10px 0;border-top:1px solid \$\{PANEL_DIVIDER_COLOR\}/);
assert.match(ensurePanelBody, /PANEL_COPY\.section\.singleOrder/);
assert.match(ensurePanelBody, /PANEL_COPY\.section\.ladderMaker/);
assert.match(ladderPanelBody, /ORDERBOOK_PRECISION_RECOMMENDATION_ID/);
From 7cbe38871a0d4bb6be5c9fde85c3932b2a6aea3c Mon Sep 17 00:00:00 2001
From: LiZhenhai-MBP14 <5935568+jackhai9@users.noreply.github.com>
Date: Thu, 27 Aug 2026 00:09:17 +0800
Subject: [PATCH 2/3] feat: refresh precision from latest trades
---
.../specs/control-flows.pw.js | 22 ++
.../close-fixed-visual.json | 38 ++--
.../open-fixed-visual.json | 38 ++--
scripts/binance-orderbook-trade.user.js | 192 ++++--------------
.../contracts/panel-copy.js | 12 +-
src/binance-orderbook-trade/core/precision.js | 29 ---
src/binance-orderbook-trade/index.user.js | 167 ++++-----------
.../ladder-options.test.js | 2 +-
.../panel-copy.test.js | 11 +-
.../binance-orderbook-trade/precision.test.js | 67 ++----
.../source-regressions.test.js | 53 ++---
11 files changed, 190 insertions(+), 441 deletions(-)
diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js
index cc23c7a..9c97b26 100644
--- a/e2e/binance-orderbook/specs/control-flows.pw.js
+++ b/e2e/binance-orderbook/specs/control-flows.pw.js
@@ -91,6 +91,28 @@ test('a precision shortcut selects the exact native orderbook option once', asyn
expect(errors).toEqual([]);
});
+test('precision refresh uses the visible latest trades immediately without a sampling wait', async ({ page }) => {
+ const scenario = createCancelScenario({
+ ui: { orderbookPrecision: '0.1' },
+ });
+ const { errors } = await openUserscriptScenario(page, scenario);
+ const panel = page.locator(PANEL_SELECTOR);
+ const refresh = panel.locator('[data-orderbook-precision-refresh]');
+
+ await expect(refresh).toBeEnabled({ timeout: 8_000 });
+ await installInteractionProbe(page, '[data-orderbook-precision-refresh]');
+ await refresh.click();
+ await expect(refresh).toBeEnabled();
+ await expect(panel.locator('[data-orderbook-precision-value="0.01"]'))
+ .toHaveAttribute('aria-label', '切换价格精度到 0.01,推荐档位');
+ const probe = await finishInteractionProbe(page);
+ assertResponsiveInteraction(expect, probe);
+ expect(await page.evaluate((symbol) => JSON.parse(
+ localStorage.getItem(`jh_binance_orderbook_precision_samples_v3:${symbol}`)
+ ), scenario.currentSymbol)).toEqual(['0.01', '0.01', '0.01', '0.01', '0.01']);
+ expect(errors).toEqual([]);
+});
+
test('starting a ladder disables every start action and exposes one stop control', async ({ page }) => {
const scenario = createCancelScenario({
ui: { tradeMode: 'OPEN', orderbookPrecision: '0.1' },
diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json
index 0d239f9..79a0134 100644
--- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json
+++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json
@@ -157,7 +157,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -178,7 +178,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -199,7 +199,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -220,7 +220,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -241,7 +241,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -262,7 +262,7 @@
"pressed": null,
"rect": {
"top": 217,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -283,7 +283,7 @@
"pressed": null,
"rect": {
"top": 217,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -304,7 +304,7 @@
"pressed": null,
"rect": {
"top": 217,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -325,7 +325,7 @@
"pressed": null,
"rect": {
"top": 217,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -346,7 +346,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -367,7 +367,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -388,7 +388,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -409,7 +409,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -430,7 +430,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -451,7 +451,7 @@
"pressed": "false",
"rect": {
"top": 298,
- "width": 44.5,
+ "width": 51,
"height": 32
},
"style": {
@@ -472,7 +472,7 @@
"pressed": "false",
"rect": {
"top": 298,
- "width": 44.5,
+ "width": 51,
"height": 32
},
"style": {
@@ -493,7 +493,7 @@
"pressed": "true",
"rect": {
"top": 298,
- "width": 44.5,
+ "width": 51,
"height": 32
},
"style": {
@@ -514,7 +514,7 @@
"pressed": "false",
"rect": {
"top": 298,
- "width": 44.5,
+ "width": 51,
"height": 32
},
"style": {
@@ -594,7 +594,7 @@
"text": {
"mode": "单击订单簿时",
"formula": "0.01 × 1 =0.01",
- "precision": "价格精度0.0010.010.11",
+ "precision": "精度0.0010.010.11",
"status": "空闲"
}
}
diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json
index 4457566..4045c0a 100644
--- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json
+++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/open-fixed-visual.json
@@ -157,7 +157,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -178,7 +178,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -199,7 +199,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -220,7 +220,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -241,7 +241,7 @@
"pressed": null,
"rect": {
"top": 177,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -262,7 +262,7 @@
"pressed": null,
"rect": {
"top": 217,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -283,7 +283,7 @@
"pressed": null,
"rect": {
"top": 217,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -304,7 +304,7 @@
"pressed": null,
"rect": {
"top": 217,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -325,7 +325,7 @@
"pressed": null,
"rect": {
"top": 217,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -346,7 +346,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -367,7 +367,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -388,7 +388,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -409,7 +409,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -430,7 +430,7 @@
"pressed": null,
"rect": {
"top": 257,
- "width": 44.8,
+ "width": 47.2,
"height": 28
},
"style": {
@@ -451,7 +451,7 @@
"pressed": "false",
"rect": {
"top": 298,
- "width": 44.5,
+ "width": 51,
"height": 32
},
"style": {
@@ -472,7 +472,7 @@
"pressed": "false",
"rect": {
"top": 298,
- "width": 44.5,
+ "width": 51,
"height": 32
},
"style": {
@@ -493,7 +493,7 @@
"pressed": "true",
"rect": {
"top": 298,
- "width": 44.5,
+ "width": 51,
"height": 32
},
"style": {
@@ -514,7 +514,7 @@
"pressed": "false",
"rect": {
"top": 298,
- "width": 44.5,
+ "width": 51,
"height": 32
},
"style": {
@@ -594,7 +594,7 @@
"text": {
"mode": "单击订单簿时",
"formula": "0.07 × 1 =0.07≥5U @ 81",
- "precision": "价格精度0.0010.010.11",
+ "precision": "精度0.0010.010.11",
"status": "空闲"
}
}
diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js
index bc96ad0..eff8dde 100644
--- a/scripts/binance-orderbook-trade.user.js
+++ b/scripts/binance-orderbook-trade.user.js
@@ -3,7 +3,7 @@
// @namespace binance.orderbook.trade
// @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
// @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
-// @version 2.7.123
+// @version 2.7.124
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -138,7 +138,7 @@
ratio: "比例",
orderCount: "笔数",
interval: "间距",
- pricePrecision: "价格精度"
+ pricePrecision: "精度"
}),
action: freezeCopy({
openLong: "阶梯开多",
@@ -159,12 +159,12 @@
pricePrecision: "与订单簿中的价格精度联动。黄点表示推荐值。比例、笔数、间距会随所选精度恢复对应设置。"
})
});
- function formatPrecisionRefreshTooltip(durationMs) {
- const seconds = Number(durationMs) / 1e3;
- if (!Number.isInteger(seconds) || seconds <= 0) {
- throw new Error(`Invalid precision sample duration: ${durationMs}`);
+ function formatPrecisionRefreshTooltip(tradeCount) {
+ const count = Number(tradeCount);
+ if (!Number.isInteger(count) || count <= 1) {
+ throw new Error(`Invalid precision trade count: ${tradeCount}`);
}
- return `根据最近 ${seconds} 秒的成交价变动,重新计算推荐的价格精度。`;
+ return `根据最新 ${count} 条成交价中的有效价格变动,重新计算推荐精度。`;
}
// src/binance-orderbook-trade/core/cancel-orders.js
@@ -553,31 +553,6 @@
}
return moves;
}
- function mergePrecisionSamples(existingSamples, newSamples, maxSamples = 64) {
- const merged = [...existingSamples || [], ...newSamples || []].map((sample) => normalizeDecimalString(sample)).filter((sample) => sample && isPositiveDecimalString(sample));
- return merged.slice(Math.max(0, merged.length - maxSamples));
- }
- function resolveOrderbookPrecisionSampleState({
- sampling,
- scheduled,
- status,
- recommendation
- }) {
- const busy = Boolean(sampling || scheduled);
- if (busy) {
- return {
- busy,
- status: status === "刷新中" ? "刷新中" : "采样中"
- };
- }
- if (status && /^(未定位|未找到|数据不足)/.test(status)) {
- return { busy, status };
- }
- return {
- busy,
- status: recommendation ? "ready" : "数据不足"
- };
- }
function sortedPositiveDecimals(values) {
return (values || []).map((value) => normalizeDecimalString(value)).filter((value) => value && isPositiveDecimalString(value)).sort((a, b) => compareDecimalStrings(a, b));
}
@@ -1831,13 +1806,10 @@
const LADDER_OPEN_QTY_READY_TIMEOUT_MS = 1200;
const TRADE_INPUT_SYNC_TIMEOUT_MS = 350;
const TRADE_INPUT_SYNC_STABLE_FRAMES = 2;
- const ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS = 6e3;
- const ORDERBOOK_PRECISION_SAMPLE_POLL_MS = 300;
const ORDERBOOK_PRECISION_READY_POLL_MS = 100;
const ORDERBOOK_PRECISION_READY_TIMEOUT_MS = 5e3;
const ORDERBOOK_PRECISION_OPTION_WAIT_MS = 1200;
- const ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS = 6;
- const ORDERBOOK_PRECISION_SAMPLE_MAX = 96;
+ const ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT = 10;
const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4;
const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [
"0.00000001",
@@ -1919,10 +1891,6 @@
let panelResizeObserver = null;
let ladderSubmitCaptureSequence = 0;
let activeLadderSubmitCapture = null;
- let orderbookPrecisionSampling = false;
- let orderbookPrecisionSampleTimer = 0;
- let orderbookPrecisionActiveRequest = null;
- let orderbookPrecisionPendingRequest = null;
let orderbookPrecisionSelectionTask = null;
let orderbookPrecisionOptionsLoadRequestedSymbol = null;
let orderbookPrecisionOptionsLoadAttemptedSymbol = null;
@@ -1936,8 +1904,7 @@
current: null,
nativeOptions: [],
nativeOptionsStatus: null,
- status: "数据不足",
- sampleEndsAt: 0
+ status: "数据不足"
};
const controlledNativeButtons = /* @__PURE__ */ new Set();
let lastObservedSymbol = getCurrentSymbol();
@@ -2539,18 +2506,11 @@
function getBestOrderbookPrice(side) {
return getOrderbookPrices(side, 1)[0] || null;
}
- function getLatestTradePrices(limit = 20) {
- return Array.from(document.querySelectorAll(".tradew-tradelist .price.emit-price")).filter((node) => isVisibleElement(node)).map((node) => parsePrice(node)).filter(Boolean).slice(0, Math.max(1, Number(limit) || 20));
- }
- async function waitForLatestTradePricesReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) {
- const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
- while (!document.hidden && isFuturesTradingPage() && isCurrentObservedSymbol(symbol)) {
- const prices = getLatestTradePrices();
- if (prices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) return prices;
- if (Date.now() >= deadline) return prices;
- await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS);
+ function getLatestTradePrices(limit = ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT) {
+ if (!Number.isInteger(limit) || limit < 2) {
+ throw new Error(`Invalid latest trade price limit: ${limit}`);
}
- return [];
+ return Array.from(document.querySelectorAll(".tradew-tradelist .price.emit-price")).filter((node) => isVisibleElement(node)).map((node) => parsePrice(node)).filter(Boolean).slice(0, limit);
}
async function waitForOrderbookPrecisionBootstrapReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) {
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
@@ -2577,9 +2537,12 @@
function saveStoredOrderbookPrecisionSamples(symbol, samples) {
const key = orderbookPrecisionSamplesKey(symbol);
if (!key) return [];
- const merged = mergePrecisionSamples([], samples, ORDERBOOK_PRECISION_SAMPLE_MAX);
- localStorage.setItem(key, JSON.stringify(merged));
- return merged;
+ if (!Array.isArray(samples) || samples.length > ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT - 1) {
+ throw new Error("Invalid latest trade movement snapshot");
+ }
+ const normalizedSamples = samples.map((sample) => normalizeDecimalString(sample)).filter((sample) => sample && isPositiveDecimalString(sample));
+ localStorage.setItem(key, JSON.stringify(normalizedSamples));
+ return normalizedSamples;
}
function getOrderbookPrecisionRecommendation(symbol = getCurrentSymbol()) {
const samples = readStoredOrderbookPrecisionSamples(symbol);
@@ -2804,12 +2767,7 @@
});
const current = readCurrentOrderbookPrecisionValue();
const existingStatus = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.status : null;
- const { busy, status } = resolveOrderbookPrecisionSampleState({
- sampling: orderbookPrecisionSampling,
- scheduled: Boolean(orderbookPrecisionSampleTimer),
- status: existingStatus,
- recommendation
- });
+ const status = recommendation ? "ready" : existingStatus && /^(未定位|未找到|数据不足)/.test(existingStatus) ? existingStatus : "数据不足";
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
@@ -2819,7 +2777,7 @@
status
};
const selectionBusy = Boolean(orderbookPrecisionSelectionTask);
- const controlsBusy = busy || selectionBusy;
+ const controlsBusy = selectionBusy;
const nativeOptions = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.nativeOptions : [];
const shortcutOptions = getOrderbookPrecisionShortcutOptions(
nativeOptions,
@@ -2827,10 +2785,10 @@
);
if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol);
const canRefresh = !controlsBusy;
- const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS);
+ const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT);
const recommendationHtml = [
'
',
- '
',
+ '
',
`
${PANEL_COPY.field.pricePrecision}`,
...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy),
`
`,
@@ -3007,101 +2965,37 @@
}
}, 0);
}
- async function runOrderbookPrecisionSampleRound(request) {
- orderbookPrecisionSampleTimer = 0;
- if (orderbookPrecisionSampling || document.hidden || !isFuturesTradingPage()) return;
- const symbol = request.symbol;
- if (!isCurrentObservedSymbol(symbol)) return false;
- orderbookPrecisionSampling = true;
- orderbookPrecisionActiveRequest = request;
- const tradeMoveSamples = [];
- const sampleDurationMs = Math.max(0, Number(request.durationMs) || ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS);
- try {
- const readyPrices = await waitForLatestTradePricesReady(symbol);
- if (!isCurrentObservedSymbol(symbol)) return false;
- if (readyPrices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) {
- tradeMoveSamples.push(...collectNonZeroPriceMoves(readyPrices));
- }
- const deadline = Date.now() + sampleDurationMs;
- orderbookPrecisionState = {
- ...orderbookPrecisionState,
- symbol,
- sampleEndsAt: deadline
- };
- scheduleRenderPanel({ followUpMs: 1e3 });
- while (Date.now() < deadline && !document.hidden && isFuturesTradingPage() && isCurrentObservedSymbol(symbol)) {
- tradeMoveSamples.push(...collectNonZeroPriceMoves(getLatestTradePrices()));
- await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS);
- }
- if (!isCurrentObservedSymbol(symbol)) return false;
- const newSamples = tradeMoveSamples;
- const samples = saveStoredOrderbookPrecisionSamples(symbol, newSamples);
- const recommendation = recommendOrderbookPrecision({
- samples,
- options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS
- });
+ function refreshOrderbookPrecisionSamplesNow() {
+ const symbol = getCurrentSymbol();
+ if (!symbol || !isCurrentObservedSymbol(symbol)) {
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
- samples,
- recommendation,
- current: readCurrentOrderbookPrecisionValue(),
- status: recommendation ? "ready" : "数据不足",
- sampleEndsAt: 0
+ status: "未定位当前交易对"
};
- refreshOrderbookPrecisionRecommendation();
scheduleRenderPanel();
- return true;
- } finally {
- orderbookPrecisionSampling = false;
- orderbookPrecisionActiveRequest = null;
- const pending = orderbookPrecisionPendingRequest;
- orderbookPrecisionPendingRequest = null;
- if (pending && isCurrentObservedSymbol(pending.symbol)) {
- scheduleOrderbookPrecisionSampleRound(0, { ...pending, force: true });
- }
- }
- }
- function scheduleOrderbookPrecisionSampleRound(delayMs = 0, options) {
- const {
- force = false,
- durationMs = ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS
- } = options || {};
- if (document.hidden || !isFuturesTradingPage()) return;
- const symbol = getCurrentSymbol();
- if (!isCurrentObservedSymbol(symbol)) return;
- const request = { symbol, durationMs };
- if (orderbookPrecisionSampling) {
- if (force) orderbookPrecisionPendingRequest = request;
return;
}
- if (orderbookPrecisionSampling || orderbookPrecisionSampleTimer) return;
- orderbookPrecisionSampleTimer = window.setTimeout(
- () => runOrderbookPrecisionSampleRound(request),
- Math.max(0, Number(delayMs) || 0)
+ const samples = saveStoredOrderbookPrecisionSamples(
+ symbol,
+ collectNonZeroPriceMoves(getLatestTradePrices(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT))
);
- }
- function stopOrderbookPrecisionSampler() {
- window.clearTimeout(orderbookPrecisionSampleTimer);
- orderbookPrecisionSampleTimer = 0;
- orderbookPrecisionPendingRequest = null;
- }
- function refreshOrderbookPrecisionSamplesNow() {
- const symbol = getCurrentSymbol();
+ const recommendation = recommendOrderbookPrecision({
+ samples,
+ options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS
+ });
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
- status: "刷新中",
- sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS
+ samples,
+ recommendation,
+ current: readCurrentOrderbookPrecisionValue(),
+ status: recommendation ? "ready" : "数据不足"
};
- stopOrderbookPrecisionSampler();
if (!orderbookPrecisionState.nativeOptions.length) {
queueOrderbookPrecisionOptionsLoad(symbol, true);
}
- scheduleOrderbookPrecisionSampleRound(0, {
- force: true,
- durationMs: ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS
- });
+ refreshOrderbookPrecisionRecommendation();
scheduleRenderPanel();
}
function getBufferedMakerPrices(side, levels, ladderStep = DEFAULT_LADDER_STEP) {
@@ -5572,7 +5466,7 @@
}
function ladderOptionRow(title, tooltip, options, selected, group, suffix = "") {
return [
- '
',
+ '
',
`${title}`,
...options.map((value) => ladderOptionButton(`${value}${suffix}`, value, Number(value) === Number(selected), group)),
"
"
@@ -6153,7 +6047,6 @@
stopTradingTimers();
invalidateTradeButtonCache();
lastDisplayCloseState = null;
- stopOrderbookPrecisionSampler();
}
function renderPanel() {
if (!isFuturesTradingPage()) {
@@ -6497,7 +6390,6 @@
lastObservedAccountPositionState = null;
closeGuard = null;
invalidateTradeButtonCache();
- stopOrderbookPrecisionSampler();
orderbookPrecisionOptionsLoadRequestedSymbol = null;
orderbookPrecisionOptionsLoadAttemptedSymbol = null;
const recommendation = getOrderbookPrecisionRecommendation(symbol);
@@ -6508,8 +6400,7 @@
current: readCurrentOrderbookPrecisionValue(),
nativeOptions: [],
nativeOptionsStatus: null,
- status: recommendation ? "ready" : "数据不足",
- sampleEndsAt: 0
+ status: recommendation ? "ready" : "数据不足"
};
}
function checkSymbolChangeForLeverage() {
@@ -6560,7 +6451,6 @@
stopOrderbookPrecisionObserver();
clearTradeUiMutationWait();
stopRenderPanelTimer();
- stopOrderbookPrecisionSampler();
}
function syncRouteState() {
if (document.hidden) return;
diff --git a/src/binance-orderbook-trade/contracts/panel-copy.js b/src/binance-orderbook-trade/contracts/panel-copy.js
index cfa637d..4e408ea 100644
--- a/src/binance-orderbook-trade/contracts/panel-copy.js
+++ b/src/binance-orderbook-trade/contracts/panel-copy.js
@@ -13,7 +13,7 @@ export const PANEL_COPY = Object.freeze({
ratio: '比例',
orderCount: '笔数',
interval: '间距',
- pricePrecision: '价格精度',
+ pricePrecision: '精度',
}),
action: freezeCopy({
openLong: '阶梯开多',
@@ -35,10 +35,10 @@ export const PANEL_COPY = Object.freeze({
}),
});
-export function formatPrecisionRefreshTooltip(durationMs) {
- const seconds = Number(durationMs) / 1000;
- if (!Number.isInteger(seconds) || seconds <= 0) {
- throw new Error(`Invalid precision sample duration: ${durationMs}`);
+export function formatPrecisionRefreshTooltip(tradeCount) {
+ const count = Number(tradeCount);
+ if (!Number.isInteger(count) || count <= 1) {
+ throw new Error(`Invalid precision trade count: ${tradeCount}`);
}
- return `根据最近 ${seconds} 秒的成交价变动,重新计算推荐的价格精度。`;
+ return `根据最新 ${count} 条成交价中的有效价格变动,重新计算推荐精度。`;
}
diff --git a/src/binance-orderbook-trade/core/precision.js b/src/binance-orderbook-trade/core/precision.js
index 4ba1f8a..fb377de 100644
--- a/src/binance-orderbook-trade/core/precision.js
+++ b/src/binance-orderbook-trade/core/precision.js
@@ -65,35 +65,6 @@ export function collectNonZeroPriceMoves(prices) {
return moves;
}
-export function mergePrecisionSamples(existingSamples, newSamples, maxSamples = 64) {
- const merged = [...(existingSamples || []), ...(newSamples || [])]
- .map((sample) => normalizeDecimalString(sample))
- .filter((sample) => sample && isPositiveDecimalString(sample));
- return merged.slice(Math.max(0, merged.length - maxSamples));
-}
-
-export function resolveOrderbookPrecisionSampleState({
- sampling,
- scheduled,
- status,
- recommendation,
-}) {
- const busy = Boolean(sampling || scheduled);
- if (busy) {
- return {
- busy,
- status: status === '刷新中' ? '刷新中' : '采样中',
- };
- }
- if (status && /^(未定位|未找到|数据不足)/.test(status)) {
- return { busy, status };
- }
- return {
- busy,
- status: recommendation ? 'ready' : '数据不足',
- };
-}
-
function sortedPositiveDecimals(values) {
return (values || [])
.map((value) => normalizeDecimalString(value))
diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js
index 79cf8f7..3e171ff 100644
--- a/src/binance-orderbook-trade/index.user.js
+++ b/src/binance-orderbook-trade/index.user.js
@@ -3,7 +3,7 @@
// @namespace binance.orderbook.trade
// @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
// @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
-// @version 2.7.123
+// @version 2.7.124
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -68,9 +68,7 @@ import {
collectNonZeroPriceMoves,
formatOrderbookPrecisionShortcutLabel,
getOrderbookPrecisionShortcutOptions,
- mergePrecisionSamples,
recommendOrderbookPrecision,
- resolveOrderbookPrecisionSampleState,
} from './core/precision.js';
import { allocateLadderQuantities } from './core/quantity.js';
import {
@@ -269,13 +267,10 @@ import {
const LADDER_OPEN_QTY_READY_TIMEOUT_MS = 1200;
const TRADE_INPUT_SYNC_TIMEOUT_MS = 350;
const TRADE_INPUT_SYNC_STABLE_FRAMES = 2;
- const ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS = 6000;
- const ORDERBOOK_PRECISION_SAMPLE_POLL_MS = 300;
const ORDERBOOK_PRECISION_READY_POLL_MS = 100;
const ORDERBOOK_PRECISION_READY_TIMEOUT_MS = 5000;
const ORDERBOOK_PRECISION_OPTION_WAIT_MS = 1200;
- const ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS = 6;
- const ORDERBOOK_PRECISION_SAMPLE_MAX = 96;
+ const ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT = 10;
const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4;
const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [
'0.00000001',
@@ -359,10 +354,6 @@ import {
let panelResizeObserver = null;
let ladderSubmitCaptureSequence = 0;
let activeLadderSubmitCapture = null;
- let orderbookPrecisionSampling = false;
- let orderbookPrecisionSampleTimer = 0;
- let orderbookPrecisionActiveRequest = null;
- let orderbookPrecisionPendingRequest = null;
let orderbookPrecisionSelectionTask = null;
let orderbookPrecisionOptionsLoadRequestedSymbol = null;
let orderbookPrecisionOptionsLoadAttemptedSymbol = null;
@@ -377,7 +368,6 @@ import {
nativeOptions: [],
nativeOptionsStatus: null,
status: '数据不足',
- sampleEndsAt: 0,
};
const controlledNativeButtons = new Set();
let lastObservedSymbol = getCurrentSymbol();
@@ -1170,23 +1160,15 @@ import {
return getOrderbookPrices(side, 1)[0] || null;
}
- function getLatestTradePrices(limit = 20) {
+ function getLatestTradePrices(limit = ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT) {
+ if (!Number.isInteger(limit) || limit < 2) {
+ throw new Error(`Invalid latest trade price limit: ${limit}`);
+ }
return Array.from(document.querySelectorAll('.tradew-tradelist .price.emit-price'))
.filter((node) => isVisibleElement(node))
.map((node) => parsePrice(node))
.filter(Boolean)
- .slice(0, Math.max(1, Number(limit) || 20));
- }
-
- async function waitForLatestTradePricesReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) {
- const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
- while (!document.hidden && isFuturesTradingPage() && isCurrentObservedSymbol(symbol)) {
- const prices = getLatestTradePrices();
- if (prices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) return prices;
- if (Date.now() >= deadline) return prices;
- await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS);
- }
- return [];
+ .slice(0, limit);
}
async function waitForOrderbookPrecisionBootstrapReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) {
@@ -1219,9 +1201,14 @@ import {
function saveStoredOrderbookPrecisionSamples(symbol, samples) {
const key = orderbookPrecisionSamplesKey(symbol);
if (!key) return [];
- const merged = mergePrecisionSamples([], samples, ORDERBOOK_PRECISION_SAMPLE_MAX);
- localStorage.setItem(key, JSON.stringify(merged));
- return merged;
+ if (!Array.isArray(samples) || samples.length > ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT - 1) {
+ throw new Error('Invalid latest trade movement snapshot');
+ }
+ const normalizedSamples = samples
+ .map((sample) => normalizeDecimalString(sample))
+ .filter((sample) => sample && isPositiveDecimalString(sample));
+ localStorage.setItem(key, JSON.stringify(normalizedSamples));
+ return normalizedSamples;
}
function getOrderbookPrecisionRecommendation(symbol = getCurrentSymbol()) {
@@ -1507,12 +1494,11 @@ import {
});
const current = readCurrentOrderbookPrecisionValue();
const existingStatus = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.status : null;
- const { busy, status } = resolveOrderbookPrecisionSampleState({
- sampling: orderbookPrecisionSampling,
- scheduled: Boolean(orderbookPrecisionSampleTimer),
- status: existingStatus,
- recommendation,
- });
+ const status = recommendation
+ ? 'ready'
+ : existingStatus && /^(未定位|未找到|数据不足)/.test(existingStatus)
+ ? existingStatus
+ : '数据不足';
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
@@ -1523,7 +1509,7 @@ import {
};
const selectionBusy = Boolean(orderbookPrecisionSelectionTask);
- const controlsBusy = busy || selectionBusy;
+ const controlsBusy = selectionBusy;
const nativeOptions = orderbookPrecisionState.symbol === symbol
? orderbookPrecisionState.nativeOptions
: [];
@@ -1533,10 +1519,10 @@ import {
);
if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol);
const canRefresh = !controlsBusy;
- const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS);
+ const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT);
const recommendationHtml = [
'
',
- '
',
+ '
',
`
${PANEL_COPY.field.pricePrecision}`,
...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy),
`
`,
@@ -1747,106 +1733,37 @@ import {
}, 0);
}
- async function runOrderbookPrecisionSampleRound(request) {
- orderbookPrecisionSampleTimer = 0;
- if (orderbookPrecisionSampling || document.hidden || !isFuturesTradingPage()) return;
- const symbol = request.symbol;
- if (!isCurrentObservedSymbol(symbol)) return false;
-
- orderbookPrecisionSampling = true;
- orderbookPrecisionActiveRequest = request;
- const tradeMoveSamples = [];
- const sampleDurationMs = Math.max(0, Number(request.durationMs) || ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS);
- try {
- const readyPrices = await waitForLatestTradePricesReady(symbol);
- if (!isCurrentObservedSymbol(symbol)) return false;
- if (readyPrices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) {
- tradeMoveSamples.push(...collectNonZeroPriceMoves(readyPrices));
- }
- const deadline = Date.now() + sampleDurationMs;
- orderbookPrecisionState = {
- ...orderbookPrecisionState,
- symbol,
- sampleEndsAt: deadline,
- };
- scheduleRenderPanel({ followUpMs: 1000 });
- while (Date.now() < deadline && !document.hidden && isFuturesTradingPage() && isCurrentObservedSymbol(symbol)) {
- tradeMoveSamples.push(...collectNonZeroPriceMoves(getLatestTradePrices()));
- await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS);
- }
-
- if (!isCurrentObservedSymbol(symbol)) return false;
- const newSamples = tradeMoveSamples;
- const samples = saveStoredOrderbookPrecisionSamples(symbol, newSamples);
- const recommendation = recommendOrderbookPrecision({
- samples,
- options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS,
- });
+ function refreshOrderbookPrecisionSamplesNow() {
+ const symbol = getCurrentSymbol();
+ if (!symbol || !isCurrentObservedSymbol(symbol)) {
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
- samples,
- recommendation,
- current: readCurrentOrderbookPrecisionValue(),
- status: recommendation ? 'ready' : '数据不足',
- sampleEndsAt: 0,
+ status: '未定位当前交易对',
};
- refreshOrderbookPrecisionRecommendation();
scheduleRenderPanel();
- return true;
- } finally {
- orderbookPrecisionSampling = false;
- orderbookPrecisionActiveRequest = null;
- const pending = orderbookPrecisionPendingRequest;
- orderbookPrecisionPendingRequest = null;
- if (pending && isCurrentObservedSymbol(pending.symbol)) {
- scheduleOrderbookPrecisionSampleRound(0, { ...pending, force: true });
- }
- }
- }
-
- function scheduleOrderbookPrecisionSampleRound(delayMs = 0, options) {
- const {
- force = false,
- durationMs = ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS,
- } = options || {};
- if (document.hidden || !isFuturesTradingPage()) return;
- const symbol = getCurrentSymbol();
- if (!isCurrentObservedSymbol(symbol)) return;
- const request = { symbol, durationMs };
- if (orderbookPrecisionSampling) {
- if (force) orderbookPrecisionPendingRequest = request;
return;
}
- if (orderbookPrecisionSampling || orderbookPrecisionSampleTimer) return;
- orderbookPrecisionSampleTimer = window.setTimeout(
- () => runOrderbookPrecisionSampleRound(request),
- Math.max(0, Number(delayMs) || 0)
+ const samples = saveStoredOrderbookPrecisionSamples(
+ symbol,
+ collectNonZeroPriceMoves(getLatestTradePrices(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT))
);
- }
-
- function stopOrderbookPrecisionSampler() {
- window.clearTimeout(orderbookPrecisionSampleTimer);
- orderbookPrecisionSampleTimer = 0;
- orderbookPrecisionPendingRequest = null;
- }
-
- function refreshOrderbookPrecisionSamplesNow() {
- const symbol = getCurrentSymbol();
+ const recommendation = recommendOrderbookPrecision({
+ samples,
+ options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS,
+ });
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
- status: '刷新中',
- sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS,
+ samples,
+ recommendation,
+ current: readCurrentOrderbookPrecisionValue(),
+ status: recommendation ? 'ready' : '数据不足',
};
- stopOrderbookPrecisionSampler();
if (!orderbookPrecisionState.nativeOptions.length) {
queueOrderbookPrecisionOptionsLoad(symbol, true);
}
- scheduleOrderbookPrecisionSampleRound(0, {
- force: true,
- durationMs: ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS,
- });
+ refreshOrderbookPrecisionRecommendation();
scheduleRenderPanel();
}
@@ -4742,7 +4659,7 @@ import {
function ladderOptionRow(title, tooltip, options, selected, group, suffix = '') {
return [
- '
',
+ '
',
`${title}`,
...options.map((value) => ladderOptionButton(`${value}${suffix}`, value, Number(value) === Number(selected), group)),
'
',
@@ -5387,7 +5304,6 @@ import {
stopTradingTimers();
invalidateTradeButtonCache();
lastDisplayCloseState = null;
- stopOrderbookPrecisionSampler();
}
function renderPanel() {
@@ -5797,7 +5713,6 @@ import {
lastObservedAccountPositionState = null;
closeGuard = null;
invalidateTradeButtonCache();
- stopOrderbookPrecisionSampler();
orderbookPrecisionOptionsLoadRequestedSymbol = null;
orderbookPrecisionOptionsLoadAttemptedSymbol = null;
const recommendation = getOrderbookPrecisionRecommendation(symbol);
@@ -5809,7 +5724,6 @@ import {
nativeOptions: [],
nativeOptionsStatus: null,
status: recommendation ? 'ready' : '数据不足',
- sampleEndsAt: 0,
};
}
@@ -5869,7 +5783,6 @@ import {
stopOrderbookPrecisionObserver();
clearTradeUiMutationWait();
stopRenderPanelTimer();
- stopOrderbookPrecisionSampler();
}
function syncRouteState() {
diff --git a/test/unit/binance-orderbook-trade/ladder-options.test.js b/test/unit/binance-orderbook-trade/ladder-options.test.js
index 8e1dd03..b7f5b43 100644
--- a/test/unit/binance-orderbook-trade/ladder-options.test.js
+++ b/test/unit/binance-orderbook-trade/ladder-options.test.js
@@ -237,7 +237,7 @@ test('open and close ladder percentage rows share the centralized ratio label',
test('ladder quantity levels and step options share one stable five-slot grid', () => {
const optionRow = source.match(/function ladderOptionRow[\s\S]*?\n }/)?.[0] || '';
- assert.match(optionRow, /grid-template-columns:48px repeat\(5,minmax\(0,1fr\)\)/);
+ assert.match(optionRow, /grid-template-columns:36px repeat\(5,minmax\(0,1fr\)\)/);
assert.doesNotMatch(optionRow, /flex-wrap/);
assert.match(source, /data-ladder-value="\$\{value\}" style="box-sizing:border-box;width:100%;min-width:0;height:28px/);
assert.equal((source.match(/ladderOptionRow\(PANEL_COPY\.field\.orderCount, PANEL_COPY\.tooltip\.orderCount, LADDER_LEVEL_OPTIONS/g) || []).length, 2);
diff --git a/test/unit/binance-orderbook-trade/panel-copy.test.js b/test/unit/binance-orderbook-trade/panel-copy.test.js
index 44b078f..4dc3320 100644
--- a/test/unit/binance-orderbook-trade/panel-copy.test.js
+++ b/test/unit/binance-orderbook-trade/panel-copy.test.js
@@ -20,7 +20,7 @@ test('panel copy keeps the approved labels and tooltips in one contract', () =>
ratio: '比例',
orderCount: '笔数',
interval: '间距',
- pricePrecision: '价格精度',
+ pricePrecision: '精度',
},
action: {
openLong: '阶梯开多',
@@ -43,10 +43,11 @@ test('panel copy keeps the approved labels and tooltips in one contract', () =>
});
});
-test('precision refresh tooltip describes the sampling window without calling it a minimum', () => {
+test('precision refresh tooltip describes the latest trade snapshot without calling it a minimum', () => {
assert.equal(
- formatPrecisionRefreshTooltip(6000),
- '根据最近 6 秒的成交价变动,重新计算推荐的价格精度。',
+ formatPrecisionRefreshTooltip(10),
+ '根据最新 10 条成交价中的有效价格变动,重新计算推荐精度。',
);
- assert.doesNotMatch(formatPrecisionRefreshTooltip(6000), /最小值/);
+ assert.doesNotMatch(formatPrecisionRefreshTooltip(10), /最小值/);
+ assert.throws(() => formatPrecisionRefreshTooltip(1), /Invalid precision trade count/);
});
diff --git a/test/unit/binance-orderbook-trade/precision.test.js b/test/unit/binance-orderbook-trade/precision.test.js
index adf6257..1bd2788 100644
--- a/test/unit/binance-orderbook-trade/precision.test.js
+++ b/test/unit/binance-orderbook-trade/precision.test.js
@@ -6,9 +6,7 @@ import {
formatOrderbookPrecisionShortcutLabel,
getOrderbookPrecisionDecadeTarget,
getOrderbookPrecisionShortcutOptions,
- mergePrecisionSamples,
recommendOrderbookPrecision,
- resolveOrderbookPrecisionSampleState,
} from '../../../src/binance-orderbook-trade/core/precision.js';
test('keeps only the four smallest exact native precision shortcuts', () => {
@@ -57,14 +55,7 @@ test('collects only non-zero price moves from consecutive observations', () => {
);
});
-test('keeps multiple sampling rounds bounded and newest samples last', () => {
- assert.deepEqual(
- mergePrecisionSamples(['0.001', '0.002', '0.003'], ['0.004', '0.005'], 4),
- ['0.002', '0.003', '0.004', '0.005']
- );
-});
-
-test('recommends precision from accumulated effective price movement instead of tick size', () => {
+test('recommends precision from the latest effective price movement instead of tick size', () => {
assert.equal(recommendOrderbookPrecision({
samples: ['0.0001', '0.0061', '0.0107', '0.0089', '0.0112', '0.0075'],
options: ['0.0001', '0.001', '0.01', '0.1', '1'],
@@ -107,46 +98,20 @@ test('does not treat display precision fallback as a recommendation', () => {
}), null);
});
-test('does not keep a stale sampling label busy after the sampler has stopped', () => {
- assert.deepEqual(resolveOrderbookPrecisionSampleState({
- sampling: false,
- scheduled: false,
- status: '采样中',
- recommendation: '0.00001',
- }), {
- busy: false,
- status: 'ready',
- });
-
- assert.deepEqual(resolveOrderbookPrecisionSampleState({
- sampling: false,
- scheduled: false,
- status: '采样中',
- recommendation: null,
- }), {
- busy: false,
- status: '数据不足',
- });
-});
-
-test('keeps precision controls busy only for an active or scheduled sample', () => {
- assert.deepEqual(resolveOrderbookPrecisionSampleState({
- sampling: true,
- scheduled: false,
- status: '采样中',
- recommendation: null,
- }), {
- busy: true,
- status: '采样中',
- });
+test('ten latest trade rows provide enough movement evidence when eight do not', () => {
+ const prices = [
+ '80.757', '80.757', '80.756', '80.756', '80.756',
+ '80.757', '80.756', '80.755', '80.783', '80.781',
+ ];
+ const options = ['0.0001', '0.001', '0.01', '0.1', '1'];
- assert.deepEqual(resolveOrderbookPrecisionSampleState({
- sampling: false,
- scheduled: true,
- status: '刷新中',
- recommendation: '0.00001',
- }), {
- busy: true,
- status: '刷新中',
- });
+ assert.equal(collectNonZeroPriceMoves(prices.slice(0, 8)).length, 4);
+ assert.equal(recommendOrderbookPrecision({
+ samples: collectNonZeroPriceMoves(prices.slice(0, 8)),
+ options,
+ }), null);
+ assert.equal(recommendOrderbookPrecision({
+ samples: collectNonZeroPriceMoves(prices),
+ options,
+ }), '0.001');
});
diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js
index 9da8d3a..136d8c3 100644
--- a/test/unit/binance-orderbook-trade/source-regressions.test.js
+++ b/test/unit/binance-orderbook-trade/source-regressions.test.js
@@ -338,7 +338,7 @@ test('dynamic panel text keeps fixed single-line slots', () => {
assert.match(source, /data-panel-group="direction" style="display:flex;align-items:center;justify-content:flex-start;gap:6px;height:32px;overflow:hidden/);
assert.match(source, /data-side-selector role="radiogroup"[^>]*display:grid;grid-template-columns:54px 54px;[^>]*border-radius:6px;[^>]*overflow:hidden/);
assert.match(source, new RegExp(`id="\\$\\{MODE_HINT_ID\\}" style="width:78px;flex:0 0 78px;[^\"]*white-space:nowrap;overflow:hidden;text-overflow:ellipsis`));
- assert.match(source, /grid-template-columns:62px repeat\(4,minmax\(0,1fr\)\) 32px;align-items:center;gap:4px;height:32px;overflow:hidden/);
+ assert.match(source, /grid-template-columns:36px repeat\(4,minmax\(0,1fr\)\) 32px;align-items:center;gap:4px;height:32px;overflow:hidden/);
assert.doesNotMatch(source, /buttonBaseStyle = `width:68px;height:24px/);
assert.match(readFunctionBody('renderOrderbookPrecisionShortcut'), /height:32px[^`]*font-size:12px;line-height:30px/);
assert.match(readFunctionBody('renderOrderbookPrecisionShortcutSlots'), /while \(slots\.length < ORDERBOOK_PRECISION_SHORTCUT_LIMIT\)/);
@@ -929,7 +929,9 @@ test('stable panel refreshes avoid writing unchanged text and state attributes',
});
test('orderbook precision recommendation marks one shortcut without applying it automatically', () => {
- assert.match(source, /ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS = 6000/);
+ assert.match(source, /ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT = 10/);
+ assert.doesNotMatch(source, /ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS/);
+ assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_POLL_MS/);
assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_DURATION_MS/);
assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_PAUSE_MS/);
assert.match(source, /LOCAL_ORDERBOOK_PRECISION_SAMPLES_PREFIX = 'jh_binance_orderbook_precision_samples_v3'/);
@@ -938,32 +940,27 @@ test('orderbook precision recommendation marks one shortcut without applying it
assert.match(source, /data-orderbook-precision-value/);
assert.doesNotMatch(source, /data-orderbook-precision-adjust/);
assert.match(source, /data-orderbook-precision-refresh/);
- assert.match(source, /orderbookPrecisionPendingRequest/);
+ assert.doesNotMatch(source, /orderbookPrecisionPendingRequest/);
+ assert.doesNotMatch(source, /orderbookPrecisionSampleTimer/);
+ assert.doesNotMatch(source, /mergePrecisionSamples/);
assert.match(source, /orderbookPrecisionOptionsLoadAttemptedSymbol/);
- const sampleBody = readFunctionBody('runOrderbookPrecisionSampleRound');
- assert.match(sampleBody, /collectNonZeroPriceMoves/);
- assert.match(sampleBody, /saveStoredOrderbookPrecisionSamples\(symbol,\s*newSamples\)/);
- assert.doesNotMatch(sampleBody, /mergePrecisionSamples\(\s*readStoredOrderbookPrecisionSamples/);
- assert.match(sampleBody, /waitForLatestTradePricesReady/);
- assert.match(sampleBody, /ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS/);
- assert.match(sampleBody, /getLatestTradePrices/);
- assert.doesNotMatch(sampleBody, /getCurrentOrderbookDisplayStep/);
- assert.doesNotMatch(sampleBody, /fallbackMovement/);
- assert.match(sampleBody, /orderbookPrecisionPendingRequest/);
- assert.doesNotMatch(sampleBody, /ORDERBOOK_PRECISION_SAMPLE_PAUSE_MS/);
- assert.match(sampleBody, /orderbookPrecisionState[\s\S]*sampleEndsAt: deadline/);
- assert.match(sampleBody, /scheduleRenderPanel\(\{ followUpMs: 1000 \}\)/);
+ const sampleBody = readFunctionBody('refreshOrderbookPrecisionSamplesNow');
+ assert.match(sampleBody, /collectNonZeroPriceMoves\(getLatestTradePrices\(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT\)\)/);
+ assert.match(sampleBody, /saveStoredOrderbookPrecisionSamples/);
+ assert.match(sampleBody, /recommendOrderbookPrecision/);
+ assert.match(sampleBody, /status: recommendation \? 'ready' : '数据不足'/);
+ assert.doesNotMatch(sampleBody, /setTimeout|setInterval|await delay/);
+ assert.doesNotMatch(sampleBody, /getCurrentOrderbookDisplayStep|fallbackMovement/);
const refreshBody = readFunctionBody('refreshOrderbookPrecisionRecommendation');
assert.match(refreshBody, /recommendOrderbookPrecision/);
- assert.match(refreshBody, /resolveOrderbookPrecisionSampleState/);
- assert.match(refreshBody, /scheduled: Boolean\(orderbookPrecisionSampleTimer\)/);
+ assert.doesNotMatch(refreshBody, /resolveOrderbookPrecisionSampleState|orderbookPrecisionSampleTimer/);
assert.match(refreshBody, /data-orderbook-precision-refresh="true"[\s\S]*disabled/);
- assert.match(refreshBody, /const controlsBusy = busy \|\| selectionBusy/);
+ assert.match(refreshBody, /const controlsBusy = selectionBusy/);
assert.match(refreshBody, /getOrderbookPrecisionShortcutOptions\([\s\S]*ORDERBOOK_PRECISION_SHORTCUT_LIMIT/);
assert.match(refreshBody, /queueOrderbookPrecisionOptionsLoad\(symbol\)/);
- assert.match(refreshBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS\)/);
+ assert.match(refreshBody, /formatPrecisionRefreshTooltip\(ORDERBOOK_PRECISION_LATEST_TRADE_LIMIT\)/);
const shortcutBody = readFunctionBody('renderOrderbookPrecisionShortcut');
assert.match(shortcutBody, /const recommended = value === recommendation/);
assert.match(shortcutBody, /position:absolute;top:3px;right:3px;width:6px;height:6px/);
@@ -981,7 +978,7 @@ test('orderbook precision recommendation marks one shortcut without applying it
assert.match(refreshBody, /PANEL_COPY\.field\.pricePrecision/);
assert.match(refreshBody, /PANEL_COPY\.tooltip\.pricePrecision/);
assert.match(refreshBody, /renderOrderbookPrecisionShortcutSlots\(shortcutOptions, current, recommendation, controlsBusy\)/);
- assert.match(refreshBody, /grid-template-columns:62px repeat\(4,minmax\(0,1fr\)\) 32px/);
+ assert.match(refreshBody, /grid-template-columns:36px repeat\(4,minmax\(0,1fr\)\) 32px/);
assert.match(refreshBody, /data-orderbook-precision-refresh="true"[^`]*