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
13 changes: 13 additions & 0 deletions docs/binance-orderbook-trade-ui-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,19 @@ isolated samples. Every sample must prove restored UI state, no fills, zero resi
test-owned orders, zero uncaught errors, and bounded long-task observations. Missing
segments are invalid data, not zero-duration work.

Live capture parameters use explicit scenario kinds:

- `no-orders`: requires no capacity evidence and an empty test-order ledger;
- `dialog-cancel`: declares a positive `testOrderCount`, proves capacity for those
test-owned orders, cancels the native dialog, and cleans up those orders afterward;
- `dialog-confirm`: declares a positive `testOrderCount`, proves capacity, confirms
native cancellation, and observes zero residual test-owned orders; and
- `order-scale`: retains the named small/medium/large scale contract.

Do not label a dialog run as `no-orders`. One-order dialog smoke uses standalone
capacity evidence and does not require enough balance or order slots to construct
three distinct scale levels. The three-level minimum applies only to `order-scale`.

Order-scale scenarios also persist the internal profile name, semantic scale label,
preferred and effective target counts, and sample count. Every sample persists its
live capacity evidence. Capacity shortfalls must skip or abort the scale explicitly;
Expand Down
36 changes: 17 additions & 19 deletions e2e/binance-orderbook/helpers/live-order-scale-config.js
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,7 @@ function deriveScaleCount(capacity, ratio, minimum) {
return Math.max(minimum, Math.round(capacity * ratio));
}

export function createLiveOrderScalePlan(profile, liveContext) {
validateLiveOrderScaleProfile(profile);
export function createLiveOrderCapacityEvidence(liveContext) {
validateLiveContext(liveContext);
const perOrderNotional = multiplyDecimalStrings(
liveContext.perOrderPrice,
Expand All @@ -151,10 +150,21 @@ export function createLiveOrderScalePlan(profile, liveContext) {
...liveContext,
perOrderNotional,
});
const effectiveCapacity = Math.min(
profile.maxOrderCount,
return Object.freeze({
...liveContext,
perOrderNotional,
maxNewOrdersBySlots,
maxNewOrdersByMargin,
});
}

export function createLiveOrderScalePlan(profile, liveContext) {
validateLiveOrderScaleProfile(profile);
const capacityEvidence = createLiveOrderCapacityEvidence(liveContext);
const effectiveCapacity = Math.min(
profile.maxOrderCount,
capacityEvidence.maxNewOrdersBySlots,
capacityEvidence.maxNewOrdersByMargin,
);
if (effectiveCapacity < 3) {
throw new Error(`Live order capacity ${effectiveCapacity} cannot form three distinct scales`);
Expand All @@ -170,12 +180,6 @@ export function createLiveOrderScalePlan(profile, liveContext) {
throw new Error(`Scale ratios collapse at effective capacity ${effectiveCapacity}`);
}

const capacityEvidence = Object.freeze({
...liveContext,
perOrderNotional,
maxNewOrdersBySlots,
maxNewOrdersByMargin,
});
return Object.freeze({
profileName: profile.profileName,
sampleCount: profile.sampleCount,
Expand All @@ -197,18 +201,12 @@ export function validateLiveOrderCapacityEvidence(evidence, path = 'capacityEvid
assertRecord(evidence, path);
assertExactKeys(evidence, CAPACITY_EVIDENCE_FIELDS, path);
const context = Object.fromEntries(LIVE_CONTEXT_FIELDS.map((field) => [field, evidence[field]]));
const plan = createLiveOrderScalePlan({
schemaVersion: 1,
profileName: 'evidence-validation',
sampleCount: 3,
maxOrderCount: 3,
scaleRatios: { small: 0.25, medium: 0.5, large: 1 },
}, context);
if (evidence.perOrderNotional !== plan.capacityEvidence.perOrderNotional) {
const expected = createLiveOrderCapacityEvidence(context);
if (evidence.perOrderNotional !== expected.perOrderNotional) {
throw new Error(`${path}.perOrderNotional does not match price x quantity`);
}
for (const field of ['maxNewOrdersBySlots', 'maxNewOrdersByMargin']) {
if (evidence[field] !== plan.capacityEvidence[field]) {
if (evidence[field] !== expected[field]) {
throw new Error(`${path}.${field} does not match its inputs`);
}
}
Expand Down
23 changes: 18 additions & 5 deletions scripts/binance-live-performance.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -113,8 +113,15 @@ function validateScenarioParameters(parameters, path) {
assertExactKeys(parameters, ['kind'], path);
return parameters;
}
if (['dialog-cancel', 'dialog-confirm'].includes(parameters.kind)) {
assertExactKeys(parameters, ['kind', 'testOrderCount'], path);
if (!Number.isInteger(parameters.testOrderCount) || parameters.testOrderCount <= 0) {
throw new Error(`${path}.testOrderCount must be a positive integer`);
}
return parameters;
}
if (parameters.kind !== 'order-scale') {
throw new Error(`${path}.kind must equal no-orders or order-scale`);
throw new Error(`${path}.kind must equal no-orders, dialog-cancel, dialog-confirm, or order-scale`);
}
assertExactKeys(parameters, [
'kind',
Expand Down Expand Up @@ -153,14 +160,20 @@ function validateCapacityEvidenceForScenario(evidence, parameters, ledger, path)
return;
}
validateLiveOrderCapacityEvidence(evidence, path);
if (parameters.effectiveTargetOrderCount > evidence.maxNewOrdersBySlots) {
const expectedOrderCount = parameters.kind === 'order-scale'
? parameters.effectiveTargetOrderCount
: parameters.testOrderCount;
if (expectedOrderCount > evidence.maxNewOrdersBySlots) {
throw new Error(`${path} has insufficient order slots for the effective target`);
}
if (parameters.effectiveTargetOrderCount > evidence.maxNewOrdersByMargin) {
if (expectedOrderCount > evidence.maxNewOrdersByMargin) {
throw new Error(`${path} has insufficient margin capacity for the effective target`);
}
if (ledger.created.length !== parameters.effectiveTargetOrderCount) {
throw new Error(`${path} created ledger count must match the effective target`);
if (ledger.created.length !== expectedOrderCount) {
const label = parameters.kind === 'order-scale'
? 'the effective target'
: 'the declared test order count';
throw new Error(`${path} created ledger count must match ${label}`);
}
}

Expand Down
22 changes: 22 additions & 0 deletions test/unit/binance-live-order-scale-config.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises';
import test from 'node:test';

import {
createLiveOrderCapacityEvidence,
createLiveOrderScalePlan,
validateLiveOrderCapacityEvidence,
validateLiveOrderScaleProfile,
Expand Down Expand Up @@ -65,6 +66,27 @@ 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',
currentLeverage: 1,
perOrderPrice: '1',
perOrderQuantity: '1',
}));

assert.equal(evidence.maxNewOrdersByMargin, 1);
assert.equal(validateLiveOrderCapacityEvidence(evidence), evidence);
assert.throws(
() => createLiveOrderScalePlan(profile, liveContext({
availableBalance: '1.25',
currentLeverage: 1,
perOrderPrice: '1',
perOrderQuantity: '1',
})),
/cannot form three distinct scales/,
);
});

test('live order slots subtract existing and outstanding test-owned orders', () => {
const plan = createLiveOrderScalePlan(profile, liveContext({
liveMaxNumOrdersLimit: 8,
Expand Down
84 changes: 71 additions & 13 deletions test/unit/binance-live-performance.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,46 @@ import {
validateLivePerformanceBaseline,
validateLivePerformanceCapture,
} from '../../scripts/binance-live-performance.mjs';
import { createLiveOrderScalePlan } from '../../e2e/binance-orderbook/helpers/live-order-scale-config.js';
import {
createLiveOrderCapacityEvidence,
createLiveOrderScalePlan,
} from '../../e2e/binance-orderbook/helpers/live-order-scale-config.js';

const EMPTY_ORDER_LEDGER = Object.freeze({ created: [], fills: [], residual: [] });

function testOrder(index = 0) {
return {
symbol: 'XRPUSDT',
side: 'SELL',
positionSide: 'SHORT',
price: '2',
quantity: '2.5',
createdAt: `2026-08-26T08:00:0${index + 1}.000Z`,
};
}

function singleOrderCapacityEvidence() {
return createLiveOrderCapacityEvidence({
availableBalance: '13.28',
currentLeverage: 5,
perOrderPrice: '2',
perOrderQuantity: '2.5',
safetyFactor: '0.8',
liveMaxNumOrdersLimit: 200,
existingCurrentSymbolOpenOrders: 0,
outstandingTestOwnedOrders: 0,
});
}

function sample(clickToFeedback, dialog, finalReady, maxLongTaskMs = 0) {
return {
capacityEvidence: null,
capacityEvidence: singleOrderCapacityEvidence(),
segmentsMs: {
clickToFirstFeedback: clickToFeedback,
clickToDialog: dialog,
decisionToFinalReady: finalReady,
},
testOrderLedger: structuredClone(EMPTY_ORDER_LEDGER),
testOrderLedger: { created: [testOrder()], fills: [], residual: [] },
stateRestored: true,
noFills: true,
residualTestOrders: 0,
Expand All @@ -47,7 +74,7 @@ function capture(samples = [sample(90, 150, 140), sample(100, 160, 150), sample(
},
scenarios: [{
name: 'cancel-dialog-cancel',
parameters: { kind: 'no-orders' },
parameters: { kind: 'dialog-cancel', testOrderCount: 1 },
applicableSegments: ['clickToFirstFeedback', 'clickToDialog', 'decisionToFinalReady'],
samples,
}],
Expand All @@ -72,14 +99,7 @@ function scaleSample(index) {
existingCurrentSymbolOpenOrders: 0,
outstandingTestOwnedOrders: 0,
}).capacityEvidence;
result.testOrderLedger.created.push({
symbol: 'XRPUSDT',
side: 'SELL',
positionSide: 'SHORT',
price: '2',
quantity: '2.5',
createdAt: `2026-08-26T08:00:0${index + 1}.000Z`,
});
result.testOrderLedger.created = [testOrder(index)];
return result;
}

Expand All @@ -104,6 +124,44 @@ test('live performance capture requires three complete isolated samples', () =>
);
});

test('live performance capture models no-orders, dialog-cancel, and dialog-confirm explicitly', () => {
const dialogCancel = capture();
assert.equal(validateLivePerformanceCapture(dialogCancel).scenarios[0].parameters.kind, 'dialog-cancel');

const dialogConfirm = structuredClone(dialogCancel);
dialogConfirm.scenarios[0].name = 'cancel-dialog-confirm';
dialogConfirm.scenarios[0].parameters.kind = 'dialog-confirm';
assert.equal(validateLivePerformanceCapture(dialogConfirm).scenarios[0].parameters.kind, 'dialog-confirm');

const noOrders = structuredClone(dialogCancel);
noOrders.scenarios[0].name = 'cancel-current-symbol-no-orders';
noOrders.scenarios[0].parameters = { kind: 'no-orders' };
for (const entry of noOrders.scenarios[0].samples) {
entry.capacityEvidence = null;
entry.testOrderLedger = structuredClone(EMPTY_ORDER_LEDGER);
}
assert.equal(validateLivePerformanceCapture(noOrders).scenarios[0].parameters.kind, 'no-orders');
});

test('dialog captures bind the declared test-order count to capacity and ledger evidence', () => {
const missingOrder = capture();
missingOrder.scenarios[0].samples[0].testOrderLedger.created = [];
assert.throws(
() => validateLivePerformanceCapture(missingOrder),
/created ledger count must match the declared test order count/,
);

const insufficientCapacity = capture();
insufficientCapacity.scenarios[0].samples[0].capacityEvidence = {
...insufficientCapacity.scenarios[0].samples[0].capacityEvidence,
maxNewOrdersByMargin: 0,
};
assert.throws(
() => validateLivePerformanceCapture(insufficientCapacity),
/does not match its inputs|insufficient margin capacity/,
);
});

test('live performance capture rejects missing segments and failed cleanup invariants', () => {
const missingSegment = capture();
delete missingSegment.scenarios[0].samples[0].segmentsMs.clickToDialog;
Expand Down Expand Up @@ -224,7 +282,7 @@ test('baseline comparison rejects the same scale name with different effective c
scenario: 'cancel-dialog-cancel',
metric: null,
reason: 'configuration-mismatch',
baseline: { kind: 'no-orders' },
baseline: { kind: 'dialog-cancel', testOrderCount: 1 },
current: current.scenarios[0].parameters,
}],
);
Expand Down
Loading