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
3 changes: 3 additions & 0 deletions docs/binance-orderbook-trade-development.md
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,8 @@ Ladder replacement must stay scoped and direction-aware. Automatic replacement m

Continuous ladder trading is available only for close actions through `Option/Alt + click`; an ordinary click remains one round. A round is the complete existing ladder-close workflow, including any scoped same-direction replacement and cleanup. After a completed round, the runner must observe the same symbol, close mode, current precision, and native close button as ready before starting a full one-second cooldown. It must validate readiness again after the cooldown; losing readiness restarts the wait and a new full cooldown. Every new round must rebuild its plan from the latest panel profile and live trading context. A failed, stopped, or interrupted round ends the continuous session.

Continuous-session feedback stays in the shared ladder status row and uses compact Chinese counters. `2/3 轮` means two rounds completed out of three started, `本轮 1/3 笔` reports the latest partial plan, and `累计 7 笔` reports all confirmed submissions across the session. Confirmed cancellations are appended only when greater than zero. Round outcomes must expose a detached progress snapshot so a terminal continuous summary cannot be overwritten by the latest single-round message.

When selecting account-order tabs, scope to the bottom account-orders tab group. Do not globally match `当前委托` or `Open Orders`.

When a pane is found through `aria-controls`, confirm it contains current-orders controls such as `隐藏其他合约` or `全撤`. Binance may reuse pane ids in unrelated tab systems.
Expand Down Expand Up @@ -196,6 +198,7 @@ Run manual checks when behavior touches trading flow, DOM selectors, account ord
- start ladder order, confirm start buttons are disabled while running
- Option/Alt-click a close ladder button, confirm the next round starts only after the prior round is complete, the native close action is ready, and one full second has elapsed; change ratio, levels, row span, and precision during the wait and confirm the next plan uses the updated profile
- during a continuous close cooldown, make the native close action temporarily unavailable and confirm the cooldown restarts only after readiness returns; confirm Stop also aborts the cooldown immediately
- complete multiple continuous close rounds, then stop both during a round and during cooldown; confirm the Chinese status shows completed/started rounds, latest-round order ratio, cumulative confirmed submissions, and no zero-cancellation segment
- cancel current-symbol orders, verify only Binance native confirmation opens
- keep the native cancel-all dialog open for longer than the former decision deadline, verify the script remains in the dialog-tracking state, then cancel and confirm the original order count and temporary page state are restored
- cancel the native cancel-all dialog through its secondary button, Escape, and backdrop; verify chart and account-order UI state restores immediately without waiting for order clearing
Expand Down
271 changes: 205 additions & 66 deletions scripts/binance-orderbook-trade.user.js

Large diffs are not rendered by default.

91 changes: 91 additions & 0 deletions src/binance-orderbook-trade/core/continuous-ladder.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,101 @@ import {
throwIfAborted,
waitForPromiseOrAbort,
} from './abort.js';
import { snapshotLadderProgress } from './ladder-progress.js';

export const CONTINUOUS_LADDER_COOLDOWN_MS = 1000;
export const CONTINUOUS_LADDER_READY_CHECK_MS = 50;

const recordedRoundOutcomes = new WeakSet();
const CONTINUOUS_LADDER_PHASE_TEXT = Object.freeze({
running: '连续中',
stopping: '连续停止中',
stopped: '连续已停止',
failed: '连续失败',
interrupted: '连续已中止',
});

function assertContinuousLadderProgress(progress) {
if (
!progress
|| !Number.isInteger(progress.startedRounds)
|| progress.startedRounds < 0
|| !Number.isInteger(progress.completedRounds)
|| progress.completedRounds < 0
|| progress.completedRounds > progress.startedRounds
|| !Number.isInteger(progress.submittedOrders)
|| progress.submittedOrders < 0
|| !Number.isInteger(progress.cancelledOrders)
|| progress.cancelledOrders < 0
|| (progress.startedRounds === 0) !== (progress.lastRound === null)
) {
throw new Error('Invalid continuous ladder progress');
}
if (progress.lastRound !== null) snapshotLadderProgress(progress.lastRound);
}

export function createContinuousLadderProgress() {
return {
startedRounds: 0,
completedRounds: 0,
submittedOrders: 0,
cancelledOrders: 0,
lastRound: null,
};
}

export function recordContinuousLadderRound(progress, outcome) {
assertContinuousLadderProgress(progress);
if (!outcome || typeof outcome !== 'object') {
throw new Error('Invalid continuous ladder round outcome');
}
if (recordedRoundOutcomes.has(outcome)) {
throw new Error('Continuous ladder round was already recorded');
}
if (!['completed', 'stopped', 'failed', 'interrupted'].includes(outcome.status)) {
throw new Error('Invalid continuous ladder round outcome');
}
const roundProgress = snapshotLadderProgress(outcome.progress);
progress.startedRounds += 1;
if (outcome.status === 'completed') progress.completedRounds += 1;
progress.submittedOrders += roundProgress.submittedOrders;
progress.cancelledOrders += roundProgress.cancelledOrders;
progress.lastRound = {
status: outcome.status,
...roundProgress,
};
recordedRoundOutcomes.add(outcome);
assertContinuousLadderProgress(progress);
}

export function formatContinuousLadderProgress(label, phase, progress, reason = null) {
if (typeof label !== 'string' || label.trim() === '') {
throw new Error('Invalid continuous ladder label');
}
const phaseText = CONTINUOUS_LADDER_PHASE_TEXT[phase];
if (!phaseText) throw new Error('Invalid continuous ladder phase');
if (reason !== null && (typeof reason !== 'string' || reason.trim() === '')) {
throw new Error('Invalid continuous ladder reason');
}
assertContinuousLadderProgress(progress);

const parts = [`${label}${phaseText}`];
parts.push(progress.startedRounds === 0
? '0 轮'
: `${progress.completedRounds}/${progress.startedRounds} 轮`);
if (progress.lastRound?.plannedOrders !== null) {
parts.push(
`本轮 ${progress.lastRound.currentPlanSubmittedOrders}/${progress.lastRound.plannedOrders} 笔`,
);
} else if (progress.lastRound?.submittedOrders > 0) {
parts.push(`本轮 ${progress.lastRound.submittedOrders} 笔`);
}
parts.push(`累计 ${progress.submittedOrders} 笔`);
if (progress.cancelledOrders > 0) parts.push(`撤 ${progress.cancelledOrders} 笔`);
if (reason !== null) parts.push(reason);
return parts.join(' · ');
}

function assertReadinessState(state) {
if (!['ready', 'waiting', 'stopped'].includes(state?.status)) {
throw new Error('Invalid continuous ladder readiness state');
Expand Down
10 changes: 10 additions & 0 deletions src/binance-orderbook-trade/core/ladder-progress.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,16 @@ export function createLadderProgress() {
};
}

export function snapshotLadderProgress(progress) {
assertLadderProgress(progress);
return {
submittedOrders: progress.submittedOrders,
cancelledOrders: progress.cancelledOrders,
plannedOrders: progress.plannedOrders,
currentPlanSubmittedOrders: progress.currentPlanSubmittedOrders,
};
}

export function setLadderPlannedOrders(progress, plannedOrders) {
assertLadderProgress(progress);
if (!Number.isInteger(plannedOrders) || plannedOrders <= 0) {
Expand Down
111 changes: 91 additions & 20 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.156
// @version 2.7.157
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -78,6 +78,9 @@ import {
} from './core/ladder-plan.js';
import { keepInteractionFeedbackVisible } from './core/interaction-feedback.js';
import {
createContinuousLadderProgress,
formatContinuousLadderProgress,
recordContinuousLadderRound,
waitForContinuousLadderNextRound,
} from './core/continuous-ladder.js';
import {
Expand All @@ -89,6 +92,7 @@ import {
recordLadderCancelledOrder,
recordLadderSubmittedOrder,
setLadderPlannedOrders,
snapshotLadderProgress,
} from './core/ladder-progress.js';
import {
throwIfAborted,
Expand Down Expand Up @@ -377,6 +381,7 @@ import {
let continuousLadderAbortController = null;
let activeLadderActionType = null;
let activeContinuousLadderActionType = null;
let activeContinuousLadderProgress = null;
let activeLadderPanelContext = null;
let cancelCurrentSymbolOpenOrdersTask = null;
let cancelCurrentSymbolOpenOrdersBlocksLadderActions = false;
Expand Down Expand Up @@ -2759,7 +2764,11 @@ import {
'交易对已切换',
progress,
));
return { status: 'interrupted' };
return {
status: 'interrupted',
reason: '交易对已切换',
progress: snapshotLadderProgress(progress),
};
}
const diagnostics = formatLadderRepriceDiagnostics(repriceAttempts, lastRepriceApiErrorCode);
setLadderStatus(
Expand All @@ -2772,14 +2781,20 @@ import {
progress,
)}${diagnostics}`,
);
return { status: wasStopped ? 'stopped' : 'completed' };
return {
status: wasStopped ? 'stopped' : 'completed',
progress: snapshotLadderProgress(progress),
};
})
.catch((e) => {
if (isLadderStoppedError(e)) {
if (isCurrentObservedSymbol(actionSymbol)) {
setLadderStatus(formatStoppedLadderProgress(spec.label, progress));
}
return { status: 'stopped' };
return {
status: 'stopped',
progress: snapshotLadderProgress(progress),
};
}
err('Maker 阶梯执行失败:', e);
if (!isCurrentObservedSymbol(actionSymbol)) {
Expand All @@ -2788,15 +2803,25 @@ import {
'交易对已切换',
progress,
));
return { status: 'interrupted' };
return {
status: 'interrupted',
reason: '交易对已切换',
progress: snapshotLadderProgress(progress),
};
}
const failureMessage = e?.message || '未知错误';
const failureText = formatFailedLadderProgress(spec.label, failureMessage, progress);
const failureTitle = e?.statusTitle
? formatFailedLadderProgress(spec.label, e.statusTitle, progress)
: failureText;
setLadderStatus(failureText, failureTitle);
return { status: 'failed', error: e };
return {
status: 'failed',
error: e,
reason: failureMessage,
titleReason: e?.statusTitle || failureMessage,
progress: snapshotLadderProgress(progress),
};
})
.finally(() => {
if (ladderAbortController === abortController) ladderAbortController = null;
Expand All @@ -2813,7 +2838,7 @@ import {
function readContinuousLadderReadiness(actionType, actionSymbol) {
const spec = getLadderActionSpec(actionType);
if (!spec || spec.mode !== 'CLOSE') {
throw new Error('Continuous ladder trading requires a close action');
throw new Error('连续交易仅支持阶梯平仓');
}
if (!isCurrentObservedSymbol(actionSymbol)) {
return { status: 'stopped', reason: 'symbol_changed' };
Expand Down Expand Up @@ -2843,14 +2868,27 @@ import {
return { status: 'ready' };
}

function setContinuousLadderStoppedStatus(reason) {
function getContinuousLadderStopReason(reason) {
const reasonTextByCode = {
symbol_changed: 'symbol changed',
mode_changed: 'trade mode changed',
symbol_changed: '交易对已切换',
mode_changed: '开仓/平仓模式已切换',
};
const reasonText = reasonTextByCode[reason];
if (!reasonText) throw new Error(`Unknown continuous ladder stop reason: ${reason}`);
setLadderStatus(`Continuous trading stopped: ${reasonText}`);
if (!reasonText) throw new Error(`未知连续交易停止原因:${reason}`);
return reasonText;
}

function setContinuousLadderProgressStatus(
label,
phase,
progress,
reason = null,
titleReason = reason,
) {
setLadderStatus(
formatContinuousLadderProgress(label, phase, progress, reason),
formatContinuousLadderProgress(label, phase, progress, titleReason),
);
}

async function startContinuousLadder(actionType) {
Expand All @@ -2865,32 +2903,60 @@ import {
}

const abortController = new AbortController();
const continuousProgress = createContinuousLadderProgress();
continuousLadderAbortController = abortController;
activeContinuousLadderActionType = actionType;
activeContinuousLadderProgress = continuousProgress;

const executionTask = (async () => {
while (true) {
throwIfAborted(abortController.signal);
const outcome = await startLadder(actionType, true);
if (outcome?.status !== 'completed') return outcome;
if (!outcome?.progress) return outcome;
recordContinuousLadderRound(continuousProgress, outcome);
if (outcome.status !== 'completed') {
setContinuousLadderProgressStatus(
spec.label,
outcome.status,
continuousProgress,
outcome.reason || null,
outcome.titleReason || outcome.reason || null,
);
return outcome;
}
setContinuousLadderProgressStatus(spec.label, 'running', continuousProgress);

const readiness = await waitForContinuousLadderNextRound({
readReadiness: () => readContinuousLadderReadiness(actionType, actionSymbol),
delay,
signal: abortController.signal,
});
if (readiness.status === 'stopped') {
setContinuousLadderStoppedStatus(readiness.reason);
setContinuousLadderProgressStatus(
spec.label,
'stopped',
continuousProgress,
getContinuousLadderStopReason(readiness.reason),
);
return readiness;
}
}
})();

continuousLadderTask = executionTask
.catch((e) => {
if (isLadderStoppedError(e)) return { status: 'stopped' };
err('Continuous ladder trading failed:', e);
setLadderStatus(`Continuous trading failed: ${e?.message || 'unknown error'}`);
if (isLadderStoppedError(e)) {
setContinuousLadderProgressStatus(spec.label, 'stopped', continuousProgress);
return { status: 'stopped' };
}
err('连续阶梯交易失败:', e);
setContinuousLadderProgressStatus(
spec.label,
'failed',
continuousProgress,
e?.message || '未知错误',
e?.statusTitle || e?.message || '未知错误',
);
return { status: 'failed', error: e };
})
.finally(() => {
Expand All @@ -2899,6 +2965,7 @@ import {
}
continuousLadderTask = null;
activeContinuousLadderActionType = null;
activeContinuousLadderProgress = null;
scheduleRenderPanel();
});
scheduleRenderPanel();
Expand All @@ -2922,9 +2989,13 @@ import {
if (ladderAbortController && !ladderAbortController.signal.aborted) {
ladderAbortController.abort(stoppedError);
}
setLadderStatus(`${activeSpec.label}停止中`);
setLadderStatus(`${activeSpec.label}${continuousLadderTask ? '连续' : ''}停止中`);
} else {
setLadderStatus(`${ladderStatusText} · Continuous trading stopped`);
setContinuousLadderProgressStatus(
activeSpec.label,
'stopping',
activeContinuousLadderProgress,
);
}
scheduleRenderPanel();
}
Expand Down Expand Up @@ -5466,7 +5537,7 @@ import {
const background = isBuyTone ? 'var(--color-GreenAlpha01)' : 'var(--color-RedAlpha01)';
const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : '';
const continuousHint = actionType.startsWith('CLOSE_')
? ' title="Option/Alt + click: continuous trading"'
? ' title="Option/Alt + 单击:连续交易"'
: '';
const cursor = disabled ? 'not-allowed' : 'pointer';
return `<button type="button" data-ladder-action="${actionType}"${disabledAttrs}${continuousHint} style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid ${borderColor};border-radius:6px;background:${background};color:${borderColor};font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:${cursor};opacity:1;">${label}</button>`;
Expand Down
Loading
Loading