// ==UserScript== // @name 【自写】Binance CoinMarketCap 数据面板 // @namespace binance.coinmarketcap.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 0.1.13 // @author jackhai9 // @description 在 Binance 合约页面显示当前币种的 CoinMarketCap 中文页关键估值与供应量数据 // @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/* // @connect api.coinmarketcap.com // @connect dapi.coinmarketcap.com // @connect coinmarketcap.com // @updateURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-coinmarketcap-data.user.js // @downloadURL https://raw.githubusercontent.com/jackhai9/userscripts/main/scripts/binance-coinmarketcap-data.user.js // @run-at document-idle // @grant GM_xmlhttpRequest // ==/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-coinmarketcap-data/index.user.js (function() { "use strict"; function isFuturesTradingPage() { return isFuturesTradingPathname(location.pathname); } if (!isFuturesTradingPage()) return; const PANEL_ID = "jh-binance-cmc-data-panel"; const STORAGE_POS_KEY = "jh_binance_cmc_data_pos"; const STORAGE_COLLAPSED_KEY = "jh_binance_cmc_data_collapsed"; const PANEL_WIDTH = 240; const REFRESH_MS = 30 * 1e3; const SYMBOL_CHECK_MS = 1500; const CMC_BASE = "https://coinmarketcap.com/zh/currencies/"; const CMC_MAP_API = "https://api.coinmarketcap.com/data-api/v1/cryptocurrency/map"; const CMC_DETAIL_API = "https://api.coinmarketcap.com/data-api/v3/cryptocurrency/detail"; const CMC_HOLDER_API = "https://dapi.coinmarketcap.com/dex-stats/v3/dexer/crypto-holder/show_holders"; const ASSET_OVERRIDES = { RAVE: { id: 38967, symbol: "RAVE", slug: "ravedao" } }; const C = { long: "var(--color-Buy, #0ecb81)", short: "var(--color-Sell, #f6465d)", bg: "#ffffff", text: "#1e2329", sub: "#5e6673", border: "#eaecef", accent: "#3861fb" }; let panelClosed = false; let lastSymbol = null; let lastUpdateTs = 0; let refreshTimer = null; let symbolTimer = null; let routeTimer = null; let dragCleanup = null; let unloadCleanup = null; let inFlightSymbol = null; let lastRowsHtml = ""; let lastPath = location.pathname; const assetCache = /* @__PURE__ */ Object.create(null); function getCurrentSymbol() { return parseFuturesTradingSymbolFromPathname(location.pathname); } function baseAssetFromSymbol(symbol) { if (!symbol) return null; return symbol.replace(/_PERP$/i, "").replace(/USDT$/i, "").replace(/USDC$/i, "").replace(/USD$/i, "").toUpperCase(); } function cmcSymbolFromBaseAsset(baseAsset) { if (!baseAsset) return null; return baseAsset.replace(/^(1000000|1000)(?=[A-Z])/, ""); } function normalizeCmcAsset(rawAsset, fallbackBaseAsset) { if (!rawAsset || typeof rawAsset !== "object") return null; const id = numberOrNull(rawAsset.id); const symbol = typeof rawAsset.symbol === "string" ? rawAsset.symbol.trim().toUpperCase() : ""; const slug = typeof rawAsset.slug === "string" ? rawAsset.slug.trim() : ""; if (id === null || !symbol || !slug) return null; return { id, symbol, slug, baseAsset: fallbackBaseAsset || symbol }; } function mapApiUrlForBaseAsset(baseAsset) { const cmcSymbol = cmcSymbolFromBaseAsset(baseAsset); if (!cmcSymbol) return null; const params = new URLSearchParams({ symbol: cmcSymbol, listing_status: "active", _: String(Date.now()) }); return CMC_MAP_API + "?" + params.toString(); } async function resolveCmcAsset(symbol) { const base = baseAssetFromSymbol(symbol); if (!base) return null; if (assetCache[base]) return assetCache[base]; const override = ASSET_OVERRIDES[base]; if (override) { assetCache[base] = normalizeCmcAsset(override, base); return assetCache[base]; } const url = mapApiUrlForBaseAsset(base); if (!url) return null; const payload = await requestJson(url); const cmcSymbol = cmcSymbolFromBaseAsset(base); const matches = Array.isArray(payload && payload.data) ? payload.data.filter(function(row) { return row && row.is_active === 1 && String(row.symbol || "").trim().toUpperCase() === cmcSymbol; }) : []; if (matches.length !== 1) { throw new Error( matches.length > 1 ? "CMC symbol ambiguous: " + cmcSymbol : "CMC symbol not found: " + cmcSymbol ); } assetCache[base] = normalizeCmcAsset(matches[0], base); return assetCache[base]; } function cmcUrlForAsset(asset) { return asset && asset.slug ? CMC_BASE + asset.slug + "/" : null; } function formatUsd(value) { if (!Number.isFinite(value)) return "--"; const sign = value < 0 ? "-" : ""; const abs = Math.abs(value); if (abs >= 1e12) return sign + "$" + formatCompactNumber(abs / 1e12, 4) + "万亿"; if (abs >= 1e8) return sign + "$" + formatCompactNumber(abs / 1e8, 4) + "亿"; if (abs >= 1e4) return sign + "$" + formatCompactNumber(abs / 1e4, 4) + "万"; return sign + "$" + formatCompactNumber(abs, 4); } function formatToken(value, symbol) { if (!Number.isFinite(value)) return "--"; const sign = value < 0 ? "-" : ""; const abs = Math.abs(value); let formatted; if (abs >= 1e12) formatted = sign + formatCompactNumber(abs / 1e12, 4) + "万亿"; else if (abs >= 1e8) formatted = sign + formatCompactNumber(abs / 1e8, 4) + "亿"; else if (abs >= 1e4) formatted = sign + formatCompactNumber(abs / 1e4, 4) + "万"; else formatted = sign + formatCompactNumber(abs, 4); return symbol ? formatted + " " + symbol : formatted; } function formatPercent(value) { if (!Number.isFinite(value)) return "--"; const sign = value > 0 ? "+" : ""; return sign + value.toFixed(2) + "%"; } function formatPlainPercent(value) { if (!Number.isFinite(value)) return "--"; return value.toFixed(2) + "%"; } function formatCount(value) { if (!Number.isFinite(value)) return "--"; return formatToken(value, ""); } function formatCompactNumber(value, maxDecimals) { if (!Number.isFinite(value)) return "--"; return value.toLocaleString("en-US", { minimumFractionDigits: 0, maximumFractionDigits: maxDecimals, useGrouping: false }); } function numberOrNull(value) { const number = Number(value); return Number.isFinite(number) ? number : null; } function requestText(url) { return new Promise(function(resolve, reject) { GM_xmlhttpRequest({ method: "GET", url, timeout: 2e4, headers: { Accept: "text/html,application/xhtml+xml", "Cache-Control": "no-cache", Pragma: "no-cache" }, onload(response) { if (response.status < 200 || response.status >= 300) { reject(new Error("CMC HTTP " + response.status)); return; } resolve(response.responseText || ""); }, onerror() { reject(new Error("CMC request failed")); }, ontimeout() { reject(new Error("CMC request timeout")); } }); }); } function requestJson(url) { return new Promise(function(resolve, reject) { GM_xmlhttpRequest({ method: "GET", url, timeout: 2e4, headers: { Accept: "application/json, text/plain, */*", "Cache-Control": "no-cache", Pragma: "no-cache" }, onload(response) { if (response.status < 200 || response.status >= 300) { reject(new Error("CMC API HTTP " + response.status)); return; } try { resolve(JSON.parse(response.responseText || "{}")); } catch (error) { reject(new Error("CMC API JSON parse failed")); } }, onerror() { reject(new Error("CMC API request failed")); }, ontimeout() { reject(new Error("CMC API request timeout")); } }); }); } function detailApiUrlForAsset(asset) { if (!asset) return null; const params = new URLSearchParams({ id: String(asset.id), convertId: "2781", languageCode: "zh", _: String(Date.now()) }); return CMC_DETAIL_API + "?" + params.toString(); } function holderApiUrlForCryptoId(cryptoId) { const id = numberOrNull(cryptoId); if (id === null) return null; const params = new URLSearchParams({ cryptoId: String(id), _: String(Date.now()) }); return CMC_HOLDER_API + "?" + params.toString(); } function extractNextData(html) { const doc = new DOMParser().parseFromString(html, "text/html"); const script = doc.getElementById("__NEXT_DATA__"); if (!script || !script.textContent) { throw new Error("CMC page missing __NEXT_DATA__"); } return JSON.parse(script.textContent); } function collectDetail(nextData) { const detail = nextData && nextData.props && nextData.props.pageProps && nextData.props.pageProps.detailRes && nextData.props.pageProps.detailRes.detail; if (!detail || !detail.statistics) { throw new Error("CMC page missing detail statistics"); } return detail; } function collectApiDetail(payload) { const detail = payload && payload.data; if (!detail || !detail.statistics) { throw new Error("CMC API missing detail statistics"); } return detail; } function holderDisplayMetric(detail, symbol) { const treasuryHoldings = numberOrNull(detail.treasuryHoldings); if (detail.showTreasuriesFlag && treasuryHoldings !== null && treasuryHoldings > 0) { return { label: "金库资产", value: formatToken(treasuryHoldings, symbol) }; } const holdersFromPageMetric = numberOrNull(detail.cmcHolderCount); const holders = holdersFromPageMetric !== null ? holdersFromPageMetric : detail.holders && typeof detail.holders === "object" ? numberOrNull(detail.holders.holderCount || detail.holders.total || detail.holders.count) : null; return { label: "持有者", value: formatCount(holders) }; } function formatProfileScore(value) { if (value && typeof value === "object") { const percentage2 = numberOrNull(value.percentage); return percentage2 !== null ? percentage2.toFixed(0) + "%" : "--"; } const percentage = numberOrNull(value); return percentage !== null ? percentage.toFixed(0) + "%" : "--"; } function buildRows(detail) { const stats = detail.statistics || {}; const symbol = detail.symbol || ""; const volumeToMarketCap = numberOrNull(stats.turnover) !== null ? numberOrNull(stats.turnover) * 100 : null; const liquidityToMarketCap = numberOrNull(stats.liquidityMcapRatio) !== null ? numberOrNull(stats.liquidityMcapRatio) * 100 : null; const holderMetric = holderDisplayMetric(detail, symbol); return [ { label: "价格", value: formatUsd(numberOrNull(stats.price)), change: formatPercent(numberOrNull(stats.priceChangePercentage24h)), highlight: false }, { label: "流通市值", value: formatUsd(numberOrNull(stats.marketCap)), change: formatPercent(numberOrNull(stats.marketCapChangePercentage24h)), highlight: true }, { label: "Unlocked Mkt Cap", value: formatUsd(numberOrNull(stats.ucm)), highlight: false }, { label: "交易量(24h)", value: formatUsd(numberOrNull(stats.volume24h)), highlight: false }, { label: "Vol/Mkt Cap(24h)", value: formatPlainPercent(volumeToMarketCap), highlight: false }, { label: "FDV/总估值", value: formatUsd(numberOrNull(stats.fullyDilutedMarketCap)), change: formatPercent(numberOrNull(stats.fullyDilutedMarketCapChangePercentage24h)), highlight: true }, { label: "Liq/Mkt Cap", value: formatPlainPercent(liquidityToMarketCap), highlight: false }, { label: "总供应量", value: formatToken(numberOrNull(stats.totalSupply), symbol), highlight: false }, { label: "最大供应量", value: formatToken(numberOrNull(stats.maxSupply), symbol), highlight: false }, { label: "流通供应量", value: formatToken(numberOrNull(stats.circulatingSupply), symbol), highlight: false }, { label: holderMetric.label, value: holderMetric.value, highlight: false }, { label: "Profile score", value: formatProfileScore(detail.profileCompletionScore), highlight: false } ]; } async function fetchCmcApiData(asset) { const url = detailApiUrlForAsset(asset); if (!url) throw new Error("无法识别当前合约"); const payload = await requestJson(url); return collectApiDetail(payload); } async function fetchCmcHolderData(cryptoId) { const url = holderApiUrlForCryptoId(cryptoId); if (!url) return null; const payload = await requestJson(url); const data = payload && payload.data; if (!data || !data.showFlag) return null; return numberOrNull(data.count); } async function fetchCmcPageData(asset) { const url = cmcUrlForAsset(asset); if (!url) throw new Error("无法识别当前合约"); const html = await requestText(url); const nextData = extractNextData(html); return collectDetail(nextData); } async function fetchCmcData(symbol) { const asset = await resolveCmcAsset(symbol); const url = cmcUrlForAsset(asset); let detail; let source = "data-api"; try { detail = await fetchCmcApiData(asset); } catch (apiError) { detail = await fetchCmcPageData(asset); source = "page-snapshot"; } let holderCount = null; if (!detail.showTreasuriesFlag) { try { holderCount = await fetchCmcHolderData(detail.id); } catch (holderError) { holderCount = null; } } if (holderCount !== null) detail = { ...detail, cmcHolderCount: holderCount }; return { url, name: detail.name || "", symbol: detail.symbol || asset && asset.symbol || baseAssetFromSymbol(symbol), rank: detail.statistics && detail.statistics.rank, lastUpdated: detail.latestUpdateTime || "", source, rows: buildRows(detail) }; } function ensurePanel() { let panel = document.getElementById(PANEL_ID); if (panel) return panel; 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" : "360px", left: savedPos ? savedPos.left + "px" : "auto", right: savedPos ? "auto" : "16px", width: PANEL_WIDTH + "px", zIndex: "999997", 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 = [ '