diff --git a/docs/binance-orderbook-trade-development.md b/docs/binance-orderbook-trade-development.md
index b34e689..f1d370a 100644
--- a/docs/binance-orderbook-trade-development.md
+++ b/docs/binance-orderbook-trade-development.md
@@ -136,6 +136,8 @@ The userscript UI supports exactly `zh-CN` and English. Resolve the locale from
Keep script-owned UI copy in `contracts/panel-copy.js` as paired `localizedText(zhCN, en)` values. Dynamic status formatters must retain both languages until render time so current order, round, and cancellation counters survive a locale switch. Raw technical exceptions, Binance response messages, error codes, and native `TypeError` text stay unchanged so they remain copyable for diagnosis.
+Strategy27 and Strategy29 also import this module's locale helpers. Changes to its initialized copy map affect all three generated artifacts; bump their source metadata and rebuild each affected script together.
+
Do not merge this contract with `contracts/binance-page-text.js`. `BINANCE_PAGE_TEXT` recognizes Binance-owned DOM in every supported page language; `PANEL_COPY` and localized status values render userscript-owned UI in the current two-language contract.
## Testing Strategy
@@ -253,7 +255,11 @@ Orderbook precision recommendations must remain non-invasive and on demand. The
Numeric panel profiles are scoped by current symbol, Binance orderbook precision, and trade mode (`OPEN` or `CLOSE`). Quantity multiplier, ladder percentage, level count, and row span must restore independently for each profile. Direction remains symbol-and-mode scoped, while panel expansion and precision samples keep their existing scopes. An unrecognized precision is not a default profile: disable numeric editing and reject single-order or ladder execution until the live `.orderbook-tickSize` value is available. Ladder planning, repricing, and open-order replacement must retain the captured precision and stop if it changes.
-Binance's orderbook precision dropdown is not the generic `bn-sdd-option` select path. Current live source renders the orderbook header as `.orderbook-tickSize`, wraps the clickable trigger in `.tick-content`, and renders precision choices inside the same control's unique visible `.ob-ticksize-overlay` as `.ob-ticksize-item`. Precision selection must stay inside that concrete overlay; do not scan generic option, dropdown, popup, or menu selectors elsewhere on the page.
+Binance's current orderbook precision control keeps `.orderbook-tickSize` and its `.tick-content` field, but portals the menu to `body`. Its native Select renders `.bn-select-overlay-options[role="listbox"]` inside `.bn-select-overlay`, with `.bn-select-option[role="option"]` choices. The custom field omits `aria-controls`; the native Select source generates a stable listbox ID and passes it through `Dropdown.overlay.props.id`.
+
+`dom/orderbook-precision.js` resolves that ID from the current `.bn-select` host's React owner chain, stopping at the captured tick-size root. The host fiber and its alternate must each yield one identical ID; this is a stable-identity check, not a current-fiber algorithm. Missing, escaped, ambiguous, or disagreeing ownership is an explicit DOM-contract error. Resolve the menu by that exact ID and validate its current structure and visibility; never choose an unrelated numeric menu by a global option scan or by matching its values. This adapter depends on the native React ownership contract verified against the live Select/Dropdown/Tooltips source on 2026-09-12, so future host changes require fresh DOM and source evidence rather than a selector fallback.
+
+Precision jobs and the existing tick-size observer handle only this named contract error, clear stale native options, and show a visible unavailable state. Reading the current displayed precision remains independent of menu ownership. Bootstrap rechecks symbol, root, trigger, and displayed precision after menu cleanup before accepting its option snapshot. Cleanup toggles only the captured native trigger and never selects an option; menus that were already open remain open. The observer remains scoped to tick-size child and text changes, with no document-wide portal observer or background retry loop.
Closed-dropdown precision interaction, Tampermonkey source activation, live test
scope, and state-based browser evidence are specified in
diff --git a/docs/binance-orderbook-trade-ui-automation.md b/docs/binance-orderbook-trade-ui-automation.md
index 6d5bcdb..1625d5e 100644
--- a/docs/binance-orderbook-trade-ui-automation.md
+++ b/docs/binance-orderbook-trade-ui-automation.md
@@ -98,7 +98,7 @@ present the behavior as proven.
The orderbook precision control is a fragile closed-dropdown path. The current
semantic contract is documented in the development manual; browser validation
-must exercise the closed control, dispatch the verified pointer/mouse sequence,
+must exercise the closed control, dispatch the verified native trigger click,
wait for the control-owned visible option list, select the exact native option,
and verify the displayed precision changed. Do not use a generic page-wide option
selector or treat an already-open dropdown as proof that the script's Apply or
diff --git a/docs/binance-strategy27-events-development.md b/docs/binance-strategy27-events-development.md
index 00a00e4..f3c7570 100644
--- a/docs/binance-strategy27-events-development.md
+++ b/docs/binance-strategy27-events-development.md
@@ -11,7 +11,7 @@ V10 live projection. The VPS remains the only market-data and event-analysis
authority. The userscript opens no Binance market-data WebSocket, uses no
Binance API key, and does not recalculate the four force groups.
-Version 0.6.0 retains this installation's private gateway configuration and
+Version 0.6.1 retains this installation's private gateway configuration and
provides a shared read-only transport. Strategy29 owns its own summary panel,
lifecycle and panel position. The existing `strategy27GatewayOrigin` and
`strategy27GatewayAuthSecret` storage keys remain the single credential source;
@@ -165,7 +165,7 @@ ADR 032 in CorsairQuant owns the server-side rule and transport contract. The
browser does not reconstruct candidates from ordinary events or recalculate
market evidence. The client, lifecycle, panel, native chart layer and optional-job
controller are wired into the entrypoint and tested together. The source and
-generated install artifact are version 0.6.0 with identical metadata headers.
+generated install artifact are version 0.6.1 with identical metadata headers.
The generated artifact passes syntax, release-contract and isolated execution
checks, including candidate delivery, paired entities, clear and context stop.
Binance operator-page validation remains outstanding. Server/gateway rollout
diff --git a/docs/binance-strategy29-bollinger-development.md b/docs/binance-strategy29-bollinger-development.md
index aa2ec1b..a1d7ce3 100644
--- a/docs/binance-strategy29-bollinger-development.md
+++ b/docs/binance-strategy29-bollinger-development.md
@@ -10,8 +10,8 @@ already-loaded native chart candles. The summary reads the authenticated
unified loopback gateway; it does not call Binance market-data or account APIs,
submit orders, rotate hidden charts, or add remote events as chart drawings.
-Install Strategy29 0.5.1 with orderbook 2.7.199 or later, or use it alone.
-Install CorsairQuant signal client 0.6.0 for the remote summary.
+Install Strategy29 0.5.2 with orderbook 2.7.199 or later, or use it alone.
+Install CorsairQuant signal client 0.6.1 for the remote summary.
Do not combine it with the embedded observer in orderbook 2.7.198.
After updating/disabling the old script, reload the page. An embedded observer
is an explicit conflict: Strategy29 stops and displays an upgrade/reload notice.
diff --git a/e2e/binance-orderbook/fixtures/binance-futures.js b/e2e/binance-orderbook/fixtures/binance-futures.js
index 68b6298..8fd1bc3 100644
--- a/e2e/binance-orderbook/fixtures/binance-futures.js
+++ b/e2e/binance-orderbook/fixtures/binance-futures.js
@@ -29,8 +29,8 @@ export function renderBinanceFuturesFixture(scenario) {
.orderbook-tickSize, .row-content, .tradew-tradelist { min-height: 24px; }
.orderbook-tickSize { position: relative; width: 120px; }
.bn-tooltips-ele { display: inline-block; min-width: 70px; min-height: 24px; }
- .ob-ticksize-overlay { position: absolute; top: 24px; left: 0; z-index: 20; width: 120px; padding: 4px; background: #fff; border: 1px solid #d8dce1; }
- .ob-ticksize-item { display: block; min-height: 28px; padding: 4px; }
+ .bn-select-bubble { position: fixed; z-index: 20; width: 120px; padding: 4px; background: #fff; border: 1px solid #d8dce1; }
+ .bn-select-option { display: block; min-height: 28px; padding: 4px; }
.row-content { display: flex; gap: 16px; }
.emit-price { display: inline-block; min-width: 70px; min-height: 20px; }
.chart-widget-root { width: 700px; height: 300px; margin-top: 20px; border: 1px solid #d8dce1; }
@@ -55,7 +55,7 @@ export function renderBinanceFuturesFixture(scenario) {
-
+
81.2
81.1
81.05
@@ -237,31 +237,103 @@ export function renderBinanceFuturesFixture(scenario) {
});
}
- function renderPrecisionOverlay() {
- const tickSize = document.querySelector('#futuresOrderbook .orderbook-tickSize');
- const existing = tickSize.querySelector('.ob-ticksize-overlay');
- if (existing) {
- existing.remove();
- record('precision-overlay-closed');
+ let precisionControlSequence = 0;
+ let precisionControl;
+
+ /**
+ * The native Select keeps its portal ID in Dropdown.overlay on both owner
+ * branches; its custom field does not expose an aria-controls shortcut.
+ */
+ function bindPrecisionControl(root) {
+ const select = root.querySelector('.bn-select');
+ const trigger = select.querySelector('.bn-select-trigger');
+ const listboxId = 'bn-select-fixture-precision-' + (++precisionControlSequence);
+ const ownerBranch = () => ({
+ stateNode: select,
+ return: {
+ memoizedProps: { overlay: { props: { id: listboxId } } },
+ return: { stateNode: root, return: null },
+ },
+ });
+ const fiber = ownerBranch();
+ fiber.alternate = ownerBranch();
+ select.__reactFiber$fixture = fiber;
+ const control = {
+ root,
+ select,
+ listboxId,
+ symbol: scenario.currentSymbol,
+ options: [...scenario.host.precisionOptions],
+ bubble: null,
+ };
+ trigger.addEventListener('click', () => renderPrecisionOverlay(control));
+ return control;
+ }
+
+ function closePrecisionOverlay(control) {
+ control.bubble.remove();
+ control.bubble = null;
+ control.select.classList.remove('active');
+ record('precision-overlay-closed', { symbol: control.symbol, listboxId: control.listboxId });
+ }
+
+ function renderPrecisionOverlay(control) {
+ if (control.select.classList.contains('active')) {
+ closePrecisionOverlay(control);
return;
}
- const overlay = document.createElement('div');
- overlay.className = 'ob-ticksize-overlay';
- overlay.innerHTML = scenario.host.precisionOptions.map((value) => (
- '' + value + '
'
- )).join('');
- tickSize.append(overlay);
- record('precision-overlay-opened');
- overlay.querySelectorAll('[data-precision-value]').forEach((option) => {
+ const bubble = document.createElement('div');
+ bubble.className = 'bn-bubble bn-tooltips active bn-select-bubble';
+ const rect = control.select.getBoundingClientRect();
+ bubble.style.top = rect.bottom + 'px';
+ bubble.style.left = rect.left + 'px';
+ bubble.innerHTML = '' +
+ '
' +
+ control.options.map((value) => (
+ '
' + value + '
'
+ )).join('') + '
';
+ control.bubble = bubble;
+ document.body.append(bubble);
+ control.select.classList.add('active');
+ record('precision-overlay-opened', { symbol: control.symbol, listboxId: control.listboxId });
+ bubble.querySelectorAll('[data-precision-value]').forEach((option) => {
option.addEventListener('click', () => {
state.orderbookPrecision = option.dataset.precisionValue;
- tickSize.querySelector('.tick-content').textContent = state.orderbookPrecision;
- record('precision-selected', { value: state.orderbookPrecision });
- overlay.remove();
+ control.root.querySelector('.tick-content').textContent = state.orderbookPrecision;
+ record('precision-selected', {
+ value: state.orderbookPrecision,
+ symbol: control.symbol,
+ listboxId: control.listboxId,
+ });
+ closePrecisionOverlay(control);
});
});
}
+ function replacePrecisionControl({ scope, symbol, value, options }) {
+ if (!['select', 'root'].includes(scope)) throw new Error('Unsupported precision replacement scope');
+ if (!options.includes(value)) throw new Error('Replacement precision must be a native option');
+ const previousControl = precisionControl;
+ if (previousControl.bubble) closePrecisionOverlay(previousControl);
+ scenario.host.precisionOptions = [...options];
+ state.orderbookPrecision = value;
+ const root = scope === 'root' ? previousControl.root.cloneNode(true) : previousControl.root;
+ if (scope === 'select') {
+ previousControl.select.replaceWith(previousControl.select.cloneNode(true));
+ } else {
+ previousControl.root.replaceWith(root);
+ }
+ root.querySelector('.tick-content').textContent = value;
+ if (scenario.currentSymbol !== symbol) {
+ scenario.currentSymbol = symbol;
+ history.pushState({}, '', '/zh-CN/futures/' + symbol);
+ }
+ precisionControl = bindPrecisionControl(root);
+ record('precision-control-replaced', { scope, symbol, listboxId: precisionControl.listboxId });
+ return { previousListboxId: previousControl.listboxId, listboxId: precisionControl.listboxId };
+ }
+
localStorage.setItem('jh_binance_orderbook_precision_samples_v3:' + scenario.currentSymbol, '["81.0","81.01","81.02","81.03","81.04","81.05"]');
function renderOrdersRows() {
@@ -477,6 +549,7 @@ export function renderBinanceFuturesFixture(scenario) {
renderChartOrdersPopover();
window.__BINANCE_FIXTURE__ = {
+ replacePrecisionControl,
snapshot: () => JSON.parse(JSON.stringify({
positions: state.positions,
orders: state.orders,
@@ -493,7 +566,7 @@ export function renderBinanceFuturesFixture(scenario) {
};
document.querySelector('.tick-content').textContent = state.orderbookPrecision;
document.querySelector('.quick-controls button:nth-child(2)').textContent = state.leverage + 'x';
- document.querySelector('.bn-tooltips-ele').addEventListener('click', renderPrecisionOverlay);
+ precisionControl = bindPrecisionControl(document.querySelector('#futuresOrderbook .orderbook-tickSize'));
renderTradeMode();
renderAccountWidget();
window.fetch('/bapi/fixture-bootstrap', {
diff --git a/e2e/binance-orderbook/specs/control-flows.pw.js b/e2e/binance-orderbook/specs/control-flows.pw.js
index 8742638..41b9e84 100644
--- a/e2e/binance-orderbook/specs/control-flows.pw.js
+++ b/e2e/binance-orderbook/specs/control-flows.pw.js
@@ -43,15 +43,58 @@ test('multiplier controls show local press feedback without changing operation s
const increment = panel.locator('#jh-binance-close-qty-multiplier-inc');
await expect(input).toHaveValue('1');
- await increment.click();
- await expect(increment).toHaveAttribute('data-jh-press-feedback', 'true');
- await expect(increment).toHaveCSS('background-color', 'rgb(245, 245, 245)');
- await increment.click();
- await expect(input).toHaveValue('3');
- await expect(status).toHaveText(statusBefore);
- await expect(increment).not.toHaveAttribute('data-jh-press-feedback', 'true', {
- timeout: 1_000,
+ await increment.evaluate((button) => {
+ const events = [];
+ // The 140 ms feedback can end before Playwright's click round trip returns.
+ const observer = new MutationObserver((mutations) => {
+ for (const mutation of mutations) {
+ events.push({
+ feedback: button.getAttribute(mutation.attributeName),
+ backgroundColor: getComputedStyle(button).backgroundColor,
+ sameButton: document.getElementById(button.id) === button,
+ connected: button.isConnected,
+ multiplier: document.getElementById('jh-binance-close-qty-multiplier-input').value,
+ status: document.getElementById('jh-binance-ladder-status').textContent,
+ });
+ }
+ });
+ observer.observe(button, {
+ attributes: true,
+ attributeFilter: ['data-jh-press-feedback'],
+ });
+ window.__MULTIPLIER_FEEDBACK_PROBE__ = { events, observer };
});
+ try {
+ await increment.click();
+ await expect.poll(() => page.evaluate(() => window.__MULTIPLIER_FEEDBACK_PROBE__.events), {
+ timeout: 1_000,
+ }).toEqual([
+ {
+ feedback: 'true',
+ backgroundColor: 'rgb(245, 245, 245)',
+ sameButton: true,
+ connected: true,
+ multiplier: '2',
+ status: statusBefore,
+ },
+ {
+ feedback: null,
+ backgroundColor: 'rgb(255, 255, 255)',
+ sameButton: true,
+ connected: true,
+ multiplier: '2',
+ status: statusBefore,
+ },
+ ]);
+ await expect(input).toHaveValue('2');
+ await expect(status).toHaveText(statusBefore);
+ await expect(increment).not.toHaveAttribute('data-jh-press-feedback', 'true');
+ } finally {
+ await page.evaluate(() => {
+ window.__MULTIPLIER_FEEDBACK_PROBE__.observer.disconnect();
+ delete window.__MULTIPLIER_FEEDBACK_PROBE__;
+ });
+ }
expect(errors).toEqual([]);
});
@@ -139,7 +182,7 @@ test('a precision shortcut selects the exact native orderbook option once', asyn
await target.click();
await expect(target).toHaveAttribute('aria-pressed', 'true');
await expect(page.locator('#futuresOrderbook .tick-content')).toHaveText('0.01');
- await expect(page.locator('.ob-ticksize-overlay')).toHaveCount(0);
+ await expect(page.locator('.bn-select-bubble')).toHaveCount(0);
await expect(status).toHaveText(statusBefore);
const probe = await finishInteractionProbe(page);
assertResponsiveInteraction(expect, probe);
diff --git a/e2e/binance-orderbook/specs/precision-controls.pw.js b/e2e/binance-orderbook/specs/precision-controls.pw.js
new file mode 100644
index 0000000..b20722d
--- /dev/null
+++ b/e2e/binance-orderbook/specs/precision-controls.pw.js
@@ -0,0 +1,135 @@
+import { test, expect } from '../test.js';
+
+import {
+ CURRENT_SYMBOL,
+ OTHER_SYMBOL,
+ createCancelScenario,
+} from '../scenarios/cancel-current-symbol.js';
+import { openUserscriptScenario, readFixtureState } from '../helpers/userscript-page.js';
+
+const PANEL_SELECTOR = '#jh-binance-close-qty-multiplier-panel';
+const NATIVE_ROOT_SELECTOR = '#futuresOrderbook .orderbook-tickSize';
+
+async function expectPrecisionOptions(page, options, current) {
+ const panel = page.locator(PANEL_SELECTOR);
+ await expect.poll(() => panel.locator('[data-orderbook-precision-value]').evaluateAll((nodes) => (
+ nodes.map((node) => node.getAttribute('data-orderbook-precision-value'))
+ )), { timeout: 8_000 }).toEqual(options);
+ await expect(panel.locator('[data-orderbook-precision-value="' + current + '"]'))
+ .toHaveAttribute('aria-pressed', 'true');
+ await expect(panel.locator('[data-orderbook-precision-value]').first()).toBeEnabled();
+}
+
+test('precision bootstrap shows every native numeric shortcut without selecting an option', async ({ page }) => {
+ const options = ['0.000001', '0.00001', '0.0001'];
+ const { errors } = await openUserscriptScenario(page, createCancelScenario({
+ ui: { orderbookPrecision: options[0] },
+ host: { precisionOptions: options },
+ }));
+
+ await expectPrecisionOptions(page, options, options[0]);
+ await expect(page.locator(NATIVE_ROOT_SELECTOR + ' .tick-content')).toHaveText(options[0]);
+ await expect(page.locator(NATIVE_ROOT_SELECTOR + ' [aria-controls]')).toHaveCount(0);
+ await expect(page.locator(NATIVE_ROOT_SELECTOR + ' .bn-select.active')).toHaveCount(0);
+ await expect(page.locator('.bn-select-bubble')).toHaveCount(0);
+ const state = await readFixtureState(page);
+ expect(state.orderbookPrecision).toBe(options[0]);
+ expect(state.events.filter((event) => event.type.startsWith('precision-')).map((event) => event.type))
+ .toEqual(['precision-overlay-opened', 'precision-overlay-closed']);
+ expect(errors).toEqual([]);
+});
+
+test('selecting a native precision portal option updates the numeric shortcuts', async ({ page }) => {
+ const scenario = createCancelScenario();
+ const { errors } = await openUserscriptScenario(page, scenario);
+ await expectPrecisionOptions(page, scenario.host.precisionOptions, scenario.ui.orderbookPrecision);
+
+ await page.locator(NATIVE_ROOT_SELECTOR + ' .bn-select-trigger').click();
+ const listbox = page.getByRole('listbox');
+ await expect(listbox).toBeVisible();
+ await expect(page.locator('body > .bn-select-bubble.active')).toBeVisible();
+ await expect(page.locator(NATIVE_ROOT_SELECTOR + ' [role="listbox"]')).toHaveCount(0);
+ await listbox.getByRole('option', { name: '0.001', exact: true }).click();
+
+ await expectPrecisionOptions(page, scenario.host.precisionOptions, '0.001');
+ await expect(page.locator(NATIVE_ROOT_SELECTOR + ' .tick-content')).toHaveText('0.001');
+ await expect(listbox).toHaveCount(0);
+ expect((await readFixtureState(page)).events.filter((event) => event.type === 'precision-selected'))
+ .toEqual([expect.objectContaining({ symbol: CURRENT_SYMBOL, value: '0.001' })]);
+ expect(errors).toEqual([]);
+});
+
+for (const scope of ['select', 'root']) {
+ test('precision shortcuts reacquire a replaced native ' + scope + ' for the same symbol', async ({ page }) => {
+ const scenario = createCancelScenario();
+ const { errors } = await openUserscriptScenario(page, scenario);
+ await expectPrecisionOptions(page, scenario.host.precisionOptions, scenario.ui.orderbookPrecision);
+ const previousRoot = await page.locator(NATIVE_ROOT_SELECTOR).elementHandle();
+ const previousSelect = await page.locator(NATIVE_ROOT_SELECTOR + ' .bn-select').elementHandle();
+ const replacement = await page.evaluate((input) => (
+ window.__BINANCE_FIXTURE__.replacePrecisionControl(input)
+ ), {
+ scope,
+ symbol: CURRENT_SYMBOL,
+ value: scenario.ui.orderbookPrecision,
+ options: scenario.host.precisionOptions,
+ });
+
+ expect(await previousRoot.evaluate((node) => node.isConnected)).toBe(scope === 'select');
+ expect(await previousSelect.evaluate((node) => node.isConnected)).toBe(false);
+ expect(replacement.listboxId).not.toBe(replacement.previousListboxId);
+ await page.locator(PANEL_SELECTOR + ' [data-orderbook-precision-value="0.01"]').click();
+ await expectPrecisionOptions(page, scenario.host.precisionOptions, '0.01');
+ await expect(page.locator(NATIVE_ROOT_SELECTOR + ' .tick-content')).toHaveText('0.01');
+ await expect(page.locator('.bn-select-bubble')).toHaveCount(0);
+ expect((await readFixtureState(page)).events.filter((event) => event.type === 'precision-selected'))
+ .toEqual([expect.objectContaining({
+ symbol: CURRENT_SYMBOL,
+ value: '0.01',
+ listboxId: replacement.listboxId,
+ })]);
+ expect(errors).toEqual([]);
+ });
+}
+
+test('precision shortcuts follow each symbol native menu across an A to B to A switch', async ({ page }) => {
+ const scenario = createCancelScenario();
+ const { errors } = await openUserscriptScenario(page, scenario);
+ await expectPrecisionOptions(page, scenario.host.precisionOptions, scenario.ui.orderbookPrecision);
+ const otherOptions = ['1', '10', '100', '1000'];
+ await page.evaluate((input) => window.__BINANCE_FIXTURE__.replacePrecisionControl(input), {
+ scope: 'root',
+ symbol: OTHER_SYMBOL,
+ value: '10',
+ options: otherOptions,
+ });
+
+ await expect(page).toHaveURL('https://www.binance.com/zh-CN/futures/' + OTHER_SYMBOL);
+ await expectPrecisionOptions(page, otherOptions, '10');
+ await page.locator(PANEL_SELECTOR + ' [data-orderbook-precision-value="100"]').click();
+ await expectPrecisionOptions(page, otherOptions, '100');
+ await expect(page.locator(NATIVE_ROOT_SELECTOR + ' .tick-content')).toHaveText('100');
+
+ await page.evaluate((input) => window.__BINANCE_FIXTURE__.replacePrecisionControl(input), {
+ scope: 'root',
+ symbol: CURRENT_SYMBOL,
+ value: scenario.ui.orderbookPrecision,
+ options: scenario.host.precisionOptions,
+ });
+ await expect(page).toHaveURL('https://www.binance.com/zh-CN/futures/' + CURRENT_SYMBOL);
+ await expectPrecisionOptions(page, scenario.host.precisionOptions, scenario.ui.orderbookPrecision);
+ await page.locator(PANEL_SELECTOR + ' [data-orderbook-precision-value="0.01"]').click();
+ await expectPrecisionOptions(page, scenario.host.precisionOptions, '0.01');
+ await expect(page.locator(NATIVE_ROOT_SELECTOR + ' .tick-content')).toHaveText('0.01');
+ await expect(page.locator('.bn-select-bubble')).toHaveCount(0);
+ const state = await readFixtureState(page);
+ expect(state.events.filter((event) => event.type === 'precision-selected').map(({ symbol, value }) => (
+ { symbol, value }
+ ))).toEqual([
+ { symbol: OTHER_SYMBOL, value: '100' },
+ { symbol: CURRENT_SYMBOL, value: '0.01' },
+ ]);
+ expect(state.events.filter((event) => ['order-submitted', 'cancel-requested'].includes(event.type)))
+ .toEqual([]);
+ expect(errors).toEqual([]);
+});
diff --git a/scripts/binance-orderbook-trade.user.js b/scripts/binance-orderbook-trade.user.js
index def0bda..28c5120 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.203
+// @version 2.7.204
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -207,12 +207,15 @@
allPositionsClosed: localizedText("已全部平仓", "All positions closed"),
waitingTradeMode: localizedText("等待开仓/平仓状态", "Waiting for trade mode"),
waitingPricePrecision: localizedText("等待价格精度", "Waiting for precision"),
+ waitingPrecisionOptions: localizedText("等待精度档位", "Waiting for options"),
+ loadingPrecisionOptions: localizedText("读取精度档位", "Loading options"),
minimumQuantityLoading: localizedText("最小量读取中", "Loading minimum qty"),
positiveIntegerMultiplier: localizedText("请输入正整数倍数", "Enter a positive integer"),
noClosablePosition: localizedText("暂无可平仓位", "No position to close")
}),
status: freezeCopy({
precisionUpdated: localizedText("精度推荐已更新", "Precision recommendation updated"),
+ precisionOptionsUnavailable: localizedText("档位读取失败,请刷新", "Options unavailable. Refresh."),
precisionInsufficient: localizedText(
"近期价格变化不足,请稍后重试",
"Recent price movement is insufficient. Try again later."
@@ -2244,6 +2247,73 @@
return scopes.length === 1 ? scopes[0] : null;
}
+ // src/binance-orderbook-trade/dom/orderbook-precision.js
+ var OrderbookPrecisionDomError = class extends Error {
+ constructor(message) {
+ super(message);
+ this.name = "OrderbookPrecisionDomError";
+ }
+ };
+ function findPrecisionSelect(triggerElement) {
+ if (!triggerElement?.isConnected) return null;
+ const root = triggerElement.closest(".orderbook-tickSize");
+ if (!root?.closest("#futuresOrderbook")) return null;
+ const select = triggerElement.closest(".bn-select");
+ if (!select || select.closest(".orderbook-tickSize") !== root || select.querySelector(".bn-select-trigger .tick-content") !== triggerElement) {
+ throw new OrderbookPrecisionDomError("Native precision trigger structure has changed");
+ }
+ return { root, select };
+ }
+ function readDropdownListboxId(fiber, root) {
+ const ids = [];
+ let owner = fiber;
+ while (owner && owner.stateNode !== root) {
+ const overlay = owner.memoizedProps?.overlay;
+ if (overlay !== void 0) {
+ const id = overlay?.props?.id;
+ if (typeof id !== "string" || !id.startsWith("bn-select-")) {
+ throw new OrderbookPrecisionDomError("Native precision listbox identity is invalid");
+ }
+ ids.push(id);
+ }
+ owner = owner.return;
+ }
+ if (!owner || ids.length !== 1) {
+ throw new OrderbookPrecisionDomError("Native precision listbox ownership is missing or ambiguous");
+ }
+ return ids[0];
+ }
+ function getPrecisionListboxId(select, root) {
+ const keys = Object.getOwnPropertyNames(select).filter((key) => key.startsWith("__reactFiber$"));
+ if (keys.length !== 1) {
+ throw new OrderbookPrecisionDomError("Native precision React ownership is unavailable");
+ }
+ const fiber = select[keys[0]];
+ const id = readDropdownListboxId(fiber, root);
+ if (fiber.alternate && readDropdownListboxId(fiber.alternate, root) !== id) {
+ throw new OrderbookPrecisionDomError("Native precision listbox identity changed between renders");
+ }
+ return id;
+ }
+ function isNativeOrderbookPrecisionMenuOpen(triggerElement) {
+ const target = findPrecisionSelect(triggerElement);
+ return target !== null && target.select.classList.contains("active");
+ }
+ function findNativeOrderbookPrecisionOverlay(triggerElement, isVisibleElement) {
+ const target = findPrecisionSelect(triggerElement);
+ if (!target || !target.select.classList.contains("active")) return null;
+ const { root, select } = target;
+ const listbox = root.ownerDocument.getElementById(getPrecisionListboxId(select, root));
+ if (!listbox) return null;
+ const overlay = listbox.parentElement;
+ const bubble = overlay?.closest(".bn-select-bubble");
+ if (!listbox.matches('.bn-select-overlay-options[role="listbox"]') || !overlay?.matches(".bn-select-overlay") || !bubble) {
+ throw new OrderbookPrecisionDomError("Native precision listbox structure has changed");
+ }
+ if (!bubble.classList.contains("active") || !isVisibleElement(bubble) || !isVisibleElement(overlay) || !isVisibleElement(listbox)) return null;
+ return overlay;
+ }
+
// src/binance-orderbook-trade/dom/trade-form.js
function buttonTextMatches(button, labels) {
return includesBinancePageText(button?.textContent, labels);
@@ -6207,9 +6277,15 @@
function handleOrderbookPrecisionChange() {
const precision = readCurrentOrderbookPrecisionValue();
const symbol = getCurrentSymbol();
+ if (precision !== lastObservedOrderbookPrecision) {
+ lastObservedOrderbookPrecision = precision;
+ stopMultiplierEdit();
+ ladderPanelBodySignature = "";
+ scheduleRenderPanel();
+ }
const nativeOptions = readVisibleOrderbookPrecisionOptionValues();
const previousNativeOptions = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.nativeOptions : [];
- const nativeOptionsChanged = nativeOptions.length > 0 && JSON.stringify(nativeOptions) !== JSON.stringify(previousNativeOptions);
+ const nativeOptionsChanged = isCurrentObservedSymbol(symbol) && nativeOptions.includes(precision) && JSON.stringify(nativeOptions) !== JSON.stringify(previousNativeOptions);
if (nativeOptionsChanged) {
orderbookPrecisionState = {
...orderbookPrecisionState,
@@ -6217,14 +6293,17 @@
nativeOptions,
nativeOptionsStatus: null
};
+ scheduleRenderPanel();
}
- if (precision === lastObservedOrderbookPrecision) {
- if (nativeOptionsChanged) scheduleRenderPanel();
- return;
- }
- lastObservedOrderbookPrecision = precision;
- stopMultiplierEdit();
- ladderPanelBodySignature = "";
+ }
+ function recordOrderbookPrecisionDomFailure(symbol, error) {
+ if (!isCurrentObservedSymbol(symbol)) return;
+ orderbookPrecisionState = {
+ ...orderbookPrecisionState,
+ symbol,
+ nativeOptions: [],
+ nativeOptionsStatus: error.message
+ };
scheduleRenderPanel();
}
function stopOrderbookPrecisionObserver() {
@@ -6248,7 +6327,13 @@
orderbookPrecisionObserverRoot = root;
lastObservedOrderbookPrecision = readCurrentOrderbookPrecisionValue();
orderbookPrecisionObserver = new MutationObserver(() => {
- handleOrderbookPrecisionChange();
+ const symbol = getCurrentSymbol();
+ try {
+ handleOrderbookPrecisionChange();
+ } catch (error) {
+ if (!(error instanceof OrderbookPrecisionDomError)) throw error;
+ recordOrderbookPrecisionDomFailure(symbol, error);
+ }
});
orderbookPrecisionObserver.observe(root, {
subtree: true,
@@ -6258,20 +6343,17 @@
}
function readOrderbookPrecisionOptionValue(node) {
if (!node) return null;
- const item = node.matches?.(".ob-ticksize-item") ? node : node.closest?.(".ob-ticksize-item");
+ const item = node.matches?.('.bn-select-option[role="option"]') ? node : node.closest?.('.bn-select-option[role="option"]');
const textNode = item?.querySelector("span") || node;
return normalizeDecimalString(textNode?.textContent || "");
}
function getVisibleOrderbookPrecisionOverlay(triggerElement = findOrderbookPrecisionTrigger()?.element) {
- const tickSize = triggerElement?.closest?.(".orderbook-tickSize");
- if (!tickSize || !tickSize.closest("#futuresOrderbook")) return null;
- const overlays = Array.from(tickSize.querySelectorAll(".ob-ticksize-overlay")).filter((overlay) => isVisibleElement(overlay));
- return overlays.length === 1 ? overlays[0] : null;
+ return findNativeOrderbookPrecisionOverlay(triggerElement, isVisibleElement);
}
function getVisibleOrderbookPrecisionOptionNodes(triggerElement = findOrderbookPrecisionTrigger()?.element) {
const overlay = getVisibleOrderbookPrecisionOverlay(triggerElement);
if (!overlay) return [];
- return Array.from(overlay.querySelectorAll(".ob-ticksize-item")).filter((node) => node.closest(".ob-ticksize-overlay") === overlay).filter((node) => isVisibleElement(node)).filter((node) => isOrderbookPrecisionNumericText(readOrderbookPrecisionOptionValue(node)));
+ return Array.from(overlay.querySelectorAll('.bn-select-option[role="option"]')).filter((node) => node.closest(".bn-select-overlay") === overlay).filter((node) => isVisibleElement(node)).filter((node) => isOrderbookPrecisionNumericText(readOrderbookPrecisionOptionValue(node)));
}
function readVisibleOrderbookPrecisionOptionValues(triggerElement = findOrderbookPrecisionTrigger()?.element) {
const values = /* @__PURE__ */ new Set();
@@ -6286,26 +6368,6 @@
if (!normalized) return null;
return getVisibleOrderbookPrecisionOptionNodes(triggerElement).find((node) => readOrderbookPrecisionOptionValue(node) === normalized) || null;
}
- function dispatchOrderbookPrecisionOpenEvent(target, type) {
- const EventCtor = type.startsWith("pointer") && typeof PointerEvent === "function" ? PointerEvent : MouseEvent;
- return target.dispatchEvent(new EventCtor(type, {
- bubbles: true,
- cancelable: true,
- view: window,
- button: 0,
- buttons: type === "pointerup" || type === "mouseup" || type === "click" ? 0 : 1,
- pointerId: 1,
- pointerType: "mouse",
- isPrimary: true
- }));
- }
- function dispatchOrderbookPrecisionToggleSequence(target) {
- dispatchOrderbookPrecisionOpenEvent(target, "pointerdown");
- dispatchOrderbookPrecisionOpenEvent(target, "mousedown");
- dispatchOrderbookPrecisionOpenEvent(target, "pointerup");
- dispatchOrderbookPrecisionOpenEvent(target, "mouseup");
- dispatchOrderbookPrecisionOpenEvent(target, "click");
- }
async function waitForVisibleOrderbookPrecisionOptions(triggerElement, timeoutMs = ORDERBOOK_PRECISION_OPTION_WAIT_MS) {
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
while (!document.hidden && isFuturesTradingPage()) {
@@ -6331,30 +6393,25 @@
if (!await openOrderbookPrecisionOptions(triggerElement)) return [];
return getVisibleOrderbookPrecisionOptionNodes(triggerElement);
}
- async function waitForOrderbookPrecisionOptionsClosed(triggerElement, timeoutMs = ORDERBOOK_PRECISION_OPTION_WAIT_MS) {
+ async function waitForOrderbookPrecisionMenuState(triggerElement, open, timeoutMs = ORDERBOOK_PRECISION_OPTION_WAIT_MS) {
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
while (!document.hidden && isFuturesTradingPage()) {
- if (!getVisibleOrderbookPrecisionOverlay(triggerElement)) return true;
+ if (isNativeOrderbookPrecisionMenuOpen(triggerElement) === open) return true;
if (Date.now() >= deadline) return false;
await delay(50);
}
- return !getVisibleOrderbookPrecisionOverlay(triggerElement);
+ return isNativeOrderbookPrecisionMenuOpen(triggerElement) === open;
}
- async function closeOrderbookPrecisionOptions(triggerElement, currentPrecision, waitForLateOpen = false) {
- if (waitForLateOpen && !getVisibleOrderbookPrecisionOverlay(triggerElement)) {
- await waitForVisibleOrderbookPrecisionOptions(triggerElement, ORDERBOOK_PRECISION_OPTION_WAIT_MS);
- }
- if (!getVisibleOrderbookPrecisionOverlay(triggerElement)) return true;
- const currentOption = findVisibleOrderbookPrecisionOption(currentPrecision, triggerElement);
- if (currentOption) {
- if (!clickDomTarget(currentOption)) return false;
- return waitForOrderbookPrecisionOptionsClosed(triggerElement);
+ async function closeOrderbookPrecisionOptions(triggerElement, waitForLateOpen = false) {
+ if (!triggerElement?.isConnected) return true;
+ if (waitForLateOpen && !isNativeOrderbookPrecisionMenuOpen(triggerElement)) {
+ await waitForOrderbookPrecisionMenuState(triggerElement, true);
}
- const tickSize = triggerElement?.closest?.(".orderbook-tickSize");
- const toggleTarget = tickSize?.querySelector?.(".tick-content") || triggerElement;
- if (!toggleTarget || !isVisibleElement(toggleTarget)) return false;
- dispatchOrderbookPrecisionToggleSequence(toggleTarget);
- return waitForOrderbookPrecisionOptionsClosed(triggerElement);
+ if (!isNativeOrderbookPrecisionMenuOpen(triggerElement)) return true;
+ if (findOrderbookPrecisionTrigger()?.element !== triggerElement) return false;
+ const toggleTarget = triggerElement.closest(".bn-select-trigger");
+ if (!toggleTarget || !isVisibleElement(toggleTarget) || !clickDomTarget(toggleTarget)) return false;
+ return waitForOrderbookPrecisionMenuState(triggerElement, false);
}
async function waitForOrderbookPrecisionValue(options, timeoutMs = ORDERBOOK_PRECISION_OPTION_WAIT_MS) {
const { symbol, startPrecision, targetPrecision } = options;
@@ -6382,7 +6439,10 @@
));
return `${recommendationMarker}${formatOrderbookPrecisionShortcutLabel(value)} `;
}
- function renderOrderbookPrecisionShortcutSlots(options, current, recommendation, disabled) {
+ function renderOrderbookPrecisionShortcutSlots(options, current, recommendation, disabled, emptyLabel) {
+ if (!options.length) {
+ return [`${emptyLabel} `];
+ }
const slots = options.map((value) => renderOrderbookPrecisionShortcut(value, current, recommendation, disabled));
while (slots.length < ORDERBOOK_PRECISION_SHORTCUT_LIMIT) {
slots.push(' ');
@@ -6457,11 +6517,12 @@
);
if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol);
const canRefresh = !controlsBusy;
+ const emptyPrecisionLabel = ui(selectionBusy ? PANEL_COPY.state.loadingPrecisionOptions : orderbookPrecisionState.nativeOptionsStatus ? PANEL_COPY.status.precisionOptionsUnavailable : PANEL_COPY.state.waitingPrecisionOptions);
const recommendationHtml = [
'',
`
`,
`${ui(PANEL_COPY.field.pricePrecision)} `,
- ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy),
+ ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy, emptyPrecisionLabel),
renderOrderbookPrecisionRefreshButton(symbol, !canRefresh),
"
",
"
"
@@ -6484,6 +6545,7 @@
const remainingMs = Math.max(0, deadline - Date.now());
const trigger = await waitForOrderbookPrecisionBootstrapReady(symbol, remainingMs);
if (!trigger?.element) return { status: "订单簿尚未就绪" };
+ const tickSize = trigger.element.closest(".orderbook-tickSize");
const startPrecision = trigger.value;
lastPrecision = startPrecision;
const optionsInitiallyVisible = getVisibleOrderbookPrecisionOptionNodes(trigger.element).length > 0;
@@ -6497,12 +6559,12 @@
if (trigger.element.isConnected && currentTrigger?.element === trigger.element && currentTrigger.value === startPrecision && options.length > 0 && values.includes(startPrecision)) snapshot = { precision: startPrecision, values };
} finally {
if (!optionsInitiallyVisible) {
- const cleanupPrecision = isCurrentObservedSymbol(symbol) ? readCurrentOrderbookPrecisionValue() : startPrecision;
- closed = await closeOrderbookPrecisionOptions(trigger.element, cleanupPrecision, true);
+ closed = await closeOrderbookPrecisionOptions(trigger.element, true);
}
}
if (!closed) return { status: "无法关闭价格精度下拉" };
- if (snapshot) return snapshot;
+ const settledTrigger = findOrderbookPrecisionTrigger();
+ if (snapshot && isCurrentObservedSymbol(symbol) && trigger.element.isConnected && tickSize?.isConnected && settledTrigger?.element === trigger.element && settledTrigger.element.closest(".orderbook-tickSize") === tickSize && settledTrigger.value === startPrecision) return snapshot;
if (Date.now() >= deadline) break;
await delay(ORDERBOOK_PRECISION_READY_POLL_MS);
}
@@ -6587,11 +6649,16 @@
}
async function runOrderbookPrecisionSelectionTask(operation) {
if (orderbookPrecisionSelectionTask) return orderbookPrecisionSelectionTask;
+ const symbol = getCurrentSymbol();
const task = operation();
orderbookPrecisionSelectionTask = task;
scheduleRenderPanel();
try {
return await task;
+ } catch (error) {
+ if (!(error instanceof OrderbookPrecisionDomError)) throw error;
+ recordOrderbookPrecisionDomFailure(symbol, error);
+ return false;
} finally {
if (orderbookPrecisionSelectionTask === task) orderbookPrecisionSelectionTask = null;
scheduleRenderPanel();
diff --git a/scripts/binance-strategy27-events.user.js b/scripts/binance-strategy27-events.user.js
index 65ef1df..d4deaac 100644
--- a/scripts/binance-strategy27-events.user.js
+++ b/scripts/binance-strategy27-events.user.js
@@ -3,7 +3,7 @@
// @namespace binance.strategy27.events
// @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 0.6.0
+// @version 0.6.1
// @author jackhai9
// @description Display Strategy 27 events and provide the shared private CorsairQuant gateway connection
// @match https://www.binance.com/*/futures/*
@@ -876,12 +876,15 @@
allPositionsClosed: localizedText("已全部平仓", "All positions closed"),
waitingTradeMode: localizedText("等待开仓/平仓状态", "Waiting for trade mode"),
waitingPricePrecision: localizedText("等待价格精度", "Waiting for precision"),
+ waitingPrecisionOptions: localizedText("等待精度档位", "Waiting for options"),
+ loadingPrecisionOptions: localizedText("读取精度档位", "Loading options"),
minimumQuantityLoading: localizedText("最小量读取中", "Loading minimum qty"),
positiveIntegerMultiplier: localizedText("请输入正整数倍数", "Enter a positive integer"),
noClosablePosition: localizedText("暂无可平仓位", "No position to close")
}),
status: freezeCopy({
precisionUpdated: localizedText("精度推荐已更新", "Precision recommendation updated"),
+ precisionOptionsUnavailable: localizedText("档位读取失败,请刷新", "Options unavailable. Refresh."),
precisionInsufficient: localizedText(
"近期价格变化不足,请稍后重试",
"Recent price movement is insufficient. Try again later."
diff --git a/scripts/binance-strategy29-bollinger.user.js b/scripts/binance-strategy29-bollinger.user.js
index b7c4f1b..7ede58f 100644
--- a/scripts/binance-strategy29-bollinger.user.js
+++ b/scripts/binance-strategy29-bollinger.user.js
@@ -3,7 +3,7 @@
// @namespace binance.strategy29.bollinger
// @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 0.5.1
+// @version 0.5.2
// @author jackhai9
// @description Native Bollinger/SMA60 markers and the default read-only cross-timeframe summary
// @match https://www.binance.com/*/futures/*
@@ -1890,12 +1890,15 @@
allPositionsClosed: localizedText("已全部平仓", "All positions closed"),
waitingTradeMode: localizedText("等待开仓/平仓状态", "Waiting for trade mode"),
waitingPricePrecision: localizedText("等待价格精度", "Waiting for precision"),
+ waitingPrecisionOptions: localizedText("等待精度档位", "Waiting for options"),
+ loadingPrecisionOptions: localizedText("读取精度档位", "Loading options"),
minimumQuantityLoading: localizedText("最小量读取中", "Loading minimum qty"),
positiveIntegerMultiplier: localizedText("请输入正整数倍数", "Enter a positive integer"),
noClosablePosition: localizedText("暂无可平仓位", "No position to close")
}),
status: freezeCopy({
precisionUpdated: localizedText("精度推荐已更新", "Precision recommendation updated"),
+ precisionOptionsUnavailable: localizedText("档位读取失败,请刷新", "Options unavailable. Refresh."),
precisionInsufficient: localizedText(
"近期价格变化不足,请稍后重试",
"Recent price movement is insufficient. Try again later."
diff --git a/src/binance-orderbook-trade/contracts/panel-copy.js b/src/binance-orderbook-trade/contracts/panel-copy.js
index 8dcb018..4465fb7 100644
--- a/src/binance-orderbook-trade/contracts/panel-copy.js
+++ b/src/binance-orderbook-trade/contracts/panel-copy.js
@@ -92,12 +92,15 @@ export const PANEL_COPY = Object.freeze({
allPositionsClosed: localizedText('已全部平仓', 'All positions closed'),
waitingTradeMode: localizedText('等待开仓/平仓状态', 'Waiting for trade mode'),
waitingPricePrecision: localizedText('等待价格精度', 'Waiting for precision'),
+ waitingPrecisionOptions: localizedText('等待精度档位', 'Waiting for options'),
+ loadingPrecisionOptions: localizedText('读取精度档位', 'Loading options'),
minimumQuantityLoading: localizedText('最小量读取中', 'Loading minimum qty'),
positiveIntegerMultiplier: localizedText('请输入正整数倍数', 'Enter a positive integer'),
noClosablePosition: localizedText('暂无可平仓位', 'No position to close'),
}),
status: freezeCopy({
precisionUpdated: localizedText('精度推荐已更新', 'Precision recommendation updated'),
+ precisionOptionsUnavailable: localizedText('档位读取失败,请刷新', 'Options unavailable. Refresh.'),
precisionInsufficient: localizedText(
'近期价格变化不足,请稍后重试',
'Recent price movement is insufficient. Try again later.',
diff --git a/src/binance-orderbook-trade/dom/orderbook-precision.js b/src/binance-orderbook-trade/dom/orderbook-precision.js
new file mode 100644
index 0000000..b1a4ef7
--- /dev/null
+++ b/src/binance-orderbook-trade/dom/orderbook-precision.js
@@ -0,0 +1,90 @@
+export class OrderbookPrecisionDomError extends Error {
+ constructor(message) {
+ super(message);
+ this.name = 'OrderbookPrecisionDomError';
+ }
+}
+
+function findPrecisionSelect(triggerElement) {
+ if (!triggerElement?.isConnected) return null;
+ const root = triggerElement.closest('.orderbook-tickSize');
+ if (!root?.closest('#futuresOrderbook')) return null;
+ const select = triggerElement.closest('.bn-select');
+ if (
+ !select
+ || select.closest('.orderbook-tickSize') !== root
+ || select.querySelector('.bn-select-trigger .tick-content') !== triggerElement
+ ) {
+ throw new OrderbookPrecisionDomError('Native precision trigger structure has changed');
+ }
+ return { root, select };
+}
+
+function readDropdownListboxId(fiber, root) {
+ const ids = [];
+ let owner = fiber;
+ while (owner && owner.stateNode !== root) {
+ const overlay = owner.memoizedProps?.overlay;
+ if (overlay !== undefined) {
+ const id = overlay?.props?.id;
+ if (typeof id !== 'string' || !id.startsWith('bn-select-')) {
+ throw new OrderbookPrecisionDomError('Native precision listbox identity is invalid');
+ }
+ ids.push(id);
+ }
+ owner = owner.return;
+ }
+ if (!owner || ids.length !== 1) {
+ throw new OrderbookPrecisionDomError('Native precision listbox ownership is missing or ambiguous');
+ }
+ return ids[0];
+}
+
+/**
+ * Binance's custom Select field omits aria-controls, while Dropdown.overlay keeps
+ * the generated listbox ID. Read that lifecycle-stable ID only below this native
+ * control's React owner. A host expando can point at either fiber branch; their
+ * IDs must agree instead of treating either ancestor chain as the current tree.
+ */
+function getPrecisionListboxId(select, root) {
+ const keys = Object.getOwnPropertyNames(select)
+ .filter((key) => key.startsWith('__reactFiber$'));
+ if (keys.length !== 1) {
+ throw new OrderbookPrecisionDomError('Native precision React ownership is unavailable');
+ }
+ const fiber = select[keys[0]];
+ const id = readDropdownListboxId(fiber, root);
+ if (fiber.alternate && readDropdownListboxId(fiber.alternate, root) !== id) {
+ throw new OrderbookPrecisionDomError('Native precision listbox identity changed between renders');
+ }
+ return id;
+}
+
+export function isNativeOrderbookPrecisionMenuOpen(triggerElement) {
+ const target = findPrecisionSelect(triggerElement);
+ return target !== null && target.select.classList.contains('active');
+}
+
+export function findNativeOrderbookPrecisionOverlay(triggerElement, isVisibleElement) {
+ const target = findPrecisionSelect(triggerElement);
+ if (!target || !target.select.classList.contains('active')) return null;
+ const { root, select } = target;
+ const listbox = root.ownerDocument.getElementById(getPrecisionListboxId(select, root));
+ if (!listbox) return null;
+ const overlay = listbox.parentElement;
+ const bubble = overlay?.closest('.bn-select-bubble');
+ if (
+ !listbox.matches('.bn-select-overlay-options[role="listbox"]')
+ || !overlay?.matches('.bn-select-overlay')
+ || !bubble
+ ) {
+ throw new OrderbookPrecisionDomError('Native precision listbox structure has changed');
+ }
+ if (
+ !bubble.classList.contains('active')
+ || !isVisibleElement(bubble)
+ || !isVisibleElement(overlay)
+ || !isVisibleElement(listbox)
+ ) return null;
+ return overlay;
+}
diff --git a/src/binance-orderbook-trade/index.user.js b/src/binance-orderbook-trade/index.user.js
index 60b4862..2f5490d 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.203
+// @version 2.7.204
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
@@ -161,6 +161,11 @@ import {
parseAccountPositionTabCount,
waitForAccountOrdersMutationState,
} from './dom/account-orders.js';
+import {
+ OrderbookPrecisionDomError,
+ findNativeOrderbookPrecisionOverlay,
+ isNativeOrderbookPrecisionMenuOpen,
+} from './dom/orderbook-precision.js';
import {
calculateFloatingPanelLayout,
collectTradeButtonsFromScopes,
@@ -1763,11 +1768,18 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
function handleOrderbookPrecisionChange() {
const precision = readCurrentOrderbookPrecisionValue();
const symbol = getCurrentSymbol();
+ if (precision !== lastObservedOrderbookPrecision) {
+ lastObservedOrderbookPrecision = precision;
+ stopMultiplierEdit();
+ ladderPanelBodySignature = '';
+ scheduleRenderPanel();
+ }
const nativeOptions = readVisibleOrderbookPrecisionOptionValues();
const previousNativeOptions = orderbookPrecisionState.symbol === symbol
? orderbookPrecisionState.nativeOptions
: [];
- const nativeOptionsChanged = nativeOptions.length > 0
+ const nativeOptionsChanged = isCurrentObservedSymbol(symbol)
+ && nativeOptions.includes(precision)
&& JSON.stringify(nativeOptions) !== JSON.stringify(previousNativeOptions);
if (nativeOptionsChanged) {
orderbookPrecisionState = {
@@ -1776,14 +1788,18 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
nativeOptions,
nativeOptionsStatus: null,
};
+ scheduleRenderPanel();
}
- if (precision === lastObservedOrderbookPrecision) {
- if (nativeOptionsChanged) scheduleRenderPanel();
- return;
- }
- lastObservedOrderbookPrecision = precision;
- stopMultiplierEdit();
- ladderPanelBodySignature = '';
+ }
+
+ function recordOrderbookPrecisionDomFailure(symbol, error) {
+ if (!isCurrentObservedSymbol(symbol)) return;
+ orderbookPrecisionState = {
+ ...orderbookPrecisionState,
+ symbol,
+ nativeOptions: [],
+ nativeOptionsStatus: error.message,
+ };
scheduleRenderPanel();
}
@@ -1810,7 +1826,13 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
orderbookPrecisionObserverRoot = root;
lastObservedOrderbookPrecision = readCurrentOrderbookPrecisionValue();
orderbookPrecisionObserver = new MutationObserver(() => {
- handleOrderbookPrecisionChange();
+ const symbol = getCurrentSymbol();
+ try {
+ handleOrderbookPrecisionChange();
+ } catch (error) {
+ if (!(error instanceof OrderbookPrecisionDomError)) throw error;
+ recordOrderbookPrecisionDomFailure(symbol, error);
+ }
});
orderbookPrecisionObserver.observe(root, {
subtree: true,
@@ -1821,27 +1843,22 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
function readOrderbookPrecisionOptionValue(node) {
if (!node) return null;
- const item = node.matches?.('.ob-ticksize-item')
+ const item = node.matches?.('.bn-select-option[role="option"]')
? node
- : node.closest?.('.ob-ticksize-item');
+ : node.closest?.('.bn-select-option[role="option"]');
const textNode = item?.querySelector('span') || node;
return normalizeDecimalString(textNode?.textContent || '');
}
function getVisibleOrderbookPrecisionOverlay(triggerElement = findOrderbookPrecisionTrigger()?.element) {
- const tickSize = triggerElement?.closest?.('.orderbook-tickSize');
- if (!tickSize || !tickSize.closest('#futuresOrderbook')) return null;
- // Other numeric menus can be open simultaneously; only the overlay owned by this tick-size control is valid.
- const overlays = Array.from(tickSize.querySelectorAll('.ob-ticksize-overlay'))
- .filter((overlay) => isVisibleElement(overlay));
- return overlays.length === 1 ? overlays[0] : null;
+ return findNativeOrderbookPrecisionOverlay(triggerElement, isVisibleElement);
}
function getVisibleOrderbookPrecisionOptionNodes(triggerElement = findOrderbookPrecisionTrigger()?.element) {
const overlay = getVisibleOrderbookPrecisionOverlay(triggerElement);
if (!overlay) return [];
- return Array.from(overlay.querySelectorAll('.ob-ticksize-item'))
- .filter((node) => node.closest('.ob-ticksize-overlay') === overlay)
+ return Array.from(overlay.querySelectorAll('.bn-select-option[role="option"]'))
+ .filter((node) => node.closest('.bn-select-overlay') === overlay)
.filter((node) => isVisibleElement(node))
.filter((node) => isOrderbookPrecisionNumericText(readOrderbookPrecisionOptionValue(node)));
}
@@ -1862,30 +1879,6 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
.find((node) => readOrderbookPrecisionOptionValue(node) === normalized) || null;
}
- function dispatchOrderbookPrecisionOpenEvent(target, type) {
- const EventCtor = type.startsWith('pointer') && typeof PointerEvent === 'function'
- ? PointerEvent
- : MouseEvent;
- return target.dispatchEvent(new EventCtor(type, {
- bubbles: true,
- cancelable: true,
- view: window,
- button: 0,
- buttons: type === 'pointerup' || type === 'mouseup' || type === 'click' ? 0 : 1,
- pointerId: 1,
- pointerType: 'mouse',
- isPrimary: true,
- }));
- }
-
- function dispatchOrderbookPrecisionToggleSequence(target) {
- dispatchOrderbookPrecisionOpenEvent(target, 'pointerdown');
- dispatchOrderbookPrecisionOpenEvent(target, 'mousedown');
- dispatchOrderbookPrecisionOpenEvent(target, 'pointerup');
- dispatchOrderbookPrecisionOpenEvent(target, 'mouseup');
- dispatchOrderbookPrecisionOpenEvent(target, 'click');
- }
-
async function waitForVisibleOrderbookPrecisionOptions(triggerElement, timeoutMs = ORDERBOOK_PRECISION_OPTION_WAIT_MS) {
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
while (!document.hidden && isFuturesTradingPage()) {
@@ -1919,33 +1912,27 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
return getVisibleOrderbookPrecisionOptionNodes(triggerElement);
}
- async function waitForOrderbookPrecisionOptionsClosed(triggerElement, timeoutMs = ORDERBOOK_PRECISION_OPTION_WAIT_MS) {
+ async function waitForOrderbookPrecisionMenuState(triggerElement, open, timeoutMs = ORDERBOOK_PRECISION_OPTION_WAIT_MS) {
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
while (!document.hidden && isFuturesTradingPage()) {
- if (!getVisibleOrderbookPrecisionOverlay(triggerElement)) return true;
+ if (isNativeOrderbookPrecisionMenuOpen(triggerElement) === open) return true;
if (Date.now() >= deadline) return false;
await delay(50);
}
- return !getVisibleOrderbookPrecisionOverlay(triggerElement);
+ return isNativeOrderbookPrecisionMenuOpen(triggerElement) === open;
}
- async function closeOrderbookPrecisionOptions(triggerElement, currentPrecision, waitForLateOpen = false) {
- if (waitForLateOpen && !getVisibleOrderbookPrecisionOverlay(triggerElement)) {
- await waitForVisibleOrderbookPrecisionOptions(triggerElement, ORDERBOOK_PRECISION_OPTION_WAIT_MS);
+ async function closeOrderbookPrecisionOptions(triggerElement, waitForLateOpen = false) {
+ if (!triggerElement?.isConnected) return true;
+ if (waitForLateOpen && !isNativeOrderbookPrecisionMenuOpen(triggerElement)) {
+ await waitForOrderbookPrecisionMenuState(triggerElement, true);
}
- if (!getVisibleOrderbookPrecisionOverlay(triggerElement)) return true;
- const currentOption = findVisibleOrderbookPrecisionOption(currentPrecision, triggerElement);
- if (currentOption) {
- if (!clickDomTarget(currentOption)) return false;
- return waitForOrderbookPrecisionOptionsClosed(triggerElement);
- }
- // Binance can expose the new symbol's displayed precision before replacing the old menu items.
- // Toggling the linked trigger closes that transitional menu without selecting an unrelated value.
- const tickSize = triggerElement?.closest?.('.orderbook-tickSize');
- const toggleTarget = tickSize?.querySelector?.('.tick-content') || triggerElement;
- if (!toggleTarget || !isVisibleElement(toggleTarget)) return false;
- dispatchOrderbookPrecisionToggleSequence(toggleTarget);
- return waitForOrderbookPrecisionOptionsClosed(triggerElement);
+ if (!isNativeOrderbookPrecisionMenuOpen(triggerElement)) return true;
+ if (findOrderbookPrecisionTrigger()?.element !== triggerElement) return false;
+ // Closing the captured trigger must not invoke a native option's onSelect handler.
+ const toggleTarget = triggerElement.closest('.bn-select-trigger');
+ if (!toggleTarget || !isVisibleElement(toggleTarget) || !clickDomTarget(toggleTarget)) return false;
+ return waitForOrderbookPrecisionMenuState(triggerElement, false);
}
async function waitForOrderbookPrecisionValue(options, timeoutMs = ORDERBOOK_PRECISION_OPTION_WAIT_MS) {
@@ -1988,7 +1975,10 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
return `${recommendationMarker}${formatOrderbookPrecisionShortcutLabel(value)} `;
}
- function renderOrderbookPrecisionShortcutSlots(options, current, recommendation, disabled) {
+ function renderOrderbookPrecisionShortcutSlots(options, current, recommendation, disabled, emptyLabel) {
+ if (!options.length) {
+ return [`${emptyLabel} `];
+ }
const slots = options.map((value) => (
renderOrderbookPrecisionShortcut(value, current, recommendation, disabled)
));
@@ -2086,11 +2076,16 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
);
if (!nativeOptions.length) queueOrderbookPrecisionOptionsLoad(symbol);
const canRefresh = !controlsBusy;
+ const emptyPrecisionLabel = ui(selectionBusy
+ ? PANEL_COPY.state.loadingPrecisionOptions
+ : orderbookPrecisionState.nativeOptionsStatus
+ ? PANEL_COPY.status.precisionOptionsUnavailable
+ : PANEL_COPY.state.waitingPrecisionOptions);
const recommendationHtml = [
'',
`
`,
`${ui(PANEL_COPY.field.pricePrecision)} `,
- ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy),
+ ...renderOrderbookPrecisionShortcutSlots(shortcutOptions, current, recommendation, controlsBusy, emptyPrecisionLabel),
renderOrderbookPrecisionRefreshButton(symbol, !canRefresh),
'
',
'
',
@@ -2120,6 +2115,7 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
const remainingMs = Math.max(0, deadline - Date.now());
const trigger = await waitForOrderbookPrecisionBootstrapReady(symbol, remainingMs);
if (!trigger?.element) return { status: '订单簿尚未就绪' };
+ const tickSize = trigger.element.closest('.orderbook-tickSize');
const startPrecision = trigger.value;
lastPrecision = startPrecision;
const optionsInitiallyVisible = getVisibleOrderbookPrecisionOptionNodes(trigger.element).length > 0;
@@ -2139,14 +2135,20 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
) snapshot = { precision: startPrecision, values };
} finally {
if (!optionsInitiallyVisible) {
- const cleanupPrecision = isCurrentObservedSymbol(symbol)
- ? readCurrentOrderbookPrecisionValue()
- : startPrecision;
- closed = await closeOrderbookPrecisionOptions(trigger.element, cleanupPrecision, true);
+ closed = await closeOrderbookPrecisionOptions(trigger.element, true);
}
}
if (!closed) return { status: '无法关闭价格精度下拉' };
- if (snapshot) return snapshot;
+ const settledTrigger = findOrderbookPrecisionTrigger();
+ if (
+ snapshot
+ && isCurrentObservedSymbol(symbol)
+ && trigger.element.isConnected
+ && tickSize?.isConnected
+ && settledTrigger?.element === trigger.element
+ && settledTrigger.element.closest('.orderbook-tickSize') === tickSize
+ && settledTrigger.value === startPrecision
+ ) return snapshot;
if (Date.now() >= deadline) break;
await delay(ORDERBOOK_PRECISION_READY_POLL_MS);
}
@@ -2237,11 +2239,16 @@ import { showUsdtRebalanceDialog } from './dom/usdt-rebalance-dialog.js';
async function runOrderbookPrecisionSelectionTask(operation) {
if (orderbookPrecisionSelectionTask) return orderbookPrecisionSelectionTask;
+ const symbol = getCurrentSymbol();
const task = operation();
orderbookPrecisionSelectionTask = task;
scheduleRenderPanel();
try {
return await task;
+ } catch (error) {
+ if (!(error instanceof OrderbookPrecisionDomError)) throw error;
+ recordOrderbookPrecisionDomFailure(symbol, error);
+ return false;
} finally {
if (orderbookPrecisionSelectionTask === task) orderbookPrecisionSelectionTask = null;
scheduleRenderPanel();
diff --git a/src/binance-strategy27-events/index.user.js b/src/binance-strategy27-events/index.user.js
index 35df503..8b0ebd2 100644
--- a/src/binance-strategy27-events/index.user.js
+++ b/src/binance-strategy27-events/index.user.js
@@ -3,7 +3,7 @@
// @namespace binance.strategy27.events
// @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 0.6.0
+// @version 0.6.1
// @author jackhai9
// @description Display Strategy 27 events and provide the shared private CorsairQuant gateway connection
// @match https://www.binance.com/*/futures/*
diff --git a/src/binance-strategy29-bollinger/index.user.js b/src/binance-strategy29-bollinger/index.user.js
index 510cb60..b96d311 100644
--- a/src/binance-strategy29-bollinger/index.user.js
+++ b/src/binance-strategy29-bollinger/index.user.js
@@ -3,7 +3,7 @@
// @namespace binance.strategy29.bollinger
// @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 0.5.1
+// @version 0.5.2
// @author jackhai9
// @description Native Bollinger/SMA60 markers and the default read-only cross-timeframe summary
// @match https://www.binance.com/*/futures/*
diff --git a/test/dom/binance-orderbook-trade/orderbook-precision.test.js b/test/dom/binance-orderbook-trade/orderbook-precision.test.js
new file mode 100644
index 0000000..c1f2ee5
--- /dev/null
+++ b/test/dom/binance-orderbook-trade/orderbook-precision.test.js
@@ -0,0 +1,379 @@
+import test from 'node:test';
+import assert from 'node:assert/strict';
+import { readFileSync } from 'node:fs';
+import vm from 'node:vm';
+
+import { isVisibleElement, loadFixtureDom } from '../../helpers/dom.js';
+import { normalizeDecimalString } from '../../../src/binance-orderbook-trade/core/decimal.js';
+import {
+ OrderbookPrecisionDomError,
+ findNativeOrderbookPrecisionOverlay,
+ isNativeOrderbookPrecisionMenuOpen,
+} from '../../../src/binance-orderbook-trade/dom/orderbook-precision.js';
+
+const source = readFileSync(new URL('../../../src/binance-orderbook-trade/index.user.js', import.meta.url), 'utf8');
+const markup = readFileSync(new URL('../../fixtures/binance-orderbook-trade/orderbook-precision.html', import.meta.url), 'utf8');
+
+function declaration(name) {
+ const start = source.indexOf('function ' + name + '(');
+ assert.notEqual(start, -1, name + ' must exist');
+ const brace = source.indexOf('{', start);
+ let depth = 0;
+ for (let index = brace; index < source.length; index += 1) {
+ if (source[index] === '{') depth += 1;
+ if (source[index] === '}') depth -= 1;
+ if (depth === 0) return source.slice(source.slice(start - 6, start) === 'async ' ? start - 6 : start, index + 1);
+ }
+ assert.fail(name + ' must have a complete body');
+}
+
+function attachDropdownOwner(root, id = 'bn-select-precision') {
+ const host = root.querySelector('.bn-select');
+ const branch = () => ({
+ stateNode: host,
+ return: {
+ memoizedProps: { overlay: { props: { id } } },
+ return: { stateNode: root, return: null },
+ },
+ });
+ const fiber = branch();
+ fiber.alternate = branch();
+ host.__reactFiber$fixture = fiber;
+ return fiber;
+}
+
+function setup() {
+ const dom = loadFixtureDom(markup);
+ const document = dom.window.document;
+ Object.defineProperty(document, 'hidden', { configurable: true, value: false });
+ const root = document.querySelector('.orderbook-tickSize');
+ const fiber = attachDropdownOwner(root);
+ const context = vm.createContext({
+ document,
+ isVisibleElement,
+ normalizeDecimalString,
+ findNativeOrderbookPrecisionOverlay,
+ isNativeOrderbookPrecisionMenuOpen,
+ OrderbookPrecisionDomError,
+ ORDERBOOK_PRECISION_SHORTCUT_LIMIT: 4,
+ MUTED_TEXT_COLOR: '#76808f',
+ renderOrderbookPrecisionShortcut: (value) => '' + value + ' ',
+ });
+ for (const name of [
+ 'isOrderbookPrecisionNumericText',
+ 'findOrderbookPrecisionTrigger',
+ 'readCurrentOrderbookPrecisionValue',
+ 'getVisibleOrderbookPrecisionOverlay',
+ 'getVisibleOrderbookPrecisionOptionNodes',
+ 'readOrderbookPrecisionOptionValue',
+ 'readVisibleOrderbookPrecisionOptionValues',
+ 'renderOrderbookPrecisionShortcutSlots',
+ ]) vm.runInContext(declaration(name), context);
+ return { dom, document, root, fiber, context };
+}
+
+function installController(context, overrides = {}) {
+ Object.assign(context, {
+ window: context.document.defaultView,
+ getCurrentSymbol: () => 'SOPHUSDT',
+ isCurrentObservedSymbol: (symbol) => symbol === 'SOPHUSDT',
+ isFuturesTradingPage: () => true,
+ orderbookPrecisionState: { symbol: 'SOPHUSDT', nativeOptions: ['stale'], nativeOptionsStatus: null },
+ orderbookPrecisionSelectionTask: null,
+ orderbookPrecisionObserver: null,
+ orderbookPrecisionObserverRoot: null,
+ lastObservedOrderbookPrecision: null,
+ ladderPanelBodySignature: 'old',
+ scheduleRenderPanel() {},
+ stopMultiplierEdit() {},
+ delay: () => Promise.resolve(),
+ ORDERBOOK_PRECISION_OPTION_WAIT_MS: 50,
+ ORDERBOOK_PRECISION_READY_TIMEOUT_MS: 500,
+ ORDERBOOK_PRECISION_READY_POLL_MS: 1,
+ ...overrides,
+ });
+ for (const name of [
+ 'handleOrderbookPrecisionChange',
+ 'recordOrderbookPrecisionDomFailure',
+ 'stopOrderbookPrecisionObserver',
+ 'ensureOrderbookPrecisionObserver',
+ 'runOrderbookPrecisionSelectionTask',
+ 'waitForOrderbookPrecisionMenuState',
+ 'closeOrderbookPrecisionOptions',
+ 'waitForStableOrderbookPrecisionOptions',
+ 'runLoadOrderbookPrecisionOptions',
+ ]) vm.runInContext(declaration(name), context);
+}
+
+test('reads exact native precision options from the control-owned body portal', () => {
+ const { document, root, context } = setup();
+ const trigger = root.querySelector('.tick-content');
+ const listbox = document.getElementById('bn-select-precision');
+ assert.equal(root.contains(listbox), false);
+ const nodes = context.getVisibleOrderbookPrecisionOptionNodes(trigger);
+ assert.deepEqual(Array.from(nodes, context.readOrderbookPrecisionOptionValue), [
+ '0.000001', '0.00001', '0.0001',
+ ]);
+});
+
+test('empty precision options have a visible status instead of four blank slots', () => {
+ const { document, context } = setup();
+ const row = document.createElement('div');
+ row.innerHTML = context.renderOrderbookPrecisionShortcutSlots([], '0.000001', null, false, 'Waiting for options').join('');
+ assert.equal(row.textContent, 'Waiting for options');
+ assert.equal(row.querySelector('[role="status"]').getAttribute('data-orderbook-precision-status'), 'true');
+ assert.equal(row.querySelectorAll('[data-orderbook-precision-value]').length, 0);
+});
+
+test('native options keep their exact values and reserve only the unused fourth slot', () => {
+ const { document, context } = setup();
+ const row = document.createElement('div');
+ row.innerHTML = context.renderOrderbookPrecisionShortcutSlots(
+ ['0.000001', '0.00001', '0.0001'], '0.000001', null, false, 'Waiting for options',
+ ).join('');
+ assert.deepEqual(Array.from(row.querySelectorAll('button'), (node) => node.dataset.orderbookPrecisionValue), [
+ '0.000001', '0.00001', '0.0001',
+ ]);
+ assert.equal(row.querySelectorAll('[aria-hidden="true"]').length, 1);
+ assert.equal(row.querySelector('[role="status"]'), null);
+});
+
+test('a lookalike numeric portal cannot supply the precision menu options', () => {
+ const { document, context } = setup();
+ const owned = document.getElementById('bn-select-precision');
+ const otherBubble = owned.closest('.bn-select-bubble').cloneNode(true);
+ otherBubble.querySelector('[role="listbox"]').id = 'bn-select-unrelated';
+ document.body.prepend(otherBubble);
+ assert.deepEqual(Array.from(context.getVisibleOrderbookPrecisionOptionNodes(), (node) => node.closest('[role="listbox"]').id), [
+ 'bn-select-precision', 'bn-select-precision', 'bn-select-precision',
+ ]);
+ owned.remove();
+ assert.deepEqual(Array.from(context.getVisibleOrderbookPrecisionOptionNodes()), []);
+});
+
+test('closed, unmounted, or detached menus return no options', () => {
+ for (const transition of ['closed', 'hidden', 'unmounted', 'detached']) {
+ const { root, document, context } = setup();
+ const trigger = root.querySelector('.tick-content');
+ if (transition === 'closed') root.querySelector('.bn-select').classList.remove('active');
+ if (transition === 'hidden') document.querySelector('.bn-select-bubble').setAttribute('data-hidden', '');
+ if (transition === 'unmounted') document.querySelector('.bn-select-bubble').remove();
+ if (transition === 'detached') root.remove();
+ assert.equal(context.getVisibleOrderbookPrecisionOverlay(trigger), null, transition);
+ }
+});
+
+test('missing, ambiguous, escaped, or disagreeing React owners fail explicitly', () => {
+ const mutations = [
+ (root) => { delete root.querySelector('.bn-select').__reactFiber$fixture; },
+ (_root, fiber) => { fiber.return.return = null; },
+ (_root, fiber) => { fiber.return.return = { memoizedProps: { overlay: { props: { id: 'bn-select-second' } } }, return: fiber.return.return }; },
+ (_root, fiber) => { fiber.alternate.return.memoizedProps.overlay.props.id = 'bn-select-replaced'; },
+ ];
+ for (const mutate of mutations) {
+ const { root, fiber, context } = setup();
+ mutate(root, fiber);
+ assert.throws(() => context.getVisibleOrderbookPrecisionOverlay(), OrderbookPrecisionDomError);
+ assert.equal(context.readCurrentOrderbookPrecisionValue(), '0.000001');
+ }
+});
+
+test('the current native root is reacquired after a same-symbol replacement', () => {
+ const { document, root, context } = setup();
+ const oldTrigger = root.querySelector('.tick-content');
+ const replacement = root.cloneNode(true);
+ root.replaceWith(replacement);
+ attachDropdownOwner(replacement, 'bn-select-new');
+ const listbox = document.getElementById('bn-select-precision');
+ listbox.id = 'bn-select-new';
+ assert.equal(context.getVisibleOrderbookPrecisionOverlay(oldTrigger), null);
+ assert.equal(context.getVisibleOrderbookPrecisionOverlay(), listbox.parentElement);
+});
+
+test('an owner-linked element with a changed listbox contract fails explicitly', () => {
+ const { document, context } = setup();
+ document.getElementById('bn-select-precision').setAttribute('role', 'menu');
+ assert.throws(() => context.getVisibleOrderbookPrecisionOverlay(), /listbox structure has changed/);
+});
+
+test('cleanup closes only the captured trigger without selecting an option', async () => {
+ const { root, context } = setup();
+ const trigger = root.querySelector('.tick-content');
+ const clicks = [];
+ installController(context, {
+ clickDomTarget(node) {
+ clicks.push(node);
+ root.querySelector('.bn-select').classList.remove('active');
+ return true;
+ },
+ });
+ delete root.querySelector('.bn-select').__reactFiber$fixture;
+ assert.equal(await context.closeOrderbookPrecisionOptions(trigger), true);
+ assert.deepEqual(clicks, [root.querySelector('.bn-select-trigger')]);
+ assert.equal(trigger.textContent.trim(), '0.000001');
+});
+
+test('cleanup never clicks a replacement trigger or a user-closed menu', async () => {
+ for (const transition of ['replaced', 'closed']) {
+ const { root, context } = setup();
+ const trigger = root.querySelector('.tick-content');
+ let clicks = 0;
+ installController(context, { clickDomTarget() { clicks += 1; return true; } });
+ if (transition === 'replaced') root.replaceWith(root.cloneNode(true));
+ else root.querySelector('.bn-select').classList.remove('active');
+ assert.equal(await context.closeOrderbookPrecisionOptions(trigger), true);
+ assert.equal(clicks, 0, transition);
+ }
+});
+
+test('precision jobs expose DOM contract failures and discard cached options', async () => {
+ const { context } = setup();
+ installController(context);
+ const error = new OrderbookPrecisionDomError('Native precision listbox identity changed between renders');
+ assert.equal(await context.runOrderbookPrecisionSelectionTask(async () => { throw error; }), false);
+ assert.deepEqual(Array.from(context.orderbookPrecisionState.nativeOptions), []);
+ assert.equal(context.orderbookPrecisionState.nativeOptionsStatus, error.message);
+ assert.equal(context.orderbookPrecisionSelectionTask, null);
+ const unexpected = new TypeError('Unexpected programmer error');
+ await assert.rejects(context.runOrderbookPrecisionSelectionTask(async () => { throw unexpected; }), unexpected);
+});
+
+test('a failed precision job cannot overwrite the next symbol state', async () => {
+ const { context } = setup();
+ let symbol = 'SOPHUSDT';
+ installController(context, {
+ getCurrentSymbol: () => symbol,
+ isCurrentObservedSymbol: (captured) => captured === symbol,
+ });
+ const nextState = { symbol: 'BTCUSDT', nativeOptions: ['0.1', '1'], nativeOptionsStatus: null };
+ const result = await context.runOrderbookPrecisionSelectionTask(async () => {
+ await Promise.resolve();
+ symbol = 'BTCUSDT';
+ context.orderbookPrecisionState = nextState;
+ throw new OrderbookPrecisionDomError('Native precision React ownership is unavailable');
+ });
+ assert.equal(result, false);
+ assert.equal(context.orderbookPrecisionState, nextState);
+ assert.equal(context.orderbookPrecisionSelectionTask, null);
+});
+
+test('observer contract failures preserve current-precision invalidation and visible error state', () => {
+ const { root, context } = setup();
+ let callback;
+ let stoppedEdits = 0;
+ installController(context, {
+ MutationObserver: class {
+ constructor(handler) { callback = handler; }
+ observe() {}
+ disconnect() {}
+ },
+ stopMultiplierEdit() { stoppedEdits += 1; },
+ });
+ context.ensureOrderbookPrecisionObserver();
+ root.querySelector('.tick-content span').textContent = '0.00001';
+ delete root.querySelector('.bn-select').__reactFiber$fixture;
+ callback();
+ assert.equal(context.lastObservedOrderbookPrecision, '0.00001');
+ assert.equal(stoppedEdits, 1);
+ assert.equal(context.ladderPanelBodySignature, '');
+ assert.deepEqual(Array.from(context.orderbookPrecisionState.nativeOptions), []);
+ assert.equal(context.orderbookPrecisionState.nativeOptionsStatus, 'Native precision React ownership is unavailable');
+ context.handleOrderbookPrecisionChange = () => { throw new TypeError('Unexpected programmer error'); };
+ assert.throws(callback, /Unexpected programmer error/);
+});
+
+test('bootstrap discards a snapshot replaced during cleanup before committing new options', async () => {
+ const { document, root, context } = setup();
+ let menuOpen = false;
+ let closes = 0;
+ let currentOptions = ['0.000001', '0.00001', '0.0001'];
+ installController(context);
+ Object.assign(context, {
+ waitForOrderbookPrecisionBootstrapReady: async () => context.findOrderbookPrecisionTrigger(),
+ getVisibleOrderbookPrecisionOptionNodes: () => menuOpen ? currentOptions : [],
+ ensureVisibleOrderbookPrecisionOptions: async () => { menuOpen = true; return currentOptions; },
+ readVisibleOrderbookPrecisionOptionValues: () => currentOptions,
+ closeOrderbookPrecisionOptions: async () => {
+ await Promise.resolve();
+ closes += 1;
+ menuOpen = false;
+ if (closes === 1) {
+ const replacement = root.cloneNode(true);
+ replacement.querySelector('.tick-content span').textContent = '0.001';
+ root.replaceWith(replacement);
+ attachDropdownOwner(replacement, 'bn-select-new');
+ currentOptions = ['0.001', '0.01'];
+ }
+ return true;
+ },
+ });
+ assert.equal(await context.runLoadOrderbookPrecisionOptions(), true);
+ assert.equal(closes, 2);
+ assert.equal(root.isConnected, false);
+ assert.equal(document.querySelector('.tick-content').textContent, '0.001');
+ assert.deepEqual(Array.from(context.orderbookPrecisionState.nativeOptions), ['0.001', '0.01']);
+ assert.equal(context.orderbookPrecisionState.current, '0.001');
+});
+
+test('bootstrap preserves an already open native precision menu', async () => {
+ const { context } = setup();
+ installController(context);
+ let closes = 0;
+ Object.assign(context, {
+ waitForOrderbookPrecisionBootstrapReady: async () => context.findOrderbookPrecisionTrigger(),
+ getVisibleOrderbookPrecisionOptionNodes: () => ['option'],
+ ensureVisibleOrderbookPrecisionOptions: async () => ['option'],
+ readVisibleOrderbookPrecisionOptionValues: () => ['0.000001', '0.00001'],
+ closeOrderbookPrecisionOptions: async () => { closes += 1; return true; },
+ });
+ assert.equal(await context.runLoadOrderbookPrecisionOptions(), true);
+ assert.equal(closes, 0);
+ assert.deepEqual(Array.from(context.orderbookPrecisionState.nativeOptions), ['0.000001', '0.00001']);
+});
+
+test('bootstrap reacquires the displayed precision after it changes during cleanup', async () => {
+ const { root, context } = setup();
+ installController(context);
+ let closes = 0;
+ const values = ['0.000001', '0.00001', '0.0001'];
+ Object.assign(context, {
+ waitForOrderbookPrecisionBootstrapReady: async () => context.findOrderbookPrecisionTrigger(),
+ getVisibleOrderbookPrecisionOptionNodes: () => [],
+ ensureVisibleOrderbookPrecisionOptions: async () => values,
+ readVisibleOrderbookPrecisionOptionValues: () => values,
+ closeOrderbookPrecisionOptions: async () => {
+ await Promise.resolve();
+ closes += 1;
+ root.querySelector('.tick-content span').textContent = '0.00001';
+ return true;
+ },
+ });
+ assert.equal(await context.runLoadOrderbookPrecisionOptions(), true);
+ assert.equal(closes, 2);
+ assert.equal(context.orderbookPrecisionState.current, '0.00001');
+ assert.deepEqual(Array.from(context.orderbookPrecisionState.nativeOptions), values);
+});
+
+test('bootstrap discards its snapshot when the symbol changes during cleanup', async () => {
+ const { context } = setup();
+ let symbol = 'SOPHUSDT';
+ installController(context, {
+ getCurrentSymbol: () => symbol,
+ isCurrentObservedSymbol: (captured) => captured === symbol,
+ });
+ const nextState = { symbol: 'BTCUSDT', nativeOptions: ['0.1', '1'], nativeOptionsStatus: null };
+ Object.assign(context, {
+ waitForOrderbookPrecisionBootstrapReady: async () => context.findOrderbookPrecisionTrigger(),
+ getVisibleOrderbookPrecisionOptionNodes: () => [],
+ ensureVisibleOrderbookPrecisionOptions: async () => ['0.000001', '0.00001'],
+ readVisibleOrderbookPrecisionOptionValues: () => ['0.000001', '0.00001'],
+ closeOrderbookPrecisionOptions: async () => {
+ await Promise.resolve();
+ symbol = 'BTCUSDT';
+ context.orderbookPrecisionState = nextState;
+ return true;
+ },
+ });
+ assert.equal(await context.runLoadOrderbookPrecisionOptions(), false);
+ assert.equal(context.orderbookPrecisionState, nextState);
+});
diff --git a/test/fixtures/binance-orderbook-trade/orderbook-precision.html b/test/fixtures/binance-orderbook-trade/orderbook-precision.html
new file mode 100644
index 0000000..24d4dcc
--- /dev/null
+++ b/test/fixtures/binance-orderbook-trade/orderbook-precision.html
@@ -0,0 +1,22 @@
+
+
diff --git a/test/unit/binance-orderbook-trade/source-regressions.test.js b/test/unit/binance-orderbook-trade/source-regressions.test.js
index e7fadd2..af3ede0 100644
--- a/test/unit/binance-orderbook-trade/source-regressions.test.js
+++ b/test/unit/binance-orderbook-trade/source-regressions.test.js
@@ -536,7 +536,7 @@ test('dynamic panel text keeps fixed single-line slots', () => {
assert.doesNotMatch(source, /buttonBaseStyle = `width:68px;height:24px/);
assert.match(readFunctionBody('renderOrderbookPrecisionShortcut'), /height:32px[^`]*font-size:12px;line-height:30px/);
assert.match(readFunctionBody('renderOrderbookPrecisionShortcutSlots'), /while \(slots\.length < ORDERBOOK_PRECISION_SHORTCUT_LIMIT\)/);
- assert.doesNotMatch(source, /data-orderbook-precision-status/);
+ assert.match(source, /data-orderbook-precision-status/);
const ladderBody = readFunctionBody('refreshLadderPanel');
const actionButtonBody = readFunctionBody('ladderActionButton');
@@ -1477,12 +1477,12 @@ test('orderbook precision recommendation marks one shortcut without applying it
assert.match(refreshBody, /margin-top:10px;/);
assert.match(refreshBody, /PANEL_COPY\.field\.pricePrecision/);
assert.match(refreshBody, /PANEL_COPY\.tooltip\.pricePrecision/);
- assert.match(refreshBody, /renderOrderbookPrecisionShortcutSlots\(shortcutOptions, current, recommendation, controlsBusy\)/);
+ assert.match(refreshBody, /renderOrderbookPrecisionShortcutSlots\(shortcutOptions, current, recommendation, controlsBusy, emptyPrecisionLabel\)/);
assert.match(refreshBody, /activeUiLocale === 'en' \? '52px' : '36px'/);
assert.match(refreshBody, /repeat\(4,minmax\(0,1fr\)\) 32px/);
assert.match(refreshBody, /renderOrderbookPrecisionRefreshButton\(symbol, !canRefresh\)/);
assert.match(refreshButtonBody, /data-orderbook-precision-refresh="true"[^`]*\$\{feedback\.icon\}/);
- assert.doesNotMatch(refreshBody, /data-orderbook-precision-status/);
+ assert.match(refreshBody, /PANEL_COPY\.status\.precisionOptionsUnavailable/);
assert.equal((source.match(/PANEL_COPY\.field\.interval, PANEL_COPY\.tooltip\.interval, LADDER_STEP_OPTIONS/g) || []).length, 2);
assert.doesNotMatch(source, /data-ladder-step-action|function ladderStepRow/);
@@ -1501,12 +1501,14 @@ test('orderbook precision recommendation marks one shortcut without applying it
assert.match(stableLoadBody, /findOrderbookPrecisionTrigger\(\)/);
assert.match(stableLoadBody, /currentTrigger\?\.element === trigger\.element/);
assert.match(stableLoadBody, /values\.includes\(startPrecision\)/);
- assert.match(stableLoadBody, /finally\s*\{[\s\S]*if \(!optionsInitiallyVisible\)[\s\S]*closeOrderbookPrecisionOptions\(trigger\.element, cleanupPrecision, true\)/);
+ assert.match(stableLoadBody, /finally\s*\{[\s\S]*if \(!optionsInitiallyVisible\)[\s\S]*closeOrderbookPrecisionOptions\(trigger\.element, true\)/);
+ assert.match(stableLoadBody, /settledTrigger\?\.element === trigger\.element/);
assert.match(stableLoadBody, /await delay\(ORDERBOOK_PRECISION_READY_POLL_MS\)/);
const closeOptionsBody = readFunctionBody('closeOrderbookPrecisionOptions');
- assert.match(closeOptionsBody, /findVisibleOrderbookPrecisionOption\(currentPrecision, triggerElement\)/);
- assert.match(closeOptionsBody, /dispatchOrderbookPrecisionToggleSequence\(toggleTarget\)/);
+ assert.doesNotMatch(closeOptionsBody, /findVisibleOrderbookPrecisionOption|currentOption/);
+ assert.match(closeOptionsBody, /findOrderbookPrecisionTrigger\(\)\?\.element !== triggerElement/);
+ assert.match(closeOptionsBody, /triggerElement\.closest\('\.bn-select-trigger'\)/);
const selectBody = readFunctionBody('runSelectOrderbookPrecision');
assert.match(selectBody, /getOrderbookPrecisionShortcutOptions\([\s\S]*ORDERBOOK_PRECISION_SHORTCUT_LIMIT/);
@@ -1543,15 +1545,8 @@ test('orderbook precision recommendation marks one shortcut without applying it
assert.match(openOptionsBody, /return waitForVisibleOrderbookPrecisionOptions\(triggerElement\)/);
assert.doesNotMatch(openOptionsBody, /candidates|for \(const target|dispatchOrderbookPrecisionToggleSequence/);
- const openEventBody = readFunctionBody('dispatchOrderbookPrecisionOpenEvent');
- assert.match(openEventBody, /PointerEvent/);
- assert.match(openEventBody, /MouseEvent/);
- const toggleSequenceBody = readFunctionBody('dispatchOrderbookPrecisionToggleSequence');
- assert.match(toggleSequenceBody, /pointerdown/);
- assert.match(toggleSequenceBody, /mousedown/);
- assert.match(toggleSequenceBody, /pointerup/);
- assert.match(toggleSequenceBody, /mouseup/);
- assert.match(toggleSequenceBody, /click/);
+ assert.doesNotMatch(source, /function dispatchOrderbookPrecisionToggleSequence/);
+ assert.match(closeOptionsBody, /clickDomTarget\(toggleTarget\)/);
const waitOptionsBody = readFunctionBody('waitForVisibleOrderbookPrecisionOptions');
assert.match(waitOptionsBody, /getVisibleOrderbookPrecisionOptionNodes\(triggerElement\)\.length/);
@@ -1563,9 +1558,8 @@ test('orderbook precision recommendation marks one shortcut without applying it
assert.match(bootstrapReadyBody, /getOrderbookPrices\('ASK', 1\)/);
assert.match(bootstrapReadyBody, /isCurrentObservedSymbol\(symbol\)/);
- assert.match(closeOptionsBody, /waitForVisibleOrderbookPrecisionOptions\(triggerElement, ORDERBOOK_PRECISION_OPTION_WAIT_MS\)/);
- assert.match(closeOptionsBody, /findVisibleOrderbookPrecisionOption\(currentPrecision, triggerElement\)/);
- assert.match(closeOptionsBody, /waitForOrderbookPrecisionOptionsClosed\(triggerElement\)/);
+ assert.match(closeOptionsBody, /waitForOrderbookPrecisionMenuState\(triggerElement, true\)/);
+ assert.match(closeOptionsBody, /waitForOrderbookPrecisionMenuState\(triggerElement, false\)/);
const queueOptionsBody = readFunctionBody('queueOrderbookPrecisionOptionsLoad');
assert.match(source, /function queueOrderbookPrecisionOptionsLoad\(symbol, force = false\)/);
@@ -1585,20 +1579,18 @@ test('orderbook precision recommendation marks one shortcut without applying it
assert.doesNotMatch(triggerBody, /querySelectorAll/);
const optionsBody = readFunctionBody('getVisibleOrderbookPrecisionOptionNodes');
- assert.match(optionsBody, /\.ob-ticksize-item/);
+ assert.match(optionsBody, /\.bn-select-option/);
assert.match(optionsBody, /readOrderbookPrecisionOptionValue\(node\)/);
assert.match(optionsBody, /getVisibleOrderbookPrecisionOverlay\(triggerElement\)/);
- assert.match(optionsBody, /node\.closest\('\.ob-ticksize-overlay'\) === overlay/);
+ assert.match(optionsBody, /node\.closest\('\.bn-select-overlay'\) === overlay/);
assert.doesNotMatch(optionsBody, /document\.querySelectorAll/);
assert.doesNotMatch(optionsBody, /ORDERBOOK_PRECISION_CANDIDATE_OPTIONS/);
const overlayBody = readFunctionBody('getVisibleOrderbookPrecisionOverlay');
- assert.match(overlayBody, /tickSize\.querySelectorAll\('\.ob-ticksize-overlay'\)/);
- assert.match(overlayBody, /overlays\.length === 1/);
- assert.match(overlayBody, /tickSize\.closest\('#futuresOrderbook'\)/);
+ assert.match(overlayBody, /findNativeOrderbookPrecisionOverlay\(triggerElement, isVisibleElement\)/);
const optionValueBody = readFunctionBody('readOrderbookPrecisionOptionValue');
- assert.match(optionValueBody, /\.ob-ticksize-item/);
+ assert.match(optionValueBody, /\.bn-select-option/);
assert.match(optionValueBody, /querySelector\('span'\)/);
const findOptionBody = readFunctionBody('findVisibleOrderbookPrecisionOption');
diff --git a/test/unit/binance-strategy29-bollinger/source-regressions.test.js b/test/unit/binance-strategy29-bollinger/source-regressions.test.js
index 860b139..5221246 100644
--- a/test/unit/binance-strategy29-bollinger/source-regressions.test.js
+++ b/test/unit/binance-strategy29-bollinger/source-regressions.test.js
@@ -52,7 +52,7 @@ test('Bollinger chart alert failures distinguish snapshot races from contract fa
});
test('Strategy29 sandbox metadata is exact and coordinates through unsafeWindow', () => {
- assert.equal(readUserscriptVersion(entrySource), '0.5.1');
+ assert.equal(readUserscriptVersion(entrySource), '0.5.2');
assert.deepEqual(
[...entrySource.matchAll(/^\/\/ @grant\s+(\S+)\s*$/gm)].map(match => match[1]),
['unsafeWindow', 'GM_getValue', 'GM_setValue'],
diff --git a/test/unit/userscript-release-contract.test.js b/test/unit/userscript-release-contract.test.js
index 358e545..8d2d4ba 100644
--- a/test/unit/userscript-release-contract.test.js
+++ b/test/unit/userscript-release-contract.test.js
@@ -21,7 +21,7 @@ test('Strategy29 has an independent observation-only install identity', async ()
const metadata = parseUserscriptMetadata(text);
assert.equal(contract.name, '【自写】Binance Strategy 29 布林带信号');
assert.equal(contract.namespace, 'binance.strategy29.bollinger');
- assert.equal(contract.version, '0.5.1');
+ assert.equal(contract.version, '0.5.2');
assert.equal(contract.runAt, 'document-start');
assert.equal(contract.updateURL, 'https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-strategy29-bollinger.user.js');
assert.equal(contract.downloadURL, contract.updateURL);
@@ -61,7 +61,7 @@ test('release contract identifies the generated Strategy 27 annotation artifact'
assert.equal(contract.name, '【自写】Binance Strategy 27 事件标注');
assert.equal(contract.namespace, 'binance.strategy27.events');
- assert.equal(contract.version, '0.6.0');
+ assert.equal(contract.version, '0.6.1');
assert.equal(contract.runAt, 'document-idle');
assert.equal(contract.updateURL, contract.downloadURL);
assert.deepEqual(contract.matches, [