// ==UserScript== // @name 【自写】Binance 合约交易数据面板 // @namespace binance.trading.data // @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 1.1.11 // @author jackhai9 // @description 在合约交易页面叠加浮动面板,定时拉取交易数据(持仓量、多空比、资金费率等)并显示当前值 + 多空信号 // @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-trading-data.user.js // @downloadURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-trading-data.user.js // @run-at document-idle // @grant none // ==/UserScript== (() => { // 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-trading-data/index.user.js (function() { "use strict"; function isFuturesTradingPage() { return isFuturesTradingPathname(location.pathname); } if (!isFuturesTradingPage()) return; const PREFIX = "[交易数据]"; const PANEL_ID = "jh-binance-trading-data-panel"; const STORAGE_POS_KEY = "jh_binance_trading_data_pos"; const STORAGE_COLLAPSED_KEY = "jh_binance_trading_data_collapsed"; const PANEL_WIDTH = 240; const DEBUG = false; const PERIOD_MS = 5 * 60 * 1e3; const FIRST_DELAY = 5e3; const RETRY_DELAYS = [1e4, 15e3, 2e4]; const RETRY_FALLBACK = 3e4; const DEFAULT_PERIOD = "5m"; const DATA_LIMIT = 30; const OI_TREND_PERIODS = 6; const FUNDING_RATE_THRESHOLD = 1e-4; const API_BASE = "https://www.binance.com"; const API_PATHS = { openInterest: "/futures/data/openInterestHist", topAccountRatio: "/futures/data/topLongShortAccountRatio", topPositionRatio: "/futures/data/topLongShortPositionRatio", globalAccountRatio: "/futures/data/globalLongShortAccountRatio", takerRatio: "/futures/data/takerlongshortRatio", basis: "/futures/data/basis", fundingRate: "/fapi/v1/fundingRate", serverTime: "/fapi/v1/time" }; const PERIOD_KEYS = ["openInterest", "topAccountRatio", "topPositionRatio", "globalAccountRatio", "takerRatio", "basis"]; function emit(level, ...args) { if (!DEBUG && level !== "ERR") return; console.error(PREFIX, `[${level}]`, ...args); } function log(...args) { emit("LOG", ...args); } function err(...args) { emit("ERR", ...args); } let lastSymbol = null; function getCurrentSymbol() { return parseFuturesTradingSymbolFromPathname(location.pathname); } function isActiveTradingPage() { return !panelClosed && !document.hidden && isFuturesTradingPage(); } async function fetchJson(path, params) { const url = new URL(path, API_BASE); for (const [k, v] of Object.entries(params)) url.searchParams.set(k, v); const href = url.toString(); try { const resp = await fetch(href); if (!resp.ok) throw Object.assign(new Error(`HTTP ${resp.status}`), { status: resp.status }); return await resp.json(); } catch (e1) { if (e1.status && e1.status >= 400 && e1.status < 500) throw e1; log("重试:", path); const resp = await fetch(href); if (!resp.ok) throw new Error(`HTTP ${resp.status} (retry)`); return await resp.json(); } } function fetchOpenInterest(symbol) { return fetchJson(API_PATHS.openInterest, { symbol, period: DEFAULT_PERIOD, limit: DATA_LIMIT }); } function fetchTopAccountRatio(symbol) { return fetchJson(API_PATHS.topAccountRatio, { symbol, period: DEFAULT_PERIOD, limit: DATA_LIMIT }); } function fetchTopPositionRatio(symbol) { return fetchJson(API_PATHS.topPositionRatio, { symbol, period: DEFAULT_PERIOD, limit: DATA_LIMIT }); } function fetchGlobalAccountRatio(symbol) { return fetchJson(API_PATHS.globalAccountRatio, { symbol, period: DEFAULT_PERIOD, limit: DATA_LIMIT }); } function fetchTakerRatio(symbol) { return fetchJson(API_PATHS.takerRatio, { symbol, period: DEFAULT_PERIOD, limit: DATA_LIMIT }); } function fetchBasis(symbol) { return fetchJson(API_PATHS.basis, { pair: symbol, period: DEFAULT_PERIOD, limit: DATA_LIMIT, contractType: "PERPETUAL" }); } function fetchFundingRate(symbol) { return fetchJson(API_PATHS.fundingRate, { symbol, limit: 1 }); } const FETCHER_MAP = { openInterest: fetchOpenInterest, topAccountRatio: fetchTopAccountRatio, topPositionRatio: fetchTopPositionRatio, globalAccountRatio: fetchGlobalAccountRatio, takerRatio: fetchTakerRatio, basis: fetchBasis }; let serverOffset = 0; async function syncServerTime() { try { const resp = await fetch(API_BASE + API_PATHS.serverTime); if (!resp.ok) throw new Error("HTTP " + resp.status); const json = await resp.json(); serverOffset = json.serverTime - Date.now(); log("服务器时间偏移:", serverOffset + "ms"); } catch (e) { err("获取服务器时间失败,使用本地时间"); serverOffset = 0; } } function serverNow() { return Date.now() + serverOffset; } let dataStore = {}; let dataCache = {}; let failedKeys = /* @__PURE__ */ new Set(); function extractEndpointTs(data) { if (!Array.isArray(data) || data.length === 0) return 0; return Number(data[data.length - 1].timestamp) || 0; } async function fetchPeriodData(symbol, keys) { if (!keys || keys.length === 0) return {}; var fetchers = keys.map(function(k) { return FETCHER_MAP[k](symbol); }); var results = await Promise.allSettled(fetchers); var backup = dataCache[symbol] || {}; var entries = {}; keys.forEach(function(key, i) { if (results[i].status === "fulfilled") { entries[key] = { data: results[i].value, cached: false }; } else { err(key + " 请求失败:", results[i].reason?.message || results[i].reason); if (backup[key]) { entries[key] = { data: backup[key], cached: true }; log(key + " 使用缓存数据"); } else { entries[key] = { data: null, cached: true }; } } }); return entries; } async function fetchFundingRateData(symbol) { var backup = dataCache[symbol] || {}; try { var data = await fetchFundingRate(symbol); return { data, cached: false }; } catch (e) { err("fundingRate 请求失败:", e); return { data: backup.fundingRate || null, cached: true }; } } function applyResults(symbol, periodEntries, fundingEntry) { if (!dataStore[symbol]) dataStore[symbol] = {}; if (!dataCache[symbol]) dataCache[symbol] = {}; if (periodEntries) { for (var key in periodEntries) { var e = periodEntries[key]; dataStore[symbol][key] = e.data; if (!e.cached) { dataCache[symbol][key] = e.data; failedKeys.delete(key); } else { failedKeys.add(key); } } } if (fundingEntry) { dataStore[symbol].fundingRate = fundingEntry.data; if (!fundingEntry.cached) { dataCache[symbol].fundingRate = fundingEntry.data; failedKeys.delete("fundingRate"); } else { failedKeys.add("fundingRate"); } } } function getPendingKeys(symbol, targetTs) { var store = dataStore[symbol] || {}; return PERIOD_KEYS.filter(function(key) { return extractEndpointTs(store[key]) < targetTs; }); } function parseOpenInterest(data) { if (!Array.isArray(data) || data.length === 0) return null; const latest = data[data.length - 1]; const value = parseFloat(latest.sumOpenInterest); const valueUsd = parseFloat(latest.sumOpenInterestValue); let trend = null; if (data.length > OI_TREND_PERIODS) { const prev = parseFloat(data[data.length - 1 - OI_TREND_PERIODS].sumOpenInterest); trend = value > prev ? "up" : value < prev ? "down" : "neutral"; } return { value, valueUsd, trend }; } function parseOIMarketCapRatio(data) { if (!Array.isArray(data) || data.length === 0) return null; const latest = data[data.length - 1]; const oi = parseFloat(latest.sumOpenInterest); const supply = parseFloat(latest.CMCCirculatingSupply); if (!supply || supply === 0) return null; return { value: oi / supply }; } function parseRatio(data, field) { if (!Array.isArray(data) || data.length === 0) return null; const latest = data[data.length - 1]; return { value: parseFloat(latest[field]) }; } function parseBasis(data) { if (!Array.isArray(data) || data.length === 0) return null; const latest = data[data.length - 1]; return { value: parseFloat(latest.basisRate) }; } function parseFundingRate(data) { if (!Array.isArray(data) || data.length === 0) return null; return { value: parseFloat(data[0].fundingRate) }; } function signalOpenInterest(parsed) { if (!parsed || !parsed.trend) return "neutral"; return parsed.trend === "up" ? "long" : parsed.trend === "down" ? "short" : "neutral"; } function signalRatio(parsed) { if (!parsed) return "neutral"; return parsed.value > 1 ? "long" : parsed.value < 1 ? "short" : "neutral"; } function signalBasis(parsed) { if (!parsed) return "neutral"; return parsed.value > 0 ? "long" : parsed.value < 0 ? "short" : "neutral"; } function signalFundingRate(parsed) { if (!parsed) return "neutral"; if (parsed.value < -FUNDING_RATE_THRESHOLD) return "long"; if (parsed.value > FUNDING_RATE_THRESHOLD) return "short"; return "neutral"; } function computeSignals(data, cachedKeys) { const oi = parseOpenInterest(data.openInterest); const oiMcRatio = parseOIMarketCapRatio(data.openInterest); const topAccount = parseRatio(data.topAccountRatio, "longShortRatio"); const topPosition = parseRatio(data.topPositionRatio, "longShortRatio"); const globalAccount = parseRatio(data.globalAccountRatio, "longShortRatio"); const taker = parseRatio(data.takerRatio, "buySellRatio"); const basis = parseBasis(data.basis); const funding = parseFundingRate(data.fundingRate); const c = cachedKeys || /* @__PURE__ */ new Set(); const indicators = [ { name: "合约持仓量", signal: signalOpenInterest(oi), display: fmtOI(oi), vote: true, cached: c.has("openInterest") }, { name: "大户账户多空比", signal: signalRatio(topAccount), display: fmtRatio(topAccount), vote: true, cached: c.has("topAccountRatio") }, { name: "大户持仓多空比", signal: signalRatio(topPosition), display: fmtRatio(topPosition), vote: true, cached: c.has("topPositionRatio") }, { name: "多空账户数比", signal: signalRatio(globalAccount), display: fmtRatio(globalAccount), vote: true, cached: c.has("globalAccountRatio") }, { name: "主动买卖比", signal: signalRatio(taker), display: fmtRatio(taker), vote: true, cached: c.has("takerRatio") }, { name: "基差", signal: signalBasis(basis), display: fmtBasis(basis), vote: true, cached: c.has("basis") }, { name: "资金费率", signal: signalFundingRate(funding), display: fmtFunding(funding), vote: true, cached: c.has("fundingRate") }, { name: "未平仓量/市值", signal: "neutral", display: fmtOIMarketCap(oiMcRatio), vote: false, cached: c.has("openInterest") } ]; const voters = indicators.filter((i) => i.vote && !i.cached); const total = voters.length; const longCount = voters.filter((i) => i.signal === "long").length; const shortCount = voters.filter((i) => i.signal === "short").length; return { indicators, longCount, shortCount, total }; } function fmtOI(parsed) { if (!parsed) return "--"; const v = parsed.value; const arrow = parsed.trend === "up" ? " ▲" : parsed.trend === "down" ? " ▼" : ""; if (v >= 1e9) return (v / 1e9).toFixed(2) + "B" + arrow; if (v >= 1e6) return (v / 1e6).toFixed(2) + "M" + arrow; if (v >= 1e3) return (v / 1e3).toFixed(0) + "K" + arrow; return v.toFixed(2) + arrow; } function fmtRatio(parsed) { if (!parsed) return "--"; return parsed.value.toFixed(4); } function fmtBasis(parsed) { if (!parsed) return "--"; const sign = parsed.value >= 0 ? "+" : ""; return sign + (parsed.value * 100).toFixed(4) + "%"; } function fmtFunding(parsed) { if (!parsed) return "--"; return (parsed.value * 100).toFixed(4) + "%"; } function fmtOIMarketCap(parsed) { if (!parsed) return "--"; return (parsed.value * 100).toFixed(2) + "%"; } const FLASH_STYLE_ID = "jh-trading-data-flash-style"; function injectFlashStyle() { if (document.getElementById(FLASH_STYLE_ID)) return; const style = document.createElement("style"); style.id = FLASH_STYLE_ID; style.textContent = [ "@keyframes jh-td-flash {", " 0%, 100% { background: transparent; }", " 50% { background: rgba(240, 160, 0, 0.45); }", "}", ".jh-td-flash { animation: jh-td-flash 1s ease-in-out 5; }" ].join("\n"); (document.head || document.documentElement).appendChild(style); } let prevDisplayValues = {}; const C = { long: "var(--color-Buy, #0ecb81)", short: "var(--color-Sell, #f6465d)", neutral: "#76808f", bg: "#ffffff", text: "#1e2329", sub: "#5e6673", border: "#eaecef" }; function signalColor(s) { return s === "long" ? C.long : s === "short" ? C.short : C.neutral; } function ensurePanel() { let panel = document.getElementById(PANEL_ID); if (panel) return panel; injectFlashStyle(); panel = document.createElement("div"); panel.id = PANEL_ID; const savedPos = normalizeSavedPosition(loadPosition(), PANEL_WIDTH); const collapsed = loadCollapsed(); Object.assign(panel.style, { position: "fixed", top: savedPos ? savedPos.top + "px" : "60px", left: savedPos ? savedPos.left + "px" : "auto", right: savedPos ? "auto" : "16px", width: PANEL_WIDTH + "px", zIndex: "999998", background: C.bg, border: "1px solid " + C.border, borderRadius: "8px", boxShadow: "0 2px 8px rgba(0,0,0,0.08)", fontFamily: "BinancePlex, system-ui, -apple-system, sans-serif", fontSize: "13px", color: C.text, userSelect: "none", overflow: "hidden" }); panel.innerHTML = [ // --- header --- '