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
16 changes: 14 additions & 2 deletions docs/binance-orderbook-trade-ui-automation.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,8 +195,14 @@ used as a reason to retain event buffers or persistent probes.

- Hard reload the logged-in page and verify the loaded source through raw CDP.
- Run no-order, dialog-cancel, and explicitly authorized dialog-confirm paths.
- Run 1/20/40/120 order-scale performance samples without exceeding the current live
exchange limit minus one.
- Start live financial smoke coverage with one far-from-market order and one cleanup
cycle. Expand to multiple order scales only when the diagnostic goal explicitly
requires scale evidence and the live account has sufficient capacity.
- Run configurable small/medium/large order-scale performance samples. Derive the
effective counts from the internal test profile, exact rounded order notional,
available balance, current leverage, current-symbol order count, outstanding
test-owned count, and the live exchange limit minus one. These scale counts are
test-run inputs, not userscript UI settings or permanent product defaults.
- Verify no fills, no residual test-owned orders, and restored page state after each run.

Store every live run as a strict capture rather than copying timing numbers from chat:
Expand Down Expand Up @@ -224,6 +230,12 @@ 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.

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;
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.

`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
Expand Down
216 changes: 216 additions & 0 deletions e2e/binance-orderbook/helpers/live-order-scale-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
const PROFILE_FIELDS = [
'schemaVersion',
'profileName',
'sampleCount',
'maxOrderCount',
'scaleRatios',
];
const LIVE_CONTEXT_FIELDS = [
'availableBalance',
'currentLeverage',
'perOrderPrice',
'perOrderQuantity',
'safetyFactor',
'liveMaxNumOrdersLimit',
'existingCurrentSymbolOpenOrders',
'outstandingTestOwnedOrders',
];
export const CAPACITY_EVIDENCE_FIELDS = Object.freeze([
...LIVE_CONTEXT_FIELDS,
'perOrderNotional',
'maxNewOrdersBySlots',
'maxNewOrdersByMargin',
]);

function assertRecord(value, path) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
throw new Error(`${path} must be an object`);
}
}

function assertExactKeys(record, expectedKeys, path) {
const actual = Object.keys(record).sort();
const expected = [...expectedKeys].sort();
if (JSON.stringify(actual) !== JSON.stringify(expected)) {
throw new Error(`${path} keys must be exactly: ${expected.join(', ')}`);
}
}

function parsePositiveDecimal(value, path) {
if (typeof value !== 'string' || !/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(value)) {
throw new Error(`${path} must be an unsigned decimal string`);
}
const [whole, fraction = ''] = value.split('.');
const numerator = BigInt(`${whole}${fraction}`);
if (numerator <= 0n) throw new Error(`${path} must be greater than zero`);
return { numerator, scale: 10n ** BigInt(fraction.length) };
}

function normalizeFraction(numerator, scale) {
let value = numerator;
let divisor = scale;
while (divisor > 1n && value % 10n === 0n) {
value /= 10n;
divisor /= 10n;
}
if (divisor === 1n) return value.toString();
const digits = divisor.toString().length - 1;
return `${value / divisor}.${(value % divisor).toString().padStart(digits, '0')}`;
}

function multiplyDecimalStrings(left, right, path) {
const a = parsePositiveDecimal(left, `${path}.left`);
const b = parsePositiveDecimal(right, `${path}.right`);
return normalizeFraction(a.numerator * b.numerator, a.scale * b.scale);
}

function floorAffordableOrders({
availableBalance,
currentLeverage,
safetyFactor,
perOrderNotional,
}) {
const balance = parsePositiveDecimal(availableBalance, 'liveContext.availableBalance');
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;
return Number(numerator / denominator);
}

export function validateLiveOrderScaleProfile(profile) {
assertRecord(profile, 'profile');
assertExactKeys(profile, PROFILE_FIELDS, 'profile');
if (profile.schemaVersion !== 1) throw new Error('profile.schemaVersion must equal 1');
if (typeof profile.profileName !== 'string' || profile.profileName.length === 0) {
throw new Error('profile.profileName must be a non-empty string');
}
for (const field of ['sampleCount', 'maxOrderCount']) {
if (!Number.isInteger(profile[field]) || profile[field] < 3) {
throw new Error(`profile.${field} must be an integer of at least 3`);
}
}
assertRecord(profile.scaleRatios, 'profile.scaleRatios');
assertExactKeys(profile.scaleRatios, ['small', 'medium', 'large'], 'profile.scaleRatios');
const ratios = ['small', 'medium', 'large'].map((label) => profile.scaleRatios[label]);
if (!ratios.every((ratio) => Number.isFinite(ratio) && ratio > 0 && ratio <= 1)) {
throw new Error('profile.scaleRatios must contain ratios in (0, 1]');
}
if (!(ratios[0] < ratios[1] && ratios[1] < ratios[2] && ratios[2] === 1)) {
throw new Error('profile.scaleRatios must be strictly increasing and large must equal 1');
}
return profile;
}

function validateLiveContext(context) {
assertRecord(context, 'liveContext');
assertExactKeys(context, LIVE_CONTEXT_FIELDS, 'liveContext');
for (const field of ['availableBalance', 'perOrderPrice', 'perOrderQuantity', 'safetyFactor']) {
parsePositiveDecimal(context[field], `liveContext.${field}`);
}
const safety = parsePositiveDecimal(context.safetyFactor, 'liveContext.safetyFactor');
if (safety.numerator > safety.scale) {
throw new Error('liveContext.safetyFactor must not exceed 1');
}
for (const field of [
'currentLeverage',
'liveMaxNumOrdersLimit',
'existingCurrentSymbolOpenOrders',
'outstandingTestOwnedOrders',
]) {
if (!Number.isInteger(context[field]) || context[field] < 0) {
throw new Error(`liveContext.${field} must be a non-negative integer`);
}
}
if (context.currentLeverage === 0) throw new Error('liveContext.currentLeverage must be positive');
if (context.liveMaxNumOrdersLimit < 2) {
throw new Error('liveContext.liveMaxNumOrdersLimit must be at least 2');
}
return context;
}

function deriveScaleCount(capacity, ratio, minimum) {
return Math.max(minimum, Math.round(capacity * ratio));
}

export function createLiveOrderScalePlan(profile, liveContext) {
validateLiveOrderScaleProfile(profile);
validateLiveContext(liveContext);
const perOrderNotional = multiplyDecimalStrings(
liveContext.perOrderPrice,
liveContext.perOrderQuantity,
'capacity.perOrderNotional',
);
const maxNewOrdersBySlots = Math.max(
0,
Math.min(liveContext.liveMaxNumOrdersLimit - 1, 199)
- liveContext.existingCurrentSymbolOpenOrders
- liveContext.outstandingTestOwnedOrders,
);
const maxNewOrdersByMargin = floorAffordableOrders({
...liveContext,
perOrderNotional,
});
const effectiveCapacity = Math.min(
profile.maxOrderCount,
maxNewOrdersBySlots,
maxNewOrdersByMargin,
);
if (effectiveCapacity < 3) {
throw new Error(`Live order capacity ${effectiveCapacity} cannot form three distinct scales`);
}

const labels = ['small', 'medium', 'large'];
const counts = labels.map((label, index) => deriveScaleCount(
effectiveCapacity,
profile.scaleRatios[label],
index + 1,
));
if (new Set(counts).size !== counts.length || counts[2] !== effectiveCapacity) {
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,
effectiveCapacity,
capacityEvidence,
scales: labels.map((label, index) => Object.freeze({
label,
preferredTargetOrderCount: deriveScaleCount(
profile.maxOrderCount,
profile.scaleRatios[label],
index + 1,
),
effectiveTargetOrderCount: counts[index],
})),
});
}

export function validateLiveOrderCapacityEvidence(evidence, path = 'capacityEvidence') {
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) {
throw new Error(`${path}.perOrderNotional does not match price x quantity`);
}
for (const field of ['maxNewOrdersBySlots', 'maxNewOrdersByMargin']) {
if (evidence[field] !== plan.capacityEvidence[field]) {
throw new Error(`${path}.${field} does not match its inputs`);
}
}
return evidence;
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,16 @@
"scenarios": [
{
"name": "cancel-current-symbol-no-orders",
"parameters": {
"kind": "no-orders"
},
"applicableSegments": [
"clickToFirstFeedback",
"clickToFinalReady"
],
"samples": [
{
"capacityEvidence": null,
"segmentsMs": {
"clickToFirstFeedback": 30.6,
"clickToFinalReady": 637.1
Expand All @@ -35,6 +39,7 @@
"maxLongAnimationFrameMs": 0
},
{
"capacityEvidence": null,
"segmentsMs": {
"clickToFirstFeedback": 33,
"clickToFinalReady": 630.8
Expand All @@ -52,6 +57,7 @@
"maxLongAnimationFrameMs": 0
},
{
"capacityEvidence": null,
"segmentsMs": {
"clickToFirstFeedback": 38.3,
"clickToFinalReady": 646.7
Expand Down
3 changes: 3 additions & 0 deletions e2e/binance-orderbook/live-baselines/no-orders.baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@
"scenarios": [
{
"name": "cancel-current-symbol-no-orders",
"parameters": {
"kind": "no-orders"
},
"sampleCount": 3,
"segmentsMs": {
"clickToFirstFeedback": {
Expand Down
11 changes: 11 additions & 0 deletions e2e/binance-orderbook/live-configs/smoke.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
{
"schemaVersion": 1,
"profileName": "smoke",
"sampleCount": 3,
"maxOrderCount": 3,
"scaleRatios": {
"small": 0.25,
"medium": 0.5,
"large": 1
}
}
Loading
Loading