diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js
index cc23c7a..19262b2 100644
--- a/e2e/binance-orderbook/specs/control-flows.pw.js
+++ b/e2e/binance-orderbook/specs/control-flows.pw.js
@@ -91,6 +91,48 @@ 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('precision refresh explains when the complete visible trade list still lacks movement', async ({ page }) => {
+ const scenario = createCancelScenario({
+ ui: { orderbookPrecision: '0.1' },
+ });
+ const { errors } = await openUserscriptScenario(page, scenario);
+ const panel = page.locator(PANEL_SELECTOR);
+ await page.locator('.tradew-tradelist .price.emit-price').evaluateAll((nodes) => {
+ nodes.forEach((node) => { node.textContent = '81.00'; });
+ });
+
+ await panel.locator('[data-orderbook-precision-refresh]').click();
+ await expect(panel.locator('#jh-binance-ladder-status'))
+ .toHaveText('近期价格变化不足,请稍后重试');
+ await expect(panel.locator('[aria-label*="推荐档位"]')).toHaveCount(0);
+ expect(await page.evaluate((symbol) => JSON.parse(
+ localStorage.getItem(`jh_binance_orderbook_precision_samples_v3:${symbol}`)
+ ), scenario.currentSymbol)).toEqual([]);
+ expect(errors).toEqual([]);
+});
+
test('starting a ladder disables every start action and exposes one stop control', async ({ page }) => {
const scenario = createCancelScenario({
ui: { tradeMode: 'OPEN', orderbookPrecision: '0.1' },
diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js
index 8b2ca26..b688119 100644
--- a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js
+++ b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js
@@ -19,11 +19,33 @@ async function expectVisualContract(page, name) {
expect(`${JSON.stringify(contract, null, 2)}\n`).toMatchSnapshot(name);
}
+async function expectNativeDivider(page) {
+ const divider = await page.locator(PANEL_SELECTOR).evaluate((panel) => {
+ const panelRect = panel.getBoundingClientRect();
+ const ladder = panel.querySelector('[data-panel-group="ladder"]');
+ const ladderRect = ladder.getBoundingClientRect();
+ const style = getComputedStyle(ladder);
+ return {
+ leftInset: ladderRect.left - panelRect.left,
+ rightInset: panelRect.right - ladderRect.right,
+ borderTopWidth: style.borderTopWidth,
+ borderTopColor: style.borderTopColor,
+ };
+ });
+ expect(divider).toEqual({
+ leftInset: 1,
+ rightInset: 1,
+ borderTopWidth: '1px',
+ borderTopColor: 'rgb(237, 237, 237)',
+ });
+}
+
test('open panel matches the fixed visual contract', async ({ page }) => {
await openUserscriptScenario(page, createCancelScenario({
ui: { tradeMode: 'OPEN' },
}));
await expectPrecisionReady(page);
+ await expectNativeDivider(page);
await expectVisualContract(page, 'open-fixed.visual.json');
});
@@ -36,5 +58,6 @@ test('close panel matches the disabled-state visual contract', async ({ page })
await expect(panel.getByRole('radio', { name: '平多' })).toBeEnabled();
await expect(panel.getByRole('radio', { name: '平空' })).toBeDisabled();
await expectPrecisionReady(page);
+ await expectNativeDivider(page);
await expectVisualContract(page, 'close-fixed.visual.json');
});
diff --git a/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json b/e2e/binance-orderbook/specs/panel-visual-contract.pw.js-snapshots/close-fixed-visual.json
index 78316ce..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
@@ -27,7 +27,7 @@
"ladder": {
"rect": {
"top": 135,
- "width": 292,
+ "width": 312,
"height": 263
},
"display": "block",
@@ -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 5147355..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
@@ -27,7 +27,7 @@
"ladder": {
"rect": {
"top": 135,
- "width": 292,
+ "width": 312,
"height": 263
},
"display": "block",
@@ -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 70e29dd..4ed0047 100644
--- a/scripts/binance-orderbook-trade.user.js
+++ b/scripts/binance-orderbook-trade.user.js
@@ -3,7 +3,7 @@
// @namespace binance.orderbook.trade
// @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
// @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
-// @version 2.7.122
+// @version 2.7.125
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -138,7 +138,7 @@
ratio: "比例",
orderCount: "笔数",
interval: "间距",
- pricePrecision: "价格精度"
+ pricePrecision: "精度"
}),
action: freezeCopy({
openLong: "阶梯开多",
@@ -150,6 +150,10 @@
noOrders: "无挂单",
stopLadder: "停止阶梯挂单"
}),
+ status: freezeCopy({
+ precisionUpdated: "精度推荐已更新",
+ precisionInsufficient: "近期价格变化不足,请稍后重试"
+ }),
tooltip: freezeCopy({
singleOrder: "单击订单簿中的某个价格,按当前方向和数量设置提交一笔订单。",
ladderMaker: "根据当前比例、笔数、间距和价格精度设置,依次提交只做 Maker 的阶梯订单。",
@@ -159,12 +163,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,30 +557,28 @@
}
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 === "刷新中" ? "刷新中" : "采样中"
- };
+ function collectPriceMovesWithExpandingWindow(prices, {
+ initialLimit = 10,
+ expansionStep = 10,
+ minSamples = 5
+ } = {}) {
+ if (!Number.isInteger(initialLimit) || initialLimit < 2) {
+ throw new Error(`Invalid initial precision trade limit: ${initialLimit}`);
}
- if (status && /^(未定位|未找到|数据不足)/.test(status)) {
- return { busy, status };
+ if (!Number.isInteger(expansionStep) || expansionStep < 1) {
+ throw new Error(`Invalid precision trade expansion step: ${expansionStep}`);
}
- return {
- busy,
- status: recommendation ? "ready" : "数据不足"
- };
+ if (!Number.isInteger(minSamples) || minSamples < 1) {
+ throw new Error(`Invalid minimum precision sample count: ${minSamples}`);
+ }
+ const observedPrices = Array.isArray(prices) ? prices : [];
+ let usedCount = Math.min(initialLimit, observedPrices.length);
+ let samples = collectNonZeroPriceMoves(observedPrices.slice(0, usedCount));
+ while (samples.length < minSamples && usedCount < observedPrices.length) {
+ usedCount = Math.min(usedCount + expansionStep, observedPrices.length);
+ samples = collectNonZeroPriceMoves(observedPrices.slice(0, usedCount));
+ }
+ return { samples, usedCount };
}
function sortedPositiveDecimals(values) {
return (values || []).map((value) => normalizeDecimalString(value)).filter((value) => value && isPositiveDecimalString(value)).sort((a, b) => compareDecimalStrings(a, b));
@@ -1831,13 +1833,12 @@
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_INITIAL_TRADE_LIMIT = 10;
+ const ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10;
+ const ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5;
const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4;
const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [
"0.00000001",
@@ -1864,6 +1865,7 @@
const PRIMARY_EMPHASIS_COLOR = "#000000";
const PRIMARY_EMPHASIS_FONT_WEIGHT = "500";
const CONTROL_BORDER_COLOR = "#d5d9e2";
+ const PANEL_DIVIDER_COLOR = "#ededed";
const CONTROL_BACKGROUND_COLOR = "#ffffff";
const CONTROL_TEXT_COLOR = "#5e6673";
const CONTROL_FONT_WEIGHT = "500";
@@ -1918,10 +1920,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;
@@ -1935,8 +1933,7 @@
current: null,
nativeOptions: [],
nativeOptionsStatus: null,
- status: "数据不足",
- sampleEndsAt: 0
+ status: PANEL_COPY.status.precisionInsufficient
};
const controlledNativeButtons = /* @__PURE__ */ new Set();
let lastObservedSymbol = getCurrentSymbol();
@@ -2538,18 +2535,8 @@
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);
- }
- return [];
+ function getLatestTradePrices() {
+ return Array.from(document.querySelectorAll(".tradew-tradelist .price.emit-price")).filter((node) => isVisibleElement(node)).map((node) => parsePrice(node)).filter(Boolean);
}
async function waitForOrderbookPrecisionBootstrapReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) {
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
@@ -2576,9 +2563,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)) {
+ 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);
@@ -2803,12 +2793,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 && existingStatus !== "ready" ? existingStatus : PANEL_COPY.status.precisionInsufficient;
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
@@ -2818,7 +2803,7 @@
status
};
const selectionBusy = Boolean(orderbookPrecisionSelectionTask);
- const controlsBusy = busy || selectionBusy;
+ const controlsBusy = selectionBusy;
const nativeOptions = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.nativeOptions : [];
const shortcutOptions = getOrderbookPrecisionShortcutOptions(
nativeOptions,
@@ -2826,10 +2811,10 @@
);
if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol);
const canRefresh = !controlsBusy;
- const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS);
+ const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT);
const recommendationHtml = [
'
',
- '
',
+ '
',
`
${PANEL_COPY.field.pricePrecision}`,
...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy),
`
`,
@@ -3006,101 +2991,42 @@
}
}, 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)
- );
- }
- function stopOrderbookPrecisionSampler() {
- window.clearTimeout(orderbookPrecisionSampleTimer);
- orderbookPrecisionSampleTimer = 0;
- orderbookPrecisionPendingRequest = null;
- }
- function refreshOrderbookPrecisionSamplesNow() {
- const symbol = getCurrentSymbol();
+ const { samples: latestSamples } = collectPriceMovesWithExpandingWindow(getLatestTradePrices(), {
+ initialLimit: ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT,
+ expansionStep: ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP,
+ minSamples: ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES
+ });
+ const samples = saveStoredOrderbookPrecisionSamples(symbol, latestSamples);
+ const recommendation = recommendOrderbookPrecision({
+ samples,
+ options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS
+ });
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
- status: "刷新中",
- sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS
+ samples,
+ recommendation,
+ current: readCurrentOrderbookPrecisionValue(),
+ status: recommendation ? "ready" : PANEL_COPY.status.precisionInsufficient
};
- stopOrderbookPrecisionSampler();
if (!orderbookPrecisionState.nativeOptions.length) {
queueOrderbookPrecisionOptionsLoad(symbol, true);
}
- scheduleOrderbookPrecisionSampleRound(0, {
- force: true,
- durationMs: ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS
- });
+ refreshOrderbookPrecisionRecommendation();
+ setLadderStatus(
+ recommendation ? PANEL_COPY.status.precisionUpdated : PANEL_COPY.status.precisionInsufficient
+ );
scheduleRenderPanel();
}
function getBufferedMakerPrices(side, levels, ladderStep = DEFAULT_LADDER_STEP) {
@@ -5571,7 +5497,7 @@
}
function ladderOptionRow(title, tooltip, options, selected, group, suffix = "") {
return [
- '
',
+ '
',
`${title}`,
...options.map((value) => ladderOptionButton(`${value}${suffix}`, value, Number(value) === Number(selected), group)),
"
"
@@ -5973,7 +5899,7 @@
"
",
"
",
"
",
- '
',
+ `
`,
`
${PANEL_COPY.section.ladderMaker}
`,
`
`,
`
空闲
`,
@@ -6152,7 +6078,6 @@
stopTradingTimers();
invalidateTradeButtonCache();
lastDisplayCloseState = null;
- stopOrderbookPrecisionSampler();
}
function renderPanel() {
if (!isFuturesTradingPage()) {
@@ -6496,7 +6421,6 @@
lastObservedAccountPositionState = null;
closeGuard = null;
invalidateTradeButtonCache();
- stopOrderbookPrecisionSampler();
orderbookPrecisionOptionsLoadRequestedSymbol = null;
orderbookPrecisionOptionsLoadAttemptedSymbol = null;
const recommendation = getOrderbookPrecisionRecommendation(symbol);
@@ -6507,8 +6431,7 @@
current: readCurrentOrderbookPrecisionValue(),
nativeOptions: [],
nativeOptionsStatus: null,
- status: recommendation ? "ready" : "数据不足",
- sampleEndsAt: 0
+ status: recommendation ? "ready" : PANEL_COPY.status.precisionInsufficient
};
}
function checkSymbolChangeForLeverage() {
@@ -6559,7 +6482,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..95b3613 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: '阶梯开多',
@@ -25,6 +25,10 @@ export const PANEL_COPY = Object.freeze({
noOrders: '无挂单',
stopLadder: '停止阶梯挂单',
}),
+ status: freezeCopy({
+ precisionUpdated: '精度推荐已更新',
+ precisionInsufficient: '近期价格变化不足,请稍后重试',
+ }),
tooltip: freezeCopy({
singleOrder: '单击订单簿中的某个价格,按当前方向和数量设置提交一笔订单。',
ladderMaker: '根据当前比例、笔数、间距和价格精度设置,依次提交只做 Maker 的阶梯订单。',
@@ -35,10 +39,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..f065fee 100644
--- a/src/binance-orderbook-trade/core/precision.js
+++ b/src/binance-orderbook-trade/core/precision.js
@@ -65,33 +65,28 @@ 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 === '刷新中' ? '刷新中' : '采样中',
- };
+export function collectPriceMovesWithExpandingWindow(prices, {
+ initialLimit = 10,
+ expansionStep = 10,
+ minSamples = 5,
+} = {}) {
+ if (!Number.isInteger(initialLimit) || initialLimit < 2) {
+ throw new Error(`Invalid initial precision trade limit: ${initialLimit}`);
+ }
+ if (!Number.isInteger(expansionStep) || expansionStep < 1) {
+ throw new Error(`Invalid precision trade expansion step: ${expansionStep}`);
+ }
+ if (!Number.isInteger(minSamples) || minSamples < 1) {
+ throw new Error(`Invalid minimum precision sample count: ${minSamples}`);
}
- if (status && /^(未定位|未找到|数据不足)/.test(status)) {
- return { busy, status };
+ const observedPrices = Array.isArray(prices) ? prices : [];
+ let usedCount = Math.min(initialLimit, observedPrices.length);
+ let samples = collectNonZeroPriceMoves(observedPrices.slice(0, usedCount));
+ while (samples.length < minSamples && usedCount < observedPrices.length) {
+ usedCount = Math.min(usedCount + expansionStep, observedPrices.length);
+ samples = collectNonZeroPriceMoves(observedPrices.slice(0, usedCount));
}
- return {
- busy,
- status: recommendation ? 'ready' : '数据不足',
- };
+ return { samples, usedCount };
}
function sortedPositiveDecimals(values) {
diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js
index 18a7831..da8738a 100644
--- a/src/binance-orderbook-trade/index.user.js
+++ b/src/binance-orderbook-trade/index.user.js
@@ -3,7 +3,7 @@
// @namespace binance.orderbook.trade
// @icon data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
// @icon64 data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20viewBox%3D%220%200%2064%2064%22%3E%3Crect%20width%3D%2264%22%20height%3D%2264%22%20rx%3D%2214%22%20fill%3D%22%23f0b90b%22%2F%3E%3Ctext%20x%3D%2232%22%20y%3D%2249%22%20text-anchor%3D%22middle%22%20font-family%3D%22Arial%2C%20sans-serif%22%20font-size%3D%2242%22%20font-weight%3D%22800%22%20fill%3D%22%23111827%22%3EJ%3C%2Ftext%3E%3C%2Fsvg%3E
-// @version 2.7.122
+// @version 2.7.125
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -65,12 +65,10 @@ import {
isPositiveDecimalString,
} from './core/decimal.js';
import {
- collectNonZeroPriceMoves,
+ collectPriceMovesWithExpandingWindow,
formatOrderbookPrecisionShortcutLabel,
getOrderbookPrecisionShortcutOptions,
- mergePrecisionSamples,
recommendOrderbookPrecision,
- resolveOrderbookPrecisionSampleState,
} from './core/precision.js';
import { allocateLadderQuantities } from './core/quantity.js';
import {
@@ -269,13 +267,12 @@ 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_INITIAL_TRADE_LIMIT = 10;
+ const ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10;
+ const ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5;
const ORDERBOOK_PRECISION_SHORTCUT_LIMIT = 4;
const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [
'0.00000001',
@@ -302,6 +299,7 @@ import {
const PRIMARY_EMPHASIS_COLOR = '#000000';
const PRIMARY_EMPHASIS_FONT_WEIGHT = '500';
const CONTROL_BORDER_COLOR = '#d5d9e2';
+ const PANEL_DIVIDER_COLOR = '#ededed';
const CONTROL_BACKGROUND_COLOR = '#ffffff';
const CONTROL_TEXT_COLOR = '#5e6673';
const CONTROL_FONT_WEIGHT = '500';
@@ -358,10 +356,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;
@@ -375,8 +369,7 @@ import {
current: null,
nativeOptions: [],
nativeOptionsStatus: null,
- status: '数据不足',
- sampleEndsAt: 0,
+ status: PANEL_COPY.status.precisionInsufficient,
};
const controlledNativeButtons = new Set();
let lastObservedSymbol = getCurrentSymbol();
@@ -1169,23 +1162,11 @@ import {
return getOrderbookPrices(side, 1)[0] || null;
}
- function getLatestTradePrices(limit = 20) {
+ function getLatestTradePrices() {
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 [];
+ .filter(Boolean);
}
async function waitForOrderbookPrecisionBootstrapReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) {
@@ -1218,9 +1199,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)) {
+ 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()) {
@@ -1506,12 +1492,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 && existingStatus !== 'ready'
+ ? existingStatus
+ : PANEL_COPY.status.precisionInsufficient;
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
@@ -1522,7 +1507,7 @@ import {
};
const selectionBusy = Boolean(orderbookPrecisionSelectionTask);
- const controlsBusy = busy || selectionBusy;
+ const controlsBusy = selectionBusy;
const nativeOptions = orderbookPrecisionState.symbol === symbol
? orderbookPrecisionState.nativeOptions
: [];
@@ -1532,10 +1517,10 @@ import {
);
if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol);
const canRefresh = !controlsBusy;
- const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS);
+ const refreshTooltip = formatPrecisionRefreshTooltip(ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT);
const recommendationHtml = [
'
',
- '
',
+ '
',
`
${PANEL_COPY.field.pricePrecision}`,
...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy),
`
`,
@@ -1746,106 +1731,44 @@ 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)
- );
- }
-
- function stopOrderbookPrecisionSampler() {
- window.clearTimeout(orderbookPrecisionSampleTimer);
- orderbookPrecisionSampleTimer = 0;
- orderbookPrecisionPendingRequest = null;
- }
-
- function refreshOrderbookPrecisionSamplesNow() {
- const symbol = getCurrentSymbol();
+ const { samples: latestSamples } = collectPriceMovesWithExpandingWindow(getLatestTradePrices(), {
+ initialLimit: ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT,
+ expansionStep: ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP,
+ minSamples: ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES,
+ });
+ const samples = saveStoredOrderbookPrecisionSamples(symbol, latestSamples);
+ const recommendation = recommendOrderbookPrecision({
+ samples,
+ options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS,
+ });
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
- status: '刷新中',
- sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS,
+ samples,
+ recommendation,
+ current: readCurrentOrderbookPrecisionValue(),
+ status: recommendation ? 'ready' : PANEL_COPY.status.precisionInsufficient,
};
- stopOrderbookPrecisionSampler();
if (!orderbookPrecisionState.nativeOptions.length) {
queueOrderbookPrecisionOptionsLoad(symbol, true);
}
- scheduleOrderbookPrecisionSampleRound(0, {
- force: true,
- durationMs: ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS,
- });
+ refreshOrderbookPrecisionRecommendation();
+ setLadderStatus(
+ recommendation
+ ? PANEL_COPY.status.precisionUpdated
+ : PANEL_COPY.status.precisionInsufficient
+ );
scheduleRenderPanel();
}
@@ -4741,7 +4664,7 @@ import {
function ladderOptionRow(title, tooltip, options, selected, group, suffix = '') {
return [
- '
',
+ '
',
`${title}`,
...options.map((value) => ladderOptionButton(`${value}${suffix}`, value, Number(value) === Number(selected), group)),
'
',
@@ -5197,7 +5120,7 @@ import {
'
',
'
',
'
',
- '
',
+ `
`,
`
${PANEL_COPY.section.ladderMaker}
`,
`
`,
`
空闲
`,
@@ -5386,7 +5309,6 @@ import {
stopTradingTimers();
invalidateTradeButtonCache();
lastDisplayCloseState = null;
- stopOrderbookPrecisionSampler();
}
function renderPanel() {
@@ -5796,7 +5718,6 @@ import {
lastObservedAccountPositionState = null;
closeGuard = null;
invalidateTradeButtonCache();
- stopOrderbookPrecisionSampler();
orderbookPrecisionOptionsLoadRequestedSymbol = null;
orderbookPrecisionOptionsLoadAttemptedSymbol = null;
const recommendation = getOrderbookPrecisionRecommendation(symbol);
@@ -5807,8 +5728,7 @@ import {
current: readCurrentOrderbookPrecisionValue(),
nativeOptions: [],
nativeOptionsStatus: null,
- status: recommendation ? 'ready' : '数据不足',
- sampleEndsAt: 0,
+ status: recommendation ? 'ready' : PANEL_COPY.status.precisionInsufficient,
};
}
@@ -5868,7 +5788,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..b436252 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: '阶梯开多',
@@ -32,6 +32,10 @@ test('panel copy keeps the approved labels and tooltips in one contract', () =>
noOrders: '无挂单',
stopLadder: '停止阶梯挂单',
},
+ status: {
+ precisionUpdated: '精度推荐已更新',
+ precisionInsufficient: '近期价格变化不足,请稍后重试',
+ },
tooltip: {
singleOrder: '单击订单簿中的某个价格,按当前方向和数量设置提交一笔订单。',
ladderMaker: '根据当前比例、笔数、间距和价格精度设置,依次提交只做 Maker 的阶梯订单。',
@@ -43,10 +47,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..385cd53 100644
--- a/test/unit/binance-orderbook-trade/precision.test.js
+++ b/test/unit/binance-orderbook-trade/precision.test.js
@@ -3,12 +3,11 @@ import assert from 'node:assert/strict';
import {
collectNonZeroPriceMoves,
+ collectPriceMovesWithExpandingWindow,
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 +56,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 +99,44 @@ 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',
- });
+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: false,
- status: '采样中',
- recommendation: null,
- }), {
- busy: false,
- 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');
});
-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('expands the latest-trade window only until enough effective price moves exist', () => {
+ const prices = [
+ '80.757', '80.757', '80.756', '80.756', '80.756',
+ '80.757', '80.756', '80.755', '80.755', '80.755',
+ '80.755', '80.754', '80.753', '80.752', '80.751',
+ '80.750', '80.749', '80.748', '80.747', '80.746',
+ '80.700',
+ ];
+
+ const result = collectPriceMovesWithExpandingWindow(prices);
+ assert.equal(result.usedCount, 20);
+ assert.equal(result.samples.length, 13);
+ assert.deepEqual([...new Set(result.samples)], ['0.001']);
+});
- assert.deepEqual(resolveOrderbookPrecisionSampleState({
- sampling: false,
- scheduled: true,
- status: '刷新中',
- recommendation: '0.00001',
- }), {
- busy: true,
- status: '刷新中',
+test('returns the complete visible snapshot when price changes remain insufficient', () => {
+ assert.deepEqual(collectPriceMovesWithExpandingWindow([
+ '80.7', '80.7', '80.7', '80.7', '80.7', '80.7',
+ ]), {
+ samples: [],
+ usedCount: 6,
});
});
diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js
index 5343187..bb3f907 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\)/);
@@ -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/);
@@ -928,7 +929,11 @@ 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_INITIAL_TRADE_LIMIT = 10/);
+ assert.match(source, /ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP = 10/);
+ assert.match(source, /ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES = 5/);
+ assert.doesNotMatch(source, /ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS/);
+ assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_POLL_MS/);
assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_DURATION_MS/);
assert.doesNotMatch(source, /ORDERBOOK_PRECISION_SAMPLE_PAUSE_MS/);
assert.match(source, /LOCAL_ORDERBOOK_PRECISION_SAMPLES_PREFIX = 'jh_binance_orderbook_precision_samples_v3'/);
@@ -937,32 +942,31 @@ 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, /collectPriceMovesWithExpandingWindow\(getLatestTradePrices\(\),/);
+ assert.match(sampleBody, /initialLimit: ORDERBOOK_PRECISION_INITIAL_TRADE_LIMIT/);
+ assert.match(sampleBody, /expansionStep: ORDERBOOK_PRECISION_TRADE_EXPANSION_STEP/);
+ assert.match(sampleBody, /minSamples: ORDERBOOK_PRECISION_MIN_EFFECTIVE_MOVES/);
+ assert.match(sampleBody, /saveStoredOrderbookPrecisionSamples/);
+ assert.match(sampleBody, /recommendOrderbookPrecision/);
+ assert.match(sampleBody, /status: recommendation \? 'ready' : PANEL_COPY\.status\.precisionInsufficient/);
+ assert.match(sampleBody, /PANEL_COPY\.status\.precisionUpdated/);
+ 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_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/);
@@ -980,7 +984,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"[^`]*