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 @@ -159,6 +159,8 @@ Continuous ladder trading is available only for close actions through `Option/Al

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.

The active continuous-close control keeps a direction-specific stop action (`停止连续平多` / `停止连续平空`) throughout both execution and inter-round waiting. Before the native submit control is ready, the status places `等待按钮恢复` immediately after the continuous-state label. Only after the fixed cooldown actually begins may it show `1s 后继续` in that same priority position; this is a static duration label, not a countdown. The button must not temporarily revert to a ladder-start action between rounds.

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 @@ -199,6 +201,7 @@ Run manual checks when behavior touches trading flow, DOM selectors, account ord
- 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
- confirm continuous close keeps the direction-specific stop button between rounds, places `等待按钮恢复` or `1s 后继续` before the progress counters, and never exposes a start button during the cooldown
- 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
72 changes: 62 additions & 10 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.157
// @version 2.7.158
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -153,6 +153,10 @@
OPEN_SHORT: "停止开空",
CLOSE_LONG: "停止平多",
CLOSE_SHORT: "停止平空"
}),
stopContinuousLadderByAction: freezeCopy({
CLOSE_LONG: "停止连续平多",
CLOSE_SHORT: "停止连续平空"
})
}),
status: freezeCopy({
Expand Down Expand Up @@ -1014,15 +1018,12 @@
recordedRoundOutcomes.add(outcome);
assertContinuousLadderProgress(progress);
}
function formatContinuousLadderProgress(label, phase, progress, reason = null) {
function buildContinuousLadderProgressParts(label, phase, progress) {
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} 轮`);
Expand All @@ -1035,9 +1036,30 @@
}
parts.push(`累计 ${progress.submittedOrders} 笔`);
if (progress.cancelledOrders > 0) parts.push(`撤 ${progress.cancelledOrders} 笔`);
return parts;
}
function formatContinuousLadderProgress(label, phase, progress, reason = null) {
if (reason !== null && (typeof reason !== "string" || reason.trim() === "")) {
throw new Error("Invalid continuous ladder reason");
}
const parts = buildContinuousLadderProgressParts(label, phase, progress);
if (reason !== null) parts.push(reason);
return parts.join(" · ");
}
function formatContinuousLadderWaitReason(phase, cooldownMs) {
if (!Number.isFinite(cooldownMs) || cooldownMs < 0) {
throw new Error("Invalid continuous ladder cooldown");
}
if (phase === "waiting_ready") return "等待按钮恢复";
if (phase !== "cooldown") throw new Error("Invalid continuous ladder wait phase");
const duration = cooldownMs % 1e3 === 0 ? `${cooldownMs / 1e3}s` : `${cooldownMs}ms`;
return `${duration} 后继续`;
}
function formatContinuousLadderWaitProgress(label, progress, phase, cooldownMs) {
const parts = buildContinuousLadderProgressParts(label, "running", progress);
parts.splice(1, 0, formatContinuousLadderWaitReason(phase, cooldownMs));
return parts.join(" · ");
}
function assertReadinessState(state) {
if (!["ready", "waiting", "stopped"].includes(state?.status)) {
throw new Error("Invalid continuous ladder readiness state");
Expand All @@ -1048,12 +1070,20 @@
readReadiness,
delay,
signal,
readyCheckMs
readyCheckMs,
cooldownMs,
onWaitStateChange,
waitingAlreadyReported
}) {
let reported = waitingAlreadyReported;
while (true) {
throwIfAborted(signal);
const state = assertReadinessState(readReadiness());
if (state.status !== "waiting") return state;
if (!reported) {
onWaitStateChange({ phase: "waiting_ready", cooldownMs });
reported = true;
}
await waitForPromiseOrAbort(delay(readyCheckMs), signal);
}
}
Expand All @@ -1062,22 +1092,35 @@
delay,
signal = null,
cooldownMs = CONTINUOUS_LADDER_COOLDOWN_MS,
readyCheckMs = CONTINUOUS_LADDER_READY_CHECK_MS
readyCheckMs = CONTINUOUS_LADDER_READY_CHECK_MS,
onWaitStateChange = () => {
}
}) {
if (!(cooldownMs >= 0)) throw new Error("Invalid continuous ladder cooldown");
if (!(readyCheckMs > 0)) throw new Error("Invalid continuous ladder readiness interval");
if (typeof onWaitStateChange !== "function") {
throw new Error("Invalid continuous ladder wait-state callback");
}
let waitingAlreadyReported = false;
while (true) {
const readyState = await waitUntilReadyOrStopped({
readReadiness,
delay,
signal,
readyCheckMs
readyCheckMs,
cooldownMs,
onWaitStateChange,
waitingAlreadyReported
});
if (readyState.status === "stopped") return readyState;
waitingAlreadyReported = false;
onWaitStateChange({ phase: "cooldown", cooldownMs });
await waitForPromiseOrAbort(delay(cooldownMs), signal);
throwIfAborted(signal);
const afterCooldown = assertReadinessState(readReadiness());
if (afterCooldown.status !== "waiting") return afterCooldown;
onWaitStateChange({ phase: "waiting_ready", cooldownMs });
waitingAlreadyReported = true;
}
}

Expand Down Expand Up @@ -4776,7 +4819,15 @@
const readiness = await waitForContinuousLadderNextRound({
readReadiness: () => readContinuousLadderReadiness(actionType, actionSymbol),
delay,
signal: abortController.signal
signal: abortController.signal,
onWaitStateChange: ({ phase, cooldownMs }) => {
setLadderStatus(formatContinuousLadderWaitProgress(
spec.label,
continuousProgress,
phase,
cooldownMs
));
}
});
if (readiness.status === "stopped") {
setContinuousLadderProgressStatus(
Expand Down Expand Up @@ -7016,7 +7067,8 @@
if (activeActionType !== actionType) {
return ladderActionButton(actionType, label, tone, disabled);
}
return `<button type="button" data-ladder-stop="true" data-ladder-action-origin="${actionType}" style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid var(--color-PrimaryYellow);border-radius:6px;background:var(--color-BadgeBg);color:#9a6700;font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:pointer;">${PANEL_COPY.action.stopLadderByAction[actionType]}</button>`;
const stopLabel = activeContinuousLadderActionType === actionType ? PANEL_COPY.action.stopContinuousLadderByAction[actionType] : PANEL_COPY.action.stopLadderByAction[actionType];
return `<button type="button" data-ladder-stop="true" data-ladder-action-origin="${actionType}" style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid var(--color-PrimaryYellow);border-radius:6px;background:var(--color-BadgeBg);color:#9a6700;font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:pointer;">${stopLabel}</button>`;
}
function getLadderControlSections(tradeMode, closeContext, symbol, precision) {
const ladderRunning = !!ladderTask || !!continuousLadderTask;
Expand Down
4 changes: 4 additions & 0 deletions src/binance-orderbook-trade/contracts/panel-copy.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ export const PANEL_COPY = Object.freeze({
CLOSE_LONG: '停止平多',
CLOSE_SHORT: '停止平空',
}),
stopContinuousLadderByAction: freezeCopy({
CLOSE_LONG: '停止连续平多',
CLOSE_SHORT: '停止连续平空',
}),
}),
status: freezeCopy({
precisionUpdated: '精度推荐已更新',
Expand Down
51 changes: 47 additions & 4 deletions src/binance-orderbook-trade/core/continuous-ladder.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,15 +69,12 @@ export function recordContinuousLadderRound(progress, outcome) {
assertContinuousLadderProgress(progress);
}

export function formatContinuousLadderProgress(label, phase, progress, reason = null) {
function buildContinuousLadderProgressParts(label, phase, progress) {
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}`];
Expand All @@ -93,10 +90,36 @@ export function formatContinuousLadderProgress(label, phase, progress, reason =
}
parts.push(`累计 ${progress.submittedOrders} 笔`);
if (progress.cancelledOrders > 0) parts.push(`撤 ${progress.cancelledOrders} 笔`);
return parts;
}

export function formatContinuousLadderProgress(label, phase, progress, reason = null) {
if (reason !== null && (typeof reason !== 'string' || reason.trim() === '')) {
throw new Error('Invalid continuous ladder reason');
}
const parts = buildContinuousLadderProgressParts(label, phase, progress);
if (reason !== null) parts.push(reason);
return parts.join(' · ');
}

export function formatContinuousLadderWaitReason(phase, cooldownMs) {
if (!Number.isFinite(cooldownMs) || cooldownMs < 0) {
throw new Error('Invalid continuous ladder cooldown');
}
if (phase === 'waiting_ready') return '等待按钮恢复';
if (phase !== 'cooldown') throw new Error('Invalid continuous ladder wait phase');
const duration = cooldownMs % 1000 === 0
? `${cooldownMs / 1000}s`
: `${cooldownMs}ms`;
return `${duration} 后继续`;
}

export function formatContinuousLadderWaitProgress(label, progress, phase, cooldownMs) {
const parts = buildContinuousLadderProgressParts(label, 'running', progress);
parts.splice(1, 0, formatContinuousLadderWaitReason(phase, cooldownMs));
return parts.join(' · ');
}

function assertReadinessState(state) {
if (!['ready', 'waiting', 'stopped'].includes(state?.status)) {
throw new Error('Invalid continuous ladder readiness state');
Expand All @@ -109,11 +132,19 @@ async function waitUntilReadyOrStopped({
delay,
signal,
readyCheckMs,
cooldownMs,
onWaitStateChange,
waitingAlreadyReported,
}) {
let reported = waitingAlreadyReported;
while (true) {
throwIfAborted(signal);
const state = assertReadinessState(readReadiness());
if (state.status !== 'waiting') return state;
if (!reported) {
onWaitStateChange({ phase: 'waiting_ready', cooldownMs });
reported = true;
}
await waitForPromiseOrAbort(delay(readyCheckMs), signal);
}
}
Expand All @@ -128,22 +159,34 @@ export async function waitForContinuousLadderNextRound({
signal = null,
cooldownMs = CONTINUOUS_LADDER_COOLDOWN_MS,
readyCheckMs = CONTINUOUS_LADDER_READY_CHECK_MS,
onWaitStateChange = () => {},
}) {
if (!(cooldownMs >= 0)) throw new Error('Invalid continuous ladder cooldown');
if (!(readyCheckMs > 0)) throw new Error('Invalid continuous ladder readiness interval');
if (typeof onWaitStateChange !== 'function') {
throw new Error('Invalid continuous ladder wait-state callback');
}

let waitingAlreadyReported = false;
while (true) {
const readyState = await waitUntilReadyOrStopped({
readReadiness,
delay,
signal,
readyCheckMs,
cooldownMs,
onWaitStateChange,
waitingAlreadyReported,
});
if (readyState.status === 'stopped') return readyState;

waitingAlreadyReported = false;
onWaitStateChange({ phase: 'cooldown', cooldownMs });
await waitForPromiseOrAbort(delay(cooldownMs), signal);
throwIfAborted(signal);
const afterCooldown = assertReadinessState(readReadiness());
if (afterCooldown.status !== 'waiting') return afterCooldown;
onWaitStateChange({ phase: 'waiting_ready', cooldownMs });
waitingAlreadyReported = true;
}
}
16 changes: 14 additions & 2 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.157
// @version 2.7.158
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
Expand Down Expand Up @@ -80,6 +80,7 @@ import { keepInteractionFeedbackVisible } from './core/interaction-feedback.js';
import {
createContinuousLadderProgress,
formatContinuousLadderProgress,
formatContinuousLadderWaitProgress,
recordContinuousLadderRound,
waitForContinuousLadderNextRound,
} from './core/continuous-ladder.js';
Expand Down Expand Up @@ -2930,6 +2931,14 @@ import {
readReadiness: () => readContinuousLadderReadiness(actionType, actionSymbol),
delay,
signal: abortController.signal,
onWaitStateChange: ({ phase, cooldownMs }) => {
setLadderStatus(formatContinuousLadderWaitProgress(
spec.label,
continuousProgress,
phase,
cooldownMs,
));
},
});
if (readiness.status === 'stopped') {
setContinuousLadderProgressStatus(
Expand Down Expand Up @@ -5548,7 +5557,10 @@ import {
if (activeActionType !== actionType) {
return ladderActionButton(actionType, label, tone, disabled);
}
return `<button type="button" data-ladder-stop="true" data-ladder-action-origin="${actionType}" style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid var(--color-PrimaryYellow);border-radius:6px;background:var(--color-BadgeBg);color:#9a6700;font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:pointer;">${PANEL_COPY.action.stopLadderByAction[actionType]}</button>`;
const stopLabel = activeContinuousLadderActionType === actionType
? PANEL_COPY.action.stopContinuousLadderByAction[actionType]
: PANEL_COPY.action.stopLadderByAction[actionType];
return `<button type="button" data-ladder-stop="true" data-ladder-action-origin="${actionType}" style="height:${LADDER_CONTROL_BUTTON_HEIGHT}px;border:1px solid var(--color-PrimaryYellow);border-radius:6px;background:var(--color-BadgeBg);color:#9a6700;font-size:${LADDER_CONTROL_BUTTON_FONT_SIZE}px;font-weight:${CONTROL_FONT_WEIGHT};line-height:${LADDER_CONTROL_BUTTON_HEIGHT - 2}px;cursor:pointer;">${stopLabel}</button>`;
}

function getLadderControlSections(tradeMode, closeContext, symbol, precision) {
Expand Down
Loading
Loading