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
10 changes: 8 additions & 2 deletions scripts/binance-orderbook-trade.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.136
// @version 2.7.137
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -1238,6 +1238,7 @@
requiredStableMismatchFrames = 2,
requiredStableMatchFrames = 1,
maxWriteAttempts = 2,
recoverProvisionalMatchRollback = false,
isRecoveryWriteAllowed = () => true
}) {
if (typeof resolveInputs !== "function" || typeof normalizeValue !== "function" || typeof compareValues !== "function" || typeof writeValue !== "function" || typeof isRecoveryWriteAllowed !== "function") {
Expand All @@ -1252,6 +1253,9 @@
if (!Number.isInteger(maxWriteAttempts) || maxWriteAttempts < 1) {
throw new Error("Trade input write attempts must be a positive integer");
}
if (typeof recoverProvisionalMatchRollback !== "boolean") {
throw new Error("Provisional trade input recovery flag must be boolean");
}
const createSyncSlot = () => {
let root = null;
let input = null;
Expand All @@ -1276,7 +1280,7 @@
writeCount += 1;
const postWriteValue = normalizeValue(currentInput.value);
const rejected = compareValues(expectedValue, postWriteValue) !== 0;
recoveryEligible = rejected && writeCount < maxWriteAttempts;
recoveryEligible = writeCount < maxWriteAttempts && (rejected || recoverProvisionalMatchRollback);
rollbackValue = rejected ? postWriteValue : submittedValue;
stableRollbackFrames = 0;
stableMatchFrames = 0;
Expand Down Expand Up @@ -3692,6 +3696,7 @@
requiredStableMismatchFrames: stableMismatchFrames,
requiredStableMatchFrames: settleControlledForm ? LADDER_INPUT_SETTLE_MISMATCH_FRAMES : 1,
maxWriteAttempts,
recoverProvisionalMatchRollback: settleControlledForm,
isRecoveryWriteAllowed
});
readQtyState();
Expand Down Expand Up @@ -3719,6 +3724,7 @@
requiredStableMismatchFrames: stableMismatchFrames,
requiredStableMatchFrames: settleControlledForm ? LADDER_INPUT_SETTLE_MISMATCH_FRAMES : 1,
maxWriteAttempts,
recoverProvisionalMatchRollback: settleControlledForm,
isRecoveryWriteAllowed
});
readTradeState();
Expand Down
16 changes: 12 additions & 4 deletions src/binance-orderbook-trade/dom/trade-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -146,9 +146,10 @@ export function createBoundedInputWriter({ writeValue, maxWriteAttempts }) {
/**
* Synchronize each live React input identity with a bounded post-transition budget.
* Binance can synchronously restore a controlled input while a replacement form is
* settling. Only an identical same-node rollback observed across consecutive frame
* reads earns another bounded write. Callers must opt into more than one recovery
* write and define when that recovery remains safe.
* settling. A caller may preserve the pre-write value as a provisional rollback
* contract because React can restore it after the write returns but before the
* first frame observation. Only an identical same-node rollback observed across
* consecutive frame reads earns another bounded write.
*/
export function createTradeInputStateReader({
resolveInputs,
Expand All @@ -161,6 +162,7 @@ export function createTradeInputStateReader({
requiredStableMismatchFrames = 2,
requiredStableMatchFrames = 1,
maxWriteAttempts = 2,
recoverProvisionalMatchRollback = false,
isRecoveryWriteAllowed = () => true,
}) {
if (
Expand All @@ -184,6 +186,9 @@ export function createTradeInputStateReader({
if (!Number.isInteger(maxWriteAttempts) || maxWriteAttempts < 1) {
throw new Error('Trade input write attempts must be a positive integer');
}
if (typeof recoverProvisionalMatchRollback !== 'boolean') {
throw new Error('Provisional trade input recovery flag must be boolean');
}

const createSyncSlot = () => {
let root = null;
Expand Down Expand Up @@ -211,7 +216,10 @@ export function createTradeInputStateReader({
writeCount += 1;
const postWriteValue = normalizeValue(currentInput.value);
const rejected = compareValues(expectedValue, postWriteValue) !== 0;
recoveryEligible = rejected && writeCount < maxWriteAttempts;
recoveryEligible = (
writeCount < maxWriteAttempts
&& (rejected || recoverProvisionalMatchRollback)
);
rollbackValue = rejected ? postWriteValue : submittedValue;
stableRollbackFrames = 0;
stableMatchFrames = 0;
Expand Down
4 changes: 3 additions & 1 deletion src/binance-orderbook-trade/index.user.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.136
// @version 2.7.137
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -2272,6 +2272,7 @@ import {
? LADDER_INPUT_SETTLE_MISMATCH_FRAMES
: 1,
maxWriteAttempts,
recoverProvisionalMatchRollback: settleControlledForm,
isRecoveryWriteAllowed,
});
readQtyState();
Expand Down Expand Up @@ -2302,6 +2303,7 @@ import {
? LADDER_INPUT_SETTLE_MISMATCH_FRAMES
: 1,
maxWriteAttempts,
recoverProvisionalMatchRollback: settleControlledForm,
isRecoveryWriteAllowed,
});
readTradeState();
Expand Down
4 changes: 4 additions & 0 deletions test/unit/binance-orderbook-trade/source-regressions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,10 @@ test('trade input synchronization confirms live controlled values instead of sle
assert.equal((syncBody.match(/maxWriteAttempts,/g) || []).length, 3);
assert.equal((syncBody.match(/isRecoveryWriteAllowed,/g) || []).length, 2);
assert.equal((syncBody.match(/requiredStableMatchFrames:/g) || []).length, 2);
assert.equal(
(syncBody.match(/recoverProvisionalMatchRollback:\s*settleControlledForm/g) || []).length,
2,
);
assert.match(syncBody, /settleControlledForm[\s\S]*LADDER_INPUT_SETTLE_MISMATCH_FRAMES/);
assert.match(syncBody, /waitForTradeFormFrameState/);
assert.match(syncBody, /includePrice:\s*false/);
Expand Down
81 changes: 81 additions & 0 deletions test/unit/binance-orderbook-trade/trade-form.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -421,6 +421,75 @@ test('trade input synchronization can settle a ladder input after repeated stabl
assert.deepEqual(writes.map(({ value }) => value), ['0.02', '0.02', '0.02']);
});

test('trade input synchronization recovers a provisional match rolled back before the first observation frame', () => {
const currentInputs = {
root: {},
priceInput: null,
qtyInput: { value: '' },
};
const writes = [];
const readState = createTradeInputStateReader({
resolveInputs: () => currentInputs,
expectedQty: '0.07',
includePrice: false,
normalizeValue: String,
compareValues: (expected, actual) => expected === actual ? 0 : 1,
writeValue: (input, value) => {
writes.push({ input, value });
input.value = value;
},
requiredStableMismatchFrames: 2,
requiredStableMatchFrames: 2,
maxWriteAttempts: 5,
recoverProvisionalMatchRollback: true,
isRecoveryWriteAllowed: ({ rollbackValue }) => rollbackValue === '',
});

assert.equal(readState(), null);
currentInputs.qtyInput.value = '';
assert.equal(readState(), null);
assert.equal(readState(), null);
assert.equal(readState(), null);
assert.deepEqual(readState(), {
...currentInputs,
submittedQty: '0.07',
});
assert.deepEqual(writes.map(({ value }) => value), ['0.07', '0.07']);
});

test('trade input synchronization cancels provisional recovery for a different non-empty value', () => {
const currentInputs = {
root: {},
priceInput: null,
qtyInput: { value: '' },
};
const writes = [];
const readState = createTradeInputStateReader({
resolveInputs: () => currentInputs,
expectedQty: '0.07',
includePrice: false,
normalizeValue: String,
compareValues: (expected, actual) => expected === actual ? 0 : 1,
writeValue: (input, value) => {
writes.push({ input, value });
input.value = value;
},
requiredStableMismatchFrames: 2,
requiredStableMatchFrames: 2,
maxWriteAttempts: 5,
recoverProvisionalMatchRollback: true,
isRecoveryWriteAllowed: ({ rollbackValue }) => rollbackValue === '',
});

assert.equal(readState(), null);
currentInputs.qtyInput.value = '0.005';
for (let frame = 0; frame < 5; frame += 1) {
assert.equal(readState(), null);
}
assert.equal(currentInputs.qtyInput.value, '0.005');
assert.deepEqual(writes.map(({ value }) => value), ['0.07']);
});

test('trade input synchronization rejects a provisionally accepted value that rolls back before settling', () => {
const currentInputs = {
root: {},
Expand Down Expand Up @@ -569,6 +638,18 @@ test('trade input synchronization rejects an invalid rollback stability contract
}),
/Trade input accepted stability must be a positive integer/,
);
assert.throws(
() => createTradeInputStateReader({
resolveInputs: () => null,
expectedQty: '0.01',
includePrice: false,
normalizeValue: String,
compareValues: () => 1,
writeValue: () => {},
recoverProvisionalMatchRollback: 'yes',
}),
/Provisional trade input recovery flag must be boolean/,
);
});

test('trade input synchronization stops when the recovery policy rejects the rollback', () => {
Expand Down
Loading