// ==UserScript==
// @name 【自写】Binance 订单簿单击下单
// @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.36
// @author jackhai9
// @description 单击订单簿价格,按当前开仓/平仓 tab 自动填数量并执行下单,内置数量倍率面板
// @match https://www.binance.com/*/futures/*
// @match https://www.binance.com/futures/*
// @exclude https://www.binance.com/*/my/wallet/futures/*
// @exclude https://www.binance.com/my/wallet/futures/*
// @updateURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-orderbook-trade.user.js
// @downloadURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-orderbook-trade.user.js
// @run-at document-start
// @grant none
// ==/UserScript==
(() => {
// src/binance-orderbook-trade/core/cancel-orders.js
function normalizeText(value) {
return String(value || "").replace(/\s+/g, " ").trim();
}
function isOpenOrdersTabText(text) {
const normalized = normalizeText(text);
return /^当前\s*委托(?:\(|\s|$)/.test(normalized) || /^Open Orders(?:\(|\s|$)/i.test(normalized);
}
function parseOpenOrdersTabCount(text) {
const normalized = normalizeText(text);
const match = /(?:当前\s*委托|Open Orders)\s*\(?\s*(\d+)\s*\)?/i.exec(normalized);
return match ? Number(match[1]) : null;
}
function escapeRegExp(value) {
return String(value).replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function normalizeContractCandidate(candidate, separator) {
const normalized = String(candidate || "").toUpperCase();
if (separator === ":") {
const timeJoinedMatch = /^\d{1,2}([A-Z][A-Z0-9]*USDT)$/.exec(normalized);
if (timeJoinedMatch) return timeJoinedMatch[1];
}
return normalized;
}
function isTimestampJoinedCandidate(candidate, symbol) {
const normalizedCandidate = String(candidate || "").toUpperCase();
const normalizedSymbol = String(symbol || "").toUpperCase();
if (!normalizedCandidate || !normalizedSymbol || !normalizedCandidate.endsWith(normalizedSymbol)) {
return false;
}
const prefix = normalizedCandidate.slice(0, -normalizedSymbol.length);
return /^\d{1,2}$/.test(prefix);
}
function hasVisibleContractText(text, symbol) {
const normalizedSymbol = String(symbol || "").toUpperCase();
if (!normalizedSymbol) return false;
const symbolPattern = escapeRegExp(normalizedSymbol);
return new RegExp(`(?:^|[^A-Z0-9]|\\d{1,2}:\\d{2})${symbolPattern}\\s*永续`, "i").test(String(text || ""));
}
function readVisibleOpenOrderSymbolsText(text) {
const normalized = String(text || "").toUpperCase();
const symbols = /* @__PURE__ */ new Set();
const pattern = /([A-Z0-9]{2,30}USDT)\s*永续/g;
let match = pattern.exec(normalized);
while (match) {
const separator = normalized[match.index - 1] || "";
if (!/[A-Z0-9]/.test(separator)) {
symbols.add(normalizeContractCandidate(match[1], separator));
}
match = pattern.exec(normalized);
}
return Array.from(symbols);
}
function isOpenOrdersScopeLimitedToSymbolText(text, symbol) {
const normalizedSymbol = String(symbol || "").toUpperCase();
if (!normalizedSymbol) return false;
const visibleSymbols = readVisibleOpenOrderSymbolsText(text);
return visibleSymbols.length > 0 && visibleSymbols.every((visibleSymbol) => visibleSymbol === normalizedSymbol || hasVisibleContractText(text, normalizedSymbol) && isTimestampJoinedCandidate(visibleSymbol, normalizedSymbol));
}
function hasCurrentSymbolOpenOrdersEvidence({
scopeText,
symbol,
symbolFilterOk,
openOrdersCount,
cancelAllAvailable
}) {
const normalizedSymbol = String(symbol || "").toUpperCase();
if (!normalizedSymbol) return false;
const visibleSymbols = readVisibleOpenOrderSymbolsText(scopeText);
if (visibleSymbols.some((visibleSymbol) => visibleSymbol === normalizedSymbol || hasVisibleContractText(scopeText, normalizedSymbol) && isTimestampJoinedCandidate(visibleSymbol, normalizedSymbol))) return true;
if (visibleSymbols.length > 0) return false;
return Boolean(symbolFilterOk && (openOrdersCount !== null && openOrdersCount > 0 || cancelAllAvailable));
}
// src/binance-orderbook-trade/core/decimal.js
function pow10(exp) {
let result = 1n;
for (let i = 0; i < exp; i += 1) result *= 10n;
return result;
}
function parseDecimalString(value) {
const raw = String(value || "").replace(/,/g, "").trim();
if (!/^\d+(\.\d+)?$/.test(raw)) return null;
const [intPart, fracPart = ""] = raw.split(".");
return {
digits: BigInt(intPart + fracPart),
scale: fracPart.length
};
}
function formatDecimalParts(digits, scale) {
const negative = digits < 0n;
const absDigits = negative ? -digits : digits;
const raw = absDigits.toString();
if (scale === 0) return `${negative ? "-" : ""}${raw}`;
const padded = raw.padStart(scale + 1, "0");
const head = padded.slice(0, -scale) || "0";
const tail = padded.slice(-scale).replace(/0+$/, "");
return `${negative ? "-" : ""}${tail ? `${head}.${tail}` : head}`;
}
function normalizeDecimalString(value) {
const parsed = parseDecimalString(value);
return parsed ? formatDecimalParts(parsed.digits, parsed.scale) : null;
}
function compareDecimalStrings(a, b) {
const left = parseDecimalString(a);
const right = parseDecimalString(b);
if (!left || !right) return null;
const scale = Math.max(left.scale, right.scale);
const leftDigits = left.digits * pow10(scale - left.scale);
const rightDigits = right.digits * pow10(scale - right.scale);
if (leftDigits === rightDigits) return 0;
return leftDigits > rightDigits ? 1 : -1;
}
function addDecimalStrings(a, b) {
const left = parseDecimalString(a);
const right = parseDecimalString(b);
if (!left || !right) return null;
const scale = Math.max(left.scale, right.scale);
const leftDigits = left.digits * pow10(scale - left.scale);
const rightDigits = right.digits * pow10(scale - right.scale);
return formatDecimalParts(leftDigits + rightDigits, scale);
}
function subtractDecimalStrings(a, b) {
const left = parseDecimalString(a);
const right = parseDecimalString(b);
if (!left || !right) return null;
const scale = Math.max(left.scale, right.scale);
const leftDigits = left.digits * pow10(scale - left.scale);
const rightDigits = right.digits * pow10(scale - right.scale);
if (leftDigits < rightDigits) return null;
return formatDecimalParts(leftDigits - rightDigits, scale);
}
function maxDecimalString(a, b) {
if (!a) return normalizeDecimalString(b);
if (!b) return normalizeDecimalString(a);
const cmp = compareDecimalStrings(a, b);
if (cmp == null) return normalizeDecimalString(a) || normalizeDecimalString(b);
return cmp >= 0 ? normalizeDecimalString(a) : normalizeDecimalString(b);
}
function ceilQtyByNotional(notional, price, stepSize) {
const n = parseDecimalString(notional);
const p = parseDecimalString(price);
const s = parseDecimalString(stepSize);
if (!n || !p || !s || p.digits <= 0n || s.digits <= 0n) return null;
let numerator = n.digits;
let denominator = p.digits * s.digits;
const exp = p.scale + s.scale - n.scale;
if (exp >= 0) {
numerator *= pow10(exp);
} else {
denominator *= pow10(-exp);
}
const steps = (numerator + denominator - 1n) / denominator;
return formatDecimalParts(steps * s.digits, s.scale);
}
function multiplyDecimalByInt(decimalValue, intValue) {
const raw = String(decimalValue || "").trim();
const multiplier = String(intValue || "").trim();
if (!/^\d+(\.\d+)?$/.test(raw)) return null;
if (!/^\d+$/.test(multiplier) || Number(multiplier) <= 0) return null;
const parts = raw.split(".");
const intPart = parts[0];
const fracPart = parts[1] || "";
const scale = fracPart.length;
const base = BigInt(intPart + fracPart);
const multi = BigInt(multiplier);
const product = (base * multi).toString();
if (scale === 0) return product;
const padded = product.padStart(scale + 1, "0");
const head = padded.slice(0, -scale) || "0";
const tail = padded.slice(-scale).replace(/0+$/, "");
return tail ? `${head}.${tail}` : head;
}
function multiplyDecimalByRatio(decimalValue, numerator, denominator) {
const parsed = parseDecimalString(decimalValue);
const num = parseDecimalString(numerator);
const den = parseDecimalString(denominator);
if (!parsed || !num || !den || num.digits <= 0n || den.digits <= 0n) return null;
const denominatorIntegerDigits = Math.max(0, den.digits.toString().length - den.scale);
const resultScale = parsed.scale + num.scale + Math.max(0, denominatorIntegerDigits - 1);
let scaledNumerator = parsed.digits * num.digits;
let scaledDenominator = den.digits;
const scaleExp = den.scale + resultScale - parsed.scale - num.scale;
if (scaleExp >= 0) {
scaledNumerator *= pow10(scaleExp);
} else {
scaledDenominator *= pow10(-scaleExp);
}
const digits = scaledNumerator / scaledDenominator;
return formatDecimalParts(digits, resultScale);
}
function isPositiveDecimalString(value) {
const parsed = parseDecimalString(value);
return !!parsed && parsed.digits > 0n;
}
// src/binance-orderbook-trade/core/precision.js
function collectNonZeroPriceMoves(prices) {
const moves = [];
let previous = null;
for (const price of prices) {
const current = normalizeDecimalString(price);
if (!current) continue;
if (previous) {
const diff = subtractDecimalStrings(current, previous) || subtractDecimalStrings(previous, current);
const normalizedDiff = normalizeDecimalString(diff);
if (normalizedDiff && isPositiveDecimalString(normalizedDiff)) moves.push(normalizedDiff);
}
previous = current;
}
return moves;
}
function mergePrecisionSamples(existingSamples, newSamples, maxSamples = 64) {
const merged = [...existingSamples || [], ...newSamples || []].map((sample) => normalizeDecimalString(sample)).filter((sample) => sample && isPositiveDecimalString(sample));
return merged.slice(Math.max(0, merged.length - maxSamples));
}
function sortedPositiveDecimals(values) {
return (values || []).map((value) => normalizeDecimalString(value)).filter((value) => value && isPositiveDecimalString(value)).sort((a, b) => compareDecimalStrings(a, b));
}
function logDistance(a, b) {
const left = Number(a);
const right = Number(b);
if (!Number.isFinite(left) || !Number.isFinite(right) || left <= 0 || right <= 0) return Number.POSITIVE_INFINITY;
return Math.abs(Math.log10(left) - Math.log10(right));
}
function closestPrecisionOption(sample, options) {
let bestOption = null;
let bestDistance = Number.POSITIVE_INFINITY;
for (const option of options) {
const distance = logDistance(sample, option);
if (distance < bestDistance) {
bestDistance = distance;
bestOption = option;
}
}
return bestOption;
}
function recommendOrderbookPrecision({
samples,
options,
minSamples = 5,
minBucketShare = 0.25
}) {
const usableSamples = sortedPositiveDecimals(samples);
const usableOptions = sortedPositiveDecimals(options);
if (!usableOptions.length) return null;
if (usableSamples.length < minSamples) return null;
const bucketCounts = new Map(usableOptions.map((option) => [option, 0]));
for (const sample of usableSamples) {
const option = closestPrecisionOption(sample, usableOptions);
if (option) bucketCounts.set(option, (bucketCounts.get(option) || 0) + 1);
}
const minimumBucketCount = Math.max(minSamples, Math.ceil(usableSamples.length * minBucketShare));
let selectedOption = null;
let selectedCount = 0;
for (const option of usableOptions) {
const count = bucketCounts.get(option) || 0;
if (count < minimumBucketCount) continue;
if (count > selectedCount || count === selectedCount && selectedOption && compareDecimalStrings(option, selectedOption) < 0) {
selectedOption = option;
selectedCount = count;
}
}
return selectedOption;
}
// src/binance-orderbook-trade/core/quantity.js
function pow102(exp) {
let result = 1n;
for (let i = 0; i < exp; i += 1) result *= 10n;
return result;
}
function decimalToStepCount(decimalValue, stepSize, rounding = "floor") {
const value = parseDecimalString(decimalValue);
const step = parseDecimalString(stepSize);
if (!value || !step || step.digits <= 0n) return null;
const scale = Math.max(value.scale, step.scale);
const valueDigits = value.digits * pow102(scale - value.scale);
const stepDigits = step.digits * pow102(scale - step.scale);
if (rounding === "ceil") return (valueDigits + stepDigits - 1n) / stepDigits;
return valueDigits / stepDigits;
}
function formatStepCount(stepCount, stepSize) {
const step = parseDecimalString(stepSize);
if (!step || step.digits <= 0n || stepCount == null || stepCount < 0n) return null;
return formatDecimalParts(stepCount * step.digits, step.scale);
}
function allocateLadderQuantities(totalQty, desiredLevels, stepSize, minRequiredQty) {
const totalSteps = decimalToStepCount(totalQty, stepSize, "floor");
const minSteps = decimalToStepCount(minRequiredQty, stepSize, "ceil");
const requestedLevels = Number(desiredLevels);
if (!totalSteps || !minSteps || totalSteps <= 0n || minSteps <= 0n || requestedLevels <= 0) {
return null;
}
const maxExecutableLevels = totalSteps / minSteps;
const actualLevels = Math.min(requestedLevels, Number(maxExecutableLevels));
if (actualLevels < 1) return null;
const levelCount = BigInt(actualLevels);
const baseSteps = totalSteps / levelCount;
if (baseSteps < minSteps) return null;
const quantities = [];
let remainingSteps = totalSteps;
for (let i = 0; i < actualLevels; i += 1) {
const isLast = i === actualLevels - 1;
const steps = isLast ? remainingSteps : baseSteps;
if (steps < minSteps) {
if (quantities.length === 0) return null;
const previous = decimalToStepCount(quantities.pop(), stepSize, "floor");
const merged = previous + steps;
if (merged < minSteps) return null;
quantities.push(formatStepCount(merged, stepSize));
remainingSteps = 0n;
break;
}
quantities.push(formatStepCount(steps, stepSize));
remainingSteps -= steps;
}
return {
requestedLevels,
actualLevels: quantities.length,
totalQty: formatStepCount(totalSteps, stepSize),
quantities
};
}
// src/binance-orderbook-trade/core/ladder-plan.js
var LADDER_ACTION_SPECS = {
OPEN_LONG: {
mode: "OPEN",
label: "阶梯开多",
priceSide: "BID",
orderSide: "BUY",
side: "LONG"
},
OPEN_SHORT: {
mode: "OPEN",
label: "阶梯开空",
priceSide: "ASK",
orderSide: "SELL",
side: "SHORT"
},
CLOSE_LONG: {
mode: "CLOSE",
label: "阶梯平多",
priceSide: "ASK",
orderSide: "SELL",
side: "LONG"
},
CLOSE_SHORT: {
mode: "CLOSE",
label: "阶梯平空",
priceSide: "BID",
orderSide: "BUY",
side: "SHORT"
}
};
function getLadderActionSpec(actionType) {
const spec = LADDER_ACTION_SPECS[actionType];
return spec ? { ...spec } : null;
}
function getLadderPercentForMode(mode, openPercent, closePercent) {
if (mode === "OPEN") return openPercent;
if (mode === "CLOSE") return closePercent;
return null;
}
function pow103(exp) {
let result = 1n;
for (let i = 0; i < exp; i += 1) result *= 10n;
return result;
}
function computeMinimumLadderPercent(baseQty, minRequiredQty, levels, stepSize) {
const base = parseDecimalString(baseQty);
const requestedLevels = Number(levels);
const minSteps = decimalToStepCount(minRequiredQty, stepSize, "ceil");
if (!base || base.digits <= 0n || !minSteps || minSteps <= 0n || requestedLevels <= 0) return null;
const requiredQty = formatStepCount(minSteps * BigInt(requestedLevels), stepSize);
const required = parseDecimalString(requiredQty);
if (!required || required.digits <= 0n) return null;
const numerator = required.digits * 100n * pow103(base.scale + 2);
const denominator = base.digits * pow103(required.scale);
const scaledPercent = (numerator + denominator - 1n) / denominator;
return formatDecimalParts(scaledPercent, 2);
}
function getMinRequiredQtyForLevels(minRequiredQty, minRequiredQtyByLevel, levels) {
if (!Array.isArray(minRequiredQtyByLevel) || minRequiredQtyByLevel.length === 0) return minRequiredQty;
const candidateMinRequiredQty = minRequiredQtyByLevel.slice(0, levels).filter(Boolean).reduce((maxQty, qty) => maxDecimalString(maxQty, qty), null);
return candidateMinRequiredQty || minRequiredQty;
}
function fitLadderPlanForMinimumQty(options) {
const { baseQty, minRequiredQty, minRequiredQtyByLevel, percent, levels, stepSize, maxPercent } = options;
const requestedLevels = Number(levels);
let minimumPercent = null;
if (!maxPercent || !Number.isInteger(requestedLevels) || requestedLevels <= 0) {
return { allocation: null, minimumPercent, maxPercent };
}
for (let candidateLevels = requestedLevels; candidateLevels >= 1; candidateLevels -= 1) {
const candidateMinRequiredQty = getMinRequiredQtyForLevels(minRequiredQty, minRequiredQtyByLevel, candidateLevels);
const candidatePercent = computeMinimumLadderPercent(baseQty, candidateMinRequiredQty, candidateLevels, stepSize);
if (candidateLevels === requestedLevels) minimumPercent = candidatePercent;
if (!candidatePercent || compareDecimalStrings(candidatePercent, maxPercent) > 0) continue;
const fitPercent = compareDecimalStrings(candidatePercent, percent) > 0 ? candidatePercent : String(percent);
const fitTotalQty = multiplyDecimalByRatio(baseQty, fitPercent, 100);
const allocation = allocateLadderQuantities(fitTotalQty, candidateLevels, stepSize, candidateMinRequiredQty);
if (allocation && allocation.actualLevels >= candidateLevels) {
return {
allocation,
levels: candidateLevels,
minRequiredQty: candidateMinRequiredQty,
minimumPercent,
maxPercent,
percent: fitPercent
};
}
}
return { allocation: null, minimumPercent, maxPercent };
}
// src/binance-orderbook-trade/core/order-feedback.js
function isPotentialOrderFeedbackText(text) {
if (!text) return false;
return /订单|委托|下单|已提交|已下单|不足|拒绝|过期|order|placed|submitted|failed|rejected|error|insufficient|失败/i.test(text);
}
function classifyOrderFeedback(text) {
if (!text) return "none";
if (/失败|拒绝|错误|不足|过期|取消|failed|rejected|error|insufficient/i.test(text)) return "failure";
if (/已提交|已下单|委托已|order placed|submitted|placed/i.test(text) || /(订单|委托|下单|order)/i.test(text) && /成功|success/i.test(text)) {
return "success";
}
return "unknown";
}
function evaluateOrderSubmitAcknowledgement({ feedback, isNewFeedback }) {
if (!feedback || !isNewFeedback) return { status: "pending" };
const feedbackType = classifyOrderFeedback(feedback);
if (feedbackType === "failure") return { status: "failure", message: feedback };
if (feedbackType === "success") return { status: "success" };
return { status: "pending" };
}
function isReduceOnlyOpenOrdersConflictFeedback(text) {
if (!text) return false;
const normalized = String(text).replace(/\s+/g, "");
return normalized.includes("只减仓订单失败") && (normalized.includes("当前挂单") || normalized.includes("挂单后重试") || normalized.includes("未平仓头寸和挂单"));
}
function isOpenLadderOpenOrdersCapacityFeedback(text) {
if (!text) return false;
const normalized = String(text).replace(/\s+/g, "").toLowerCase();
const hasCapacityFailure = normalized.includes("余额不足") || normalized.includes("可用余额不足") || normalized.includes("可用数量不足") || normalized.includes("可开数量不足") || normalized.includes("可用保证金不足") || normalized.includes("insufficientmargin") || normalized.includes("insufficientbalance") || normalized.includes("insufficientavailablebalance") || normalized.includes("notenoughmargin") || normalized.includes("notenoughbalance") || normalized.includes("notenoughavailablebalance");
const hasOpenOrdersHint = normalized.includes("当前挂单") || normalized.includes("取消挂单") || normalized.includes("挂单后重试") || normalized.includes("openorders") || normalized.includes("existingopenorders");
return hasCapacityFailure && hasOpenOrdersHint;
}
// src/shared/binance-futures-route.js
var FUTURES_TRADING_PATH_RE = /^\/(?:[a-z]{2}(?:-[A-Za-z]{2})?\/)?futures\/([A-Z0-9_]{3,})\/?$/;
function parseFuturesTradingSymbolFromPathname(pathname) {
const normalized = String(pathname || "").split(/[?#]/, 1)[0];
const match = normalized.match(FUTURES_TRADING_PATH_RE);
return match?.[1] ? match[1].toUpperCase() : null;
}
function isFuturesTradingPathname(pathname) {
return Boolean(parseFuturesTradingSymbolFromPathname(pathname));
}
// src/binance-orderbook-trade/dom/account-orders.js
function getNormalizedText(el) {
return normalizeText(el?.textContent || "");
}
function hasAccountOrdersTabs(node, isVisibleElement) {
const tabTexts = Array.from(node.querySelectorAll('[role="tab"]')).filter(isVisibleElement).map(getNormalizedText).join(" ");
return /(仓位|Positions)/i.test(tabTexts) && /(当前\s*委托|Open Orders)/i.test(tabTexts) && /(历史委托|Order History|历史成交|Trade History|资金流水|Transaction)/i.test(tabTexts);
}
function containsNestedAccountOrdersGroupOutsideTab(node, tab, isVisibleElement) {
return Array.from(node.children).some((child) => !child.contains(tab) && hasAccountOrdersTabs(child, isVisibleElement));
}
function hasOpenOrdersPanelText(node) {
return /(基础单|条件委托|Open Orders|成交数量|只减仓|只做Maker|生效时间|追单)/i.test(getNormalizedText(node));
}
function hasOpenOrdersPanelEvidence(node, {
findHideOtherSymbolCheckbox,
findCurrentSymbolCancelAllButton
}) {
if (findCurrentSymbolCancelAllButton(node)) return true;
return Boolean(findHideOtherSymbolCheckbox(node) && hasOpenOrdersPanelText(node));
}
function isOpenOrdersBasicSubTabText(text) {
return /^(基础单|Basic Orders?)(?:\(|\s|$)/i.test(normalizeText(text));
}
function isOpenOrdersConditionalSubTabText(text) {
return /^(条件委托|Conditional Orders?)(?:\(|\s|$)/i.test(normalizeText(text));
}
function findOpenOrdersBasicSubTab(root, { isVisibleElement }) {
return Array.from(root.querySelectorAll('[role="tab"]')).find((tab) => isVisibleElement(tab) && isOpenOrdersBasicSubTabText(getNormalizedText(tab))) || null;
}
function findOpenOrdersConditionalSubTab(root, { isVisibleElement }) {
return Array.from(root.querySelectorAll('[role="tab"]')).find((tab) => isVisibleElement(tab) && isOpenOrdersConditionalSubTabText(getNormalizedText(tab))) || null;
}
function findSelectedOpenOrdersSubTab(root, { isVisibleElement }) {
return Array.from(root.querySelectorAll('[role="tab"][aria-selected="true"]')).find((tab) => isVisibleElement(tab) && (isOpenOrdersBasicSubTabText(getNormalizedText(tab)) || isOpenOrdersConditionalSubTabText(getNormalizedText(tab)))) || null;
}
function isAccountOrdersTab(tab, { isVisibleElement }) {
let node = tab.parentElement;
let depth = 0;
while (node && node !== tab.ownerDocument.body && depth < 5) {
if (hasAccountOrdersTabs(node, isVisibleElement) && !containsNestedAccountOrdersGroupOutsideTab(node, tab, isVisibleElement)) {
return true;
}
node = node.parentElement;
depth += 1;
}
return false;
}
function getAccountOrdersTabGroup(tab, { isVisibleElement }) {
let node = tab?.parentElement;
let depth = 0;
while (node && node !== tab.ownerDocument.body && depth < 5) {
if (hasAccountOrdersTabs(node, isVisibleElement) && !containsNestedAccountOrdersGroupOutsideTab(node, tab, isVisibleElement)) {
return node;
}
node = node.parentElement;
depth += 1;
}
return null;
}
function findOpenOrdersTab(root, { isVisibleElement }) {
const tabs = Array.from(root.querySelectorAll('[role="tab"]')).filter((tab) => isVisibleElement(tab) && isOpenOrdersTabText(getNormalizedText(tab)));
return tabs.find((tab) => isAccountOrdersTab(tab, { isVisibleElement })) || tabs[0] || null;
}
function findSelectedAccountOrdersTab(root, { isVisibleElement }) {
const openOrdersTab = findOpenOrdersTab(root, { isVisibleElement });
if (!openOrdersTab) return null;
const tabGroup = getAccountOrdersTabGroup(openOrdersTab, { isVisibleElement });
if (!tabGroup) return null;
return Array.from(tabGroup.querySelectorAll('[role="tab"][aria-selected="true"]')).filter(isVisibleElement)[0] || null;
}
function getActiveOpenOrdersScope(root, {
isVisibleElement,
findHideOtherSymbolCheckbox,
findCurrentSymbolCancelAllButton
}) {
const tab = findOpenOrdersTab(root, { isVisibleElement });
if (!tab || tab.getAttribute("aria-selected") !== "true") return null;
const doc = root.ownerDocument || root;
const paneId = tab.getAttribute("aria-controls");
const pane = paneId ? doc.getElementById(paneId) : null;
if (pane && isVisibleElement(pane) && hasOpenOrdersPanelEvidence(pane, {
findHideOtherSymbolCheckbox,
findCurrentSymbolCancelAllButton
})) {
return pane;
}
let node = tab.parentElement;
let depth = 0;
while (node && node !== doc.body && depth < 8) {
if (hasOpenOrdersPanelEvidence(node, {
findHideOtherSymbolCheckbox,
findCurrentSymbolCancelAllButton
})) {
return node;
}
node = node.parentElement;
depth += 1;
}
return null;
}
// src/binance-orderbook-trade/dom/trade-form.js
function buttonTextMatches(button, patterns) {
const text = (button?.textContent || "").trim().toLowerCase();
return patterns.some((pattern) => text.includes(pattern));
}
function isOwnPanelButton(button, panelId) {
return !!button?.closest?.(`#${panelId}`);
}
function isTradeModeTab(node, { panelId }) {
if (!node?.matches?.('[role="tab"]')) return false;
if (node.closest(`#${panelId}`)) return false;
if (!node.matches('#position-direction [role="tab"], .bn-tabs__buySell [role="tab"], [role="tab"].bn-tab__buySell')) {
return false;
}
const text = (node.textContent || "").trim();
return text.includes("开仓") || text.includes("平仓");
}
function isTradeActionButton(node, { panelId }) {
if (!node?.matches) return false;
const button = node.matches("button") ? node : node.closest("button");
if (!button || isOwnPanelButton(button, panelId)) return false;
return buttonTextMatches(button, [
"开多",
"open long",
"开空",
"open short",
"平多",
"close long",
"平空",
"close short"
]);
}
function collectTradeButtonsFromScopes(scopes, mode, {
panelId,
isVisibleElement
}) {
const modePatterns = mode === "OPEN" ? ["开多", "open long", "开空", "open short"] : ["平多", "close long", "平空", "close short"];
const buttons = [];
const seen = /* @__PURE__ */ new Set();
const collectFrom = (scope) => {
if (!scope) return;
for (const candidate of scope.querySelectorAll("button")) {
if (seen.has(candidate) || isOwnPanelButton(candidate, panelId) || !isVisibleElement(candidate)) continue;
seen.add(candidate);
if (buttonTextMatches(candidate, modePatterns)) buttons.push(candidate);
}
};
for (const scope of scopes) collectFrom(scope);
return buttons;
}
// src/binance-orderbook-trade/core/orderbook.js
function inferOrderbookDisplayStep(prices) {
let displayStep = null;
for (let i = 1; i < prices.length; i += 1) {
const prev = prices[i - 1];
const current = prices[i];
let diff = subtractDecimalStrings(current, prev) || subtractDecimalStrings(prev, current);
diff = normalizeDecimalString(diff);
if (!diff || !isPositiveDecimalString(diff)) continue;
if (!displayStep || compareDecimalStrings(diff, displayStep) < 0) displayStep = diff;
}
return displayStep;
}
function calculateDisplayStepPrice(bestPrice, displayStep, side, offsetRows) {
let price = bestPrice;
for (let i = 0; i < offsetRows; i += 1) {
price = side === "ASK" ? addDecimalStrings(price, displayStep) : subtractDecimalStrings(price, displayStep);
if (!price || !isPositiveDecimalString(price)) return null;
}
return price;
}
function planBufferedMakerPrices({
prices,
side,
levels,
ladderStep,
bufferLevels = 1,
defaultStep = 1,
minStep = 1,
maxStep = 5
}) {
const step = Math.max(minStep, Math.min(Number(ladderStep) || defaultStep, maxStep));
const bestPrice = prices[0] || null;
const displayStep = inferOrderbookDisplayStep(prices);
const result = [];
for (let i = 0; i < levels; i += 1) {
const offsetRows = bufferLevels + i * step;
const price = prices[offsetRows] || (bestPrice && displayStep ? calculateDisplayStepPrice(bestPrice, displayStep, side, offsetRows) : null);
if (price) result.push(price);
}
return result;
}
// src/binance-orderbook-trade/index.user.js
(function() {
"use strict";
function isFuturesTradingPage() {
return isFuturesTradingPathname(location.pathname);
}
if (!isFuturesTradingPage()) return;
const CFG = {
// true=只填数量;false=填数量并自动点“开多/开空/平多/平空”
SAFE_MODE: false,
// Only suppress duplicate dispatch from the same physical click, not deliberate fast clicks.
COOLDOWN_MS: 150,
DEBUG: false
};
const LOCAL_QTY_MULTIPLIER_PREFIX = "jh_binance_qty_multiplier_v2";
const LOCAL_CLOSE_SIDE_KEY = "jh_binance_close_side";
const LOCAL_OPEN_SIDE_KEY = "jh_binance_open_side";
const LOCAL_LADDER_EXPANDED_KEY = "jh_binance_ladder_expanded";
const LOCAL_LADDER_OPEN_PERCENT_KEY = "jh_binance_ladder_open_percent";
const LOCAL_LADDER_CLOSE_PERCENT_KEY = "jh_binance_ladder_close_percent";
const LOCAL_LADDER_LEVELS_KEY = "jh_binance_ladder_levels";
const LOCAL_LADDER_STEP_KEY = "jh_binance_ladder_step";
const LOCAL_ORDERBOOK_PRECISION_SAMPLES_PREFIX = "jh_binance_orderbook_precision_samples_v3";
const BINANCE_PERSIST_KEY = "persist:futures-trade-ui";
const BINANCE_POST_ONLY_ORDER_TYPE = "POST_ONLY";
const BINANCE_POST_ONLY_TIME_IN_FORCE = "GTC";
const PANEL_ID = "jh-binance-close-qty-multiplier-panel";
const SPACER_ID = "jh-binance-close-qty-multiplier-spacer";
const INPUT_ID = "jh-binance-close-qty-multiplier-input";
const DEC_ID = "jh-binance-close-qty-multiplier-dec";
const INC_ID = "jh-binance-close-qty-multiplier-inc";
const SIDE_LONG_ID = "jh-binance-close-side-long";
const SIDE_SHORT_ID = "jh-binance-close-side-short";
const LADDER_TOGGLE_ID = "jh-binance-ladder-toggle";
const LADDER_BODY_ID = "jh-binance-ladder-body";
const LADDER_STATUS_ID = "jh-binance-ladder-status";
const ORDERBOOK_PRECISION_RECOMMENDATION_ID = "jh-binance-orderbook-precision-recommendation";
const DEFAULT_MULTIPLIER = "1";
const DEFAULT_CLOSE_SIDE = "LONG";
const DEFAULT_OPEN_SIDE = "LONG";
const DEFAULT_LADDER_OPEN_PERCENT = 2;
const DEFAULT_LADDER_CLOSE_PERCENT = 0.3;
const DEFAULT_LADDER_LEVELS = 5;
const DEFAULT_LADDER_STEP = 1;
const LADDER_OPEN_PERCENTS = [2, 10, 30, 50, 70];
const LADDER_CLOSE_PERCENTS = [0.3, 1, 5, 10, 30, 100];
const LADDER_LEVEL_OPTIONS = [3, 5, 7, 9];
const LADDER_STEP_MIN = 1;
const LADDER_STEP_MAX = 5;
const LADDER_ORDER_DELAY_MS = 520;
const LADDER_SUBMIT_ACK_TIMEOUT_MS = 3500;
const LADDER_SUBMIT_POLL_MS = 80;
const LADDER_REPLACE_OPEN_ORDERS_CLEAR_TIMEOUT_MS = 6500;
const LADDER_MAKER_BUFFER_LEVELS = 1;
const LADDER_OPEN_QTY_READY_TIMEOUT_MS = 1200;
const LADDER_OPEN_QTY_POLL_MS = 80;
const SINGLE_ORDER_PRICE_SYNC_DELAY_MS = 90;
const SINGLE_ORDER_QTY_SYNC_DELAY_MS = 120;
const ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS = 6e3;
const ORDERBOOK_PRECISION_SAMPLE_DURATION_MS = ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS;
const ORDERBOOK_PRECISION_SAMPLE_POLL_MS = 300;
const ORDERBOOK_PRECISION_READY_TIMEOUT_MS = 5e3;
const ORDERBOOK_PRECISION_OPTION_WAIT_MS = 1200;
const ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS = 6;
const ORDERBOOK_PRECISION_SAMPLE_MAX = 96;
const ORDERBOOK_PRECISION_CANDIDATE_OPTIONS = [
"0.00000001",
"0.0000001",
"0.000001",
"0.00001",
"0.0001",
"0.001",
"0.01",
"0.1",
"1",
"10",
"100",
"1000"
];
const DEFAULT_OPEN_LEVERAGE = 3;
const AUTO_OPEN_LEVERAGE_DELAY_MS = 120;
const AUTO_OPEN_LEVERAGE_DEDUPE_MS = 1200;
const DOM_LOOKUP_CACHE_MS = 250;
const INPUT_BORDER_COLOR = "var(--color-InputLine)";
const INPUT_ERROR_COLOR = "var(--color-Error)";
const INPUT_FOCUS_COLOR = "var(--color-PrimaryYellow)";
const INPUT_DEFAULT_BG = "transparent";
const DISABLED_CONTROL_BORDER = "#d5d9e2";
const DISABLED_CONTROL_BG = "#f5f5f5";
const DISABLED_CONTROL_TEXT = "#b7bdc6";
const DISABLED_CONTROL_OPACITY = "0.65";
const LADDER_CONTROL_BUTTON_HEIGHT = 32;
const LADDER_CONTROL_BUTTON_FONT_SIZE = 14;
const PANEL_BOTTOM_TOOLTIP_GAP = 12;
let lastTs = 0;
let isEditingMultiplier = false;
let renderPanelQueued = false;
let renderPanelFollowUpTimer = 0;
let tradeUiMutationObserver = null;
let tradeUiMutationTimeout = 0;
let tradeUiMutationDebounceTimer = 0;
let tradeModeTabObserver = null;
let tradeModeTabObserverRoot = null;
let lastConfirmedCloseState = null;
let lastDisplayCloseState = null;
let closeGuard = null;
let lastAppliedCacheSnapshot = "";
let autoOpenLeverageTask = null;
let lastAutoOpenLeverage = { symbol: null, at: 0 };
let tradeButtonCache = { mode: null, expiresAt: 0, buttons: [] };
let tradeScopeCache = { activeTab: null, expiresAt: 0, scopes: [] };
let ladderTask = null;
let ladderStopRequested = false;
let ladderStatusText = "空闲";
let ladderPanelBodySignature = "";
let orderbookPrecisionSampling = false;
let orderbookPrecisionSampleTimer = 0;
let orderbookPrecisionResampleRequested = false;
let orderbookPrecisionResampleDurationMs = ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS;
const orderbookPrecisionInitialSampledSymbols = /* @__PURE__ */ new Set();
let orderbookPrecisionState = {
symbol: null,
samples: [],
recommendation: null,
current: null,
status: "采样中",
sampleEndsAt: 0
};
const controlledNativeButtons = /* @__PURE__ */ new Set();
const MODE_HINT_ID = "jh-binance-trade-mode-hint";
const NATIVE_ACTION_DISABLED_ATTR = "data-jh-native-action-disabled";
const PREFIX = "[订单簿下单]";
(function injectNativeDisabledStyle() {
const styleId = "jh-native-action-disabled-style";
if (document.getElementById(styleId)) return;
const style = document.createElement("style");
style.id = styleId;
style.textContent = `
button[${NATIVE_ACTION_DISABLED_ATTR}="true"] {
background: ${DISABLED_CONTROL_BG} !important;
color: ${DISABLED_CONTROL_TEXT} !important;
border-color: ${DISABLED_CONTROL_BORDER} !important;
opacity: ${DISABLED_CONTROL_OPACITY} !important;
cursor: not-allowed !important;
pointer-events: none !important;
}
`;
(document.head || document.documentElement).appendChild(style);
})();
function emit(level, ...args) {
if (!CFG.DEBUG && level !== "ERR") return;
console.error(PREFIX, `[${level}]`, ...args);
}
function log(...args) {
emit("LOG", ...args);
}
function warn(...args) {
emit("WARN", ...args);
}
function err(...args) {
emit("ERR", ...args);
}
function parseJsonSafe(raw) {
if (!raw || typeof raw !== "string") return null;
try {
return JSON.parse(raw);
} catch (_e) {
return null;
}
}
function parsePersistedField(state, key) {
const value = state?.[key];
if (typeof value === "string") return parseJsonSafe(value) || {};
return value && typeof value === "object" ? value : {};
}
function readPersistedBinanceOrderForm() {
try {
const state = parseJsonSafe(window.localStorage?.getItem(BINANCE_PERSIST_KEY));
return parsePersistedField(state, "futuresOrderForm");
} catch (_e) {
return {};
}
}
function isPersistedPostOnlyOrderType() {
const form = readPersistedBinanceOrderForm();
return form.orderType === BINANCE_POST_ONLY_ORDER_TYPE && form.subOrderType === BINANCE_POST_ONLY_ORDER_TYPE;
}
function ensurePostOnlyPreferencePersisted() {
try {
const raw = window.localStorage?.getItem(BINANCE_PERSIST_KEY);
const state = parseJsonSafe(raw) || {};
const form = parsePersistedField(state, "futuresOrderForm");
const nextForm = {
...form,
orderType: BINANCE_POST_ONLY_ORDER_TYPE,
subOrderType: BINANCE_POST_ONLY_ORDER_TYPE,
timeInForce: BINANCE_POST_ONLY_TIME_IN_FORCE
};
if (form.orderType === nextForm.orderType && form.subOrderType === nextForm.subOrderType && form.timeInForce === nextForm.timeInForce) {
return { ok: true, changed: false };
}
const nextState = {
...state,
futuresOrderForm: JSON.stringify(nextForm),
_persist: state._persist || JSON.stringify({ version: 1, rehydrated: true })
};
window.localStorage?.setItem(BINANCE_PERSIST_KEY, JSON.stringify(nextState));
return { ok: true, changed: true };
} catch (e) {
return { ok: false, changed: false, error: e };
}
}
const postOnlyPreferenceInit = ensurePostOnlyPreferencePersisted();
if (!postOnlyPreferenceInit.ok) {
warn("无法写入只做Maker偏好", postOnlyPreferenceInit.error);
} else if (postOnlyPreferenceInit.changed) {
log("已写入只做Maker偏好,Binance 会在页面初始化时读取");
}
function setInputValueReact(input, value) {
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
setter?.call(input, value);
input.dispatchEvent(new Event("input", { bubbles: true }));
input.dispatchEvent(new Event("change", { bubbles: true }));
input.dispatchEvent(new Event("blur", { bubbles: true }));
}
function delay(ms) {
return new Promise((resolve) => {
window.setTimeout(resolve, ms);
});
}
function isValidOption(value, options) {
const num = Number(value);
return options.includes(num);
}
function ladderOptionStorageKey(baseKey, symbol = getCurrentSymbol()) {
const normalizedSymbol = String(symbol || "").toUpperCase();
return normalizedSymbol ? `${baseKey}:${normalizedSymbol}` : null;
}
function isLadderOptionStorageKey(key) {
if (!key) return false;
return key.startsWith(`${LOCAL_LADDER_OPEN_PERCENT_KEY}:`) || key.startsWith(`${LOCAL_LADDER_CLOSE_PERCENT_KEY}:`) || key.startsWith(`${LOCAL_LADDER_LEVELS_KEY}:`) || key.startsWith(`${LOCAL_LADDER_STEP_KEY}:`);
}
function loadNumberOption(key, options, fallback, symbol) {
const storageKey = ladderOptionStorageKey(key, symbol);
if (!storageKey) return fallback;
const stored = localStorage.getItem(storageKey);
return isValidOption(stored, options) ? Number(stored) : fallback;
}
function saveNumberOption(key, value, options, symbol) {
if (!isValidOption(value, options)) return;
const storageKey = ladderOptionStorageKey(key, symbol);
if (!storageKey) return;
localStorage.setItem(storageKey, String(Number(value)));
}
function isLadderExpanded() {
return localStorage.getItem(LOCAL_LADDER_EXPANDED_KEY) === "true";
}
function setLadderExpanded(expanded) {
localStorage.setItem(LOCAL_LADDER_EXPANDED_KEY, expanded ? "true" : "false");
scheduleRenderPanel();
}
function getLadderOpenPercent() {
return loadNumberOption(LOCAL_LADDER_OPEN_PERCENT_KEY, LADDER_OPEN_PERCENTS, DEFAULT_LADDER_OPEN_PERCENT, getCurrentSymbol());
}
function setLadderOpenPercent(value) {
saveNumberOption(LOCAL_LADDER_OPEN_PERCENT_KEY, value, LADDER_OPEN_PERCENTS, getCurrentSymbol());
scheduleRenderPanel();
}
function getLadderClosePercent() {
return loadNumberOption(LOCAL_LADDER_CLOSE_PERCENT_KEY, LADDER_CLOSE_PERCENTS, DEFAULT_LADDER_CLOSE_PERCENT, getCurrentSymbol());
}
function setLadderClosePercent(value) {
saveNumberOption(LOCAL_LADDER_CLOSE_PERCENT_KEY, value, LADDER_CLOSE_PERCENTS, getCurrentSymbol());
scheduleRenderPanel();
}
function getLadderLevels() {
return loadNumberOption(LOCAL_LADDER_LEVELS_KEY, LADDER_LEVEL_OPTIONS, DEFAULT_LADDER_LEVELS, getCurrentSymbol());
}
function setLadderLevels(value) {
saveNumberOption(LOCAL_LADDER_LEVELS_KEY, value, LADDER_LEVEL_OPTIONS, getCurrentSymbol());
scheduleRenderPanel();
}
function getLadderStep() {
const storageKey = ladderOptionStorageKey(LOCAL_LADDER_STEP_KEY, getCurrentSymbol());
if (!storageKey) return DEFAULT_LADDER_STEP;
const value = Number(localStorage.getItem(storageKey) || DEFAULT_LADDER_STEP);
if (!Number.isInteger(value)) return DEFAULT_LADDER_STEP;
return Math.max(LADDER_STEP_MIN, Math.min(value, LADDER_STEP_MAX));
}
function setLadderStep(value) {
const num = Number(value);
if (!Number.isInteger(num)) return;
const storageKey = ladderOptionStorageKey(LOCAL_LADDER_STEP_KEY, getCurrentSymbol());
if (!storageKey) return;
localStorage.setItem(storageKey, String(Math.max(LADDER_STEP_MIN, Math.min(num, LADDER_STEP_MAX))));
scheduleRenderPanel();
}
function setLadderStatus(text, title = null) {
ladderStatusText = String(text || "空闲");
const statusEl = document.getElementById(LADDER_STATUS_ID);
if (statusEl) {
statusEl.textContent = ladderStatusText;
statusEl.title = String(title || ladderStatusText);
}
}
function isValidMultiplier(value) {
return /^\d+$/.test(String(value || "").trim()) && Number(value) > 0;
}
function applyInputVisualState(input, multiplier) {
if (!input) return;
const isFocused = document.activeElement === input;
const isValid = isValidMultiplier(multiplier);
if (!isValid) {
input.style.borderColor = INPUT_ERROR_COLOR;
input.style.background = INPUT_DEFAULT_BG;
input.style.boxShadow = "none";
return;
}
input.style.borderColor = isFocused ? INPUT_FOCUS_COLOR : INPUT_BORDER_COLOR;
input.style.background = INPUT_DEFAULT_BG;
input.style.boxShadow = "none";
}
function findQtyInput() {
return document.querySelector('input[id^="unitAmount-"]') || document.querySelector('input[aria-label="数量"]') || document.querySelector('input[placeholder="数量"]');
}
function findPriceInput() {
return document.querySelector('input[id^="limitPrice-"]') || document.querySelector('input[aria-label="委托价格"]') || document.querySelector('input[placeholder="委托价格"]') || null;
}
function isOwnPanelButton2(button) {
return !!button?.closest?.(`#${PANEL_ID}`);
}
function getActiveTradeMode() {
const activeTab = document.querySelector('#position-direction [role="tab"][aria-selected="true"]') || document.querySelector('.bn-tabs__buySell [role="tab"][aria-selected="true"]') || document.querySelector('[role="tab"].bn-tab__buySell[aria-selected="true"]');
const text = (activeTab?.textContent || "").trim();
if (text.includes("开仓")) return "OPEN";
if (text.includes("平仓")) return "CLOSE";
return "UNKNOWN";
}
function getCurrentOrderType() {
const activeTab = findVisibleTradeScopeElement(
'[role="tab"][aria-selected="true"][data-tab-key]',
(tab) => !isTradeModeTab2(tab)
);
return String(activeTab?.getAttribute("data-tab-key") || "LIMIT").toUpperCase();
}
function isPostOnlyOrderTypeActive() {
const orderType = getCurrentOrderType();
if (!orderType.includes("CONDITIONAL") && !orderType.includes(BINANCE_POST_ONLY_ORDER_TYPE)) return false;
return !!findVisibleTradeScopeElement(
'[role="tab"], [role="combobox"], .bn-select-field-input, .bn-select-trigger, .bn-select-field',
(el) => /只做Maker|Post Only/i.test((el.textContent || "").replace(/\s+/g, " ").trim())
);
}
function getActiveTradeTab() {
return document.querySelector('#position-direction [role="tab"][aria-selected="true"]') || document.querySelector('.bn-tabs__buySell [role="tab"][aria-selected="true"]') || document.querySelector('[role="tab"].bn-tab__buySell[aria-selected="true"]') || null;
}
function isTradeModeTab2(node) {
return isTradeModeTab(node, { panelId: PANEL_ID });
}
function isVisibleElement(el) {
if (!el) return false;
const style = window.getComputedStyle(el);
if (style.display === "none" || style.visibility === "hidden") return false;
const rects = Array.from(el.getClientRects());
if (!rects.length) return false;
if (el.offsetWidth || el.offsetHeight) return true;
return rects.some((rect) => rect.width > 0 && rect.height > 0);
}
function buttonTextMatches2(button, patterns) {
const text = (button?.textContent || "").trim().toLowerCase();
return patterns.some((pattern) => text.includes(pattern));
}
function isTradeActionButton2(node) {
return isTradeActionButton(node, { panelId: PANEL_ID });
}
function isTradeUiNode(node) {
if (!(node instanceof Element)) return false;
if (node.closest(`#${PANEL_ID}`) || node.closest(`#${SPACER_ID}`)) return false;
if (isTradeModeTab2(node) || isTradeActionButton2(node)) return true;
return !!node.closest(
'#position-direction, .bn-tabs__buySell, [data-testid="max-sell-amount"], [data-testid="max-buy-amount"], input[id^="unitAmount-"], input[id^="limitPrice-"]'
);
}
function mutationTouchesTradeUi(mutation) {
if (!mutation) return false;
if (mutation.type === "attributes") {
return isTradeUiNode(mutation.target);
}
if (mutation.type === "characterData") {
return isTradeUiNode(mutation.target?.parentElement || null);
}
if (mutation.type === "childList") {
if (isTradeUiNode(mutation.target)) return true;
for (const node of mutation.addedNodes || []) {
if (isTradeUiNode(node)) return true;
if (node instanceof Element && node.querySelector?.(
'#position-direction [role="tab"], .bn-tabs__buySell [role="tab"], [data-testid="max-sell-amount"], [data-testid="max-buy-amount"], input[id^="unitAmount-"], input[id^="limitPrice-"], button'
)) {
return true;
}
}
}
return false;
}
function invalidateTradeButtonCache() {
tradeButtonCache = { mode: null, expiresAt: 0, buttons: [] };
tradeScopeCache = { activeTab: null, expiresAt: 0, scopes: [] };
}
function getTradeSearchScopes() {
const now = Date.now();
const activeTab = getActiveTradeTab();
if (tradeScopeCache.activeTab === activeTab && tradeScopeCache.expiresAt > now && tradeScopeCache.scopes.every((scope) => scope?.isConnected)) {
return tradeScopeCache.scopes;
}
const scopes = [];
const seen = /* @__PURE__ */ new Set();
const pushScope = (node) => {
if (!node || seen.has(node)) return;
seen.add(node);
scopes.push(node);
};
const paneId = activeTab?.getAttribute("aria-controls");
if (paneId) pushScope(document.getElementById(paneId));
const tabRoot = activeTab?.closest("#position-direction") || activeTab?.closest(".bn-tabs__buySell") || activeTab?.parentElement || null;
if (tabRoot) {
let node = tabRoot.parentElement;
let depth = 0;
while (node && node !== document.body && depth < 6) {
pushScope(node);
node = node.parentElement;
depth += 1;
}
}
tradeScopeCache = activeTab && scopes.length ? {
activeTab,
expiresAt: now + DOM_LOOKUP_CACHE_MS,
scopes
} : { activeTab: null, expiresAt: 0, scopes: [] };
return scopes;
}
function findVisibleElementInScopes(scopes, selector, predicate = () => true) {
const seen = /* @__PURE__ */ new Set();
for (const scope of scopes) {
if (!scope) continue;
for (const el of scope.querySelectorAll(selector)) {
if (seen.has(el) || !isVisibleElement(el) || el.closest(`#${PANEL_ID}`)) continue;
seen.add(el);
if (predicate(el)) return el;
}
}
return null;
}
function findVisibleTradeScopeElement(selector, predicate) {
return findVisibleElementInScopes(getTradeSearchScopes(), selector, predicate);
}
function getTradeMutationRoot() {
return getTradeSearchScopes()[0] || findQtyFormItem(findQtyInput())?.parentElement || null;
}
function collectTradeButtons(mode) {
const now = Date.now();
if (tradeButtonCache.mode === mode && tradeButtonCache.expiresAt > now) {
return tradeButtonCache.buttons;
}
const buttons = collectTradeButtonsFromScopes(getTradeSearchScopes(), mode, {
panelId: PANEL_ID,
isVisibleElement
});
tradeButtonCache = {
mode,
expiresAt: now + DOM_LOOKUP_CACHE_MS,
buttons
};
return buttons;
}
function findTradeButton(patterns, mode) {
return collectTradeButtons(mode).find((candidate) => buttonTextMatches2(candidate, patterns)) || null;
}
function findCloseLongButton() {
return findTradeButton(["平多", "close long"], "CLOSE");
}
function findCloseShortButton() {
return findTradeButton(["平空", "close short"], "CLOSE");
}
function findOpenLongButton() {
return findTradeButton(["开多", "open long"], "OPEN");
}
function findOpenShortButton() {
return findTradeButton(["开空", "open short"], "OPEN");
}
let cachedBncHeaders = null;
const HEADER_KEYS_TO_CACHE = [
"csrftoken",
"bnc-uuid",
"device-info",
"fvideo-id",
"clienttype",
"x-passthrough-token"
];
function readHeaderValue(headers, key) {
if (!headers) return null;
if (typeof headers.get === "function") {
return headers.get(key) || headers.get(key.toUpperCase()) || null;
}
return headers[key] || headers[key.toUpperCase()] || headers[key.toLowerCase()] || null;
}
function extractHeadersFromFetchArgs(args) {
const url = typeof args[0] === "string" ? args[0] : args[0] instanceof Request ? args[0].url : args[0]?.url || "";
if (!url.includes("/bapi/")) return null;
let headers = args[1]?.headers;
if (!headers && args[0] instanceof Request) {
headers = args[0].headers;
}
if (!headers) return null;
const snapshot = {};
for (const key of HEADER_KEYS_TO_CACHE) {
const val = readHeaderValue(headers, key);
if (val != null && val !== "") snapshot[key] = val;
}
return snapshot.csrftoken ? snapshot : null;
}
(function installFetchInterceptor() {
const originalFetch = window.fetch;
window.fetch = function(...args) {
try {
const snapshot = extractHeadersFromFetchArgs(args);
if (snapshot) cachedBncHeaders = snapshot;
} catch (_e) {
}
return originalFetch.apply(this, args);
};
})();
function getBncHeaders() {
const base = cachedBncHeaders || {};
return {
"content-type": "application/json",
...base,
"x-trace-id": crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`,
"x-ui-request-trace": crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(36).slice(2)}`
};
}
async function adjustLeverageApi(symbol, leverage) {
if (!cachedBncHeaders) {
throw new Error("bapi header 尚未缓存");
}
const controller = new AbortController();
const timer = window.setTimeout(() => controller.abort(), 5e3);
try {
const resp = await fetch(
"https://www.binance.com/bapi/futures/v1/private/future/user-data/adjustLeverage",
{
method: "POST",
headers: getBncHeaders(),
body: JSON.stringify({ symbol, leverage }),
credentials: "include",
signal: controller.signal
}
);
if (!resp.ok) throw new Error(`adjustLeverage HTTP ${resp.status}`);
const data = await resp.json();
if (!data.success) throw new Error(data.message || `code=${data.code}`);
return data;
} finally {
window.clearTimeout(timer);
}
}
function findOrderbookRow(node) {
if (!node) return null;
return node.closest("#futuresOrderbook .row-content");
}
function findClickedPriceNode(node) {
if (!node) return null;
const priceNode = node.closest("#futuresOrderbook .ask-light.emit-price, #futuresOrderbook .bid-light.emit-price");
if (!priceNode) return null;
return findOrderbookRow(priceNode) ? priceNode : null;
}
function findPriceNodeFromRow(row) {
if (!row) return null;
return row.querySelector(".ask-light.emit-price, .bid-light.emit-price");
}
function parsePrice(node) {
const txt = (node.textContent || "").replace(/,/g, "").trim();
return /^\d+(\.\d+)?$/.test(txt) ? txt : null;
}
function parseNumber(text) {
if (text == null) return null;
const n = Number(String(text).replace(/,/g, "").trim());
return Number.isFinite(n) ? n : null;
}
function getOrderbookPrices(side, levels) {
const isBid = side === "BID";
const selector = isBid ? "#futuresOrderbook .bid-light.emit-price" : "#futuresOrderbook .ask-light.emit-price";
let prices = Array.from(document.querySelectorAll(selector)).filter((node) => isVisibleElement(node) && findOrderbookRow(node)).map((node) => parsePrice(node)).filter(Boolean);
if (!isBid) prices = prices.reverse();
const deduped = [];
for (const price of prices) {
if (!deduped.includes(price)) deduped.push(price);
if (deduped.length >= levels) break;
}
return deduped;
}
function getBestOrderbookPrice(side) {
return getOrderbookPrices(side, 1)[0] || null;
}
function getLatestTradePrices(limit = 20) {
return Array.from(document.querySelectorAll(".tradew-tradelist .price.emit-price")).filter((node) => isVisibleElement(node)).map((node) => parsePrice(node)).filter(Boolean).slice(0, Math.max(1, Number(limit) || 20));
}
async function waitForLatestTradePricesReady(symbol, timeoutMs = ORDERBOOK_PRECISION_READY_TIMEOUT_MS) {
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
while (!document.hidden && isFuturesTradingPage() && getCurrentSymbol() === symbol) {
const prices = getLatestTradePrices();
if (prices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) return prices;
if (Date.now() >= deadline) return prices;
await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS);
}
return [];
}
function orderbookPrecisionSamplesKey(symbol = getCurrentSymbol()) {
const normalizedSymbol = String(symbol || "").toUpperCase();
return normalizedSymbol ? `${LOCAL_ORDERBOOK_PRECISION_SAMPLES_PREFIX}:${normalizedSymbol}` : null;
}
function readStoredOrderbookPrecisionSamples(symbol = getCurrentSymbol()) {
const key = orderbookPrecisionSamplesKey(symbol);
if (!key) return [];
const parsed = parseJsonSafe(localStorage.getItem(key));
return Array.isArray(parsed) ? parsed.map((sample) => normalizeDecimalString(sample)).filter((sample) => sample && isPositiveDecimalString(sample)) : [];
}
function saveStoredOrderbookPrecisionSamples(symbol, samples) {
const key = orderbookPrecisionSamplesKey(symbol);
if (!key) return [];
const merged = mergePrecisionSamples([], samples, ORDERBOOK_PRECISION_SAMPLE_MAX);
localStorage.setItem(key, JSON.stringify(merged));
return merged;
}
function getOrderbookPrecisionRecommendation(symbol = getCurrentSymbol()) {
const samples = readStoredOrderbookPrecisionSamples(symbol);
return recommendOrderbookPrecision({
samples,
options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS
});
}
function isOrderbookPrecisionNumericText(text) {
const raw = String(text || "").replace(/,/g, "").trim();
return /^(?:\d+|\d+\.\d+|0?\.\d+)$/.test(raw) && raw.length <= 16;
}
function isInsideOrderbookPriceRow(node) {
return !!node?.closest?.("#futuresOrderbook .row-content");
}
function isInsideOwnPanel(node) {
return !!node?.closest?.(`#${PANEL_ID}`);
}
function findOrderbookPrecisionTrigger() {
const root = document.querySelector("#futuresOrderbook");
if (!root) return null;
const clickableSelector = 'button,[role="button"],[role="combobox"],[tabindex],.bn-sdd-value,.bn-select-field';
const tickSize = root.querySelector(".orderbook-tickSize");
if (tickSize && isVisibleElement(tickSize) && !isInsideOrderbookPriceRow(tickSize)) {
const tickContent = tickSize.querySelector(".tick-content");
const text = (tickSize.textContent || "").trim();
if (isOrderbookPrecisionNumericText(text)) {
return {
element: tickContent && isVisibleElement(tickContent) ? tickContent : tickSize,
value: normalizeDecimalString(text)
};
}
}
const clickables = Array.from(root.querySelectorAll(clickableSelector));
for (const candidate of clickables) {
if (!isVisibleElement(candidate) || isInsideOrderbookPriceRow(candidate)) continue;
const text = (candidate.textContent || "").trim();
if (!isOrderbookPrecisionNumericText(text)) continue;
return {
element: candidate,
value: normalizeDecimalString(text)
};
}
const numericNodes = Array.from(root.querySelectorAll("span,div"));
for (const node of numericNodes) {
if (!isVisibleElement(node) || isInsideOrderbookPriceRow(node)) continue;
const text = (node.textContent || "").trim();
if (!isOrderbookPrecisionNumericText(text)) continue;
const target = node.closest(clickableSelector) || node.parentElement || node;
if (!target || !isVisibleElement(target) || isInsideOrderbookPriceRow(target)) continue;
return {
element: target,
value: normalizeDecimalString(text)
};
}
return null;
}
function readCurrentOrderbookPrecisionValue() {
return findOrderbookPrecisionTrigger()?.value || null;
}
function readOrderbookPrecisionOptionValue(node) {
if (!node) return null;
const item = node.matches?.(".ob-ticksize-item") ? node : node.closest?.(".ob-ticksize-item");
const textNode = item?.querySelector("span") || node;
return normalizeDecimalString(textNode?.textContent || "");
}
function getOrderbookPrecisionOptionClickTarget(node) {
return node?.closest?.(".ob-ticksize-item") || node;
}
function getVisibleOrderbookPrecisionOptionNodes() {
const optionSelector = [
'[role="option"]',
'[role="menuitem"]',
".ob-ticksize-item",
".bn-sdd-option",
".bn-select-option",
'[class*="option"]',
'[class*="Option"]'
].join(",");
const popupSelector = [
'[role="listbox"]',
'[role="menu"]',
".ob-ticksize-overlay",
'[class*="dropdown"]',
'[class*="Dropdown"]',
'[class*="popup"]',
'[class*="Popup"]',
'[class*="menu"]',
'[class*="Menu"]'
].join(",");
const candidates = [
...Array.from(document.querySelectorAll(optionSelector)),
...Array.from(document.querySelectorAll(popupSelector)).flatMap((popup) => Array.from(popup.querySelectorAll("div,span,li")))
];
return candidates.map((node) => getOrderbookPrecisionOptionClickTarget(node)).filter((node, index, nodes) => node && nodes.indexOf(node) === index).filter((node) => isVisibleElement(node) && !isInsideOrderbookPriceRow(node) && !isInsideOwnPanel(node)).filter((node) => isOrderbookPrecisionNumericText(readOrderbookPrecisionOptionValue(node))).filter((node) => ORDERBOOK_PRECISION_CANDIDATE_OPTIONS.includes(readOrderbookPrecisionOptionValue(node)));
}
function readVisibleOrderbookPrecisionOptionValues() {
const values = /* @__PURE__ */ new Set();
for (const node of getVisibleOrderbookPrecisionOptionNodes()) {
const value = readOrderbookPrecisionOptionValue(node);
if (value) values.add(value);
}
return Array.from(values);
}
function findVisibleOrderbookPrecisionOption(value) {
const normalized = normalizeDecimalString(value);
if (!normalized) return null;
return getVisibleOrderbookPrecisionOptionNodes().find((node) => readOrderbookPrecisionOptionValue(node) === normalized) || null;
}
function dispatchOrderbookPrecisionOpenEvent(target, type) {
const EventCtor = type.startsWith("pointer") && typeof PointerEvent === "function" ? PointerEvent : MouseEvent;
return target.dispatchEvent(new EventCtor(type, {
bubbles: true,
cancelable: true,
view: window,
button: 0,
buttons: type === "pointerup" || type === "mouseup" || type === "click" ? 0 : 1,
pointerId: 1,
pointerType: "mouse",
isPrimary: true
}));
}
async function waitForVisibleOrderbookPrecisionOptions(timeoutMs = 250) {
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
while (!document.hidden && isFuturesTradingPage()) {
if (getVisibleOrderbookPrecisionOptionNodes().length) return true;
if (Date.now() >= deadline) return false;
await delay(50);
}
return false;
}
async function openOrderbookPrecisionOptions(triggerElement) {
if (!triggerElement || !triggerElement.isConnected) return false;
const tickSize = triggerElement.closest?.(".orderbook-tickSize");
const candidates = [
tickSize,
tickSize?.querySelector?.(".tick-content"),
triggerElement
].filter((node, index, nodes) => node && nodes.indexOf(node) === index && isVisibleElement(node) && !isInsideOrderbookPriceRow(node));
for (const target of candidates) {
dispatchOrderbookPrecisionOpenEvent(target, "pointerdown");
dispatchOrderbookPrecisionOpenEvent(target, "mousedown");
dispatchOrderbookPrecisionOpenEvent(target, "pointerup");
dispatchOrderbookPrecisionOpenEvent(target, "mouseup");
dispatchOrderbookPrecisionOpenEvent(target, "click");
if (await waitForVisibleOrderbookPrecisionOptions()) return true;
}
return false;
}
async function waitForVisibleOrderbookPrecisionOption(value, timeoutMs = ORDERBOOK_PRECISION_OPTION_WAIT_MS) {
const deadline = Date.now() + Math.max(0, Number(timeoutMs) || 0);
while (!document.hidden && isFuturesTradingPage()) {
const option = findVisibleOrderbookPrecisionOption(value);
if (option) return option;
if (Date.now() >= deadline) return null;
await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS);
}
return null;
}
function isOrderbookPrecisionBusy(status = orderbookPrecisionState.status) {
return orderbookPrecisionSampling || status === "刷新中" || status === "采样中";
}
function formatOrderbookPrecisionBusyStatus(status, sampleEndsAt = orderbookPrecisionState.sampleEndsAt) {
if (status !== "刷新中" && status !== "采样中") return status;
const remainingMs = Number(sampleEndsAt) - Date.now();
if (!(remainingMs > 0)) return status;
const remainingSeconds = Math.max(1, Math.ceil(remainingMs / 1e3));
if (status === "刷新中") return `刷新中 ${remainingSeconds}s`;
return `采样中 ${remainingSeconds}s`;
}
function refreshOrderbookPrecisionRecommendation(panel = document.getElementById(PANEL_ID)) {
const el = panel?.querySelector?.(`#${ORDERBOOK_PRECISION_RECOMMENDATION_ID}`);
if (!el) return;
const symbol = getCurrentSymbol();
if (!symbol) {
el.textContent = "";
return;
}
const samples = readStoredOrderbookPrecisionSamples(symbol);
const recommendation = recommendOrderbookPrecision({
samples,
options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS
});
const current = readCurrentOrderbookPrecisionValue();
const existingStatus = orderbookPrecisionState.symbol === symbol ? orderbookPrecisionState.status : null;
const busy = isOrderbookPrecisionBusy(existingStatus);
const stickyStatus = existingStatus && /^(未定位|未找到|数据不足)/.test(existingStatus) ? existingStatus : null;
const status = busy ? existingStatus || "采样中" : stickyStatus ? existingStatus : recommendation ? "ready" : "采样中";
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
samples,
recommendation,
current,
status
};
const canApply = !busy && recommendation && current !== recommendation;
const canRefresh = !busy;
const activeButtonStyle = "border-color:#d5d9e2;background:#ffffff;color:#5e6673;cursor:pointer;opacity:1;";
const disabledButtonStyle = `border-color:${DISABLED_CONTROL_BORDER};background:${DISABLED_CONTROL_BG};color:${DISABLED_CONTROL_TEXT};cursor:not-allowed;opacity:${DISABLED_CONTROL_OPACITY};`;
const applyButtonStyle = canApply ? activeButtonStyle : disabledButtonStyle;
const refreshButtonStyle = canRefresh ? activeButtonStyle : disabledButtonStyle;
const buttonBaseStyle = "height:32px;padding:0 12px;border-radius:6px;border:1px solid #d5d9e2;font-size:14px;line-height:30px;";
const recommendationText = recommendation || "--";
const displayStatus = busy ? formatOrderbookPrecisionBusyStatus(status) : status;
const statusText = displayStatus === "ready" ? "" : `${displayStatus}`;
el.innerHTML = [
'
',
`缩放 推荐 ${recommendationText}`,
``,
``,
statusText,
"
"
].join("");
if (busy && Number(orderbookPrecisionState.sampleEndsAt) > Date.now()) {
scheduleRenderPanel({ followUpMs: 1e3 });
}
}
async function applyRecommendedOrderbookPrecision() {
const symbol = getCurrentSymbol();
const trigger = findOrderbookPrecisionTrigger();
if (!symbol || !trigger?.element) {
orderbookPrecisionState = { ...orderbookPrecisionState, status: "未定位到缩放下拉" };
scheduleRenderPanel();
return false;
}
const samples = readStoredOrderbookPrecisionSamples(symbol);
const recommendation = recommendOrderbookPrecision({
samples,
options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS
});
if (!recommendation) {
orderbookPrecisionState = { ...orderbookPrecisionState, status: "数据不足" };
scheduleRenderPanel();
return false;
}
let option = findVisibleOrderbookPrecisionOption(recommendation);
if (!option) {
await openOrderbookPrecisionOptions(trigger.element);
option = await waitForVisibleOrderbookPrecisionOption(recommendation);
}
if (!option) {
orderbookPrecisionState = { ...orderbookPrecisionState, recommendation, status: `未找到 ${recommendation} 档` };
scheduleRenderPanel();
return false;
}
clickDomTarget(option);
orderbookPrecisionState = { ...orderbookPrecisionState, recommendation, current: recommendation, status: "已应用" };
scheduleRenderPanel({ followUpMs: 350 });
return true;
}
async function runOrderbookPrecisionSampleRound(durationMs = ORDERBOOK_PRECISION_SAMPLE_DURATION_MS) {
orderbookPrecisionSampleTimer = 0;
if (orderbookPrecisionSampling || document.hidden || !isFuturesTradingPage()) return;
const symbol = getCurrentSymbol();
if (!symbol) return;
orderbookPrecisionSampling = true;
const tradeMoveSamples = [];
const sampleDurationMs = Math.max(0, Number(durationMs) || ORDERBOOK_PRECISION_SAMPLE_DURATION_MS);
try {
const readyPrices = await waitForLatestTradePricesReady(symbol);
if (readyPrices.length >= ORDERBOOK_PRECISION_MIN_TRADE_PRICE_ROWS) {
tradeMoveSamples.push(...collectNonZeroPriceMoves(readyPrices));
}
const deadline = Date.now() + sampleDurationMs;
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
sampleEndsAt: deadline
};
scheduleRenderPanel({ followUpMs: 1e3 });
while (Date.now() < deadline && !document.hidden && isFuturesTradingPage() && getCurrentSymbol() === symbol) {
tradeMoveSamples.push(...collectNonZeroPriceMoves(getLatestTradePrices()));
await delay(ORDERBOOK_PRECISION_SAMPLE_POLL_MS);
}
if (getCurrentSymbol() !== symbol) return;
const newSamples = tradeMoveSamples;
const samples = saveStoredOrderbookPrecisionSamples(symbol, newSamples);
const recommendation = recommendOrderbookPrecision({
samples,
options: ORDERBOOK_PRECISION_CANDIDATE_OPTIONS
});
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
samples,
recommendation,
current: readCurrentOrderbookPrecisionValue(),
status: recommendation ? "ready" : "数据不足",
sampleEndsAt: 0
};
refreshOrderbookPrecisionRecommendation();
scheduleRenderPanel();
} finally {
const shouldResampleImmediately = orderbookPrecisionResampleRequested;
const resampleDurationMs = orderbookPrecisionResampleDurationMs;
orderbookPrecisionResampleRequested = false;
orderbookPrecisionSampling = false;
if (shouldResampleImmediately) {
scheduleOrderbookPrecisionSampleRound(0, { force: true, durationMs: resampleDurationMs });
}
}
}
function scheduleOrderbookPrecisionSampleRound(delayMs = 0, options) {
const {
force = false,
durationMs = ORDERBOOK_PRECISION_SAMPLE_DURATION_MS
} = options || {};
if (document.hidden || !isFuturesTradingPage()) return;
if (orderbookPrecisionSampling) {
if (force) orderbookPrecisionResampleRequested = true;
if (force) orderbookPrecisionResampleDurationMs = durationMs;
return;
}
if (orderbookPrecisionSampling || orderbookPrecisionSampleTimer) return;
orderbookPrecisionSampleTimer = window.setTimeout(
() => runOrderbookPrecisionSampleRound(durationMs),
Math.max(0, Number(delayMs) || 0)
);
}
function stopOrderbookPrecisionSampler() {
window.clearTimeout(orderbookPrecisionSampleTimer);
orderbookPrecisionSampleTimer = 0;
orderbookPrecisionResampleRequested = false;
}
function startInitialOrderbookPrecisionSample() {
const symbol = getCurrentSymbol();
if (!symbol || orderbookPrecisionInitialSampledSymbols.has(symbol)) return;
orderbookPrecisionInitialSampledSymbols.add(symbol);
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
status: "采样中",
sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_SAMPLE_DURATION_MS
};
scheduleOrderbookPrecisionSampleRound(0, {
force: true,
durationMs: ORDERBOOK_PRECISION_SAMPLE_DURATION_MS
});
}
function refreshOrderbookPrecisionSamplesNow() {
const symbol = getCurrentSymbol();
orderbookPrecisionState = {
...orderbookPrecisionState,
symbol,
status: "刷新中",
sampleEndsAt: Date.now() + ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS
};
stopOrderbookPrecisionSampler();
scheduleOrderbookPrecisionSampleRound(0, {
force: true,
durationMs: ORDERBOOK_PRECISION_MANUAL_SAMPLE_DURATION_MS
});
scheduleRenderPanel();
}
function getBufferedMakerPrices(side, levels, ladderStep = DEFAULT_LADDER_STEP) {
const step = Math.max(LADDER_STEP_MIN, Math.min(Number(ladderStep) || DEFAULT_LADDER_STEP, LADDER_STEP_MAX));
const requiredDepth = LADDER_MAKER_BUFFER_LEVELS + (levels - 1) * step + 1;
const prices = getOrderbookPrices(side, requiredDepth);
return planBufferedMakerPrices({
prices,
side,
levels,
ladderStep: step,
bufferLevels: LADDER_MAKER_BUFFER_LEVELS,
defaultStep: DEFAULT_LADDER_STEP,
minStep: LADDER_STEP_MIN,
maxStep: LADDER_STEP_MAX
});
}
function getLadderActionSpec2(actionType) {
const spec = getLadderActionSpec(actionType);
if (!spec) return null;
const buttonGetters = {
OPEN_LONG: findOpenLongButton,
OPEN_SHORT: findOpenShortButton,
CLOSE_LONG: findCloseLongButton,
CLOSE_SHORT: findCloseShortButton
};
return {
...spec,
buttonGetter: buttonGetters[actionType]
};
}
function findTradeModeTabByMode(mode) {
const label = mode === "OPEN" ? "开仓" : "平仓";
const tabs = document.querySelectorAll(
'#position-direction [role="tab"], .bn-tabs__buySell [role="tab"], [role="tab"].bn-tab__buySell'
);
return Array.from(tabs).find((tab) => (tab.textContent || "").includes(label)) || null;
}
function findConditionalOrderTab() {
return findVisibleTradeScopeElement('[role="tab"]', (tab) => {
const text = (tab.textContent || "").trim();
const key = String(tab.getAttribute("data-tab-key") || "").toUpperCase();
return key === "CONDITIONAL" || text.includes("条件委托") || /只做Maker|Post Only/i.test(text);
});
}
function findConditionalSubtypeCombobox() {
const tab = findConditionalOrderTab();
if (!tab) return null;
return Array.from(tab.querySelectorAll('[role="combobox"], .bn-select-trigger, .bn-select-field')).find(isVisibleElement) || null;
}
function clickElementLikeUser(el) {
if (!el) return;
const rect = el.getBoundingClientRect();
const clientX = (rect.left + rect.right) / 2;
const clientY = (rect.top + rect.bottom) / 2;
const PointerCtor = window.PointerEvent || MouseEvent;
el.dispatchEvent(new PointerCtor("pointerdown", { bubbles: true, clientX, clientY }));
el.dispatchEvent(new MouseEvent("mousedown", { bubbles: true, clientX, clientY }));
el.dispatchEvent(new PointerCtor("pointerup", { bubbles: true, clientX, clientY }));
el.dispatchEvent(new MouseEvent("mouseup", { bubbles: true, clientX, clientY }));
el.dispatchEvent(new MouseEvent("click", { bubbles: true, clientX, clientY }));
}
function findPostOnlyOption() {
const options = document.querySelectorAll('[role="option"], [role="menuitem"], .bn-select-option');
return Array.from(options).find((el) => {
if (!isVisibleElement(el)) return false;
const text = (el.textContent || "").replace(/\s+/g, " ").trim();
return /只做Maker|Post Only/i.test(text) && text.length < 120;
}) || null;
}
async function activateTradeMode(mode) {
if (getActiveTradeMode() === mode) return true;
const tab = findTradeModeTabByMode(mode);
if (!tab) return false;
tab.click();
await delay(260);
invalidateTradeButtonCache();
scheduleRenderPanel();
return getActiveTradeMode() === mode;
}
async function ensurePostOnlyOrderType() {
if (isPostOnlyOrderTypeActive()) return true;
const tab = findConditionalOrderTab();
if (!tab) return false;
if (!getCurrentOrderType().includes("CONDITIONAL")) {
tab.click();
await delay(320);
}
if (isPostOnlyOrderTypeActive()) return true;
const combo = findConditionalSubtypeCombobox();
if (!combo) return false;
clickElementLikeUser(combo);
await delay(260);
const option = findPostOnlyOption();
if (!option) return false;
clickElementLikeUser(option);
await delay(360);
return isPostOnlyOrderTypeActive();
}
async function readOpenBaseQtyForLadder(spec, referencePrice) {
const priceInput = findPriceInput();
if (!priceInput || !referencePrice) return null;
setInputValueReact(priceInput, referencePrice);
const startedAt = Date.now();
while (Date.now() - startedAt < LADDER_OPEN_QTY_READY_TIMEOUT_MS) {
const openLongBtn2 = findOpenLongButton();
const openShortBtn2 = findOpenShortButton();
const { longQty: longQty2, shortQty: shortQty2, qtySource: qtySource2 } = readOpenableQty(openLongBtn2, openShortBtn2);
const qty = spec.side === "LONG" ? longQty2 : shortQty2;
if (qty != null && isPositiveDecimalString(String(qty))) {
return { qty, qtySource: qtySource2 };
}
await delay(LADDER_OPEN_QTY_POLL_MS);
}
const openLongBtn = findOpenLongButton();
const openShortBtn = findOpenShortButton();
const { longQty, shortQty, qtySource } = readOpenableQty(openLongBtn, openShortBtn);
return {
qty: spec.side === "LONG" ? longQty : shortQty,
qtySource
};
}
function readCloseBaseQtyForLadder(spec) {
const raw = readCloseContext();
const display = resolveDisplayCloseState(raw, getCurrentSymbol());
const qty = spec.side === "LONG" ? display.longQty : display.shortQty;
return {
qty: qty != null ? normalizeDecimalString(String(qty)) : null,
qtySource: display.qtySource
};
}
async function buildLadderPlan(actionType, expectedSymbol = null) {
const spec = getLadderActionSpec2(actionType);
if (!spec) throw new Error("未知阶梯动作");
const startSymbol = getCurrentSymbol();
if (!startSymbol) throw new Error("未识别当前交易对");
if (expectedSymbol && startSymbol !== expectedSymbol) throw new Error("重挂前交易对已变化,已停止");
const modeReady = await activateTradeMode(spec.mode);
if (!modeReady || getCurrentSymbol() !== startSymbol) throw new Error("切换开仓/平仓失败或交易对已变化");
const postOnlyReady = await ensurePostOnlyOrderType();
if (!postOnlyReady) throw new Error("请刷新页面让只做Maker (Post Only) 生效后重试,脚本不会用普通限价继续");
const levels = getLadderLevels();
const ladderStep = getLadderStep();
const prices = getBufferedMakerPrices(spec.priceSide, levels, ladderStep);
if (prices.length < levels) {
throw new Error(`订单簿${spec.priceSide === "BID" ? "买盘" : "卖盘"}不足 ${levels} 档,档幅 ${ladderStep}`);
}
const rules = await ensureRules(startSymbol);
if (!rules || getCurrentSymbol() !== startSymbol) throw new Error("交易规则未就绪或交易对已变化");
const ruleContext = getQtyRuleContext(startSymbol, spec.mode, prices[0]);
if (ruleContext.status !== "ready" || !ruleContext.stepSize || !ruleContext.baseMinQty) {
throw new Error("数量步进/最小量未就绪");
}
const minRequiredQtyByLevel = spec.mode === "OPEN" ? prices.map((price) => getQtyRuleContext(startSymbol, spec.mode, price).effectiveMinQty || ruleContext.baseMinQty) : prices.map(() => ruleContext.baseMinQty);
let minRequiredQty = minRequiredQtyByLevel.filter(Boolean).reduce((maxQty, qty) => maxDecimalString(maxQty, qty), ruleContext.baseMinQty);
const base = spec.mode === "OPEN" ? await readOpenBaseQtyForLadder(spec, prices[0]) : readCloseBaseQtyForLadder(spec);
if (getCurrentSymbol() !== startSymbol || getActiveTradeMode() !== spec.mode || !isPostOnlyOrderTypeActive()) {
throw new Error("读取可用数量后交易上下文已变化,已停止");
}
const baseQty = normalizeDecimalString(base?.qty || "");
if (!baseQty || !isPositiveDecimalString(baseQty)) {
throw new Error(`未读取到可用${spec.mode === "OPEN" ? "可开" : "可平"}数量`);
}
let percent = getLadderPercentForMode(spec.mode, getLadderOpenPercent(), getLadderClosePercent());
if (percent == null) throw new Error("未知阶梯模式");
const totalQty = multiplyDecimalByRatio(baseQty, percent, 100);
let allocation = allocateLadderQuantities(totalQty, levels, ruleContext.stepSize, minRequiredQty);
let autoFitPercent = null;
let autoFitLevels = null;
if (!allocation || allocation.actualLevels < levels) {
const autoFit = fitLadderPlanForMinimumQty({
baseQty,
minRequiredQty,
minRequiredQtyByLevel,
percent,
levels,
stepSize: ruleContext.stepSize,
maxPercent: getMaxAutoFitLadderPercent(spec.mode)
});
if (autoFit.allocation) {
allocation = autoFit.allocation;
percent = autoFit.percent;
minRequiredQty = autoFit.minRequiredQty || minRequiredQty;
autoFitPercent = autoFit.percent;
autoFitLevels = autoFit.levels;
} else {
throw createLadderMinimumQtyFailure({
spec,
symbol: startSymbol,
mode: spec.mode,
minRequiredQty,
baseQty,
percent,
levels,
minimumPercent: autoFit.minimumPercent,
maxAutoFitPercent: autoFit.maxPercent,
replacementTotalQty: spec.mode === "OPEN" ? multiplyDecimalByInt(minRequiredQty, levels) : null
});
}
}
const orderPrices = prices.slice(0, allocation.actualLevels);
return {
spec,
symbol: startSymbol,
percent,
ladderStep,
levels: allocation.actualLevels,
requestedLevels: allocation.requestedLevels,
baseQty,
totalQty: allocation.totalQty,
minRequiredQty,
autoFitPercent,
autoFitLevels,
prices: orderPrices,
qtySource: base.qtySource,
orders: orderPrices.map((price, index) => ({ price, qty: allocation.quantities[index] }))
};
}
function getMaxAutoFitLadderPercent(mode) {
if (mode === "OPEN") return String(Math.max(...LADDER_OPEN_PERCENTS));
if (mode === "CLOSE") return "100";
return null;
}
function createLadderMinimumQtyFailure(options) {
const {
spec,
symbol,
mode,
minRequiredQty,
percent,
levels,
minimumPercent,
maxAutoFitPercent,
replacementTotalQty
} = options;
const percentLabel = mode === "OPEN" ? "开仓比例" : "平仓比例";
const actionLabel = mode === "OPEN" ? "开仓" : "平仓";
const percentHint = minimumPercent ? `,需 >= ${minimumPercent}%` : "";
const error = new Error(`数量低于最小下单量 ${minRequiredQty}${percentHint}`);
const minimumText = minimumPercent ? `至少需要${percentLabel} ${minimumPercent}% 才能保持当前档位。` : "";
const maxText = maxAutoFitPercent ? `自动上限 ${maxAutoFitPercent}%。` : "";
const levelsText = levels ? `当前档位 ${levels} 档。` : "";
const replacementText = mode === "OPEN" ? "脚本只会尝试替换当前币同向开仓基础单,不会自动全撤。" : "脚本不会自动撤单。";
error.statusTitle = `当前${percentLabel} ${percent}%,目标数量小于最小下单量 ${minRequiredQty},无法阶梯${actionLabel};${levelsText}${minimumText}${maxText}已尝试自动提高比例和自动降档;${replacementText}`;
if (mode === "OPEN" && spec && symbol && replacementTotalQty && isPositiveDecimalString(replacementTotalQty)) {
error.openOrdersReplacementPlan = {
spec,
symbol,
totalQty: replacementTotalQty
};
}
return error;
}
function assertLadderMakerPrice(plan, price) {
const oppositeSide = plan.spec.orderSide === "BUY" ? "ASK" : "BID";
const oppositePrice = getBestOrderbookPrice(oppositeSide);
if (!oppositePrice) throw new Error("盘口已刷新,未读取到对手盘价格");
const cmp = compareDecimalStrings(price, oppositePrice);
if (cmp == null) throw new Error("盘口价格校验失败");
if (plan.spec.orderSide === "BUY" && cmp >= 0) {
throw new Error(`盘口已移动,买单 ${price} 可能吃单,对手卖一 ${oppositePrice}`);
}
if (plan.spec.orderSide === "SELL" && cmp <= 0) {
throw new Error(`盘口已移动,卖单 ${price} 可能吃单,对手买一 ${oppositePrice}`);
}
}
function assertLadderExecutionContext(plan) {
if (getCurrentSymbol() !== plan.symbol) throw new Error("执行中交易对变化,已停止");
if (getActiveTradeMode() !== plan.spec.mode) throw new Error("执行中开仓/平仓模式变化,已停止");
if (!isPostOnlyOrderTypeActive()) throw new Error("执行中只做Maker (Post Only) 状态丢失,请刷新页面后重试");
}
function assertSubmittedPriceMatchesClickedPrice(clickedPrice, submittedPrice) {
const clicked = normalizeDecimalString(clickedPrice);
const submitted = normalizeDecimalString(submittedPrice);
const cmp = compareDecimalStrings(clicked, submitted);
if (cmp !== 0) {
throw new Error(`价格框未同步,点击价 ${clicked || clickedPrice},当前提交价 ${submitted || submittedPrice || "-"}`);
}
}
function isSubmitButtonBusy(button) {
if (!button) return false;
const text = (button.textContent || "").toLowerCase();
const cls = String(button.className || "").toLowerCase();
return button.disabled || button.getAttribute("aria-disabled") === "true" || button.getAttribute("data-loading") === "true" || text.includes("提交中") || text.includes("placing") || text.includes("loading") || cls.includes("loading") || !!button.querySelector('[class*="loading"], [class*="spinner"], [aria-busy="true"]');
}
function readVisibleOrderFeedbackEntries() {
const selectors = [
'[role="alert"]',
"[aria-live]",
'[class*="toast"]',
'[class*="Toast"]',
'[class*="message"]',
'[class*="Message"]',
'[class*="notification"]',
'[class*="Notification"]'
];
const seen = /* @__PURE__ */ new Set();
const entries = [];
for (const el of document.querySelectorAll(selectors.join(","))) {
if (seen.has(el) || !isVisibleElement(el)) continue;
seen.add(el);
const text = (el.textContent || "").replace(/\s+/g, " ").trim();
if (!text || text.length > 300) continue;
if (isPotentialOrderFeedbackText(text)) entries.push({ el, text });
}
return entries;
}
function takeOrderFeedbackSnapshot() {
const entries = readVisibleOrderFeedbackEntries();
return {
elements: new Set(entries.map(({ el }) => el)),
textByElement: new Map(entries.map(({ el, text }) => [el, text])),
htmlByElement: new Map(entries.map(({ el }) => [el, el.innerHTML]))
};
}
function readNewVisibleOrderFeedbackText(previousSnapshot) {
const snapshot = previousSnapshot || { elements: /* @__PURE__ */ new Set(), textByElement: /* @__PURE__ */ new Map(), htmlByElement: /* @__PURE__ */ new Map() };
for (const { el, text } of readVisibleOrderFeedbackEntries()) {
if (!snapshot.elements.has(el)) return text;
if (snapshot.textByElement.get(el) !== text) return text;
if (snapshot.htmlByElement.get(el) !== el.innerHTML) return text;
}
return "";
}
async function waitForOrderSubmitAcknowledgement(button, label, previousFeedbackSnapshot) {
const startedAt = Date.now();
let sawBusy = isSubmitButtonBusy(button);
while (Date.now() - startedAt < LADDER_SUBMIT_ACK_TIMEOUT_MS) {
const feedback = readNewVisibleOrderFeedbackText(previousFeedbackSnapshot);
const acknowledgement = evaluateOrderSubmitAcknowledgement({
feedback,
isNewFeedback: Boolean(feedback)
});
if (acknowledgement.status === "failure") throw new Error(acknowledgement.message);
if (acknowledgement.status === "success") return;
const busy = isSubmitButtonBusy(button);
if (busy) sawBusy = true;
await delay(LADDER_SUBMIT_POLL_MS);
}
const settleHint = sawBusy ? "按钮已恢复但未收到明确成功反馈" : "未观察到提交按钮状态变化";
throw new Error(`未收到明确${label}成功反馈(${settleHint}),已停止;请核对当前委托/历史成交`);
}
async function executeLadderPlan(plan) {
const priceInput = findPriceInput();
const qtyInput = findQtyInput();
if (!priceInput || !qtyInput) throw new Error("未找到价格或数量输入框");
let done = 0;
for (const order of plan.orders) {
if (ladderStopRequested) break;
assertLadderExecutionContext(plan);
if (!await ensurePostOnlyOrderType()) throw new Error("执行中只做Maker (Post Only) 状态丢失,请刷新页面后重试");
assertLadderExecutionContext(plan);
assertLadderMakerPrice(plan, order.price);
const currentPriceInput = findPriceInput();
const currentQtyInput = findQtyInput();
if (!currentPriceInput || !currentQtyInput) throw new Error("执行中价格或数量输入框丢失");
setInputValueReact(currentPriceInput, order.price);
await delay(90);
setInputValueReact(currentQtyInput, order.qty);
await delay(120);
const submittedPrice = normalizeDecimalString(currentPriceInput.value);
if (!submittedPrice) throw new Error("执行中价格输入框值无效");
assertSubmittedPriceMatchesClickedPrice(order.price, submittedPrice);
assertLadderExecutionContext(plan);
assertLadderMakerPrice(plan, submittedPrice);
const button = plan.spec.buttonGetter();
if (!button || button.disabled || button.getAttribute("aria-disabled") === "true") {
throw new Error(`未找到可点击的${plan.spec.label}按钮`);
}
if (!CFG.SAFE_MODE) {
const previousFeedback = takeOrderFeedbackSnapshot();
button.click();
setLadderStatus(`${plan.spec.label} ${done + 1}/${plan.orders.length} 确认中`);
waitForTradeUiMutation({ timeoutMs: 500 });
await waitForOrderSubmitAcknowledgement(button, plan.spec.label, previousFeedback);
}
done++;
setLadderStatus(`${plan.spec.label} ${done}/${plan.orders.length}`);
await delay(LADDER_ORDER_DELAY_MS);
}
return done;
}
async function startLadder(actionType) {
if (ladderTask) {
setLadderStatus("正在执行,先点停止");
return;
}
ladderStopRequested = false;
const spec = getLadderActionSpec2(actionType);
setLadderStatus(`${spec?.label || "阶梯"} 准备中`);
ladderTask = (async () => {
const { plan, done } = await runLadderPlanWithOpenOrderReplacement(actionType);
setLadderStatus(ladderStopRequested ? `已停止 ${done}/${plan.orders.length}` : `完成 ${done}/${plan.orders.length}`);
})().catch((e) => {
err("Maker 阶梯执行失败:", e);
setLadderStatus(e?.message || "执行失败", e?.statusTitle);
}).finally(() => {
ladderTask = null;
ladderStopRequested = false;
scheduleRenderPanel();
});
scheduleRenderPanel();
await ladderTask;
}
function stopLadder() {
if (!ladderTask) {
setLadderStatus("空闲");
return;
}
ladderStopRequested = true;
setLadderStatus("停止中");
scheduleRenderPanel();
}
function findVisibleElementByText(selector, patterns, root = document) {
for (const el of root.querySelectorAll(selector)) {
if (!isVisibleElement(el)) continue;
const text = (el.textContent || "").replace(/\s+/g, " ").trim();
if (patterns.some((pattern) => pattern.test(text))) return el;
}
return null;
}
function findVisibleTextElement(patterns, root = document) {
const candidates = Array.from(root.querySelectorAll('button, [role="button"], a, [tabindex], div, span')).filter(isVisibleElement).map((el) => ({
el,
text: (el.textContent || "").replace(/\s+/g, " ").trim(),
rect: el.getBoundingClientRect()
})).filter(({ text }) => patterns.some((pattern) => pattern.test(text)));
candidates.sort((a, b) => a.rect.width * a.rect.height - b.rect.width * b.rect.height);
return candidates[0]?.el || null;
}
function getNormalizedText2(el) {
return normalizeText(el?.textContent || "");
}
function getAccountOrdersTabGroup2(tab) {
return getAccountOrdersTabGroup(tab, { isVisibleElement });
}
function findOpenOrdersTab2() {
return findOpenOrdersTab(document, { isVisibleElement });
}
function getOpenOrdersTabCount() {
const tab = findOpenOrdersTab2();
if (!tab) return null;
return parseOpenOrdersTabCount(getNormalizedText2(tab));
}
function findSelectedAccountOrdersTab2() {
return findSelectedAccountOrdersTab(document, { isVisibleElement });
}
async function activateOpenOrdersTab() {
const tab = findOpenOrdersTab2();
if (!tab) return false;
if (tab.getAttribute("aria-selected") === "true") return true;
tab.click();
await delay(350);
const activeTab = findOpenOrdersTab2();
return activeTab?.getAttribute("aria-selected") === "true";
}
async function restoreAccountOrdersTab(previousTab) {
if (!previousTab || !previousTab.isConnected || !isVisibleElement(previousTab)) return true;
if (previousTab.getAttribute("aria-selected") === "true") return true;
previousTab.click();
await delay(250);
return previousTab.getAttribute("aria-selected") === "true";
}
function getActiveOpenOrdersScope2() {
return getActiveOpenOrdersScope(document, {
isVisibleElement,
findHideOtherSymbolCheckbox,
findCurrentSymbolCancelAllButton
});
}
function findOpenOrdersBasicSubTab2(root) {
return findOpenOrdersBasicSubTab(root, { isVisibleElement });
}
function findOpenOrdersConditionalSubTab2(root) {
return findOpenOrdersConditionalSubTab(root, { isVisibleElement });
}
function findSelectedOpenOrdersSubTab2(root) {
return findSelectedOpenOrdersSubTab(root, { isVisibleElement });
}
async function waitForActiveOpenOrdersScope() {
const deadline = Date.now() + 2200;
while (Date.now() < deadline) {
const scope = getActiveOpenOrdersScope2();
if (scope) return scope;
await delay(100);
}
return getActiveOpenOrdersScope2();
}
async function activateOpenOrdersBasicSubTab(root) {
const previousSubTab = findSelectedOpenOrdersSubTab2(root);
const basicTab = findOpenOrdersBasicSubTab2(root);
if (!basicTab) {
return {
ready: !findOpenOrdersConditionalSubTab2(root),
previousSubTab
};
}
if (basicTab.getAttribute("aria-selected") === "true") {
return { ready: true, previousSubTab };
}
basicTab.click();
await delay(250);
return {
ready: findOpenOrdersBasicSubTab2(root)?.getAttribute("aria-selected") === "true",
previousSubTab
};
}
async function restoreOpenOrdersSubTab(previousSubTab) {
if (!previousSubTab || !previousSubTab.isConnected || !isVisibleElement(previousSubTab)) return true;
if (previousSubTab.getAttribute("aria-selected") === "true") return true;
previousSubTab.click();
await delay(250);
return previousSubTab.getAttribute("aria-selected") === "true";
}
function findCurrentSymbolCancelAllButton(root) {
if (!root) return null;
const button = findVisibleElementByText(
'button, [role="button"], a',
[/^全撤$/, /^全部撤单$/, /^撤销全部$/, /^Cancel All$/i],
root
) || findVisibleTextElement([/^全撤$/, /^全部撤单$/, /^撤销全部$/, /^Cancel All$/i], root);
if (!button || button.disabled || button.getAttribute("aria-disabled") === "true") return null;
return button;
}
function findHideOtherSymbolCheckbox(root) {
if (!root) return null;
return Array.from(root.querySelectorAll('[role="checkbox"][name="hideOtherSymbol"]')).find(isVisibleElement) || null;
}
function getCheckboxCheckedState(checkbox) {
if (!checkbox) return null;
const ariaChecked = checkbox.getAttribute("aria-checked");
if (ariaChecked === "true") return true;
if (ariaChecked === "false") return false;
if (typeof checkbox.checked === "boolean") return checkbox.checked;
const input = checkbox.matches('input[type="checkbox"]') ? checkbox : checkbox.querySelector('input[type="checkbox"]');
if (input && typeof input.checked === "boolean") return input.checked;
if (checkbox.hasAttribute("checked")) return true;
return null;
}
function readVisibleOpenOrderSymbols(root) {
return readVisibleOpenOrderSymbolsText(root?.textContent || "");
}
function isOpenOrdersScopeLimitedToSymbol(root, symbol) {
return isOpenOrdersScopeLimitedToSymbolText(root?.textContent || "", symbol);
}
function hasCurrentSymbolOpenOrders(root, symbol, symbolFilterOk, cancelAllButton) {
return hasCurrentSymbolOpenOrdersEvidence({
scopeText: root?.textContent || "",
symbol,
symbolFilterOk,
openOrdersCount: getOpenOrdersTabCount(),
cancelAllAvailable: Boolean(cancelAllButton)
});
}
async function waitForCurrentSymbolOpenOrders(root, symbol, symbolFilterOk) {
const deadline = Date.now() + 1600;
while (Date.now() < deadline) {
const cancelAllButton2 = findCurrentSymbolCancelAllButton(root);
if (hasCurrentSymbolOpenOrders(root, symbol, symbolFilterOk, cancelAllButton2)) {
return { hasOrders: true, cancelAllButton: cancelAllButton2 };
}
await delay(100);
}
const cancelAllButton = findCurrentSymbolCancelAllButton(root);
return {
hasOrders: hasCurrentSymbolOpenOrders(root, symbol, symbolFilterOk, cancelAllButton),
cancelAllButton
};
}
async function waitForNoCurrentSymbolOpenOrders(root, symbol, symbolFilterOk) {
const deadline = Date.now() + LADDER_REPLACE_OPEN_ORDERS_CLEAR_TIMEOUT_MS;
while (Date.now() < deadline) {
if (getCurrentSymbol() !== symbol) return false;
const cancelAllButton2 = findCurrentSymbolCancelAllButton(root);
if (!hasCurrentSymbolOpenOrders(root, symbol, symbolFilterOk, cancelAllButton2)) return true;
await delay(120);
}
const cancelAllButton = findCurrentSymbolCancelAllButton(root);
return !hasCurrentSymbolOpenOrders(root, symbol, symbolFilterOk, cancelAllButton);
}
function getVisibleDirectChildren(el) {
return Array.from(el?.children || []).filter(isVisibleElement);
}
function findOpenOrderRowCells(row) {
const candidates = Array.from(row.querySelectorAll(
".flex.items-center.typography-caption2.text-PrimaryText.w-full, .flex.items-center.typography-caption2.text-PrimaryText"
));
const rowBody = candidates.find((el) => getVisibleDirectChildren(el).length >= 10) || row.firstElementChild;
return getVisibleDirectChildren(rowBody);
}
function findOpenOrderRowCancelButton(row) {
const icon = row.querySelector('svg[aria-label="撤销挂单"]');
if (!icon || !isVisibleElement(icon)) return null;
const target = icon.closest('button, [role="button"], a, [tabindex]') || icon;
if (!target || !row.contains(target) || !isVisibleElement(target)) return null;
if (target.disabled || target.getAttribute("aria-disabled") === "true") return null;
return target;
}
function clickDomTarget(target) {
if (!target || !target.isConnected || !isVisibleElement(target)) return false;
if (typeof target.click === "function") {
target.click();
return true;
}
return target.dispatchEvent(new MouseEvent("click", {
bubbles: true,
cancelable: true,
view: window
}));
}
function getOpenOrderRowKey(cells, row) {
const cellText = cells.slice(0, 10).map((cell) => (cell.textContent || "").replace(/\s+/g, " ").trim()).join("|");
return cellText || (row.textContent || "").replace(/\s+/g, " ").trim();
}
function readCurrentSymbolOpenOrderRows(root, symbol, plan = null) {
if (!root || !symbol) return [];
return Array.from(root.querySelectorAll(".list-item-container")).filter(isVisibleElement).map((row) => {
const cells = findOpenOrderRowCells(row);
const symbolText = (cells[1]?.textContent || "").replace(/\s+/g, " ").trim();
const sideText = (cells[3]?.textContent || "").replace(/\s+/g, " ").trim();
const qty = normalizeDecimalString((cells[5]?.textContent || "").replace(/,/g, "").match(/\d+(?:\.\d+)?/)?.[0] || "");
const cancelButton = findOpenOrderRowCancelButton(row);
return {
root,
row,
cells,
symbolText,
sideText,
qty,
cancelButton,
key: getOpenOrderRowKey(cells, row)
};
}).filter((row) => isOpenOrderRowCurrentSymbol(row.symbolText, symbol) && isOpenOrderRowForPlan(row.sideText, plan) && row.qty && isPositiveDecimalString(row.qty) && row.cancelButton);
}
function isOpenOrderRowCurrentSymbol(symbolText, symbol) {
const tokens = String(symbolText || "").toUpperCase().match(/[A-Z0-9_]+/g) || [];
return tokens.includes(String(symbol || "").toUpperCase());
}
function isOpenOrderRowForPlan(sideText, plan) {
if (!plan) return true;
const normalized = String(sideText || "").replace(/\s+/g, "").toUpperCase();
if (plan.spec?.mode === "OPEN" && plan.spec.side === "LONG") {
return normalized.includes("开多") || normalized.includes("OPENLONG");
}
if (plan.spec?.mode === "OPEN" && plan.spec.side === "SHORT") {
return normalized.includes("开空") || normalized.includes("OPENSHORT");
}
if (plan.spec?.mode === "CLOSE" && plan.spec.side === "LONG") {
return normalized.includes("平多") || normalized.includes("CLOSELONG");
}
if (plan.spec?.mode === "CLOSE" && plan.spec.side === "SHORT") {
return normalized.includes("平空") || normalized.includes("CLOSESHORT");
}
return false;
}
async function waitForCurrentSymbolOpenOrderRows(root, symbol, plan = null, options = null) {
const { openOrdersCount = null } = options || {};
const timeoutMs = openOrdersCount > 0 ? LADDER_REPLACE_OPEN_ORDERS_CLEAR_TIMEOUT_MS : 1600;
const deadline = Date.now() + timeoutMs;
let currentRoot = root;
while (Date.now() < deadline) {
const refreshedRoot = getActiveOpenOrdersScope2();
if (refreshedRoot) currentRoot = refreshedRoot;
const rows = readCurrentSymbolOpenOrderRows(currentRoot, symbol, plan);
if (rows.length) return rows;
await delay(100);
}
return readCurrentSymbolOpenOrderRows(currentRoot, symbol, plan);
}
function selectOpenOrderRowsToCancelForPlan(plan, rows, options = null) {
const { allowPartial = false } = options || {};
const rowsToCancel = [];
let cancelQty = "0";
for (const row of rows) {
if (!isOpenOrderRowForPlan(row.sideText, plan)) continue;
rowsToCancel.push(row);
cancelQty = addDecimalStrings(cancelQty, row.qty);
if (compareDecimalStrings(cancelQty, plan.totalQty) >= 0) break;
}
return compareDecimalStrings(cancelQty, plan.totalQty) >= 0 || allowPartial && rowsToCancel.length > 0 ? rowsToCancel : [];
}
function getPlanDirectionLabel(plan) {
if (plan?.spec?.mode === "OPEN" && plan.spec.side === "LONG") return "开多";
if (plan?.spec?.mode === "OPEN" && plan.spec.side === "SHORT") return "开空";
if (plan?.spec?.mode === "CLOSE" && plan.spec.side === "LONG") return "平多";
if (plan?.spec?.mode === "CLOSE" && plan.spec.side === "SHORT") return "平空";
return "";
}
function countOpenOrderRowsByKey(root, symbol, key) {
return readCurrentSymbolOpenOrderRows(root, symbol).filter((row) => row.key === key).length;
}
async function waitForOpenOrderRowKeyCountBelow(root, symbol, key, previousCount) {
const deadline = Date.now() + LADDER_REPLACE_OPEN_ORDERS_CLEAR_TIMEOUT_MS;
while (Date.now() < deadline) {
if (getCurrentSymbol() !== symbol) return false;
if (countOpenOrderRowsByKey(root, symbol, key) < previousCount) return true;
await delay(120);
}
return countOpenOrderRowsByKey(root, symbol, key) < previousCount;
}
async function cancelOpenOrderRowsForPlan(root, plan) {
let cancelQty = "0";
let currentRoot = root;
while (compareDecimalStrings(cancelQty, plan.totalQty) < 0) {
if (getCurrentSymbol() !== plan.symbol) throw new Error("逐行撤单前交易对已变化");
const remainingQty = subtractDecimalStrings(plan.totalQty, cancelQty);
const refreshedRoot = getActiveOpenOrdersScope2();
if (refreshedRoot) currentRoot = refreshedRoot;
const rows = readCurrentSymbolOpenOrderRows(currentRoot, plan.symbol, plan);
const row = selectOpenOrderRowsToCancelForPlan(
{ ...plan, totalQty: remainingQty },
rows,
{ allowPartial: true }
)[0];
if (!row) {
throw new Error(`${plan.symbol} 当前币可撤挂单数量不足,已停止重挂`);
}
currentRoot = row.root || currentRoot;
const previousKeyCount = countOpenOrderRowsByKey(row.root, plan.symbol, row.key);
if (!row.cancelButton?.isConnected || !isVisibleElement(row.cancelButton)) {
if (previousKeyCount === 0) continue;
throw new Error("当前币挂单撤单入口已失效,已停止重挂");
}
const dialogsBefore = new Set(getVisibleDialogs());
if (!clickDomTarget(row.cancelButton)) {
throw new Error("当前币挂单撤单入口点击失败,已停止重挂");
}
waitForTradeUiMutation({ timeoutMs: 800 });
const dialog = await waitForNewVisibleDialog(dialogsBefore);
if (dialog) {
setLadderStatus(`${plan.symbol} 单行撤单确认弹窗已打开`);
await waitForDialogToClose(dialog);
} else {
await delay(260);
}
if (!await waitForOpenOrderRowKeyCountBelow(row.root, plan.symbol, row.key, previousKeyCount)) {
throw new Error(`${plan.symbol} 当前币挂单仍存在,已停止重挂`);
}
cancelQty = addDecimalStrings(cancelQty, row.qty);
}
return { ok: true, cancelQty };
}
async function setHideOtherSymbolChecked(root, desiredChecked) {
const checkbox = findHideOtherSymbolCheckbox(root);
if (!checkbox) return false;
const currentChecked = getCheckboxCheckedState(checkbox);
if (currentChecked === desiredChecked) return true;
if (currentChecked === null) return false;
checkbox.click();
const deadline = Date.now() + 1e3;
while (Date.now() < deadline) {
await delay(80);
const nextChecked = getCheckboxCheckedState(findHideOtherSymbolCheckbox(root));
if (nextChecked === desiredChecked) return true;
}
return false;
}
async function ensureOpenOrdersLimitedToCurrentSymbol(root, symbol) {
const checkbox = findHideOtherSymbolCheckbox(root);
if (!checkbox) {
return {
ok: isOpenOrdersScopeLimitedToSymbol(root, symbol),
originalChecked: null
};
}
const originalChecked = getCheckboxCheckedState(checkbox);
if (originalChecked === null) {
return {
ok: isOpenOrdersScopeLimitedToSymbol(root, symbol),
originalChecked
};
}
const ok = originalChecked || await setHideOtherSymbolChecked(root, true);
return {
ok: ok || isOpenOrdersScopeLimitedToSymbol(root, symbol),
originalChecked
};
}
async function restoreOpenOrdersSymbolFilter(root, originalChecked) {
if (originalChecked !== false) return true;
return setHideOtherSymbolChecked(root, false);
}
function getVisibleDialogs() {
return Array.from(document.querySelectorAll(
'[role="dialog"], [class*="modal"], [class*="Modal"], [class*="popover"], [class*="Popover"], [class*="drawer"], [class*="Drawer"]'
)).filter(isVisibleElement);
}
function findNewVisibleDialog(dialogsBefore) {
const previousDialogs = dialogsBefore || /* @__PURE__ */ new Set();
for (const dialog of getVisibleDialogs()) {
if (previousDialogs.has(dialog)) continue;
return dialog;
}
return null;
}
async function waitForNewVisibleDialog(dialogsBefore) {
const deadline = Date.now() + 1800;
while (Date.now() < deadline) {
const dialog = findNewVisibleDialog(dialogsBefore);
if (dialog) return dialog;
await delay(100);
}
return findNewVisibleDialog(dialogsBefore);
}
async function waitForDialogToClose(dialog) {
while (dialog.isConnected && isVisibleElement(dialog)) {
await delay(500);
}
}
async function cancelCurrentSymbolOpenOrders(options = null) {
const { waitUntilCleared = false } = options || {};
const symbol = getCurrentSymbol();
if (!symbol) {
setLadderStatus("未识别当前交易对");
return { ok: false, status: "no_symbol", message: "未识别当前交易对" };
}
const previousAccountOrdersTab = findSelectedAccountOrdersTab2();
let openOrdersScope = null;
let previousOpenOrdersSubTab = null;
let symbolFilterOriginalChecked = null;
try {
setLadderStatus(`查找 ${symbol} 当前委托`);
const tabReady = await activateOpenOrdersTab();
if (!tabReady || getCurrentSymbol() !== symbol) {
const message = "当前委托页未就绪或交易对已变化";
setLadderStatus(message);
return { ok: false, status: "tab_not_ready", message };
}
openOrdersScope = await waitForActiveOpenOrdersScope();
if (!openOrdersScope) {
const message = "未定位到当前委托面板";
setLadderStatus(message);
return { ok: false, status: "scope_not_found", message };
}
const basicSubTabState = await activateOpenOrdersBasicSubTab(openOrdersScope);
previousOpenOrdersSubTab = basicSubTabState.previousSubTab;
if (!basicSubTabState.ready) {
const message = "未定位到当前委托基础单";
setLadderStatus(message);
return { ok: false, status: "basic_tab_not_ready", message };
}
const symbolFilter = await ensureOpenOrdersLimitedToCurrentSymbol(openOrdersScope, symbol);
symbolFilterOriginalChecked = symbolFilter.originalChecked;
if (!symbolFilter.ok) {
const message = "未确认只显示当前币挂单";
setLadderStatus(message);
return { ok: false, status: "symbol_filter_not_confirmed", message };
}
const openOrdersEvidence = await waitForCurrentSymbolOpenOrders(openOrdersScope, symbol, symbolFilter.ok);
if (!openOrdersEvidence.hasOrders) {
setLadderStatus(`${symbol} 当前币无挂单`);
return { ok: true, status: "no_orders" };
}
const { cancelAllButton } = openOrdersEvidence;
if (!cancelAllButton) {
const message = "未找到当前委托全撤按钮";
setLadderStatus(message);
return { ok: false, status: "cancel_button_not_found", message };
}
const dialogsBefore = new Set(getVisibleDialogs());
cancelAllButton.click();
const dialog = await waitForNewVisibleDialog(dialogsBefore);
if (getCurrentSymbol() !== symbol) {
const message = "确认撤单前交易对已变化";
setLadderStatus(message);
return { ok: false, status: "symbol_changed", message };
}
if (dialog) {
setLadderStatus(`${symbol} 撤单确认弹窗已打开`);
waitForTradeUiMutation({ timeoutMs: 800 });
await waitForDialogToClose(dialog);
if (waitUntilCleared) {
const cleared = await waitForNoCurrentSymbolOpenOrders(openOrdersScope, symbol, symbolFilter.ok);
if (!cleared) {
const message = `${symbol} 当前币挂单仍存在,已停止重挂`;
setLadderStatus(message);
return { ok: false, status: "not_cleared", message };
}
setLadderStatus(`${symbol} 当前币挂单已撤,继续重挂`);
return { ok: true, status: "cleared" };
}
setLadderStatus(`${symbol} 撤单流程结束,已恢复筛选状态`);
return { ok: true, status: "dialog_closed" };
}
setLadderStatus(`${symbol} 撤单已点击,请核对当前委托`);
waitForTradeUiMutation({ timeoutMs: 800 });
if (waitUntilCleared) {
const cleared = await waitForNoCurrentSymbolOpenOrders(openOrdersScope, symbol, symbolFilter.ok);
if (!cleared) {
const message = `${symbol} 当前币挂单仍存在,已停止重挂`;
setLadderStatus(message);
return { ok: false, status: "not_cleared", message };
}
setLadderStatus(`${symbol} 当前币挂单已撤,继续重挂`);
return { ok: true, status: "cleared" };
}
return { ok: true, status: "cancel_clicked" };
} finally {
if (openOrdersScope && symbolFilterOriginalChecked === false) {
const restored = await restoreOpenOrdersSymbolFilter(openOrdersScope, symbolFilterOriginalChecked);
if (!restored) setLadderStatus("未能恢复隐藏其他合约状态");
}
if (previousOpenOrdersSubTab) {
await restoreOpenOrdersSubTab(previousOpenOrdersSubTab);
}
await restoreAccountOrdersTab(previousAccountOrdersTab);
}
}
async function cancelCurrentSymbolOpenOrdersForPlan(plan) {
const symbol = getCurrentSymbol();
if (!symbol || symbol !== plan?.symbol) {
const message = "逐行撤单前交易对已变化";
setLadderStatus(message);
return { ok: false, status: "symbol_changed", message };
}
const previousAccountOrdersTab = findSelectedAccountOrdersTab2();
let openOrdersScope = null;
let previousOpenOrdersSubTab = null;
let symbolFilterOriginalChecked = null;
try {
setLadderStatus(`查找 ${symbol} 当前委托`);
const tabReady = await activateOpenOrdersTab();
if (!tabReady || getCurrentSymbol() !== symbol) {
const message = "当前委托页未就绪或交易对已变化";
setLadderStatus(message);
return { ok: false, status: "tab_not_ready", message };
}
openOrdersScope = await waitForActiveOpenOrdersScope();
if (!openOrdersScope) {
const message = "未定位到当前委托面板";
setLadderStatus(message);
return { ok: false, status: "scope_not_found", message };
}
const basicSubTabState = await activateOpenOrdersBasicSubTab(openOrdersScope);
previousOpenOrdersSubTab = basicSubTabState.previousSubTab;
if (!basicSubTabState.ready) {
const message = "未定位到当前委托基础单";
setLadderStatus(message);
return { ok: false, status: "basic_tab_not_ready", message };
}
openOrdersScope = await waitForActiveOpenOrdersScope();
if (!openOrdersScope) {
const message = "未定位到当前委托面板";
setLadderStatus(message);
return { ok: false, status: "scope_not_found", message };
}
const symbolFilter = await ensureOpenOrdersLimitedToCurrentSymbol(openOrdersScope, symbol);
symbolFilterOriginalChecked = symbolFilter.originalChecked;
if (!symbolFilter.ok) {
const message = "未确认只显示当前币挂单";
setLadderStatus(message);
return { ok: false, status: "symbol_filter_not_confirmed", message };
}
openOrdersScope = await waitForActiveOpenOrdersScope();
if (!openOrdersScope) {
const message = "未定位到当前委托面板";
setLadderStatus(message);
return { ok: false, status: "scope_not_found", message };
}
const openOrdersCount = getOpenOrdersTabCount();
const rows = await waitForCurrentSymbolOpenOrderRows(openOrdersScope, symbol, plan, {
openOrdersCount
});
if (!rows.length) {
const directionLabel = getPlanDirectionLabel(plan);
const message = `未定位到 ${symbol}${directionLabel ? ` ${directionLabel}` : ""} 当前币可逐行撤单的基础单`;
setLadderStatus(message);
return { ok: false, status: "rows_not_found", message };
}
const rowsToCancel = selectOpenOrderRowsToCancelForPlan(plan, rows);
if (!rowsToCancel.length) {
const message = `未选中 ${symbol} 当前币待撤挂单`;
setLadderStatus(message);
return { ok: false, status: "rows_not_selected", message };
}
setLadderStatus(`${symbol} 撤销 ${rowsToCancel.length} 笔当前币挂单`);
await cancelOpenOrderRowsForPlan(openOrdersScope, plan);
setLadderStatus(`${symbol} 当前币挂单已替换,继续重挂`);
return { ok: true, status: "rows_cleared" };
} catch (e) {
const message = e?.message || "当前币挂单逐行撤销失败,已停止重挂";
setLadderStatus(message);
return { ok: false, status: "row_cancel_failed", message };
} finally {
if (openOrdersScope && symbolFilterOriginalChecked === false) {
const restored = await restoreOpenOrdersSymbolFilter(openOrdersScope, symbolFilterOriginalChecked);
if (!restored) setLadderStatus("未能恢复隐藏其他合约状态");
}
if (previousOpenOrdersSubTab) {
await restoreOpenOrdersSubTab(previousOpenOrdersSubTab);
}
await restoreAccountOrdersTab(previousAccountOrdersTab);
}
}
function formatLadderPlanStatus(plan) {
const levelText = plan.levels === plan.requestedLevels ? `${plan.levels}档` : `${plan.levels}/${plan.requestedLevels}档`;
const stepText = plan.ladderStep > DEFAULT_LADDER_STEP ? `/幅${plan.ladderStep}` : "";
return `${plan.spec.label} ${plan.percent}%/${levelText}${stepText}`;
}
function isReplaceableCloseLadderOpenOrdersFailure(plan, error) {
if (plan?.spec?.mode !== "CLOSE") return false;
return isReduceOnlyOpenOrdersConflictFeedback(error?.message || "");
}
function isReplaceableOpenLadderOpenOrdersFailure(plan, error) {
if (plan?.spec?.mode !== "OPEN") return false;
return isOpenLadderOpenOrdersCapacityFeedback(error?.message || "");
}
function getOpenLadderMinimumQtyReplacementPlan(error) {
const plan = error?.openOrdersReplacementPlan;
if (plan?.spec?.mode === "OPEN" && plan.symbol && plan.totalQty && isPositiveDecimalString(plan.totalQty)) {
return plan;
}
return null;
}
function getReplaceableLadderOpenOrdersPlan(plan, error) {
if (isReplaceableCloseLadderOpenOrdersFailure(plan, error)) return plan;
if (isReplaceableOpenLadderOpenOrdersFailure(plan, error)) return plan;
return getOpenLadderMinimumQtyReplacementPlan(error);
}
function formatOpenOrdersReplacementStatus(plan) {
if (plan?.spec?.mode === "OPEN") return `${plan.symbol} 同向开仓挂单可能占用可开数量,准备替换`;
return `${plan.symbol} 当前挂单占用可平数量,准备替换`;
}
async function runLadderPlanWithOpenOrderReplacement(actionType) {
let replacementSymbol = null;
for (let attempt = 0; attempt < 2; attempt += 1) {
let plan = null;
try {
plan = await buildLadderPlan(actionType, replacementSymbol);
setLadderStatus(formatLadderPlanStatus(plan));
const done = await executeLadderPlan(plan);
return { plan, done };
} catch (e) {
const replacementPlan = getReplaceableLadderOpenOrdersPlan(plan, e);
if (attempt > 0 || !replacementPlan) throw e;
setLadderStatus(formatOpenOrdersReplacementStatus(replacementPlan));
replacementSymbol = replacementPlan.symbol;
const result = await cancelCurrentSymbolOpenOrdersForPlan(replacementPlan);
if (!result?.ok) throw new Error(result.message || "当前币挂单未替换,已停止重挂");
}
}
throw new Error("阶梯重挂流程异常");
}
function readQtyByDataTestId(testId) {
const el = document.querySelector(`[data-testid="${testId}"]`);
if (!el) return null;
const txt = (el.textContent || "").replace(/,/g, "");
const m = txt.match(/(\d+(?:\.\d+)?)/);
if (!m) return null;
return parseNumber(m[1]);
}
function readCloseableQtyByTestIds() {
const longQty = readQtyByDataTestId("max-sell-amount");
const shortQty = readQtyByDataTestId("max-buy-amount");
if (longQty == null && shortQty == null) return null;
return { longQty, shortQty, qtySource: "testid" };
}
function getButtonTextSearchRoot(button) {
if (!button) return null;
const localRoot = button.closest('[class*="order"], [data-testid*="order"]');
if (localRoot && localRoot !== document.body) return localRoot;
return getTradeSearchScopes().find((scope) => scope && scope !== document.body && scope.contains(button)) || null;
}
function readCloseableQtyNearButton(button) {
if (!button) return null;
const btnRect = button.getBoundingClientRect();
const root = getButtonTextSearchRoot(button);
if (!root) return null;
let best = null;
let bestScore = Infinity;
const nodes = root.querySelectorAll("div, span, p, small");
for (const node of nodes) {
const text = (node.textContent || "").trim();
if (!text.includes("可平")) continue;
const m = text.match(/可平\s*([\d,]*\.?\d+)/);
if (!m) continue;
const qty = parseNumber(m[1]);
if (!(qty >= 0)) continue;
const r = node.getBoundingClientRect();
if (!r || !Number.isFinite(r.left)) continue;
const nodeX = (r.left + r.right) / 2;
const btnX = (btnRect.left + btnRect.right) / 2;
const dy = r.top - btnRect.bottom;
if (dy < -16 || dy > 200) continue;
const dx = Math.abs(nodeX - btnX);
const score = dx + Math.abs(dy) * 2;
if (score < bestScore) {
bestScore = score;
best = qty;
}
}
return best;
}
function readCloseableQty(closeLongBtn, closeShortBtn) {
const fromTestId = readCloseableQtyByTestIds();
if (fromTestId) return fromTestId;
return {
longQty: readCloseableQtyNearButton(closeLongBtn),
shortQty: readCloseableQtyNearButton(closeShortBtn),
qtySource: "near_button"
};
}
function readQtyTextNearButton(button, label) {
if (!button) return null;
const btnRect = button.getBoundingClientRect();
const root = getButtonTextSearchRoot(button);
if (!root) return null;
let best = null;
let bestScore = Infinity;
const nodes = root.querySelectorAll("div, span, p, small");
const re = new RegExp(`${label}\\s*([\\d,]*\\.?\\d+)`, "g");
for (const node of nodes) {
const text = (node.textContent || "").replace(/\s+/g, " ").trim();
if (!text.includes(label)) continue;
const matches = Array.from(text.matchAll(re));
if (!matches.length) continue;
const r = node.getBoundingClientRect();
if (!r || !Number.isFinite(r.left)) continue;
const nodeX = (r.left + r.right) / 2;
const btnX = (btnRect.left + btnRect.right) / 2;
const dy = r.top - btnRect.bottom;
if (dy < -32 || dy > 240) continue;
const dx = Math.abs(nodeX - btnX);
const score = dx + Math.abs(dy) * 2;
if (score >= bestScore) continue;
const matchIndex = matches.length > 1 && btnX < nodeX ? 0 : matches.length - 1;
const qty = normalizeDecimalString(matches[matchIndex]?.[1] || "");
if (!qty) continue;
bestScore = score;
best = qty;
}
return best;
}
function readOpenableQty(openLongBtn, openShortBtn) {
return {
longQty: readQtyTextNearButton(openLongBtn, "可开"),
shortQty: readQtyTextNearButton(openShortBtn, "可开"),
qtySource: "near_button"
};
}
function normalizeCloseSide(value) {
return String(value || "LONG").toUpperCase() === "SHORT" ? "SHORT" : "LONG";
}
function loadCloseSide() {
return normalizeCloseSide(localStorage.getItem(LOCAL_CLOSE_SIDE_KEY) || DEFAULT_CLOSE_SIDE);
}
function saveCloseSide(value) {
localStorage.setItem(LOCAL_CLOSE_SIDE_KEY, normalizeCloseSide(value));
}
function updateCloseSide(value) {
saveCloseSide(value);
scheduleRenderPanel();
}
function loadOpenSide() {
return normalizeCloseSide(localStorage.getItem(LOCAL_OPEN_SIDE_KEY) || DEFAULT_OPEN_SIDE);
}
function saveOpenSide(value) {
localStorage.setItem(LOCAL_OPEN_SIDE_KEY, normalizeCloseSide(value));
}
function updateOpenSide(value) {
saveOpenSide(value);
scheduleRenderPanel();
}
function readCloseContext() {
const closeLongBtn = findCloseLongButton();
const closeShortBtn = findCloseShortButton();
const { longQty, shortQty, qtySource } = readCloseableQty(closeLongBtn, closeShortBtn);
const knowsLong = longQty != null;
const knowsShort = shortQty != null;
const hasLong = longQty > 0;
const hasShort = shortQty > 0;
return { closeLongBtn, closeShortBtn, longQty, shortQty, qtySource, knowsLong, knowsShort, hasLong, hasShort };
}
function resolveDisplayCloseState(rawCloseContext, symbol) {
const cache = symbol && lastConfirmedCloseState?.symbol === symbol ? lastConfirmedCloseState : null;
const isPending = !rawCloseContext.knowsLong && !rawCloseContext.knowsShort;
const isUsingCache = rawCloseContext.longQty == null && cache?.longQty != null || rawCloseContext.shortQty == null && cache?.shortQty != null;
let longQty = rawCloseContext.longQty ?? cache?.longQty ?? null;
let shortQty = rawCloseContext.shortQty ?? cache?.shortQty ?? null;
const guard = closeGuard && closeGuard.symbol === symbol && Date.now() < closeGuard.expiresAt ? closeGuard : null;
if (guard && (rawCloseContext.knowsLong || rawCloseContext.knowsShort)) {
const rawLong = rawCloseContext.longQty;
const rawShort = rawCloseContext.shortQty;
const isNewSnapshot = rawLong !== guard.lastRawLong || rawShort !== guard.lastRawShort;
guard.lastRawLong = rawLong;
guard.lastRawShort = rawShort;
if (isNewSnapshot) {
if (rawLong === 0) {
guard.longZeroStreak++;
} else if (rawLong > 0) {
guard.longZeroStreak = 0;
}
if (rawShort === 0) {
guard.shortZeroStreak++;
} else if (rawShort > 0) {
guard.shortZeroStreak = 0;
}
}
const ZERO_CONFIRM_THRESHOLD = 2;
if (rawLong === 0 && cache?.longQty > 0 && guard.longZeroStreak < ZERO_CONFIRM_THRESHOLD) {
longQty = cache.longQty;
}
if (rawShort === 0 && cache?.shortQty > 0 && guard.shortZeroStreak < ZERO_CONFIRM_THRESHOLD) {
shortQty = cache.shortQty;
}
}
const knowsLong = longQty != null;
const knowsShort = shortQty != null;
const hasLong = longQty > 0;
const hasShort = shortQty > 0;
if (symbol && getActiveTradeMode() === "CLOSE" && (rawCloseContext.knowsLong || rawCloseContext.knowsShort)) {
const closeMode = hasLong && hasShort ? "dual" : hasLong ? "single_long" : hasShort ? "single_short" : "unknown";
lastConfirmedCloseState = {
symbol,
longQty,
shortQty,
closeMode,
longDisabled: !hasLong,
shortDisabled: !hasShort
};
}
const result = {
...rawCloseContext,
symbol,
longQty,
shortQty,
knowsLong,
knowsShort,
hasLong,
hasShort,
isUsingCache,
isPending
};
lastDisplayCloseState = result;
return result;
}
function getCachedCloseState(symbol) {
return symbol && lastConfirmedCloseState?.symbol === symbol ? lastConfirmedCloseState : null;
}
function hasPositionInDom(symbol) {
const rows = document.querySelectorAll(
'[class*="position"] tr, [class*="position"] [role="row"], [data-testid*="position"] tr, [data-testid*="position"] [role="row"]'
);
for (const row of rows) {
if (!isVisibleElement(row)) continue;
const text = (row.textContent || "").toUpperCase();
if (text.includes(symbol)) return true;
}
return false;
}
function readCurrentLeverageFromDom() {
const leverageButton = findVisibleTradeScopeElement('button, [role="button"]', (el) => {
const text2 = (el.textContent || "").replace(/\s+/g, " ").trim();
if (text2.length > 48) return false;
return /(?:全仓|逐仓|cross|isolated)\s*\d{1,3}\s*[xX]/i.test(text2);
});
const text = (leverageButton?.textContent || "").replace(/\s+/g, " ").trim();
const match = text.match(/(?:全仓|逐仓|cross|isolated)\s*(\d{1,3})\s*[xX]/i);
return match ? Number(match[1]) : null;
}
function getCachedPositionState(symbol) {
if (hasPositionInDom(symbol)) {
return { status: "has_position", source: "dom" };
}
const cache = getCachedCloseState(symbol);
if (!cache) return { status: "unknown", source: "close_cache_miss" };
const longQty = typeof cache.longQty === "number" ? cache.longQty : null;
const shortQty = typeof cache.shortQty === "number" ? cache.shortQty : null;
if (!(longQty >= 0) || !(shortQty >= 0)) {
return { status: "unknown", source: "close_cache_partial" };
}
const hasPosition = longQty > 0 || shortQty > 0;
return {
status: hasPosition ? "has_position" : "flat",
source: "close_cache",
longQty,
shortQty,
closeMode: cache.closeMode
};
}
function isStableOpenContext(symbol) {
return getActiveTradeMode() === "OPEN" && getCurrentSymbol() === symbol;
}
async function autoResetOpenLeverageToDefault(symbol, positionState, triggerSource) {
await delay(AUTO_OPEN_LEVERAGE_DELAY_MS);
if (!isStableOpenContext(symbol)) return false;
if (!cachedBncHeaders) {
for (let i = 0; i < 10; i++) {
await delay(500);
if (cachedBncHeaders || !isStableOpenContext(symbol)) break;
}
}
if (!cachedBncHeaders) {
log("bapi header 尚未缓存,跳过杠杆重置", symbol);
return false;
}
if (!isStableOpenContext(symbol)) return false;
if (hasPositionInDom(symbol)) {
log("延迟后发现持仓,跳过杠杆重置", symbol);
return false;
}
const currentLeverage = readCurrentLeverageFromDom();
if (currentLeverage === DEFAULT_OPEN_LEVERAGE) {
log("开仓杠杆已是默认值", symbol, `${DEFAULT_OPEN_LEVERAGE}x`, triggerSource);
return true;
}
try {
await adjustLeverageApi(symbol, DEFAULT_OPEN_LEVERAGE);
} catch (e) {
err("自动重置杠杆失败", symbol, `${DEFAULT_OPEN_LEVERAGE}x`, e.message || e);
return false;
}
log(
"无仓切回开仓,已自动重置杠杆",
symbol,
`${DEFAULT_OPEN_LEVERAGE}x`,
triggerSource,
positionState.source
);
return true;
}
function queueAutoOpenLeverageReset(triggerSource) {
const symbol = getCurrentSymbol();
if (!symbol) return;
const positionState = getCachedPositionState(symbol);
if (positionState.status === "has_position") return;
if (positionState.status !== "flat") return;
if (!isStableOpenContext(symbol) && triggerSource === "mutation") return;
const now = Date.now();
if (autoOpenLeverageTask) return;
if (lastAutoOpenLeverage.symbol === symbol && now - lastAutoOpenLeverage.at < AUTO_OPEN_LEVERAGE_DEDUPE_MS) {
return;
}
lastAutoOpenLeverage = { symbol, at: now };
autoOpenLeverageTask = autoResetOpenLeverageToDefault(symbol, positionState, triggerSource).catch((e) => {
err("自动重置开仓杠杆失败:", e);
return false;
}).finally(() => {
autoOpenLeverageTask = null;
});
}
function applyCachedNativeCloseButtonState() {
if (getActiveTradeMode() !== "CLOSE") return false;
const cache = getCachedCloseState(getCurrentSymbol());
if (!cache) return false;
const closeLongBtn = findCloseLongButton();
const closeShortBtn = findCloseShortButton();
if (!closeLongBtn && !closeShortBtn) return false;
const snapshot = `${cache.closeMode}|${cache.longQty}|${cache.shortQty}`;
if (snapshot !== lastAppliedCacheSnapshot) {
lastAppliedCacheSnapshot = snapshot;
const activeGuard = closeGuard && Date.now() < closeGuard.expiresAt ? closeGuard : null;
log(
"应用缓存按钮状态",
cache.closeMode,
"long=",
cache.longQty,
cache.longDisabled ? "(禁)" : "(启)",
"short=",
cache.shortQty,
cache.shortDisabled ? "(禁)" : "(启)",
activeGuard ? `guard:${activeGuard.expiresAt - Date.now()}ms L0x${activeGuard.longZeroStreak} S0x${activeGuard.shortZeroStreak} raw=${activeGuard.lastRawLong}/${activeGuard.lastRawShort}` : "no-guard"
);
}
if (closeLongBtn) {
setNativeActionButtonDisabled(closeLongBtn, !!cache.longDisabled);
}
if (closeShortBtn) {
setNativeActionButtonDisabled(closeShortBtn, !!cache.shortDisabled);
}
return true;
}
function applyCachedCloseUiState() {
if (getActiveTradeMode() !== "CLOSE") return false;
const cache = getCachedCloseState(getCurrentSymbol());
if (!cache) return false;
applyCachedNativeCloseButtonState();
renderPanel();
return true;
}
function resolveCloseAction() {
const display = lastDisplayCloseState;
const currentSymbol = getCurrentSymbol();
if (!display || display.symbol !== currentSymbol) return null;
const { longQty, shortQty, qtySource, hasLong, hasShort } = display;
const closeLongBtn = findCloseLongButton();
const closeShortBtn = findCloseShortButton();
if (hasLong && hasShort) {
const sideCfg = loadCloseSide();
if (sideCfg === "SHORT") {
return { side: "平空", button: closeShortBtn, by: "dual_panel", longQty, shortQty, qtySource };
}
return { side: "平多", button: closeLongBtn, by: "dual_panel", longQty, shortQty, qtySource };
}
if (hasLong) return { side: "平多", button: closeLongBtn, by: "single_long", longQty, shortQty, qtySource };
if (hasShort) return { side: "平空", button: closeShortBtn, by: "single_short", longQty, shortQty, qtySource };
return null;
}
function resolveOpenAction() {
const openLongBtn = findOpenLongButton();
const openShortBtn = findOpenShortButton();
const sideCfg = loadOpenSide();
if (sideCfg === "SHORT") {
return { side: "开空", button: openShortBtn, by: "open_panel", mode: "OPEN" };
}
return { side: "开多", button: openLongBtn, by: "open_panel", mode: "OPEN" };
}
function resolveTradeAction() {
const mode = getActiveTradeMode();
if (mode === "OPEN") {
return resolveOpenAction();
}
const closeAction = resolveCloseAction();
return closeAction ? { ...closeAction, mode: "CLOSE" } : null;
}
function getCurrentSymbol() {
return parseFuturesTradingSymbolFromPathname(location.pathname);
}
let appDataCache = { text: "", parsed: null };
let rulesCache = {};
let rulesInflight = {};
let rulesFailedUntil = {};
const RULES_RETRY_COOLDOWN_MS = 5e3;
async function ensureRules(symbol) {
if (!symbol || rulesCache[symbol]) return rulesCache[symbol];
if (rulesInflight[symbol]) return rulesInflight[symbol];
if (rulesFailedUntil[symbol] > Date.now()) return null;
const promise = (async () => {
try {
const resp = await fetch(`https://fapi.binance.com/fapi/v1/exchangeInfo?symbol=${symbol}`);
if (!resp.ok) {
rulesFailedUntil[symbol] = Date.now() + RULES_RETRY_COOLDOWN_MS;
return null;
}
const data = await resp.json();
const sInfo = data.symbols?.find((s) => s.symbol === symbol);
if (!sInfo) {
rulesFailedUntil[symbol] = Date.now() + RULES_RETRY_COOLDOWN_MS;
return null;
}
const filters = sInfo.filters || [];
const lot = filters.find((f) => f.filterType === "LOT_SIZE") || {};
const marketLot = filters.find((f) => f.filterType === "MARKET_LOT_SIZE") || {};
const minN = filters.find((f) => f.filterType === "MIN_NOTIONAL") || {};
const entry = {
limitMinQty: lot.minQty ? String(lot.minQty) : null,
limitStepSize: lot.stepSize ? String(lot.stepSize) : null,
marketMinQty: marketLot.minQty ? String(marketLot.minQty) : null,
marketStepSize: marketLot.stepSize ? String(marketLot.stepSize) : null,
minNotional: minN.notional ? String(minN.notional) : null
};
rulesCache[symbol] = entry;
delete rulesFailedUntil[symbol];
log("exchangeInfo:", symbol, entry);
return entry;
} catch (_e) {
rulesFailedUntil[symbol] = Date.now() + RULES_RETRY_COOLDOWN_MS;
return null;
} finally {
delete rulesInflight[symbol];
}
})();
rulesInflight[symbol] = promise;
return promise;
}
function readMarkPriceFromAppData(symbol) {
try {
const el = document.querySelector("#__APP_DATA");
if (!el || !el.textContent) return null;
let data;
if (el.textContent === appDataCache.text) {
data = appDataCache.parsed;
} else {
data = JSON.parse(el.textContent);
appDataCache = { text: el.textContent, parsed: data };
}
if (!data) return null;
const reactQueryData = data?.appState?.loader?.dataByRouteId?.bd56?.reactQueryData;
const markPrice = reactQueryData?.[`queryMarkPrice,${symbol}`]?.markPrice || null;
const toStr = (v) => {
if (typeof v === "string" && v) return v;
if (typeof v === "number" && Number.isFinite(v)) return String(v);
return null;
};
return toStr(markPrice);
} catch (_e) {
return null;
}
}
function getReferencePrice(symbol, priceOverride) {
const fromOverride = normalizeDecimalString(priceOverride);
if (fromOverride) return fromOverride;
const priceInput = findPriceInput();
const fromInput = normalizeDecimalString(priceInput?.value || "");
if (fromInput) return fromInput;
const fromAppData = readMarkPriceFromAppData(symbol);
return normalizeDecimalString(fromAppData);
}
function getQtyRuleContext(symbol, tradeMode, priceOverride) {
const rules = symbol ? rulesCache[symbol] : null;
if (!rules) return { status: "pending" };
const orderType = getCurrentOrderType();
const isMarketOrder = orderType.includes("MARKET");
const baseMinQty = normalizeDecimalString(
(isMarketOrder ? rules.marketMinQty : rules.limitMinQty) || rules.limitMinQty
);
const stepSize = normalizeDecimalString(
(isMarketOrder ? rules.marketStepSize : rules.limitStepSize) || rules.limitStepSize
);
if (!baseMinQty || !stepSize) return { status: "pending" };
const referencePrice = getReferencePrice(symbol, priceOverride);
const minNotionalQty = tradeMode === "OPEN" && rules.minNotional && referencePrice && stepSize ? ceilQtyByNotional(rules.minNotional, referencePrice, stepSize) : null;
const effectiveMinQty = maxDecimalString(baseMinQty, minNotionalQty);
return {
status: "ready",
orderType,
baseMinQty,
stepSize,
minNotional: normalizeDecimalString(rules.minNotional),
referencePrice,
minNotionalQty,
effectiveMinQty
};
}
function multiplierKey(mode, symbol) {
const normalizedSymbol = String(symbol || getCurrentSymbol() || "").toUpperCase();
if (!normalizedSymbol) return null;
const normalizedMode = mode === "OPEN" ? "OPEN" : "CLOSE";
return `${LOCAL_QTY_MULTIPLIER_PREFIX}:${normalizedMode}:${normalizedSymbol}`;
}
function loadMultiplier(mode, symbol) {
const key = multiplierKey(mode || getActiveTradeMode(), symbol);
if (!key) return DEFAULT_MULTIPLIER;
const value = localStorage.getItem(key);
return isValidMultiplier(value) ? String(value) : DEFAULT_MULTIPLIER;
}
function saveMultiplier(value, mode, symbol) {
const key = multiplierKey(mode || getActiveTradeMode(), symbol);
if (!key) return;
localStorage.setItem(key, value);
}
function sanitizeMultiplier(value) {
return isValidMultiplier(value) ? String(value).trim() : DEFAULT_MULTIPLIER;
}
function updateMultiplier(nextValue) {
const input = document.getElementById(INPUT_ID);
const normalized = sanitizeMultiplier(nextValue);
isEditingMultiplier = false;
saveMultiplier(normalized, getActiveTradeMode(), getCurrentSymbol());
if (input) input.value = normalized;
renderPanel();
}
function setNativeActionButtonDisabled(button, disabled) {
if (!button) return;
const alreadyDisabled = button.getAttribute(NATIVE_ACTION_DISABLED_ATTR) === "true";
if (disabled === alreadyDisabled) {
if (disabled) controlledNativeButtons.add(button);
else controlledNativeButtons.delete(button);
return;
}
if (disabled) {
button.setAttribute(NATIVE_ACTION_DISABLED_ATTR, "true");
button.disabled = true;
button.setAttribute("aria-disabled", "true");
controlledNativeButtons.add(button);
return;
}
button.removeAttribute(NATIVE_ACTION_DISABLED_ATTR);
button.disabled = false;
button.setAttribute("aria-disabled", "false");
controlledNativeButtons.delete(button);
}
function syncNativeCloseButtons(tradeMode, closeContext) {
const { closeLongBtn, closeShortBtn, knowsLong, knowsShort, hasLong, hasShort } = closeContext;
const desiredStates = /* @__PURE__ */ new Map();
if (tradeMode === "CLOSE") {
if (knowsLong) desiredStates.set(closeLongBtn, !hasLong);
if (knowsShort) desiredStates.set(closeShortBtn, !hasShort);
}
for (const button of Array.from(controlledNativeButtons)) {
if (!button.isConnected) {
controlledNativeButtons.delete(button);
continue;
}
if (desiredStates.get(button) !== true) {
setNativeActionButtonDisabled(button, false);
}
}
for (const [button, shouldDisable] of desiredStates.entries()) {
if (!button) continue;
const isDisabledByUs = button.getAttribute(NATIVE_ACTION_DISABLED_ATTR) === "true";
if (shouldDisable === isDisabledByUs) continue;
setNativeActionButtonDisabled(button, shouldDisable);
}
}
function ladderOptionButton(label, value, selected, group) {
const activeStyle = selected ? "border-color:var(--color-PrimaryYellow);background:var(--color-BadgeBg);color:#1e2329;font-weight:600;" : "border-color:#d5d9e2;background:#ffffff;color:#5e6673;font-weight:500;";
return ``;
}
function ladderOptionRow(title, options, selected, group, suffix = "") {
return [
'',
`${title}`,
...options.map((value) => ladderOptionButton(`${value}${suffix}`, value, Number(value) === Number(selected), group)),
"
"
].join("");
}
function ladderStepRow() {
const value = getLadderStep();
const decDisabled = value <= LADDER_STEP_MIN;
const incDisabled = value >= LADDER_STEP_MAX;
const stepButton = (action, label, disabled) => {
const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : "";
const style = disabled ? `border-color:${DISABLED_CONTROL_BORDER};background:${DISABLED_CONTROL_BG};color:${DISABLED_CONTROL_TEXT};cursor:not-allowed;opacity:${DISABLED_CONTROL_OPACITY};` : "border-color:#d5d9e2;background:#ffffff;color:#5e6673;cursor:pointer;opacity:1;";
return ``;
};
return [
'',
'幅',
stepButton("dec", "-", decDisabled),
`${value}`,
stepButton("inc", "+", incDisabled),
"
"
].join("");
}
function ladderActionButton(actionType, label, tone, disabled = false) {
const isBuyTone = tone === "BUY";
const borderColor = disabled ? DISABLED_CONTROL_BORDER : isBuyTone ? "var(--color-Buy)" : "var(--color-Sell)";
const background = disabled ? DISABLED_CONTROL_BG : isBuyTone ? "var(--color-GreenAlpha01)" : "var(--color-RedAlpha01)";
const color = disabled ? DISABLED_CONTROL_TEXT : borderColor;
const disabledAttrs = disabled ? ' disabled aria-disabled="true"' : "";
return ``;
}
function getLadderActionRows(tradeMode, closeContext) {
const ladderRunning = !!ladderTask;
if (tradeMode === "OPEN") {
return [
ladderOptionRow("开", LADDER_OPEN_PERCENTS, getLadderOpenPercent(), "openPercent", "%"),
ladderOptionRow("档", LADDER_LEVEL_OPTIONS, getLadderLevels(), "levels", ""),
ladderStepRow(),
'',
ladderActionButton("OPEN_LONG", "阶梯开多", "BUY", ladderRunning),
ladderActionButton("OPEN_SHORT", "阶梯开空", "SELL", ladderRunning),
"
"
];
}
const closeLongDisabled = ladderRunning || (closeContext?.knowsLong ? !closeContext.hasLong : false);
const closeShortDisabled = ladderRunning || (closeContext?.knowsShort ? !closeContext.hasShort : false);
return [
ladderOptionRow("平", LADDER_CLOSE_PERCENTS, getLadderClosePercent(), "closePercent", "%"),
ladderOptionRow("档", LADDER_LEVEL_OPTIONS, getLadderLevels(), "levels", ""),
ladderStepRow(),
'',
ladderActionButton("CLOSE_SHORT", "阶梯平空", "BUY", closeShortDisabled),
ladderActionButton("CLOSE_LONG", "阶梯平多", "SELL", closeLongDisabled),
"
"
];
}
function refreshLadderPanel(panel, tradeMode, closeContext) {
const toggle = panel.querySelector(`#${LADDER_TOGGLE_ID}`);
const body = panel.querySelector(`#${LADDER_BODY_ID}`);
const status = panel.querySelector(`#${LADDER_STATUS_ID}`);
const expanded = isLadderExpanded();
const mode = tradeMode === "OPEN" ? "OPEN" : "CLOSE";
if (toggle) {
toggle.textContent = `Maker 阶梯 ${expanded ? "▾" : "▸"}`;
}
if (body) {
body.style.display = expanded ? "block" : "none";
if (expanded) {
const stopDisabled = !ladderTask;
const stopDisabledAttrs = stopDisabled ? ' disabled aria-disabled="true"' : "";
const stopStyle = stopDisabled ? `border-color:${DISABLED_CONTROL_BORDER};background:${DISABLED_CONTROL_BG};color:${DISABLED_CONTROL_TEXT};cursor:not-allowed;opacity:${DISABLED_CONTROL_OPACITY};` : "border-color:#d5d9e2;background:#ffffff;color:#5e6673;cursor:pointer;opacity:1;";
const bodyHtml = [
...getLadderActionRows(mode, closeContext),
'',
``,
``,
"
"
].join("");
if (ladderPanelBodySignature !== bodyHtml || body.innerHTML !== bodyHtml) {
body.innerHTML = bodyHtml;
ladderPanelBodySignature = bodyHtml;
}
}
}
if (status) {
status.textContent = ladderStatusText;
status.style.display = expanded || ladderTask || ladderStatusText !== "空闲" ? "block" : "none";
}
}
function refreshComputedInfo(panel, multiplier, qtyRuleContext) {
const minEl = panel.querySelector("#jh-binance-close-qty-min");
const finalEl = panel.querySelector("#jh-binance-close-qty-final");
const hintEl = panel.querySelector(`#${MODE_HINT_ID}`);
const decBtn = panel.querySelector(`#${DEC_ID}`);
const incBtn = panel.querySelector(`#${INC_ID}`);
const sideLongBtn = panel.querySelector(`#${SIDE_LONG_ID}`);
const sideShortBtn = panel.querySelector(`#${SIDE_SHORT_ID}`);
const tradeMode = getActiveTradeMode();
const rulesPending = qtyRuleContext?.status !== "ready";
const effectiveMinQty = rulesPending ? null : qtyRuleContext?.effectiveMinQty || null;
const finalQty = effectiveMinQty ? multiplyDecimalByInt(effectiveMinQty, multiplier) : null;
const closeSide = loadCloseSide();
const openSide = loadOpenSide();
const closeContext = resolveDisplayCloseState(readCloseContext(), getCurrentSymbol());
const { knowsLong, knowsShort, hasLong, hasShort, isPending, isUsingCache } = closeContext;
const closeMode = hasLong && hasShort ? "dual" : hasLong ? "single_long" : hasShort ? "single_short" : "unknown";
if (minEl) {
if (rulesPending) {
minEl.textContent = "最小量读取中";
} else if (tradeMode === "OPEN" && qtyRuleContext?.minNotionalQty && qtyRuleContext?.referencePrice) {
minEl.textContent = `最小 ${effectiveMinQty} (>=${qtyRuleContext.minNotional}U @ ${qtyRuleContext.referencePrice})`;
} else if (effectiveMinQty) {
minEl.textContent = `最小 ${effectiveMinQty}`;
} else {
minEl.textContent = "最小量读取中";
}
}
if (finalEl) {
if (rulesPending) {
finalEl.textContent = "--";
} else if (isValidMultiplier(multiplier) && finalQty && effectiveMinQty) {
finalEl.textContent = `${effectiveMinQty} x ${multiplier} = ${finalQty}`;
} else {
finalEl.textContent = "请输入正整数倍数";
}
}
if (hintEl) {
if (tradeMode === "OPEN") {
const action = openSide === "LONG" ? "开多" : "开空";
hintEl.textContent = `开仓模式:单击订单簿价格后将${CFG.SAFE_MODE ? "填数量" : action}`;
} else if (isPending && !isUsingCache) {
hintEl.textContent = "平仓模式:正在读取可平仓位";
} else if (isPending && isUsingCache) {
hintEl.textContent = "平仓模式:正在刷新可平仓位,暂沿用上次识别结果";
} else if (closeMode === "single_long") {
hintEl.textContent = `平仓模式:当前仅有多仓,单击订单簿价格后将${CFG.SAFE_MODE ? "填数量" : "平多"}`;
} else if (closeMode === "single_short") {
hintEl.textContent = `平仓模式:当前仅有空仓,单击订单簿价格后将${CFG.SAFE_MODE ? "填数量" : "平空"}`;
} else if (closeMode === "dual") {
const action = closeSide === "LONG" ? "平多" : "平空";
hintEl.textContent = `平仓模式:双向持仓时单击订单簿价格后将${CFG.SAFE_MODE ? "填数量" : action}`;
} else {
hintEl.textContent = "平仓模式:暂未识别到可平仓位";
}
}
if (decBtn) {
decBtn.disabled = Number(multiplier) <= 1;
decBtn.style.opacity = decBtn.disabled ? "0.45" : "1";
decBtn.style.cursor = decBtn.disabled ? "not-allowed" : "pointer";
}
if (incBtn) {
incBtn.style.opacity = "1";
incBtn.style.cursor = "pointer";
}
if (sideLongBtn) {
const isOpenMode = tradeMode === "OPEN";
const isDisabled = isOpenMode ? false : knowsLong ? !hasLong : false;
const isActive = isOpenMode ? openSide === "LONG" : closeMode === "single_long" || closeMode !== "single_short" && closeSide === "LONG";
sideLongBtn.textContent = isOpenMode ? "开多" : "平多";
sideLongBtn.style.order = isOpenMode ? "0" : "1";
sideLongBtn.disabled = isDisabled;
sideLongBtn.style.borderColor = isDisabled ? DISABLED_CONTROL_BORDER : isActive ? isOpenMode ? "var(--color-Buy)" : "var(--color-Sell)" : "var(--color-InputLine)";
sideLongBtn.style.background = isDisabled ? DISABLED_CONTROL_BG : isActive ? isOpenMode ? "var(--color-GreenAlpha01)" : "var(--color-RedAlpha01)" : "#ffffff";
sideLongBtn.style.color = isDisabled ? DISABLED_CONTROL_TEXT : isActive ? isOpenMode ? "var(--color-Buy)" : "var(--color-Sell)" : "#5e6673";
sideLongBtn.style.opacity = isDisabled ? DISABLED_CONTROL_OPACITY : "1";
sideLongBtn.style.cursor = isDisabled ? "not-allowed" : "pointer";
}
if (sideShortBtn) {
const isOpenMode = tradeMode === "OPEN";
const isDisabled = isOpenMode ? false : knowsShort ? !hasShort : false;
const isActive = isOpenMode ? openSide === "SHORT" : closeMode === "single_short" || closeMode !== "single_long" && closeSide === "SHORT";
sideShortBtn.textContent = isOpenMode ? "开空" : "平空";
sideShortBtn.style.order = isOpenMode ? "1" : "0";
sideShortBtn.disabled = isDisabled;
sideShortBtn.style.borderColor = isDisabled ? DISABLED_CONTROL_BORDER : isActive ? isOpenMode ? "var(--color-Sell)" : "var(--color-Buy)" : "var(--color-InputLine)";
sideShortBtn.style.background = isDisabled ? DISABLED_CONTROL_BG : isActive ? isOpenMode ? "var(--color-RedAlpha01)" : "var(--color-GreenAlpha01)" : "#ffffff";
sideShortBtn.style.color = isDisabled ? DISABLED_CONTROL_TEXT : isActive ? isOpenMode ? "var(--color-Sell)" : "var(--color-Buy)" : "#5e6673";
sideShortBtn.style.opacity = isDisabled ? DISABLED_CONTROL_OPACITY : "1";
sideShortBtn.style.cursor = isDisabled ? "not-allowed" : "pointer";
}
syncNativeCloseButtons(tradeMode, closeContext);
refreshOrderbookPrecisionRecommendation(panel);
refreshLadderPanel(panel, tradeMode, closeContext);
}
function findQtyFormItem(input) {
if (!input) return null;
return input.closest('div[target^="unitAmount-"]') || input.closest(".bn-formItem") || input.parentElement || null;
}
function ensureSpacer(host, panelHeight) {
let spacer = document.getElementById(SPACER_ID);
if (!host || !host.parentElement) {
if (spacer) spacer.remove();
return null;
}
if (!spacer) {
spacer = document.createElement("div");
spacer.id = SPACER_ID;
}
spacer.style.width = "100%";
spacer.style.height = `${panelHeight}px`;
spacer.style.margin = "8px 0 0 0";
spacer.style.pointerEvents = "none";
if (spacer.parentElement !== host.parentElement) {
host.parentElement.insertBefore(spacer, host.nextSibling);
} else if (spacer.previousElementSibling !== host) {
host.parentElement.insertBefore(spacer, host.nextSibling);
}
return spacer;
}
function placePanelFloating(panel, anchorRect) {
if (panel.parentElement !== document.body) {
document.body.appendChild(panel);
}
panel.style.position = "fixed";
panel.style.maxWidth = "none";
panel.style.margin = "0";
panel.style.zIndex = "999999";
if (!anchorRect || !anchorRect.width || !anchorRect.height) {
panel.style.visibility = "hidden";
panel.style.pointerEvents = "none";
return;
}
const margin = 8;
const viewportWidth = window.innerWidth || document.documentElement.clientWidth || 0;
const viewportHeight = window.innerHeight || document.documentElement.clientHeight || 0;
const panelWidth = Math.min(Math.max(anchorRect.width, 280), viewportWidth - margin * 2);
const estimatedHeight = Math.max(panel.offsetHeight || 0, 76);
let left = anchorRect.left;
left = Math.max(margin, Math.min(left, viewportWidth - panelWidth - margin));
let top = anchorRect.top;
top = Math.max(margin, Math.min(top, viewportHeight - estimatedHeight - margin));
panel.style.width = `${Math.round(panelWidth)}px`;
panel.style.left = `${Math.round(left)}px`;
panel.style.top = `${Math.round(top)}px`;
panel.style.right = "";
panel.style.bottom = "";
panel.style.visibility = "visible";
panel.style.pointerEvents = "auto";
}
function positionPanel(panel) {
const qtyInput = findQtyInput();
const host = findQtyFormItem(qtyInput);
const spacer = ensureSpacer(host, Math.max((panel.offsetHeight || 0) + PANEL_BOTTOM_TOOLTIP_GAP, 76));
const anchorRect = spacer?.getBoundingClientRect() || qtyInput?.getBoundingClientRect() || null;
placePanelFloating(panel, anchorRect);
}
function ensurePanel() {
let panel = document.getElementById(PANEL_ID);
if (panel) return panel;
panel = document.createElement("div");
panel.id = PANEL_ID;
panel.style.position = "fixed";
panel.style.zIndex = "999999";
panel.style.width = "320px";
panel.style.padding = "8px 10px";
panel.style.borderRadius = "10px";
panel.style.background = "#ffffff";
panel.style.border = "1px solid #eaecef";
panel.style.color = "#1e2329";
panel.style.fontSize = "13px";
panel.style.lineHeight = "18px";
panel.style.fontFamily = "BinancePlex, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, sans-serif";
panel.style.boxShadow = "none";
panel.style.visibility = "hidden";
panel.innerHTML = [
'',
``,
"
",
'',
'',
'',
"
",
``,
``,
``,
'',
`
`,
`
`,
`
空闲
`,
"
"
].join("");
document.body.appendChild(panel);
const input = panel.querySelector(`#${INPUT_ID}`);
const decBtn = panel.querySelector(`#${DEC_ID}`);
const incBtn = panel.querySelector(`#${INC_ID}`);
const sideLongBtn = panel.querySelector(`#${SIDE_LONG_ID}`);
const sideShortBtn = panel.querySelector(`#${SIDE_SHORT_ID}`);
const ladderToggle = panel.querySelector(`#${LADDER_TOGGLE_ID}`);
if (input) {
input.value = loadMultiplier(getActiveTradeMode(), getCurrentSymbol());
input.addEventListener("focus", () => {
isEditingMultiplier = true;
applyInputVisualState(input, input.value);
input.select();
});
input.addEventListener("input", () => {
const value = String(input.value || "").replace(/[^\d]/g, "");
if (input.value !== value) input.value = value;
if (isValidMultiplier(value)) {
saveMultiplier(value, getActiveTradeMode(), getCurrentSymbol());
}
const symbol = getCurrentSymbol() || "-";
const qtyRuleContext = getQtyRuleContext(symbol !== "-" ? symbol : null, getActiveTradeMode());
refreshComputedInfo(panel, value, qtyRuleContext);
applyInputVisualState(input, value);
});
input.addEventListener("blur", () => {
const value = String(input.value || "").trim();
const normalized = sanitizeMultiplier(value);
isEditingMultiplier = false;
saveMultiplier(normalized, getActiveTradeMode(), getCurrentSymbol());
input.value = normalized;
applyInputVisualState(input, normalized);
renderPanel();
});
applyInputVisualState(input, input.value);
}
if (decBtn) {
decBtn.addEventListener("click", () => {
const current = Number(loadMultiplier(getActiveTradeMode(), getCurrentSymbol()));
updateMultiplier(String(Math.max(1, current - 1)));
});
}
if (incBtn) {
incBtn.addEventListener("click", () => {
const current = Number(loadMultiplier(getActiveTradeMode(), getCurrentSymbol()));
updateMultiplier(String(current + 1));
});
}
if (sideLongBtn) {
sideLongBtn.addEventListener("click", () => {
if (getActiveTradeMode() === "OPEN") {
updateOpenSide("LONG");
return;
}
updateCloseSide("LONG");
});
}
if (sideShortBtn) {
sideShortBtn.addEventListener("click", () => {
if (getActiveTradeMode() === "OPEN") {
updateOpenSide("SHORT");
return;
}
updateCloseSide("SHORT");
});
}
if (ladderToggle) {
ladderToggle.addEventListener("click", () => {
setLadderExpanded(!isLadderExpanded());
});
}
panel.addEventListener("click", (event) => {
const target = event.target instanceof Element ? event.target : null;
if (!target) return;
const optionBtn = target.closest("[data-ladder-group][data-ladder-value]");
if (optionBtn) {
const group = optionBtn.getAttribute("data-ladder-group");
const value = Number(optionBtn.getAttribute("data-ladder-value"));
if (group === "openPercent") setLadderOpenPercent(value);
if (group === "closePercent") setLadderClosePercent(value);
if (group === "levels") setLadderLevels(value);
return;
}
const stepBtn = target.closest("[data-ladder-step-action]");
if (stepBtn) {
if (stepBtn.disabled || stepBtn.getAttribute("aria-disabled") === "true") return;
const delta = stepBtn.getAttribute("data-ladder-step-action") === "inc" ? 1 : -1;
setLadderStep(getLadderStep() + delta);
return;
}
const precisionApplyBtn = target.closest("[data-orderbook-precision-apply]");
if (precisionApplyBtn) {
if (precisionApplyBtn.disabled || precisionApplyBtn.getAttribute("aria-disabled") === "true") return;
applyRecommendedOrderbookPrecision();
return;
}
const precisionRefreshBtn = target.closest("[data-orderbook-precision-refresh]");
if (precisionRefreshBtn) {
if (precisionRefreshBtn.disabled || precisionRefreshBtn.getAttribute("aria-disabled") === "true") return;
refreshOrderbookPrecisionSamplesNow();
return;
}
const actionBtn = target.closest("[data-ladder-action]");
if (actionBtn) {
if (actionBtn.disabled || actionBtn.getAttribute("aria-disabled") === "true") return;
startLadder(actionBtn.getAttribute("data-ladder-action"));
return;
}
const stopBtn = target.closest("[data-ladder-stop]");
if (stopBtn) {
if (stopBtn.disabled || stopBtn.getAttribute("aria-disabled") === "true") return;
stopLadder();
return;
}
const cancelSymbolBtn = target.closest("[data-ladder-cancel-symbol]");
if (cancelSymbolBtn) {
if (cancelSymbolBtn.disabled || cancelSymbolBtn.getAttribute("aria-disabled") === "true") return;
cancelCurrentSymbolOpenOrders();
}
});
return panel;
}
function removePanel() {
document.getElementById(PANEL_ID)?.remove();
document.getElementById(SPACER_ID)?.remove();
ladderPanelBodySignature = "";
}
function pauseForNonTradingPage() {
removePanel();
stopTradingTimers();
invalidateTradeButtonCache();
lastDisplayCloseState = null;
lastAppliedCacheSnapshot = "";
stopOrderbookPrecisionSampler();
}
function renderPanel() {
if (!isFuturesTradingPage()) {
pauseForNonTradingPage();
return;
}
ensureTradeModeTabObserver();
const panel = ensurePanel();
const input = panel.querySelector(`#${INPUT_ID}`);
const symbol = getCurrentSymbol() || "-";
if (symbol !== "-" && !rulesCache[symbol]) {
ensureRules(symbol).then((rules) => {
if (rules) scheduleRenderPanel();
});
}
const storedMultiplier = loadMultiplier(getActiveTradeMode(), symbol !== "-" ? symbol : null);
if (input && !isEditingMultiplier && input.value !== storedMultiplier) {
input.value = storedMultiplier;
}
const multiplier = input ? String((isEditingMultiplier ? input.value : storedMultiplier) || "").trim() : storedMultiplier;
const qtyRuleContext = getQtyRuleContext(symbol !== "-" ? symbol : null, getActiveTradeMode());
refreshComputedInfo(panel, multiplier, qtyRuleContext);
if (input) {
applyInputVisualState(input, multiplier);
}
positionPanel(panel);
}
function scheduleRenderPanel(options = {}) {
const followUpMs = Number(options.followUpMs) > 0 ? Number(options.followUpMs) : 0;
if (!renderPanelQueued) {
renderPanelQueued = true;
window.requestAnimationFrame(() => {
renderPanelQueued = false;
renderPanel();
});
}
if (followUpMs > 0) {
window.clearTimeout(renderPanelFollowUpTimer);
renderPanelFollowUpTimer = window.setTimeout(() => {
renderPanel();
}, followUpMs);
}
}
function clearTradeUiMutationWait() {
if (tradeUiMutationObserver) {
tradeUiMutationObserver.disconnect();
tradeUiMutationObserver = null;
}
if (tradeUiMutationTimeout) {
window.clearTimeout(tradeUiMutationTimeout);
tradeUiMutationTimeout = 0;
}
if (tradeUiMutationDebounceTimer) {
window.clearTimeout(tradeUiMutationDebounceTimer);
tradeUiMutationDebounceTimer = 0;
}
}
function waitForTradeUiMutation(options = {}) {
const timeoutMs = Number(options.timeoutMs) > 0 ? Number(options.timeoutMs) : 500;
clearTradeUiMutationWait();
if (!document.body) {
scheduleRenderPanel({ followUpMs: timeoutMs });
return;
}
tradeUiMutationObserver = new MutationObserver((mutations) => {
let matched = false;
for (const mutation of mutations) {
if (mutationTouchesTradeUi(mutation)) {
matched = true;
break;
}
}
if (!matched) return;
invalidateTradeButtonCache();
applyCachedNativeCloseButtonState();
window.clearTimeout(tradeUiMutationDebounceTimer);
tradeUiMutationDebounceTimer = window.setTimeout(() => {
tradeUiMutationDebounceTimer = 0;
scheduleRenderPanel();
}, 50);
});
const mutationRoot = getTradeMutationRoot();
if (!mutationRoot) {
scheduleRenderPanel({ followUpMs: timeoutMs });
return;
}
tradeUiMutationObserver.observe(mutationRoot, {
subtree: true,
childList: true,
characterData: true,
attributes: true,
attributeFilter: ["aria-selected", "disabled", "aria-disabled", "class", "value"]
});
tradeUiMutationTimeout = window.setTimeout(() => {
clearTradeUiMutationWait();
scheduleRenderPanel();
}, timeoutMs);
}
function handleTradeModeTabTransition(tab, isEnteringClose, isEnteringOpen, source) {
if (!isEnteringClose && !isEnteringOpen) return false;
if (isEnteringClose) {
invalidateTradeButtonCache();
closeGuard = {
symbol: getCurrentSymbol(),
expiresAt: Date.now() + 500,
longZeroStreak: 0,
shortZeroStreak: 0,
lastRawLong: void 0,
lastRawShort: void 0
};
}
if (isEnteringOpen) {
invalidateTradeButtonCache();
const reset = () => queueAutoOpenLeverageReset(source);
if (source === "click") window.requestAnimationFrame(reset);
else reset();
}
const apply = () => applyCachedCloseUiState();
if (source === "click") window.requestAnimationFrame(apply);
else apply();
scheduleRenderPanel();
waitForTradeUiMutation();
return true;
}
function getTradeModeObserverRoot() {
const activeTab = getActiveTradeTab();
return activeTab?.closest("#position-direction, .bn-tabs__buySell") || activeTab?.parentElement || document.querySelector("#position-direction") || document.querySelector(".bn-tabs__buySell") || document.querySelector('[role="tab"].bn-tab__buySell')?.parentElement || null;
}
function stopTradeModeTabObserver() {
if (tradeModeTabObserver) {
tradeModeTabObserver.disconnect();
tradeModeTabObserver = null;
}
tradeModeTabObserverRoot = null;
}
function ensureTradeModeTabObserver() {
if (document.hidden) return;
const root = getTradeModeObserverRoot();
if (!root) {
stopTradeModeTabObserver();
return;
}
if (tradeModeTabObserver && tradeModeTabObserverRoot === root && root.isConnected) return;
stopTradeModeTabObserver();
tradeModeTabObserverRoot = root;
tradeModeTabObserver = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.type !== "attributes" || mutation.attributeName !== "aria-selected") continue;
if (!isTradeModeTab2(mutation.target)) continue;
const isSelected = mutation.target.getAttribute("aria-selected") === "true";
const text = mutation.target.textContent || "";
const isEnteringClose = isSelected && text.includes("平仓");
const isEnteringOpen = isSelected && text.includes("开仓");
if (handleTradeModeTabTransition(mutation.target, isEnteringClose, isEnteringOpen, "mutation")) return;
}
});
tradeModeTabObserver.observe(root, {
subtree: true,
attributes: true,
attributeFilter: ["aria-selected"]
});
}
function installUiSyncObservers() {
document.addEventListener("click", (event) => {
const tab = event.target instanceof Element ? event.target.closest('[role="tab"]') : null;
if (!isTradeModeTab2(tab)) return;
const text = tab.textContent || "";
const isEnteringClose = text.includes("平仓") && tab.getAttribute("aria-selected") !== "true";
const isEnteringOpen = text.includes("开仓") && tab.getAttribute("aria-selected") !== "true";
handleTradeModeTabTransition(tab, isEnteringClose, isEnteringOpen, "click");
ensureTradeModeTabObserver();
}, true);
const startObserve = () => {
ensureTradeModeTabObserver();
window.setTimeout(ensureTradeModeTabObserver, 1e3);
};
if (document.body) {
startObserve();
} else {
window.addEventListener("DOMContentLoaded", startObserve, { once: true });
}
}
function resolveTargetQty(tradeMode, priceOverride) {
const symbol = getCurrentSymbol();
const qtyRuleContext = getQtyRuleContext(symbol, tradeMode, priceOverride);
if (qtyRuleContext.status !== "ready" || !qtyRuleContext.effectiveMinQty) {
if (symbol && !rulesCache[symbol]) ensureRules(symbol);
return null;
}
const multiplier = loadMultiplier(tradeMode, symbol);
const qty = multiplyDecimalByInt(qtyRuleContext.effectiveMinQty, multiplier);
if (!qty) return null;
return {
qty,
source: `MULTIPLIER(${multiplier}x @ ${qtyRuleContext.effectiveMinQty})`,
symbol,
rule: qtyRuleContext
};
}
document.addEventListener("click", async (e) => {
try {
if (e.button !== 0 || e.metaKey || e.ctrlKey || e.altKey || e.shiftKey) return;
const priceNode = findClickedPriceNode(e.target);
if (!priceNode) return;
if (!e.isTrusted) return;
if (CFG.DEBUG) {
log("命中订单簿价格 click", {
targetClass: e.target?.className || "",
targetText: (e.target?.textContent || "").trim().slice(0, 24)
});
}
const now = Date.now();
if (CFG.COOLDOWN_MS > 0 && now - lastTs < CFG.COOLDOWN_MS) {
if (CFG.DEBUG) warn("跳过:cooldown");
return;
}
const clickedPrice = parsePrice(priceNode);
if (!clickedPrice) {
if (CFG.DEBUG) warn("跳过:价格解析失败");
return;
}
const qtyInput = findQtyInput();
if (!qtyInput) {
warn("未找到数量输入框");
return;
}
const priceInput = findPriceInput();
if (!priceInput) {
warn("未找到价格输入框");
return;
}
const action = resolveTradeAction();
if (!action || !action.button) {
warn(`未找到可用${getActiveTradeMode() === "OPEN" ? "开仓" : "平仓"}动作`);
return;
}
const qtyPlan = resolveTargetQty(action.mode, clickedPrice);
if (!qtyPlan || !qtyPlan.qty) {
warn("未找到可用数量来源(数量倍率/有效最小量)");
return;
}
lastTs = now;
setInputValueReact(priceInput, clickedPrice);
await delay(SINGLE_ORDER_PRICE_SYNC_DELAY_MS);
setInputValueReact(qtyInput, qtyPlan.qty);
await delay(SINGLE_ORDER_QTY_SYNC_DELAY_MS);
const submittedPriceInput = findPriceInput() || priceInput;
assertSubmittedPriceMatchesClickedPrice(clickedPrice, submittedPriceInput.value);
const currentSymbol = getCurrentSymbol();
if (currentSymbol !== qtyPlan.symbol) {
throw new Error(`交易对已变化,点击时 ${qtyPlan.symbol},当前 ${currentSymbol || "-"}`);
}
if (getActiveTradeMode() !== action.mode) {
throw new Error("开仓/平仓模式已变化,已停止提交");
}
const currentAction = resolveTradeAction();
if (!currentAction || currentAction.mode !== action.mode || currentAction.side !== action.side || !currentAction.button || currentAction.button.disabled || currentAction.button.getAttribute("aria-disabled") === "true") {
throw new Error(`提交前${action.side}按钮状态已变化,已停止`);
}
log(
"已填价格/数量",
clickedPrice,
qtyPlan.qty,
"来源",
qtyPlan.source,
"symbol",
qtyPlan.symbol,
"effectiveMinQty",
qtyPlan.rule?.effectiveMinQty,
"referencePrice",
qtyPlan.rule?.referencePrice,
"触发价格",
clickedPrice,
"mode",
action.mode,
"action",
action.side,
"by",
action.by,
"qtySource",
action.qtySource,
"longQty",
action.longQty,
"shortQty",
action.shortQty
);
if (CFG.SAFE_MODE) {
warn(`SAFE_MODE=true,仅填价格/数量,不点击${action.side}`);
return;
}
currentAction.button.click();
scheduleRenderPanel();
waitForTradeUiMutation({ timeoutMs: 400 });
log(`已点击${action.side}`);
} catch (e2) {
err("click handler 异常:", e2);
warn(e2?.message || "订单簿点击提交失败");
}
}, true);
window.addEventListener("storage", (event) => {
if (event.key?.startsWith(`${LOCAL_QTY_MULTIPLIER_PREFIX}:`) || event.key === LOCAL_CLOSE_SIDE_KEY || event.key === LOCAL_OPEN_SIDE_KEY || event.key === LOCAL_LADDER_EXPANDED_KEY || event.key?.startsWith(`${LOCAL_ORDERBOOK_PRECISION_SAMPLES_PREFIX}:`) || isLadderOptionStorageKey(event.key)) scheduleRenderPanel();
});
installUiSyncObservers();
let lastObservedSymbol = getCurrentSymbol();
function checkSymbolChangeForLeverage() {
const symbol = getCurrentSymbol();
if (!symbol || symbol === lastObservedSymbol) return;
lastObservedSymbol = symbol;
isEditingMultiplier = false;
invalidateTradeButtonCache();
orderbookPrecisionState = {
symbol,
samples: readStoredOrderbookPrecisionSamples(symbol),
recommendation: getOrderbookPrecisionRecommendation(symbol),
current: readCurrentOrderbookPrecisionValue(),
status: "采样中"
};
stopOrderbookPrecisionSampler();
startInitialOrderbookPrecisionSample();
scheduleRenderPanel();
if (getActiveTradeMode() === "OPEN") {
queueAutoOpenLeverageReset("symbol_change");
}
}
let symbolChangeTimer = null;
function startSymbolChangeTimer() {
if (symbolChangeTimer || document.hidden) return;
symbolChangeTimer = window.setInterval(checkSymbolChangeForLeverage, 500);
}
function stopSymbolChangeTimer() {
if (!symbolChangeTimer) return;
window.clearInterval(symbolChangeTimer);
symbolChangeTimer = null;
}
let renderPanelTimer = null;
let routeWatcherTimer = null;
let routeWasTrading = isFuturesTradingPage();
function startRenderPanelTimer() {
if (renderPanelTimer || document.hidden || !isFuturesTradingPage()) return;
renderPanelTimer = setInterval(renderPanel, 1e3);
}
function stopRenderPanelTimer() {
if (renderPanelTimer) {
clearInterval(renderPanelTimer);
renderPanelTimer = null;
}
}
function startTradingTimers() {
if (document.hidden || !isFuturesTradingPage()) return;
startSymbolChangeTimer();
ensureTradeModeTabObserver();
startRenderPanelTimer();
startInitialOrderbookPrecisionSample();
}
function stopTradingTimers() {
stopSymbolChangeTimer();
stopTradeModeTabObserver();
clearTradeUiMutationWait();
stopRenderPanelTimer();
stopOrderbookPrecisionSampler();
}
function syncRouteState() {
if (document.hidden) return;
const isTradingRoute = isFuturesTradingPage();
if (!isTradingRoute) {
if (routeWasTrading) {
routeWasTrading = false;
pauseForNonTradingPage();
}
return;
}
const wasTrading = routeWasTrading;
routeWasTrading = true;
if (!wasTrading) {
lastObservedSymbol = getCurrentSymbol();
isEditingMultiplier = false;
invalidateTradeButtonCache();
} else {
checkSymbolChangeForLeverage();
}
const needsRender = !renderPanelTimer || !wasTrading;
startTradingTimers();
if (needsRender) renderPanel();
if (!wasTrading && getActiveTradeMode() === "OPEN") {
queueAutoOpenLeverageReset("route_return");
}
}
function startRouteWatcher() {
if (routeWatcherTimer || document.hidden) return;
routeWatcherTimer = setInterval(syncRouteState, 1e3);
}
function stopRouteWatcher() {
if (!routeWatcherTimer) return;
clearInterval(routeWatcherTimer);
routeWatcherTimer = null;
}
startRouteWatcher();
startTradingTimers();
window.setTimeout(() => {
if (isFuturesTradingPage() && getActiveTradeMode() === "OPEN") queueAutoOpenLeverageReset("init");
}, 1500);
document.addEventListener("visibilitychange", () => {
if (document.hidden) {
stopTradingTimers();
stopRouteWatcher();
return;
}
startRouteWatcher();
syncRouteState();
});
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", renderPanel, { once: true });
} else {
renderPanel();
}
window.__TM_CLOSE_LONG_DEBUG__ = {
cfg: CFG,
get cachedCloseState() {
return getCachedCloseState(getCurrentSymbol());
},
get displayCloseState() {
return lastDisplayCloseState;
},
get closeGuard() {
return closeGuard;
},
findQtyInput,
findPriceInput,
findCloseLongButton,
findCloseShortButton,
findOpenLongButton,
findOpenShortButton,
findOrderbookRow,
findClickedPriceNode,
findPriceNodeFromRow,
getCachedPositionState,
resolveCloseAction,
resolveTradeAction,
resolveTargetQty,
getOrderbookPrices,
readPersistedBinanceOrderForm,
isPersistedPostOnlyOrderType,
ensurePostOnlyPreferencePersisted,
ensurePostOnlyOrderType,
buildLadderPlan,
startLadder,
stopLadder,
cancelCurrentSymbolOpenOrders,
queueAutoOpenLeverageReset,
renderPanel
};
log("脚本加载完成", location.href);
})();
})();