// ==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); })(); })();