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
62 changes: 50 additions & 12 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.140
// @version 2.7.141
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -1227,6 +1227,19 @@
return true;
};
}
function isScriptOwnedTradeInputRecoveryState({
preWriteValue,
rollbackValue,
submittedValue,
previousSubmittedValue,
compareValues
}) {
if (typeof compareValues !== "function") {
throw new Error("Trade input recovery comparison dependency is invalid");
}
const isScriptOwnedOrEmpty = (value) => value === null || previousSubmittedValue != null && compareValues(previousSubmittedValue, value) === 0;
return isScriptOwnedOrEmpty(preWriteValue) && isScriptOwnedOrEmpty(rollbackValue) && isScriptOwnedOrEmpty(submittedValue);
}
function createTradeInputStateReader({
resolveInputs,
expectedPrice,
Expand All @@ -1239,7 +1252,7 @@
requiredStableMatchFrames = 1,
maxWriteAttempts = 2,
recoverProvisionalMatchRollback = false,
isRecoveryWriteAllowed = () => true
isRecoveryWriteAllowed = ({ rollbackValue, submittedValue }) => rollbackValue === submittedValue
}) {
if (typeof resolveInputs !== "function" || typeof normalizeValue !== "function" || typeof compareValues !== "function" || typeof writeValue !== "function" || typeof isRecoveryWriteAllowed !== "function") {
throw new Error("Trade input synchronizer dependencies are invalid");
Expand All @@ -1256,20 +1269,23 @@
if (typeof recoverProvisionalMatchRollback !== "boolean") {
throw new Error("Provisional trade input recovery flag must be boolean");
}
const createSyncSlot = () => {
const createSyncSlot = (field) => {
let root = null;
let input = null;
let writeCount = 0;
let preWriteValue = null;
let rollbackValue = null;
let recoveryEligible = false;
let stableRollbackFrames = 0;
let stableMatchFrames = 0;
const clearRecovery = () => {
preWriteValue = null;
rollbackValue = null;
recoveryEligible = false;
stableRollbackFrames = 0;
};
const writeExpectedValue = (currentInput, expectedValue, submittedValue) => {
preWriteValue = submittedValue;
const wrote = writeValue(currentInput, expectedValue);
if (wrote === false) {
writeCount = maxWriteAttempts;
Expand Down Expand Up @@ -1307,7 +1323,7 @@
}
if (stableMatchFrames > 0) {
stableMatchFrames = 0;
recoveryEligible = rollbackValue === submittedValue && writeCount < maxWriteAttempts;
recoveryEligible = writeCount < maxWriteAttempts;
stableRollbackFrames = 0;
if (!recoveryEligible) clearRecovery();
}
Expand All @@ -1316,15 +1332,14 @@
return false;
}
if (writeCount >= maxWriteAttempts || !recoveryEligible) return false;
if (rollbackValue !== submittedValue) {
clearRecovery();
return false;
}
if (!isRecoveryWriteAllowed({
field,
currentRoot,
currentInput,
expectedValue,
preWriteValue,
rollbackValue,
submittedValue,
writeCount
})) {
clearRecovery();
Expand All @@ -1336,8 +1351,8 @@
return false;
};
};
const syncQty = createSyncSlot();
const syncPrice = createSyncSlot();
const syncQty = createSyncSlot("qty");
const syncPrice = createSyncSlot("price");
return () => {
const inputs = resolveInputs();
if (!inputs?.qtyInput || includePrice && !inputs.priceInput) return null;
Expand Down Expand Up @@ -3674,7 +3689,24 @@
const syncTimeoutMs = settleControlledForm ? LADDER_INPUT_SETTLE_TIMEOUT_MS : TRADE_INPUT_SYNC_TIMEOUT_MS;
const stableMismatchFrames = settleControlledForm ? LADDER_INPUT_SETTLE_MISMATCH_FRAMES : TRADE_INPUT_SYNC_STABLE_FRAMES;
const maxWriteAttempts = settleControlledForm ? LADDER_INPUT_SETTLE_MAX_WRITES : 2;
const isRecoveryWriteAllowed = settleControlledForm ? ({ rollbackValue }) => rollbackValue === null : () => true;
const previousSubmittedInputs = settleControlledForm ? options?.previousSubmittedInputs || null : null;
const isRecoveryWriteAllowed = settleControlledForm ? ({
field,
preWriteValue,
rollbackValue,
submittedValue
}) => {
if (field !== "qty" && field !== "price") {
throw new Error("未知交易输入字段");
}
return isScriptOwnedTradeInputRecoveryState({
preWriteValue,
rollbackValue,
submittedValue,
previousSubmittedValue: field === "qty" ? previousSubmittedInputs?.submittedQty : previousSubmittedInputs?.submittedPrice,
compareValues: compareDecimalStrings
});
} : ({ rollbackValue, submittedValue }) => rollbackValue === submittedValue;
const writeTradeInputValue = settleControlledForm ? createBoundedInputWriter({
writeValue: setInputValueReact,
maxWriteAttempts
Expand Down Expand Up @@ -3834,6 +3866,7 @@
let done = 0;
let repriceAttempts = 0;
let lastRepriceApiErrorCode = null;
let previousAcknowledgedInputs = null;
while (done < plan.orders.length) {
throwIfAborted(abortSignal);
if (ladderStopRequested) break;
Expand All @@ -3850,7 +3883,8 @@
const synchronizedInputs = await syncTradeInputs(order.price, order.qty, {
priceLabel: "计划价",
qtyLabel: "计划量",
settleControlledForm: true
settleControlledForm: true,
previousSubmittedInputs: previousAcknowledgedInputs
});
throwIfAborted(abortSignal);
const submittedPrice = synchronizedInputs.submittedPrice;
Expand Down Expand Up @@ -3879,6 +3913,10 @@
} finally {
endLadderSubmitResponseCapture(submitCaptureId);
}
previousAcknowledgedInputs = {
submittedPrice: synchronizedInputs.submittedPrice,
submittedQty: synchronizedInputs.submittedQty
};
}
} catch (e) {
if (!isRetryableLadderMakerPriceFailure(plan, e)) throw e;
Expand Down
56 changes: 42 additions & 14 deletions src/binance-orderbook-trade/dom/trade-form.js
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,40 @@ export function createBoundedInputWriter({ writeValue, maxWriteAttempts }) {
};
}

export function isScriptOwnedTradeInputRecoveryState({
preWriteValue,
rollbackValue,
submittedValue,
previousSubmittedValue,
compareValues,
}) {
if (typeof compareValues !== 'function') {
throw new Error('Trade input recovery comparison dependency is invalid');
}

const isScriptOwnedOrEmpty = (value) => (
value === null
|| (
previousSubmittedValue != null
&& compareValues(previousSubmittedValue, value) === 0
)
);

return (
isScriptOwnedOrEmpty(preWriteValue)
&& isScriptOwnedOrEmpty(rollbackValue)
&& isScriptOwnedOrEmpty(submittedValue)
);
}

/**
* 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. 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.
* first frame observation. Recovery policy owns the accepted state transition;
* generic callers retain exact rollback matching while ladder callers may identify
* a previous acknowledged script-owned value or Binance's empty post-submit state.
*/
export function createTradeInputStateReader({
resolveInputs,
Expand All @@ -163,7 +190,9 @@ export function createTradeInputStateReader({
requiredStableMatchFrames = 1,
maxWriteAttempts = 2,
recoverProvisionalMatchRollback = false,
isRecoveryWriteAllowed = () => true,
isRecoveryWriteAllowed = ({ rollbackValue, submittedValue }) => (
rollbackValue === submittedValue
),
}) {
if (
typeof resolveInputs !== 'function'
Expand All @@ -190,22 +219,25 @@ export function createTradeInputStateReader({
throw new Error('Provisional trade input recovery flag must be boolean');
}

const createSyncSlot = () => {
const createSyncSlot = (field) => {
let root = null;
let input = null;
let writeCount = 0;
let preWriteValue = null;
let rollbackValue = null;
let recoveryEligible = false;
let stableRollbackFrames = 0;
let stableMatchFrames = 0;

const clearRecovery = () => {
preWriteValue = null;
rollbackValue = null;
recoveryEligible = false;
stableRollbackFrames = 0;
};

const writeExpectedValue = (currentInput, expectedValue, submittedValue) => {
preWriteValue = submittedValue;
const wrote = writeValue(currentInput, expectedValue);
if (wrote === false) {
writeCount = maxWriteAttempts;
Expand Down Expand Up @@ -249,10 +281,7 @@ export function createTradeInputStateReader({

if (stableMatchFrames > 0) {
stableMatchFrames = 0;
recoveryEligible = (
rollbackValue === submittedValue
&& writeCount < maxWriteAttempts
);
recoveryEligible = writeCount < maxWriteAttempts;
stableRollbackFrames = 0;
if (!recoveryEligible) clearRecovery();
}
Expand All @@ -263,15 +292,14 @@ export function createTradeInputStateReader({
}
if (writeCount >= maxWriteAttempts || !recoveryEligible) return false;

if (rollbackValue !== submittedValue) {
clearRecovery();
return false;
}
if (!isRecoveryWriteAllowed({
field,
currentRoot,
currentInput,
expectedValue,
preWriteValue,
rollbackValue,
submittedValue,
writeCount,
})) {
clearRecovery();
Expand All @@ -286,8 +314,8 @@ export function createTradeInputStateReader({
};
};

const syncQty = createSyncSlot();
const syncPrice = createSyncSlot();
const syncQty = createSyncSlot('qty');
const syncPrice = createSyncSlot('price');
return () => {
const inputs = resolveInputs();
if (!inputs?.qtyInput || (includePrice && !inputs.priceInput)) return null;
Expand Down
34 changes: 31 additions & 3 deletions 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.140
// @version 2.7.141
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -121,6 +121,7 @@ import {
findTradeFormRoot,
findTradePanelInsertionPoint,
findCurrentLeverageButtonFromScopes,
isScriptOwnedTradeInputRecoveryState,
isTradeActionButton as isTradeActionButtonDom,
isTradeModeTab as isTradeModeTabDom,
mutationTouchesCloseQuantity,
Expand Down Expand Up @@ -2247,9 +2248,30 @@ import {
const maxWriteAttempts = settleControlledForm
? LADDER_INPUT_SETTLE_MAX_WRITES
: 2;
const previousSubmittedInputs = settleControlledForm
? options?.previousSubmittedInputs || null
: null;
const isRecoveryWriteAllowed = settleControlledForm
? ({ rollbackValue }) => rollbackValue === null
: () => true;
? ({
field,
preWriteValue,
rollbackValue,
submittedValue,
}) => {
if (field !== 'qty' && field !== 'price') {
throw new Error('未知交易输入字段');
}
return isScriptOwnedTradeInputRecoveryState({
preWriteValue,
rollbackValue,
submittedValue,
previousSubmittedValue: field === 'qty'
? previousSubmittedInputs?.submittedQty
: previousSubmittedInputs?.submittedPrice,
compareValues: compareDecimalStrings,
});
}
: ({ rollbackValue, submittedValue }) => rollbackValue === submittedValue;
const writeTradeInputValue = settleControlledForm
? createBoundedInputWriter({
writeValue: setInputValueReact,
Expand Down Expand Up @@ -2448,6 +2470,7 @@ import {
let done = 0;
let repriceAttempts = 0;
let lastRepriceApiErrorCode = null;
let previousAcknowledgedInputs = null;
while (done < plan.orders.length) {
throwIfAborted(abortSignal);
if (ladderStopRequested) break;
Expand All @@ -2467,6 +2490,7 @@ import {
priceLabel: '计划价',
qtyLabel: '计划量',
settleControlledForm: true,
previousSubmittedInputs: previousAcknowledgedInputs,
});
throwIfAborted(abortSignal);
const submittedPrice = synchronizedInputs.submittedPrice;
Expand Down Expand Up @@ -2497,6 +2521,10 @@ import {
} finally {
endLadderSubmitResponseCapture(submitCaptureId);
}
previousAcknowledgedInputs = {
submittedPrice: synchronizedInputs.submittedPrice,
submittedQty: synchronizedInputs.submittedQty,
};
}
} catch (e) {
if (!isRetryableLadderMakerPriceFailure(plan, e)) throw e;
Expand Down
19 changes: 13 additions & 6 deletions test/unit/binance-orderbook-trade/source-regressions.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -169,12 +169,14 @@ test('trade input synchronization confirms live controlled values instead of sle
(syncBody.match(/recoverProvisionalMatchRollback:\s*settleControlledForm/g) || []).length,
2,
);
assert.match(
syncBody,
/\?\s*\(\{\s*rollbackValue\s*\}\)\s*=>\s*rollbackValue\s*===\s*null/,
);
assert.doesNotMatch(syncBody, /rollbackValue\s*===\s*''/);
assert.match(syncBody, /isScriptOwnedTradeInputRecoveryState/);
assert.match(syncBody, /field[\s\S]*preWriteValue[\s\S]*rollbackValue[\s\S]*submittedValue/);
assert.match(syncBody, /previousSubmittedInputs\?\.submittedQty/);
assert.match(syncBody, /previousSubmittedInputs\?\.submittedPrice/);
assert.doesNotMatch(syncBody, /isRecoveryWriteAllowed\s*=\s*settleControlledForm[\s\S]*=>\s*true/);
assert.match(syncBody, /settleControlledForm[\s\S]*LADDER_INPUT_SETTLE_MISMATCH_FRAMES/);
assert.match(source, /const LADDER_INPUT_SETTLE_TIMEOUT_MS = 1200;/);
assert.match(source, /const LADDER_INPUT_SETTLE_MAX_WRITES = 5;/);
assert.match(syncBody, /waitForTradeFormFrameState/);
assert.match(syncBody, /includePrice:\s*false/);
assert.match(syncBody, /includePrice:\s*true/);
Expand All @@ -184,7 +186,12 @@ test('trade input synchronization confirms live controlled values instead of sle
assert.match(syncBody, /assertSubmittedPriceMatchesExpectedPrice/);
assert.match(syncBody, /assertSubmittedQtyMatchesExpectedQty/);
assert.doesNotMatch(syncBody, /delay\(/);
assert.match(executeBody, /syncTradeInputs\(order\.price,\s*order\.qty,\s*\{[\s\S]*priceLabel:\s*'计划价'[\s\S]*settleControlledForm:\s*true/);
assert.match(executeBody, /syncTradeInputs\(order\.price,\s*order\.qty,\s*\{[\s\S]*priceLabel:\s*'计划价'[\s\S]*settleControlledForm:\s*true[\s\S]*previousSubmittedInputs:\s*previousAcknowledgedInputs/);
assert.equal((executeBody.match(/previousAcknowledgedInputs\s*=/g) || []).length, 2);
assert.ok(
executeBody.indexOf('previousAcknowledgedInputs = {')
> executeBody.indexOf('await waitForOrderSubmitAcknowledgement'),
);
assert.doesNotMatch(executeBody, /await delay\(90\)|await delay\(120\)/);
const clickSyncCall = source.match(/syncTradeInputs\(clickedPrice,\s*qtyPlan\.qty,\s*\{[\s\S]*?\}\);/)?.[0] || '';
assert.match(clickSyncCall, /priceLabel:\s*'点击价'/);
Expand Down
Loading
Loading