diff --git a/docs/binance-orderbook-trade-ui-automation.md b/docs/binance-orderbook-trade-ui-automation.md index 2ec7da8..407306c 100644 --- a/docs/binance-orderbook-trade-ui-automation.md +++ b/docs/binance-orderbook-trade-ui-automation.md @@ -288,12 +288,24 @@ The assembler rejects unknown bundle fields and refuses to overwrite its input f or an existing capture. This command is the standard boundary between CDP collection and capture summary; do not replace it with an ad hoc Node snippet. -The current checked-in L4 reference is the isolated zero-order HYPEUSDT run. The -older captures remain immutable evidence; the active baseline comes from the latest -page-owned completion sample: +The checked-in L4 references cover the isolated zero-order path and the one-order +native cancel-dialog paths. Older captures remain immutable evidence; active +baselines come from the latest page-owned completion samples: - `e2e/binance-orderbook/live-baselines/no-orders-2026-08-27-page-ready.capture.json` - `e2e/binance-orderbook/live-baselines/no-orders.baseline.json` +- `e2e/binance-orderbook/live-baselines/dialog-smoke-2026-08-27.capture.json` +- `e2e/binance-orderbook/live-baselines/dialog-smoke.baseline.json` + +The dialog reference used HYPEUSDT userscript `2.7.126` with one far-from-market, +Post Only test order per confirm sample. It contains three isolated cancel samples +and three isolated confirm samples. Every sample restored the page state, recorded +zero fills, left zero test-owned residual orders, and observed zero uncaught errors. +The reference medians are 66.3 ms from action click to native dialog for cancel, +85.0 ms for confirm, 43.6 ms from cancel decision to final readiness, and 1285.7 ms +from confirm decision to final readiness. These values describe one captured Chrome +and Binance environment; comparison policy, rather than a copied timing constant, +defines the regression threshold. Each scenario kind owns one exact wall-clock segment contract. A scenario cannot add, omit, or reorder segments independently of its kind and must contain at least three @@ -320,6 +332,12 @@ live capacity evidence. Capacity shortfalls must skip or abort the scale explici they must never silently relabel a smaller run as medium or large. Current leverage is read-only evidence for this calculation and the live runner must not change it. +Capacity evidence persists `testBudget`, not the exact account balance. The caller +allocates a positive test budget no greater than the verified live balance, and the +capacity helper proves the requested order count against that budget. This preserves +the capacity contract without publishing account financial data in repository +artifacts. + `testOrderLedger` is the evidence behind the fill and cleanup claims. Its `created`, `fills`, and `residual` collections use the same exact order identity: symbol, side, position side, price, quantity, and creation timestamp. A fill or residual record that diff --git a/e2e/binance-orderbook/helpers/live-order-scale-config.js b/e2e/binance-orderbook/helpers/live-order-scale-config.js index f25e49e..e844785 100644 --- a/e2e/binance-orderbook/helpers/live-order-scale-config.js +++ b/e2e/binance-orderbook/helpers/live-order-scale-config.js @@ -6,7 +6,7 @@ const PROFILE_FIELDS = [ 'scaleRatios', ]; const LIVE_CONTEXT_FIELDS = [ - 'availableBalance', + 'testBudget', 'currentLeverage', 'perOrderPrice', 'perOrderQuantity', @@ -65,16 +65,16 @@ function multiplyDecimalStrings(left, right, path) { } function floorAffordableOrders({ - availableBalance, + testBudget, currentLeverage, safetyFactor, perOrderNotional, }) { - const balance = parsePositiveDecimal(availableBalance, 'liveContext.availableBalance'); + const budget = parsePositiveDecimal(testBudget, 'liveContext.testBudget'); const safety = parsePositiveDecimal(safetyFactor, 'liveContext.safetyFactor'); const notional = parsePositiveDecimal(perOrderNotional, 'capacity.perOrderNotional'); - const numerator = balance.numerator * BigInt(currentLeverage) * safety.numerator * notional.scale; - const denominator = balance.scale * safety.scale * notional.numerator; + const numerator = budget.numerator * BigInt(currentLeverage) * safety.numerator * notional.scale; + const denominator = budget.scale * safety.scale * notional.numerator; return Number(numerator / denominator); } @@ -105,7 +105,7 @@ export function validateLiveOrderScaleProfile(profile) { function validateLiveContext(context) { assertRecord(context, 'liveContext'); assertExactKeys(context, LIVE_CONTEXT_FIELDS, 'liveContext'); - for (const field of ['availableBalance', 'perOrderPrice', 'perOrderQuantity', 'safetyFactor']) { + for (const field of ['testBudget', 'perOrderPrice', 'perOrderQuantity', 'safetyFactor']) { parsePositiveDecimal(context[field], `liveContext.${field}`); } const safety = parsePositiveDecimal(context.safetyFactor, 'liveContext.safetyFactor'); @@ -133,6 +133,11 @@ function deriveScaleCount(capacity, ratio, minimum) { return Math.max(minimum, Math.round(capacity * ratio)); } +/** + * Builds serializable capacity evidence from a caller-allocated test budget. + * The caller must keep the budget within the verified live balance so persisted + * evidence proves capacity without disclosing the account's exact balance. + */ export function createLiveOrderCapacityEvidence(liveContext) { validateLiveContext(liveContext); const perOrderNotional = multiplyDecimalStrings( diff --git a/e2e/binance-orderbook/helpers/live-performance-probe.js b/e2e/binance-orderbook/helpers/live-performance-probe.js index 110e181..9ab3869 100644 --- a/e2e/binance-orderbook/helpers/live-performance-probe.js +++ b/e2e/binance-orderbook/helpers/live-performance-probe.js @@ -228,6 +228,13 @@ export function installBinanceLivePerformanceProbe(options = {}) { .reduce((innermost, dialog) => ( innermost?.contains(dialog) ? dialog : (innermost || dialog) ), null); + const readDialogContaining = (button) => Array.from(document.querySelectorAll(dialogSelector)) + .filter(isVisible) + .filter((dialog) => dialog.contains(button)) + .filter((dialog) => matchesDialogText(normalizeText(dialog.textContent))) + .reduce((innermost, dialog) => ( + innermost?.contains(dialog) ? dialog : (innermost || dialog) + ), null); const readSemanticState = () => { const panel = readPanel(); const cancelButton = readCancelButton(panel); @@ -447,8 +454,8 @@ export function installBinanceLivePerformanceProbe(options = {}) { return; } if (run.startedAtMonotonicMs === null) return; - const dialog = button.closest(dialogSelector); - if (isVisible(dialog) && matchesDialogText(normalizeText(dialog.textContent))) { + const dialog = readDialogContaining(button); + if (dialog) { appendEvent('dialog-action', { text: normalizeText(button.textContent), primary: button.matches('button.bn-button.bn-button__primary'), diff --git a/e2e/binance-orderbook/live-baselines/dialog-smoke-2026-08-27.capture.json b/e2e/binance-orderbook/live-baselines/dialog-smoke-2026-08-27.capture.json new file mode 100644 index 0000000..8c6f393 --- /dev/null +++ b/e2e/binance-orderbook/live-baselines/dialog-smoke-2026-08-27.capture.json @@ -0,0 +1,282 @@ +{ + "schemaVersion": 1, + "capturedAt": "2026-08-27T00:15:44.472Z", + "environment": { + "browser": "Chrome 151.0.7922.174", + "os": "macOS (MacIntel)", + "route": "https://www.binance.com/zh-CN/futures/HYPEUSDT", + "symbol": "HYPEUSDT", + "userscriptSha256": "f4bb60af07044e21f684d1978535795363ade1c240dde0de6b904767cfa87a18", + "userscriptVersion": "2.7.126" + }, + "scenarios": [ + { + "name": "cancel-dialog-cancel", + "parameters": { + "kind": "dialog-cancel", + "testOrderCount": 1 + }, + "applicableSegments": [ + "clickToFirstFeedback", + "clickToDialog", + "decisionToFinalReady" + ], + "samples": [ + { + "capacityEvidence": { + "currentLeverage": 2, + "perOrderPrice": "94", + "perOrderQuantity": "0.06", + "safetyFactor": "0.8", + "liveMaxNumOrdersLimit": 200, + "existingCurrentSymbolOpenOrders": 0, + "outstandingTestOwnedOrders": 0, + "perOrderNotional": "5.64", + "maxNewOrdersBySlots": 199, + "maxNewOrdersByMargin": 1, + "testBudget": "5.64" + }, + "segmentsMs": { + "clickToFirstFeedback": 24.800000190734863, + "clickToDialog": 81.40000009536743, + "decisionToFinalReady": 55.59999990463257 + }, + "testOrderLedger": { + "created": [ + { + "symbol": "HYPEUSDT", + "side": "SELL", + "positionSide": "SHORT", + "price": "94", + "quantity": "0.06", + "createdAt": "2026-08-26T23:53:32.871Z" + } + ], + "fills": [], + "residual": [] + }, + "stateRestored": true, + "noFills": true, + "residualTestOrders": 0, + "uncaughtErrors": 0, + "maxLongTaskMs": 0, + "maxLongAnimationFrameMs": 220.79999995231628 + }, + { + "capacityEvidence": { + "currentLeverage": 2, + "perOrderPrice": "94", + "perOrderQuantity": "0.06", + "safetyFactor": "0.8", + "liveMaxNumOrdersLimit": 200, + "existingCurrentSymbolOpenOrders": 0, + "outstandingTestOwnedOrders": 0, + "perOrderNotional": "5.64", + "maxNewOrdersBySlots": 199, + "maxNewOrdersByMargin": 1, + "testBudget": "5.64" + }, + "segmentsMs": { + "clickToFirstFeedback": 19.200000047683716, + "clickToDialog": 66.29999995231628, + "decisionToFinalReady": 43.60000014305115 + }, + "testOrderLedger": { + "created": [ + { + "symbol": "HYPEUSDT", + "side": "SELL", + "positionSide": "SHORT", + "price": "94", + "quantity": "0.06", + "createdAt": "2026-08-26T23:53:32.871Z" + } + ], + "fills": [], + "residual": [] + }, + "stateRestored": true, + "noFills": true, + "residualTestOrders": 0, + "uncaughtErrors": 0, + "maxLongTaskMs": 0, + "maxLongAnimationFrameMs": 0 + }, + { + "capacityEvidence": { + "currentLeverage": 2, + "perOrderPrice": "94", + "perOrderQuantity": "0.06", + "safetyFactor": "0.8", + "liveMaxNumOrdersLimit": 200, + "existingCurrentSymbolOpenOrders": 0, + "outstandingTestOwnedOrders": 0, + "perOrderNotional": "5.64", + "maxNewOrdersBySlots": 199, + "maxNewOrdersByMargin": 1, + "testBudget": "5.64" + }, + "segmentsMs": { + "clickToFirstFeedback": 15.399999856948853, + "clickToDialog": 44.29999995231629, + "decisionToFinalReady": 39.5 + }, + "testOrderLedger": { + "created": [ + { + "symbol": "HYPEUSDT", + "side": "SELL", + "positionSide": "SHORT", + "price": "94", + "quantity": "0.06", + "createdAt": "2026-08-26T23:53:32.871Z" + } + ], + "fills": [], + "residual": [] + }, + "stateRestored": true, + "noFills": true, + "residualTestOrders": 0, + "uncaughtErrors": 0, + "maxLongTaskMs": 0, + "maxLongAnimationFrameMs": 0 + } + ] + }, + { + "name": "cancel-dialog-confirm", + "parameters": { + "kind": "dialog-confirm", + "testOrderCount": 1 + }, + "applicableSegments": [ + "clickToFirstFeedback", + "clickToDialog", + "decisionToFinalReady" + ], + "samples": [ + { + "capacityEvidence": { + "currentLeverage": 2, + "perOrderPrice": "94", + "perOrderQuantity": "0.06", + "safetyFactor": "0.8", + "liveMaxNumOrdersLimit": 200, + "existingCurrentSymbolOpenOrders": 0, + "outstandingTestOwnedOrders": 0, + "perOrderNotional": "5.64", + "maxNewOrdersBySlots": 199, + "maxNewOrdersByMargin": 1, + "testBudget": "5.64" + }, + "segmentsMs": { + "clickToFirstFeedback": 19.399999856948853, + "clickToDialog": 85, + "decisionToFinalReady": 1285.7000000476837 + }, + "testOrderLedger": { + "created": [ + { + "symbol": "HYPEUSDT", + "side": "SELL", + "positionSide": "SHORT", + "price": "94", + "quantity": "0.06", + "createdAt": "2026-08-26T23:53:32.871Z" + } + ], + "fills": [], + "residual": [] + }, + "stateRestored": true, + "noFills": true, + "residualTestOrders": 0, + "uncaughtErrors": 0, + "maxLongTaskMs": 59, + "maxLongAnimationFrameMs": 94 + }, + { + "capacityEvidence": { + "currentLeverage": 2, + "perOrderPrice": "94", + "perOrderQuantity": "0.06", + "safetyFactor": "0.8", + "liveMaxNumOrdersLimit": 200, + "existingCurrentSymbolOpenOrders": 0, + "outstandingTestOwnedOrders": 0, + "perOrderNotional": "5.64", + "maxNewOrdersBySlots": 199, + "maxNewOrdersByMargin": 1, + "testBudget": "5.64" + }, + "segmentsMs": { + "clickToFirstFeedback": 22.40000009536743, + "clickToDialog": 75.40000009536743, + "decisionToFinalReady": 1265.5000000000002 + }, + "testOrderLedger": { + "created": [ + { + "symbol": "HYPEUSDT", + "side": "SELL", + "positionSide": "SHORT", + "price": "94", + "quantity": "0.06", + "createdAt": "2026-08-27T00:22:11.178Z" + } + ], + "fills": [], + "residual": [] + }, + "stateRestored": true, + "noFills": true, + "residualTestOrders": 0, + "uncaughtErrors": 0, + "maxLongTaskMs": 173, + "maxLongAnimationFrameMs": 229.1999998092651 + }, + { + "capacityEvidence": { + "currentLeverage": 2, + "perOrderPrice": "94", + "perOrderQuantity": "0.06", + "safetyFactor": "0.8", + "liveMaxNumOrdersLimit": 200, + "existingCurrentSymbolOpenOrders": 0, + "outstandingTestOwnedOrders": 0, + "perOrderNotional": "5.64", + "maxNewOrdersBySlots": 199, + "maxNewOrdersByMargin": 1, + "testBudget": "5.64" + }, + "segmentsMs": { + "clickToFirstFeedback": 19.700000047683716, + "clickToDialog": 290.7000000476837, + "decisionToFinalReady": 1368.5999999046326 + }, + "testOrderLedger": { + "created": [ + { + "symbol": "HYPEUSDT", + "side": "SELL", + "positionSide": "SHORT", + "price": "94", + "quantity": "0.06", + "createdAt": "2026-08-27T00:24:39.256Z" + } + ], + "fills": [], + "residual": [] + }, + "stateRestored": true, + "noFills": true, + "residualTestOrders": 0, + "uncaughtErrors": 0, + "maxLongTaskMs": 201, + "maxLongAnimationFrameMs": 270.89999985694885 + } + ] + } + ] +} diff --git a/e2e/binance-orderbook/live-baselines/dialog-smoke.baseline.json b/e2e/binance-orderbook/live-baselines/dialog-smoke.baseline.json new file mode 100644 index 0000000..5cb0a33 --- /dev/null +++ b/e2e/binance-orderbook/live-baselines/dialog-smoke.baseline.json @@ -0,0 +1,99 @@ +{ + "schemaVersion": 1, + "capturedAt": "2026-08-27T00:15:44.472Z", + "environment": { + "browser": "Chrome 151.0.7922.174", + "os": "macOS (MacIntel)", + "route": "https://www.binance.com/zh-CN/futures/HYPEUSDT", + "symbol": "HYPEUSDT", + "userscriptSha256": "f4bb60af07044e21f684d1978535795363ade1c240dde0de6b904767cfa87a18", + "userscriptVersion": "2.7.126" + }, + "scenarios": [ + { + "name": "cancel-dialog-cancel", + "parameters": { + "kind": "dialog-cancel", + "testOrderCount": 1 + }, + "sampleCount": 3, + "segmentsMs": { + "clickToFirstFeedback": { + "min": 15.399999856948853, + "median": 19.200000047683716, + "p95": 24.800000190734863, + "max": 24.800000190734863 + }, + "clickToDialog": { + "min": 44.29999995231629, + "median": 66.29999995231628, + "p95": 81.40000009536743, + "max": 81.40000009536743 + }, + "decisionToFinalReady": { + "min": 39.5, + "median": 43.60000014305115, + "p95": 55.59999990463257, + "max": 55.59999990463257 + } + }, + "maxLongTaskMs": { + "min": 0, + "median": 0, + "p95": 0, + "max": 0 + }, + "maxLongAnimationFrameMs": { + "min": 0, + "median": 0, + "p95": 220.79999995231628, + "max": 220.79999995231628 + } + }, + { + "name": "cancel-dialog-confirm", + "parameters": { + "kind": "dialog-confirm", + "testOrderCount": 1 + }, + "sampleCount": 3, + "segmentsMs": { + "clickToFirstFeedback": { + "min": 19.399999856948853, + "median": 19.700000047683716, + "p95": 22.40000009536743, + "max": 22.40000009536743 + }, + "clickToDialog": { + "min": 75.40000009536743, + "median": 85, + "p95": 290.7000000476837, + "max": 290.7000000476837 + }, + "decisionToFinalReady": { + "min": 1265.5000000000002, + "median": 1285.7000000476837, + "p95": 1368.5999999046326, + "max": 1368.5999999046326 + } + }, + "maxLongTaskMs": { + "min": 59, + "median": 173, + "p95": 201, + "max": 201 + }, + "maxLongAnimationFrameMs": { + "min": 94, + "median": 229.1999998092651, + "p95": 270.89999985694885, + "max": 270.89999985694885 + } + } + ], + "comparisonPolicy": { + "absoluteToleranceMs": 100, + "medianRatio": 1.5, + "p95Ratio": 1.5 + } +} diff --git a/e2e/binance-orderbook/specs/live-performance-probe.pw.js b/e2e/binance-orderbook/specs/live-performance-probe.pw.js index a2f61c8..ed20e62 100644 --- a/e2e/binance-orderbook/specs/live-performance-probe.pw.js +++ b/e2e/binance-orderbook/specs/live-performance-probe.pw.js @@ -69,6 +69,10 @@ test('live probe has no user-decision deadline and follows a replaced portal dia expect(waiting.finishedAtMonotonicMs).toBeNull(); expect(waiting.events.map((event) => event.kind)).toContain('dialog-visible'); + await page.getByRole('button', { name: '取消' }).evaluate((button) => { + button.parentElement.classList.add('bn-modal-footer'); + }); + await page.getByRole('button', { name: '取消' }).click(); const snapshot = await finishLivePerformanceProbeWhenReady(page); await expect(page.getByText('HYPEUSDT 已取消撤单,已恢复页面状态')).toBeVisible(); diff --git a/test/unit/binance-live-order-scale-config.test.js b/test/unit/binance-live-order-scale-config.test.js index 37a1383..cd83350 100644 --- a/test/unit/binance-live-order-scale-config.test.js +++ b/test/unit/binance-live-order-scale-config.test.js @@ -15,7 +15,7 @@ const profile = JSON.parse(await readFile( function liveContext(overrides = {}) { return { - availableBalance: '13.28', + testBudget: '13.28', currentLeverage: 5, perOrderPrice: '2', perOrderQuantity: '2.5', @@ -44,7 +44,7 @@ test('smoke profile derives three internal scales from live capacity', () => { test('scale counts are configuration-driven rather than fixed to smoke values', () => { const plan = createLiveOrderScalePlan({ ...profile, maxOrderCount: 40 }, liveContext({ - availableBalance: '100', + testBudget: '100', })); assert.deepEqual(plan.scales.map((scale) => scale.effectiveTargetOrderCount), [10, 20, 40]); @@ -52,7 +52,7 @@ test('scale counts are configuration-driven rather than fixed to smoke values', test('insufficient live capacity fails instead of silently collapsing scale labels', () => { assert.throws( - () => createLiveOrderScalePlan(profile, liveContext({ availableBalance: '0.4' })), + () => createLiveOrderScalePlan(profile, liveContext({ testBudget: '0.4' })), /cannot form three distinct scales/, ); }); @@ -68,7 +68,7 @@ test('capacity evidence validates actual notional and slot arithmetic', () => { test('one-order smoke capacity is valid without requiring three scale levels', () => { const evidence = createLiveOrderCapacityEvidence(liveContext({ - availableBalance: '1.25', + testBudget: '1.25', currentLeverage: 1, perOrderPrice: '1', perOrderQuantity: '1', @@ -78,7 +78,7 @@ test('one-order smoke capacity is valid without requiring three scale levels', ( assert.equal(validateLiveOrderCapacityEvidence(evidence), evidence); assert.throws( () => createLiveOrderScalePlan(profile, liveContext({ - availableBalance: '1.25', + testBudget: '1.25', currentLeverage: 1, perOrderPrice: '1', perOrderQuantity: '1', diff --git a/test/unit/binance-live-performance.test.js b/test/unit/binance-live-performance.test.js index 6b0fa0b..7c9843a 100644 --- a/test/unit/binance-live-performance.test.js +++ b/test/unit/binance-live-performance.test.js @@ -31,7 +31,7 @@ function testOrder(index = 0) { function singleOrderCapacityEvidence() { return createLiveOrderCapacityEvidence({ - availableBalance: '13.28', + testBudget: '13.28', currentLeverage: 5, perOrderPrice: '2', perOrderQuantity: '2.5', @@ -90,7 +90,7 @@ function scaleSample(index) { maxOrderCount: 3, scaleRatios: { small: 0.25, medium: 0.5, large: 1 }, }, { - availableBalance: '13.28', + testBudget: '13.28', currentLeverage: 5, perOrderPrice: '2', perOrderQuantity: '2.5',