Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion docs/binance-orderbook-trade-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion docs/binance-orderbook-trade-ui-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/binance-strategy27-events-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions docs/binance-strategy29-bollinger-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
115 changes: 94 additions & 21 deletions e2e/binance-orderbook/fixtures/binance-futures.js
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
Expand All @@ -55,7 +55,7 @@ export function renderBinanceFuturesFixture(scenario) {
<div id="fixture-layout">
<main id="fixture-main">
<section id="futuresOrderbook">
<div class="orderbook-tickSize"><div class="bn-tooltips-ele"><span class="tick-content">0.1</span></div></div>
<div class="orderbook-tickSize"><div class="bn-tooltips-wrap bn-tooltips-web bn-select bn-select__auto"><div class="bn-tooltips-ele bn-select-trigger"><span class="tick-content">0.1</span></div></div></div>
<div class="row-content"><span class="ask-light emit-price">81.2</span></div>
<div class="row-content"><span class="ask-light emit-price">81.1</span></div>
<div class="row-content"><span class="ask-light emit-price">81.05</span></div>
Expand Down Expand Up @@ -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) => (
'<div class="ob-ticksize-item" data-precision-value="' + value + '"><span>' + value + '</span></div>'
)).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 = '<div class="bn-bubble-content"><div class="bn-select-overlay">' +
'<div class="bn-select-overlay-options" role="listbox" id="' + control.listboxId + '">' +
control.options.map((value) => (
'<div class="bn-select-option" role="option" aria-selected="' + selected(value, state.orderbookPrecision) +
'" data-precision-value="' + value + '"><span>' + value + '</span></div>'
)).join('') + '</div></div></div>';
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() {
Expand Down Expand Up @@ -477,6 +549,7 @@ export function renderBinanceFuturesFixture(scenario) {
renderChartOrdersPopover();

window.__BINANCE_FIXTURE__ = {
replacePrecisionControl,
snapshot: () => JSON.parse(JSON.stringify({
positions: state.positions,
orders: state.orders,
Expand All @@ -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', {
Expand Down
61 changes: 52 additions & 9 deletions e2e/binance-orderbook/specs/control-flows.pw.js
Original file line number Diff line number Diff line change
Expand Up @@ -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([]);
});

Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading