// ==UserScript== // @name SpreadRadar for AT // @namespace https://www.arbitterminal.online/ // @version 1.56-bybit // @description Тікери з посиланнями на SpreadRadar, live spread tooltip, inline live spread, native FastTrade Open override і кнопка OrderSize // @match https://www.arbitterminal.online/* // @match https://arbitterminal.online/* // @match https://*.arbitterminal.online/* // @grant none // @connect spreadradar.net // @updateURL https://spreadradar.net/assets/SpreadRadar-Links.txt // @downloadURL https://spreadradar.net/assets/SpreadRadar-Links.txt // @run-at document-start // ==/UserScript== (function () { "use strict"; const BASE_URL = "https://spreadradar.net/"; const LIVE_SPREAD_URL = "https://spreadradar.net/api/live-spread"; const TARGET_WINDOW = "spreadradar_screener"; const DEFAULT_VIEW = "history"; const DEFAULT_HOURS = "24"; const DEFAULT_TYPE = "bbo"; const LIVE_POLL_MS = 500; const INLINE_LIVE_POLL_MS = 500; const INLINE_LIVE_LIMIT = 4; const INLINE_LIVE_TTL_MS = 12000; const INLINE_LIVE_CLEANUP_MS = 2000; const INLINE_LIVE_DOM_OBSERVER_DEBOUNCE_MS = 40; const LIVE_REQUEST_TIMEOUT_MS = 2500; const DEFAULT_ORDER_USDT = 300; const ORDER_SIZE_OPTIONS = [100, 200, 300, 400, 500]; const ORDER_SIZE_STORAGE_KEY = "spreadradar-links-order-usdt"; const ORDER_POSITION_STORAGE_KEY = "spreadradar-links-order-position"; const FAST_TRADE_SPREAD_MODE_STORAGE_KEY = "spreadradar-links-fasttrade-spread-mode"; const FAST_TRADE_SPREAD_VALUE_STORAGE_KEY = "spreadradar-links-fasttrade-spread-value"; const STYLE_ID = "spreadradar-links-clean-style"; const TOAST_ID = "spreadradar-links-toast"; const LIVE_TIP_ID = "spreadradar-live-tip"; const ORDER_CONTROL_ID = "spreadradar-order-control"; const TOP_LAYER_Z_INDEX = 2147483647; const ORDER_BUTTON_WIDTH = 66; const ORDER_BUTTON_HEIGHT = 35; const ORDER_PANEL_WIDTH = 214; const ORDER_PANEL_GAP = 10; const ORDER_EDGE_PADDING = 0; const ORDER_ATTACH_GAP = 0; const liveState = { tickerEl: null, card: null, routeKey: "", orderQtyBase: null, orderUsdt: DEFAULT_ORDER_USDT, timer: 0, controller: null, inFlight: false }; const inlineLiveState = { timer: 0, inFlight: false, pending: false, observer: null, observerTimer: 0, lastCleanupAt: 0, orderQtyByRoute: Object.create(null), lastByRoute: Object.create(null) }; const EXCHANGE_MAP = { BINANCE: "binance", MEXC: "mexc", GATE: "gate", GATEIO: "gate", "GATE.IO": "gate", BINGX: "bingx", BING: "bingx", BYBIT: "bybit", BITGET: "bitget", ASTERDEX: "aster", ASTER: "aster", "ASTER.DEX": "aster" }; const EXCHANGE_PATTERN = "BINANCE|MEXC|GATEIO|GATE\\.IO|GATE|BINGX|BING|BYBIT|BITGET|ASTERDEX|ASTER\\.DEX|ASTER"; function injectStyle() { let style = document.getElementById(STYLE_ID); if (!style) { style = document.createElement("style"); style.id = STYLE_ID; (document.head || document.documentElement).appendChild(style); } style.textContent = ` .trade-card .trade-details strong, .signals-list .signal-card [data-help="signal.symbol"], .signals-list .signal-card td.symbol, .signals-list .signal-card .header .symbol{ cursor:pointer!important; color:inherit!important; text-decoration:underline dotted rgba(13,110,253,.65)!important; text-underline-offset:2px!important; } .trade-card .trade-details strong:hover, .signals-list .signal-card [data-help="signal.symbol"]:hover, .signals-list .signal-card td.symbol:hover, .signals-list .signal-card .header .symbol:hover{ color:#0d6efd!important; } #${LIVE_TIP_ID}{ position:fixed; z-index:${TOP_LAYER_Z_INDEX}; min-width:220px; max-width:260px; padding:9px 11px; border-radius:7px; background:rgba(17,24,39,.96); border:1px solid rgba(255,255,255,.14); color:#fff; font:14.4px/1.35 Arial,system-ui,sans-serif; box-shadow:0 8px 22px rgba(0,0,0,.28); pointer-events:none; white-space:nowrap; } #${LIVE_TIP_ID} .sr-live-row{ display:flex; align-items:center; justify-content:space-between; gap:12px; margin:2px 0; white-space:nowrap; } #${LIVE_TIP_ID} .sr-live-title{font-weight:800;color:#cbd5e1;letter-spacing:.02em;} #${LIVE_TIP_ID} .sr-live-money{font-weight:900;color:#4ade80;} #${LIVE_TIP_ID} .sr-live-value{font-weight:700;color:#4ade80;} #${LIVE_TIP_ID} .sr-live-warning{font-weight:700;color:#facc15;} #${LIVE_TIP_ID} .sr-live-depth-ok{font-weight:900;color:#4ade80;} #${LIVE_TIP_ID} .sr-live-depth-no{font-weight:900;color:#ff7b7b;} #${LIVE_TIP_ID} .sr-live-muted{color:#aeb8c6;} #${LIVE_TIP_ID} .sr-live-spread{font-size:1.3em;font-weight:900;line-height:1;} #${LIVE_TIP_ID} .sr-live-chart{ width:220px; height:34px; margin-top:8px; overflow:visible; } #${LIVE_TIP_ID} .sr-live-chart svg{display:block;width:100%;height:100%;overflow:visible;} #${LIVE_TIP_ID} .sr-chart-zero{stroke:rgba(96,165,250,.62);stroke-width:1.15;stroke-dasharray:3 3;} #${LIVE_TIP_ID} .sr-chart-line{fill:none;stroke:#facc15;stroke-width:2.25;stroke-linejoin:round;stroke-linecap:round;} #${LIVE_TIP_ID} .sr-chart-fill{fill:rgba(250,204,21,.12);stroke:none;} .signals-list .signal-card .spreadradar-inline-live-host{ position:relative!important; white-space:normal!important; overflow:visible!important; text-overflow:clip!important; vertical-align:middle!important; } .signals-list .signal-card table.exchange-info td.spreadradar-inline-live-host{ min-width:62px!important; width:62px!important; max-width:72px!important; padding:2px 8px 2px 2px!important; vertical-align:middle!important; background:transparent!important; border:0!important; border-radius:0!important; box-shadow:none!important; } .spreadradar-inline-live{ position:absolute!important; left:2px!important; top:50%!important; transform:translateY(-50%)!important; display:inline-block!important; margin-top:0!important; padding:1px 5px!important; max-width:calc(100% - 6px)!important; border-radius:5px!important; background:rgba(17,24,39,.92)!important; border:1px solid rgba(148,163,184,.28)!important; box-shadow:inset 0 1px 0 rgba(255,255,255,.07),0 1px 2px rgba(15,23,42,.18)!important; font:800 15px/1 Arial,system-ui,sans-serif!important; letter-spacing:.01em!important; text-decoration:none!important; text-shadow:none!important; } .spreadradar-inline-live.sr-spread-tone, #${LIVE_TIP_ID} .sr-live-spread.sr-spread-tone{ color:var(--sr-spread-color,#94a3b8)!important; text-shadow:0 0 var(--sr-spread-glow-size,6px) var(--sr-spread-glow,transparent)!important; } .sr-spread-tone.is-spread-pos-warm-1{--sr-spread-color:#f8d27a;--sr-spread-glow:rgba(248,210,122,.18);--sr-spread-glow-size:5px;} .sr-spread-tone.is-spread-pos-warm-2{--sr-spread-color:#f5c05f;--sr-spread-glow:rgba(245,192,95,.2);--sr-spread-glow-size:5px;} .sr-spread-tone.is-spread-pos-warm-3{--sr-spread-color:#f7ad4f;--sr-spread-glow:rgba(247,173,79,.22);--sr-spread-glow-size:6px;} .sr-spread-tone.is-spread-pos-warm-4{--sr-spread-color:#fb9842;--sr-spread-glow:rgba(251,152,66,.24);--sr-spread-glow-size:6px;} .sr-spread-tone.is-spread-pos-warm-5{--sr-spread-color:#ff8533;--sr-spread-glow:rgba(255,133,51,.27);--sr-spread-glow-size:7px;} .sr-spread-tone.is-spread-pos-green-1{--sr-spread-color:#86efac;--sr-spread-glow:rgba(134,239,172,.22);--sr-spread-glow-size:6px;} .sr-spread-tone.is-spread-pos-green-2{--sr-spread-color:#5eea8f;--sr-spread-glow:rgba(94,234,143,.26);--sr-spread-glow-size:7px;} .sr-spread-tone.is-spread-pos-green-3{--sr-spread-color:#3ee879;--sr-spread-glow:rgba(62,232,121,.3);--sr-spread-glow-size:8px;} .sr-spread-tone.is-spread-pos-green-4{--sr-spread-color:#22e66a;--sr-spread-glow:rgba(34,230,106,.34);--sr-spread-glow-size:9px;} .sr-spread-tone.is-spread-pos-green-5{--sr-spread-color:#19ff73;--sr-spread-glow:rgba(25,255,115,.38);--sr-spread-glow-size:10px;} .sr-spread-tone.is-spread-neg-soft-1{--sr-spread-color:#f7aaa6;--sr-spread-glow:rgba(247,170,166,.16);--sr-spread-glow-size:5px;} .sr-spread-tone.is-spread-neg-soft-2{--sr-spread-color:#f5918d;--sr-spread-glow:rgba(245,145,141,.18);--sr-spread-glow-size:5px;} .sr-spread-tone.is-spread-neg-soft-3{--sr-spread-color:#f07878;--sr-spread-glow:rgba(240,120,120,.2);--sr-spread-glow-size:6px;} .sr-spread-tone.is-spread-neg-soft-4{--sr-spread-color:#e86468;--sr-spread-glow:rgba(232,100,104,.23);--sr-spread-glow-size:6px;} .sr-spread-tone.is-spread-neg-soft-5{--sr-spread-color:#dc555d;--sr-spread-glow:rgba(220,85,93,.26);--sr-spread-glow-size:7px;} .sr-spread-tone.is-spread-neg-deep-1{--sr-spread-color:#d64b56;--sr-spread-glow:rgba(214,75,86,.27);--sr-spread-glow-size:7px;} .sr-spread-tone.is-spread-neg-deep-2{--sr-spread-color:#c94350;--sr-spread-glow:rgba(201,67,80,.29);--sr-spread-glow-size:8px;} .sr-spread-tone.is-spread-neg-deep-3{--sr-spread-color:#bd3b4a;--sr-spread-glow:rgba(189,59,74,.31);--sr-spread-glow-size:8px;} .sr-spread-tone.is-spread-neg-deep-4{--sr-spread-color:#b13445;--sr-spread-glow:rgba(177,52,69,.33);--sr-spread-glow-size:9px;} .sr-spread-tone.is-spread-neg-deep-5{--sr-spread-color:#a62f40;--sr-spread-glow:rgba(166,47,64,.35);--sr-spread-glow-size:9px;} .spreadradar-inline-live.is-positive{color:#4ade80!important;text-shadow:0 0 6px rgba(74,222,128,.22)!important;} .spreadradar-inline-live.is-warning{color:#facc15!important;} .spreadradar-inline-live.is-negative{color:#ef4444!important;} .spreadradar-inline-live.is-muted{color:#94a3b8!important;} .spreadradar-inline-live.is-loading{color:#94a3b8!important;opacity:.72!important;} .spreadradar-inline-live.is-hidden{display:none!important;} #${TOAST_ID}{ position:fixed; left:50%; bottom:24px; transform:translateX(-50%); z-index:${TOP_LAYER_Z_INDEX}; padding:9px 14px; border-radius:10px; background:rgba(20,20,24,.94); color:#fff; font:13px/1.3 Arial,system-ui,sans-serif; box-shadow:0 10px 30px rgba(0,0,0,.35); pointer-events:none; } #${ORDER_CONTROL_ID}{ position:fixed; left:auto; right:16px; top:72px; z-index:${TOP_LAYER_Z_INDEX - 1}; width:${ORDER_BUTTON_WIDTH}px!important; min-width:${ORDER_BUTTON_WIDTH}px!important; max-width:${ORDER_BUTTON_WIDTH}px!important; height:${ORDER_BUTTON_HEIGHT}px!important; min-height:${ORDER_BUTTON_HEIGHT}px!important; max-height:${ORDER_BUTTON_HEIGHT}px!important; font:13px/1.35 Arial,sans-serif; color:#fff; user-select:none; } #${ORDER_CONTROL_ID} .sr-order-wrap{display:block;position:relative;width:${ORDER_BUTTON_WIDTH}px;height:${ORDER_BUTTON_HEIGHT}px;} #${ORDER_CONTROL_ID} .sr-order-main{ appearance:none!important; -webkit-appearance:none!important; position:relative!important; width:${ORDER_BUTTON_WIDTH}px!important; min-width:${ORDER_BUTTON_WIDTH}px!important; max-width:${ORDER_BUTTON_WIDTH}px!important; height:${ORDER_BUTTON_HEIGHT}px!important; min-height:${ORDER_BUTTON_HEIGHT}px!important; max-height:${ORDER_BUTTON_HEIGHT}px!important; box-sizing:border-box!important; display:inline-flex!important; align-items:center!important; justify-content:center!important; flex-direction:row!important; gap:1px!important; border:1px solid rgba(134,239,172,.42)!important; border-radius:10px!important; background:linear-gradient(180deg,#43b85c 0%,#2f9f49 100%)!important; color:#fff!important; padding:0 8px!important; margin:0!important; box-shadow:0 2px 7px rgba(15,23,42,.28),inset 0 1px 0 rgba(255,255,255,.2)!important; cursor:grab!important; touch-action:none!important; overflow:hidden!important; } #${ORDER_CONTROL_ID}.sr-order-dragging .sr-order-main{cursor:grabbing!important;opacity:.92;} #${ORDER_CONTROL_ID} .sr-order-main:hover{ border-color:rgba(187,247,208,.74)!important; box-shadow:0 3px 9px rgba(15,23,42,.34),0 0 0 2px rgba(34,197,94,.16),inset 0 1px 0 rgba(255,255,255,.25)!important; } #${ORDER_CONTROL_ID} .sr-order-caption{display:none!important;} #${ORDER_CONTROL_ID} .sr-order-current{font:800 15px/${ORDER_BUTTON_HEIGHT}px Arial,Helvetica,sans-serif!important;color:#fff!important;text-shadow:0 1px 1px rgba(0,0,0,.22);} #${ORDER_CONTROL_ID} .sr-order-panel{ display:none; position:absolute; right:calc(100% + ${ORDER_PANEL_GAP}px); top:0; width:${ORDER_PANEL_WIDTH}px; padding:8px; border-radius:8px; background:rgba(17,24,39,.97); border:1px solid rgba(255,255,255,.14); box-shadow:0 10px 28px rgba(0,0,0,.32); } #${ORDER_CONTROL_ID}.sr-order-panel-right .sr-order-panel{right:auto;left:calc(100% + ${ORDER_PANEL_GAP}px);} #${ORDER_CONTROL_ID}.sr-order-panel-up .sr-order-panel{top:auto;bottom:0;} #${ORDER_CONTROL_ID}:hover .sr-order-panel, #${ORDER_CONTROL_ID}.sr-order-open .sr-order-panel{display:block;} #${ORDER_CONTROL_ID} .sr-order-options{display:grid;grid-template-columns:repeat(5,1fr);gap:5px;} #${ORDER_CONTROL_ID} .sr-order-option{ border:1px solid rgba(255,255,255,.16); background:rgba(255,255,255,.08); color:#fff; border-radius:6px; padding:6px 4px; cursor:pointer; font:700 12px/1 Arial,sans-serif; } #${ORDER_CONTROL_ID} .sr-order-option:hover{background:rgba(255,255,255,.14);} #${ORDER_CONTROL_ID} .sr-order-option.is-active{border-color:#4ade80;background:rgba(74,222,128,.18);color:#bbf7d0;} #${ORDER_CONTROL_ID} .sr-ft-spread-settings{ margin-top:8px; padding-top:7px; border-top:1px solid rgba(255,255,255,.13); display:grid; grid-template-columns:1fr 74px; gap:7px; align-items:center; } #${ORDER_CONTROL_ID} .sr-ft-toggle{ display:flex; align-items:center; gap:5px; color:#dbeafe; font:800 12px/1.2 Arial,sans-serif; white-space:nowrap; } #${ORDER_CONTROL_ID} .sr-ft-toggle input{margin:0;} #${ORDER_CONTROL_ID} .sr-ft-spread-input{ width:74px; box-sizing:border-box; border:1px solid rgba(255,255,255,.22); border-radius:6px; background:rgba(255,255,255,.1); color:#fff; padding:5px 6px; font:800 12px/1 Arial,sans-serif; outline:none; text-align:right; } #${ORDER_CONTROL_ID} .sr-ft-spread-input:disabled{opacity:.55;cursor:not-allowed;} #${ORDER_CONTROL_ID} .sr-ft-spread-input:focus{border-color:#60a5fa;background:rgba(255,255,255,.15);} #${ORDER_CONTROL_ID} .sr-ft-spread-hint{ grid-column:1 / -1; color:#94a3b8; font:700 10.5px/1.25 Arial,sans-serif; margin-top:-2px; } `; } function showMessage(text) { let toast = document.getElementById(TOAST_ID); if (!toast) { toast = document.createElement("div"); toast.id = TOAST_ID; document.body.appendChild(toast); } toast.textContent = text; clearTimeout(toast._srTimer); toast._srTimer = setTimeout(() => toast.remove(), 2500); } function normalizeOrderUsdt(value) { const n = Number(value); if (!Number.isFinite(n) || n <= 0) return DEFAULT_ORDER_USDT; const rounded = Math.round(n); return ORDER_SIZE_OPTIONS.includes(rounded) ? rounded : DEFAULT_ORDER_USDT; } function getStoredOrderUsdt() { return normalizeOrderUsdt(localStorage.getItem(ORDER_SIZE_STORAGE_KEY) || DEFAULT_ORDER_USDT); } function saveOrderUsdt(value) { localStorage.setItem(ORDER_SIZE_STORAGE_KEY, String(normalizeOrderUsdt(value))); } function normalizeSpreadInput(value) { const text = String(value || "").trim().replace(",", "."); if (!text) return ""; const n = Number(text); if (!Number.isFinite(n)) return ""; return String(Math.round(n * 1000) / 1000); } function getFastTradeSpreadMode() { return localStorage.getItem(FAST_TRADE_SPREAD_MODE_STORAGE_KEY) === "custom" ? "custom" : "link"; } function saveFastTradeSpreadMode(mode) { localStorage.setItem(FAST_TRADE_SPREAD_MODE_STORAGE_KEY, mode === "custom" ? "custom" : "link"); } function getFastTradeSpreadValue() { return normalizeSpreadInput(localStorage.getItem(FAST_TRADE_SPREAD_VALUE_STORAGE_KEY) || ""); } function saveFastTradeSpreadValue(value) { const normalized = normalizeSpreadInput(value); if (normalized) localStorage.setItem(FAST_TRADE_SPREAD_VALUE_STORAGE_KEY, normalized); else localStorage.removeItem(FAST_TRADE_SPREAD_VALUE_STORAGE_KEY); return normalized; } function getFastTradeSignalSpread(urlSignalSpread) { const customValue = Number(getFastTradeSpreadValue()); if (getFastTradeSpreadMode() === "custom" && Number.isFinite(customValue)) return customValue; return Number.isFinite(urlSignalSpread) ? urlSignalSpread : 0; } function getNativeFastTradeCustomSpread() { if (getFastTradeSpreadMode() !== "custom") return ""; return getFastTradeSpreadValue(); } function applyNativeFastTradeOpenOverride() { const params = new URLSearchParams(window.location.search); if (params.get("ft") !== "1" || !params.has("s")) return; const customSpread = getNativeFastTradeCustomSpread(); if (!customSpread) return; if (params.get("sp") === customSpread) return; params.set("sp", customSpread); const query = params.toString(); const nextUrl = `${window.location.pathname}${query ? `?${query}` : ""}${window.location.hash}`; window.history.replaceState(window.history.state, document.title, nextUrl); } applyNativeFastTradeOpenOverride(); function getLiveOrderUsdt() { return normalizeOrderUsdt(liveState.orderUsdt || getStoredOrderUsdt()); } function getStoredOrderPosition() { try { const raw = JSON.parse(localStorage.getItem(ORDER_POSITION_STORAGE_KEY) || "null"); if (!raw || typeof raw !== "object") return null; const left = Number(raw.left); const top = Number(raw.top); if (Number.isFinite(left) && Number.isFinite(top)) return { left, top }; if (raw.side && Number.isFinite(Number(raw.top))) { const fallbackLeft = raw.side === "right" ? window.innerWidth - ORDER_BUTTON_WIDTH - 16 : 16; return { left: fallbackLeft, top: Number(raw.top) }; } return null; } catch (_) { return null; } } function clampOrderPosition(left, top) { const root = document.getElementById(ORDER_CONTROL_ID); const width = root?.offsetWidth || ORDER_BUTTON_WIDTH; const height = root?.offsetHeight || ORDER_BUTTON_HEIGHT; const maxLeft = Math.max(ORDER_EDGE_PADDING, window.innerWidth - width - ORDER_EDGE_PADDING); const maxTop = Math.max(ORDER_EDGE_PADDING, window.innerHeight - height - ORDER_EDGE_PADDING); return { left: Math.min(maxLeft, Math.max(ORDER_EDGE_PADDING, Math.round(Number(left) || 0))), top: Math.min(maxTop, Math.max(ORDER_EDGE_PADDING, Math.round(Number(top) || 0))) }; } function updateOrderPanelDirection(root, left, top) { if (!root) return; root.classList.toggle("sr-order-panel-right", left < ORDER_PANEL_WIDTH + ORDER_PANEL_GAP + ORDER_EDGE_PADDING); root.classList.toggle("sr-order-panel-up", top > window.innerHeight - 150); } function applyOrderPosition(root, pos) { if (!root) return; const fallback = getStoredOrderPosition() || { left: window.innerWidth - ORDER_BUTTON_WIDTH - 16, top: 72 }; const next = clampOrderPosition(pos?.left ?? fallback.left, pos?.top ?? fallback.top); root.style.left = `${next.left}px`; root.style.top = `${next.top}px`; root.style.right = "auto"; updateOrderPanelDirection(root, next.left, next.top); } function saveOrderPosition(root, pos) { const next = clampOrderPosition(pos?.left, pos?.top); localStorage.setItem(ORDER_POSITION_STORAGE_KEY, JSON.stringify(next)); applyOrderPosition(root, next); } function updateOrderControl(root = document.getElementById(ORDER_CONTROL_ID)) { if (!root) return; const current = getStoredOrderUsdt(); const valueEl = root.querySelector(".sr-order-current"); if (valueEl) valueEl.textContent = `${current}$`; root.querySelectorAll(".sr-order-option").forEach(btn => { btn.classList.toggle("is-active", Number(btn.dataset.value) === current); }); const customToggle = root.querySelector(".sr-ft-custom-toggle"); const spreadInput = root.querySelector(".sr-ft-spread-input"); const isCustom = getFastTradeSpreadMode() === "custom"; if (customToggle) customToggle.checked = isCustom; if (spreadInput) { spreadInput.value = getFastTradeSpreadValue(); spreadInput.disabled = !isCustom; } const rect = root.getBoundingClientRect(); if (rect.width > 0 && rect.height > 0) applyOrderPosition(root, { left: rect.left, top: rect.top }); } function setOrderUsdt(value) { const next = normalizeOrderUsdt(value); saveOrderUsdt(next); liveState.orderUsdt = next; liveState.orderQtyBase = null; updateOrderControl(); if (liveState.routeKey) pollLiveSpread(); } function installOrderDrag(root, handle) { if (!root || !handle || root.dataset.dragReady === "true") return; root.dataset.dragReady = "true"; let drag = null; let suppressNextClick = false; handle.addEventListener("pointerdown", event => { if (event.button !== 0) return; const rect = root.getBoundingClientRect(); drag = { pointerId: event.pointerId, startX: event.clientX, startY: event.clientY, startLeft: rect.left, startTop: rect.top, moved: false }; handle.setPointerCapture?.(event.pointerId); event.preventDefault(); }); handle.addEventListener("pointermove", event => { if (!drag || event.pointerId !== drag.pointerId) return; const dx = event.clientX - drag.startX; const dy = event.clientY - drag.startY; if (!drag.moved && Math.abs(dx) + Math.abs(dy) < 4) return; drag.moved = true; suppressNextClick = true; root.classList.add("sr-order-dragging"); root.classList.remove("sr-order-open"); applyOrderPosition(root, { left: drag.startLeft + dx, top: drag.startTop + dy }); event.preventDefault(); }); const finishDrag = event => { if (!drag || event.pointerId !== drag.pointerId) return; handle.releasePointerCapture?.(event.pointerId); root.classList.remove("sr-order-dragging"); if (drag.moved) { const rect = root.getBoundingClientRect(); saveOrderPosition(root, { left: rect.left, top: rect.top }); setTimeout(() => { suppressNextClick = false; }, 0); } drag = null; }; handle.addEventListener("pointerup", finishDrag); handle.addEventListener("pointercancel", finishDrag); handle.addEventListener("click", event => { if (suppressNextClick) { suppressNextClick = false; event.preventDefault(); event.stopPropagation(); return; } root.classList.toggle("sr-order-open"); }, true); } function ensureOrderControl() { let root = document.getElementById(ORDER_CONTROL_ID); if (!root) { root = document.createElement("div"); root.id = ORDER_CONTROL_ID; root.innerHTML = `
${ORDER_SIZE_OPTIONS.map(value => ``).join("")}
off = spread from link
`; document.body.appendChild(root); root.querySelectorAll(".sr-order-option").forEach(btn => { btn.addEventListener("click", event => { event.preventDefault(); event.stopPropagation(); setOrderUsdt(btn.dataset.value); root.classList.remove("sr-order-open"); }); }); const customToggle = root.querySelector(".sr-ft-custom-toggle"); const spreadInput = root.querySelector(".sr-ft-spread-input"); customToggle?.addEventListener("change", event => { event.stopPropagation(); saveFastTradeSpreadMode(customToggle.checked ? "custom" : "link"); updateOrderControl(root); }); spreadInput?.addEventListener("change", event => { event.stopPropagation(); const saved = saveFastTradeSpreadValue(spreadInput.value); spreadInput.value = saved; if (saved) saveFastTradeSpreadMode("custom"); updateOrderControl(root); }); spreadInput?.addEventListener("keydown", event => { if (event.key === "Enter") spreadInput.blur(); }); installOrderDrag(root, root.querySelector(".sr-order-main")); } applyOrderPosition(root, getStoredOrderPosition()); updateOrderControl(root); return root; } function normalizeSymbol(raw) { const text = String(raw || "").trim().toUpperCase(); const match = text.match(/[A-Z0-9]+_?USDT|[A-Z0-9]+/); if (!match) return ""; return match[0].replace(/[^A-Z0-9_]/g, ""); } function normalizeExchange(raw) { const key = String(raw || "").trim().toUpperCase().replace(/[^A-Z0-9.]/g, ""); return EXCHANGE_MAP[key] || ""; } function cleanInlineLiveText(host) { if (!host) return ""; const clone = host.cloneNode(true); clone.querySelectorAll?.(".spreadradar-inline-live").forEach(node => node.remove()); return String(clone.textContent || "").trim(); } function getSignalTickerHost(card) { return card?.querySelector?.('[data-help="signal.symbol"], td.symbol, .header .symbol, .symbol') || null; } function getSymbolFromCard(card) { const signalSymbol = cleanInlineLiveText(getSignalTickerHost(card)); const tradeSymbol = card?.querySelector?.(".trade-details strong")?.textContent; const text = String(card?.innerText || card?.textContent || "").replace(/\s+/g, " "); const fallback = (text.match(/([^\s|:()[\]{}<>]+_?USDT)/i) || [])[1] || ""; return normalizeSymbol(signalSymbol || tradeSymbol || fallback); } function getRouteExchanges(card) { let long = normalizeExchange(card?.querySelector?.(".arbitrage-info .long, td.long, .long")?.textContent || ""); let short = normalizeExchange(card?.querySelector?.(".arbitrage-info .short, td.short, .short")?.textContent || ""); const text = String(card?.innerText || card?.textContent || "").replace(/\s+/g, " ").trim(); if (!long || !short) { const arrowMatches = Array.from(text.matchAll(new RegExp(`\\b(${EXCHANGE_PATTERN})\\b\\s*([↑↓])`, "gi"))); for (const match of arrowMatches) { const exchange = normalizeExchange(match[1]); if (!exchange) continue; if (match[2] === "↑" && !long) long = exchange; if (match[2] === "↓" && !short) short = exchange; } } if (!long || !short) { const found = []; const exchangeRegex = new RegExp(`\\b(${EXCHANGE_PATTERN})\\b`, "gi"); for (const match of text.matchAll(exchangeRegex)) { const exchange = normalizeExchange(match[1]); if (exchange && !found.includes(exchange)) found.push(exchange); } if (found.length >= 2) { long ||= found[0]; short ||= found[1]; } } return { long, short }; } function formatRouteSymbol(ticker) { const symbol = normalizeSymbol(ticker); if (!symbol) return ""; if (/_USDT$/i.test(symbol)) return symbol; if (/USDT$/i.test(symbol)) return `${symbol.slice(0, -4)}_USDT`; return `${symbol}_USDT`; } function getRouteKey(card) { const ticker = formatRouteSymbol(getSymbolFromCard(card)); const { long, short } = getRouteExchanges(card); return ticker && long && short ? `${ticker}:${long}:${short}` : ""; } function buildUrl(route) { const params = new URLSearchParams(); params.set("ticker", route.ticker); params.set("long", route.long); params.set("short", route.short); params.set("view", DEFAULT_VIEW); params.set("hours", DEFAULT_HOURS); params.set("type", DEFAULT_TYPE); params.set("order_usdt", String(getStoredOrderUsdt())); return `${BASE_URL}?${params.toString()}`; } function openSpreadRadar(card) { const ticker = getSymbolFromCard(card); const { long, short } = getRouteExchanges(card); if (!ticker) { showMessage("Cannot open SpreadRadar: ticker is missing"); return; } if (!long || !short) { showMessage("Cannot open SpreadRadar: route exchanges are missing"); return; } const win = window.open(buildUrl({ ticker, long, short }), TARGET_WINDOW); if (win) try { win.focus(); } catch (_) {} } function findClickableTicker(target) { if (!(target instanceof Element)) return null; return target.closest( ".trade-card .trade-details strong," + ".signals-list .signal-card [data-help='signal.symbol']," + ".signals-list .signal-card td.symbol," + ".signals-list .signal-card .header .symbol" ); } function findSignalTicker(target) { if (!(target instanceof Element)) return null; return target.closest( ".signals-list .signal-card [data-help='signal.symbol']," + ".signals-list .signal-card td.symbol," + ".signals-list .signal-card .header .symbol" ); } function ensureLiveTip() { let tip = document.getElementById(LIVE_TIP_ID); if (!tip) { tip = document.createElement("div"); tip.id = LIVE_TIP_ID; tip.innerHTML = '
Loading live spread...
'; document.body.appendChild(tip); } return tip; } function positionLiveTip(anchor) { const tip = ensureLiveTip(); const rect = anchor.getBoundingClientRect(); const top = Math.max(8, Math.round(rect.bottom + 8)); const left = Math.min(window.innerWidth - tip.offsetWidth - 8, Math.max(8, Math.round(rect.left))); tip.style.top = `${top}px`; tip.style.left = `${left}px`; } function renderLiveTip(html) { const tip = ensureLiveTip(); tip.innerHTML = html; if (liveState.tickerEl) positionLiveTip(liveState.tickerEl); } function stopLiveHover() { if (liveState.timer) clearInterval(liveState.timer); liveState.timer = 0; liveState.controller?.abort?.(); liveState.controller = null; liveState.inFlight = false; liveState.tickerEl = null; liveState.card = null; liveState.routeKey = ""; liveState.orderQtyBase = null; document.getElementById(LIVE_TIP_ID)?.remove(); } function formatPct(value) { const n = Number(value); return Number.isFinite(n) ? `${n.toFixed(3)}%` : "-"; } function formatAge(tsMs) { const ts = Number(tsMs); if (!Number.isFinite(ts) || ts <= 0) return ""; const ageSec = Math.max(0, Math.round((Date.now() - ts) / 1000)); return `${ageSec}s ago`; } function formatUsdt(value) { const n = Number(value); return Number.isFinite(n) && n > 0 ? `${n.toFixed(0)}$` : `${getLiveOrderUsdt()}$`; } const SPREAD_TONE_PALETTE = { posWarm: [ { className: "is-spread-pos-warm-1", color: "#f8d27a", fill: "rgba(248,210,122,.12)" }, { className: "is-spread-pos-warm-2", color: "#f5c05f", fill: "rgba(245,192,95,.13)" }, { className: "is-spread-pos-warm-3", color: "#f7ad4f", fill: "rgba(247,173,79,.14)" }, { className: "is-spread-pos-warm-4", color: "#fb9842", fill: "rgba(251,152,66,.15)" }, { className: "is-spread-pos-warm-5", color: "#ff8533", fill: "rgba(255,133,51,.16)" } ], posGreen: [ { className: "is-spread-pos-green-1", color: "#86efac", fill: "rgba(134,239,172,.12)" }, { className: "is-spread-pos-green-2", color: "#5eea8f", fill: "rgba(94,234,143,.13)" }, { className: "is-spread-pos-green-3", color: "#3ee879", fill: "rgba(62,232,121,.14)" }, { className: "is-spread-pos-green-4", color: "#22e66a", fill: "rgba(34,230,106,.15)" }, { className: "is-spread-pos-green-5", color: "#19ff73", fill: "rgba(25,255,115,.16)" } ], negSoft: [ { className: "is-spread-neg-soft-1", color: "#f7aaa6", fill: "rgba(247,170,166,.12)" }, { className: "is-spread-neg-soft-2", color: "#f5918d", fill: "rgba(245,145,141,.13)" }, { className: "is-spread-neg-soft-3", color: "#f07878", fill: "rgba(240,120,120,.14)" }, { className: "is-spread-neg-soft-4", color: "#e86468", fill: "rgba(232,100,104,.15)" }, { className: "is-spread-neg-soft-5", color: "#dc555d", fill: "rgba(220,85,93,.16)" } ], negDeep: [ { className: "is-spread-neg-deep-1", color: "#d64b56", fill: "rgba(214,75,86,.16)" }, { className: "is-spread-neg-deep-2", color: "#c94350", fill: "rgba(201,67,80,.17)" }, { className: "is-spread-neg-deep-3", color: "#bd3b4a", fill: "rgba(189,59,74,.18)" }, { className: "is-spread-neg-deep-4", color: "#b13445", fill: "rgba(177,52,69,.19)" }, { className: "is-spread-neg-deep-5", color: "#a62f40", fill: "rgba(166,47,64,.2)" } ] }; function getSpreadTone(value) { const spread = Number(value); if (!Number.isFinite(spread)) { return { className: "is-muted", color: "#94a3b8", fill: "rgba(148,163,184,.1)" }; } if (spread >= 0.2) { const index = Math.min(Math.floor((spread - 0.2) / 0.1), SPREAD_TONE_PALETTE.posGreen.length - 1); return SPREAD_TONE_PALETTE.posGreen[Math.max(0, index)]; } if (spread >= 0) { const index = Math.min(Math.floor(spread / 0.04), SPREAD_TONE_PALETTE.posWarm.length - 1); return SPREAD_TONE_PALETTE.posWarm[Math.max(0, index)]; } const absSpread = Math.abs(spread); if (absSpread >= 0.2) { const index = Math.min(Math.floor((absSpread - 0.2) / 0.1), SPREAD_TONE_PALETTE.negDeep.length - 1); return SPREAD_TONE_PALETTE.negDeep[Math.max(0, index)]; } const index = Math.min(Math.floor(absSpread / 0.04), SPREAD_TONE_PALETTE.negSoft.length - 1); return SPREAD_TONE_PALETTE.negSoft[Math.max(0, index)]; } function renderSpreadChart(points) { const values = (Array.isArray(points) ? points : []) .filter(point => point?.entry_order_depth_ok !== false) .slice(-10) .map(point => Number(point?.entry_spread_pct)) .filter(value => Number.isFinite(value)); if (values.length < 2) return ""; const width = 220; const height = 34; const leftPad = 5; const rightPad = 5; const topPad = 7; const bottomPad = 7; const rawMin = Math.min(...values); const rawMax = Math.max(...values); let min = Math.min(0, rawMin); let max = Math.max(0, rawMax); let range = max - min; if (range < 0.001) { min -= 0.001; max += 0.001; range = max - min; } const padding = Math.max(range * 0.12, 0.01); min -= padding; max += padding; range = max - min; const xStep = values.length > 1 ? (width - leftPad - rightPad) / (values.length - 1) : 0; const yFor = (value) => { const ratio = (value - min) / range; return height - bottomPad - ratio * (height - topPad - bottomPad); }; const linePoints = values .map((value, index) => `${(leftPad + index * xStep).toFixed(1)},${yFor(value).toFixed(1)}`) .join(" "); const last = values[values.length - 1]; const tone = getSpreadTone(last); const lineColor = tone.color; const fillColor = tone.fill; const baselineY = yFor(0).toFixed(1); const areaPoints = `${leftPad},${baselineY} ${linePoints} ${width - rightPad},${baselineY}`; return ` `; } async function pollLiveSpread() { if (!liveState.routeKey || liveState.inFlight) return; liveState.inFlight = true; liveState.controller?.abort?.(); liveState.controller = new AbortController(); try { const orderUsdt = getLiveOrderUsdt(); const params = new URLSearchParams({ route_key: liveState.routeKey, limit: "10", use_raw_depth: "1" }); if (liveState.orderQtyBase) params.set("order_qty_base", String(liveState.orderQtyBase)); const { response, data } = await fetchJsonWithTimeout(`${LIVE_SPREAD_URL}?${params}`, { credentials: "include", signal: liveState.controller.signal }); if (!response.ok) { if (response.status === 402 || data?.error === "subscription_required") { renderLiveTip('
SpreadRadar login/subscription required
'); return; } throw new Error(`HTTP ${response.status}`); } const points = Array.isArray(data?.points) ? data.points : []; const last = points[points.length - 1]; if (!last) { renderLiveTip('
No live data
'); return; } if (!liveState.orderQtyBase) { const longAsk = Number(last.long_ask); if (Number.isFinite(longAsk) && longAsk > 0) { liveState.orderQtyBase = orderUsdt / longAsk; liveState.inFlight = false; return await pollLiveSpread(); } } const orderLabel = formatUsdt(last.order_notional_usdt || orderUsdt); const isDepthNo = last.entry_order_depth_ok === false; const depthText = isDepthNo ? 'DEPTH: NO' : 'DEPTH: OK'; const openTone = getSpreadTone(last.entry_spread_pct); const openValueHtml = isDepthNo ? "" : `${formatPct(last.entry_spread_pct)}`; const chartHtml = renderSpreadChart(points); renderLiveTip(`
OPEN: ${orderLabel}${openValueHtml}
${depthText}${formatAge(last.ts_ms)}
${chartHtml} `); } catch (err) { if (err?.name !== "AbortError") renderLiveTip('
Live unavailable
'); } finally { liveState.inFlight = false; } } function isSignalPanelActiveForInlineLive() { if (document.body?.classList?.contains("signals-hidden") || document.body?.classList?.contains("mix-at-hide-main-ui")) return false; const panel = document.querySelector(".signals-list"); if (!(panel instanceof HTMLElement)) return false; const rect = panel.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return false; if (!panel.querySelector(".signal-card")) return false; const style = window.getComputedStyle(panel); if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false; // MIX right-side Signal panel. if (document.body?.classList?.contains("mix-at-signal-controls-on")) return true; // Standalone/right panel and normal ArbitTerminal grid/list position. // We only require that .signals-list is visible and contains visible signal cards. // getInlineSignalCards() below will still limit processing to the first 4 visible cards. return true; } function isVisibleSignalCardInPanel(card, panel) { if (!(card instanceof HTMLElement) || !card.isConnected) return false; if (card.dataset.mixAtHidden === "true" || card.dataset.spreadradarHidden === "true") return false; const style = getComputedStyle(card); if (style.display === "none" || style.visibility === "hidden" || Number(style.opacity) === 0) return false; const rect = card.getBoundingClientRect(); if (rect.width <= 0 || rect.height <= 0) return false; const panelRect = panel?.getBoundingClientRect?.(); if (panelRect && panelRect.width > 0 && panelRect.height > 0) { return rect.bottom > panelRect.top && rect.top < panelRect.bottom && rect.right > panelRect.left && rect.left < panelRect.right; } return rect.bottom > 0 && rect.top < window.innerHeight && rect.right > 0 && rect.left < window.innerWidth; } function getInlineSignalCards() { if (!isSignalPanelActiveForInlineLive()) return []; const panel = document.querySelector(".signals-list"); if (!(panel instanceof HTMLElement)) return []; return Array.from(panel.querySelectorAll(".signal-card")) .filter(card => isVisibleSignalCardInPanel(card, panel)) .slice(0, INLINE_LIVE_LIMIT); } function getSignalInlineLiveHost(card) { if (!card) return null; const existingHost = card.querySelector?.("table.exchange-info td.spreadradar-inline-live-host"); if (existingHost) return existingHost; // Actual ArbitTerminal signal-card structure currently has no data-help on this row: // ...
......
const firstCell = card.querySelector?.("table.exchange-info tbody tr:first-child > td:first-child, table.exchange-info tr:first-child > td:first-child"); if (firstCell instanceof HTMLElement && !firstCell.classList.contains("long") && !firstCell.classList.contains("short")) { return firstCell; } const exchangeRow = card.querySelector?.('table.exchange-info tr[data-help="signal.exchanges"]'); const markedFirstCell = exchangeRow?.querySelector?.(":scope > td:first-child, :scope > th:first-child"); if (markedFirstCell instanceof HTMLElement && !markedFirstCell.classList.contains("long") && !markedFirstCell.classList.contains("short")) { return markedFirstCell; } const row = card.querySelector?.("table.exchange-info tbody tr, table.exchange-info tr"); const cells = Array.from(row?.children || []).filter(el => el instanceof HTMLElement && /^(TD|TH)$/i.test(el.tagName)); const cleanCellText = (el) => cleanInlineLiveText(el); const emptyCell = cells.find(el => !el.classList.contains("short") && !el.classList.contains("long") && cleanCellText(el) === ""); return emptyCell || getSignalTickerHost(card); } function ensureInlineLiveNode(card) { const host = getSignalInlineLiveHost(card); if (!host) return null; host.classList.add("spreadradar-inline-live-host"); let node = Array.from(host.children || []).find(el => el.classList?.contains("spreadradar-inline-live")); if (!node) { node = document.createElement("span"); node.className = "spreadradar-inline-live is-muted"; host.appendChild(node); } return node; } function setInlineLiveNode(card, text, className, routeKey = "") { const node = ensureInlineLiveNode(card); if (!node) return; const nextClass = `spreadradar-inline-live ${className || "is-muted"}`; const currentRoute = node.dataset.spreadradarInlineLiveRouteKey || ""; if (node.textContent === text && node.className === nextClass && currentRoute === (routeKey || "")) return; node.className = nextClass; node.textContent = text; node.dataset.spreadradarInlineLiveUpdatedAt = String(Date.now()); if (routeKey) node.dataset.spreadradarInlineLiveRouteKey = routeKey; else delete node.dataset.spreadradarInlineLiveRouteKey; } function removeInlineLiveNode(card) { const host = getSignalInlineLiveHost(card); if (!host) return; host.querySelectorAll?.(".spreadradar-inline-live").forEach(node => node.remove()); host.classList.remove("spreadradar-inline-live-host"); } function clearInlineLiveNodes(exceptCards = new Set()) { document.querySelectorAll(".spreadradar-inline-live").forEach(node => { const card = node.closest?.(".signal-card"); if (card && exceptCards.has(card)) return; const host = node.parentElement; node.remove(); host?.classList?.remove("spreadradar-inline-live-host"); }); } function getCachedInlineLive(routeKey) { const item = inlineLiveState.lastByRoute?.[routeKey]; if (!item || !Number.isFinite(Number(item.expiresAt)) || Date.now() > Number(item.expiresAt)) return null; return item; } function cacheInlineLive(routeKey, text, className) { if (!routeKey) return; inlineLiveState.lastByRoute[routeKey] = { text, className, expiresAt: Date.now() + INLINE_LIVE_TTL_MS }; } async function fetchJsonWithTimeout(url, options = {}, timeoutMs = LIVE_REQUEST_TIMEOUT_MS) { const controller = new AbortController(); const upstreamSignal = options?.signal; const abortFromUpstream = () => controller.abort(); if (upstreamSignal) { if (upstreamSignal.aborted) controller.abort(); else upstreamSignal.addEventListener("abort", abortFromUpstream, { once: true }); } const timer = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetch(url, { ...options, signal: controller.signal }); const data = await response.json().catch(() => null); return { response, data }; } finally { clearTimeout(timer); if (upstreamSignal) upstreamSignal.removeEventListener("abort", abortFromUpstream); } } async function fetchInlineLiveSpread(routeKey) { const orderUsdt = getLiveOrderUsdt(); const qtyKey = `${routeKey}|${orderUsdt}`; let orderQtyBase = Number(inlineLiveState.orderQtyByRoute?.[qtyKey]); if (!Number.isFinite(orderQtyBase) || orderQtyBase <= 0) orderQtyBase = 0; for (let attempt = 0; attempt < 2; attempt += 1) { const params = new URLSearchParams({ route_key: routeKey, limit: "1", use_raw_depth: "1" }); if (orderQtyBase > 0) params.set("order_qty_base", String(orderQtyBase)); const { response, data } = await fetchJsonWithTimeout(`${LIVE_SPREAD_URL}?${params}`, { credentials: "include" }); if (!response.ok) throw new Error(data?.error || `HTTP ${response.status}`); const points = Array.isArray(data?.points) ? data.points : []; const last = points[points.length - 1]; if (!last) return { status: "empty" }; if (orderQtyBase <= 0) { const longAsk = Number(last.long_ask); if (Number.isFinite(longAsk) && longAsk > 0) { orderQtyBase = orderUsdt / longAsk; inlineLiveState.orderQtyByRoute[qtyKey] = orderQtyBase; continue; } } if (last.entry_order_depth_ok === false) return { status: "depth_no" }; const spread = Number(last.entry_spread_pct); if (!Number.isFinite(spread)) return { status: "empty" }; return { status: "ok", spread }; } return { status: "empty" }; } function cleanupInlineLiveNodesIfNeeded(cardSet, force = false) { const now = Date.now(); if (!force && now - Number(inlineLiveState.lastCleanupAt || 0) < INLINE_LIVE_CLEANUP_MS) return; inlineLiveState.lastCleanupAt = now; clearInlineLiveNodes(cardSet); } function primeInlineLiveCards(cards) { for (const card of cards) { const routeKey = getRouteKey(card); if (!routeKey) continue; const cached = getCachedInlineLive(routeKey); if (cached) { setInlineLiveNode(card, cached.text, cached.className, routeKey); continue; } const node = ensureInlineLiveNode(card); if (!node) continue; if (!node.textContent) setInlineLiveNode(card, "…", "is-loading", routeKey); } } function scheduleInlineLiveUpdate(delay = 0) { if (delay <= 0) { updateInlineLiveSpreads(); return; } clearTimeout(inlineLiveState.observerTimer); inlineLiveState.observerTimer = setTimeout(updateInlineLiveSpreads, delay); } function installInlineLiveObserver() { if (inlineLiveState.observer) return; inlineLiveState.observer = new MutationObserver(mutations => { for (const mutation of mutations) { for (const node of mutation.addedNodes || []) { if (!(node instanceof Element)) continue; if (node.matches?.(".signal-card") || node.querySelector?.(".signal-card")) { scheduleInlineLiveUpdate(INLINE_LIVE_DOM_OBSERVER_DEBOUNCE_MS); return; } } } }); inlineLiveState.observer.observe(document.documentElement || document.body, { childList: true, subtree: true }); } async function updateInlineLiveCard(card) { const routeKey = getRouteKey(card); if (!routeKey) { removeInlineLiveNode(card); return; } const cached = getCachedInlineLive(routeKey); if (cached) setInlineLiveNode(card, cached.text, cached.className, routeKey); try { const result = await fetchInlineLiveSpread(routeKey); if (result.status !== "ok") { const latestCached = getCachedInlineLive(routeKey); if (latestCached) setInlineLiveNode(card, latestCached.text, latestCached.className, routeKey); else removeInlineLiveNode(card); return; } const cls = `sr-spread-tone ${getSpreadTone(result.spread).className}`; const text = formatPct(result.spread); cacheInlineLive(routeKey, text, cls); setInlineLiveNode(card, text, cls, routeKey); } catch (_) { const latestCached = getCachedInlineLive(routeKey); if (latestCached) setInlineLiveNode(card, latestCached.text, latestCached.className, routeKey); else removeInlineLiveNode(card); } } async function updateInlineLiveSpreads() { if (inlineLiveState.inFlight) { inlineLiveState.pending = true; return; } const cards = getInlineSignalCards(); const cardSet = new Set(cards); cleanupInlineLiveNodesIfNeeded(cardSet, !cards.length); if (!cards.length) return; primeInlineLiveCards(cards); inlineLiveState.inFlight = true; try { await Promise.allSettled(cards.map(card => updateInlineLiveCard(card))); } finally { inlineLiveState.inFlight = false; if (inlineLiveState.pending) { inlineLiveState.pending = false; setTimeout(updateInlineLiveSpreads, 0); } } } function startInlineLiveSpreads() { if (inlineLiveState.timer) return; installInlineLiveObserver(); updateInlineLiveSpreads(); inlineLiveState.timer = setInterval(updateInlineLiveSpreads, INLINE_LIVE_POLL_MS); } function stopInlineLiveSpreads() { if (inlineLiveState.timer) clearInterval(inlineLiveState.timer); if (inlineLiveState.observerTimer) clearTimeout(inlineLiveState.observerTimer); inlineLiveState.observer?.disconnect?.(); inlineLiveState.timer = 0; inlineLiveState.observerTimer = 0; inlineLiveState.observer = null; inlineLiveState.inFlight = false; inlineLiveState.pending = false; clearInlineLiveNodes(new Set()); } function startLiveHover(tickerEl) { const card = tickerEl.closest(".signal-card"); if (!card) return; const routeKey = getRouteKey(card); if (!routeKey) return; if (liveState.tickerEl === tickerEl && liveState.routeKey === routeKey) return; stopLiveHover(); liveState.tickerEl = tickerEl; liveState.card = card; liveState.routeKey = routeKey; liveState.orderUsdt = getStoredOrderUsdt(); renderLiveTip('
Loading live spread...
'); pollLiveSpread(); liveState.timer = setInterval(pollLiveSpread, LIVE_POLL_MS); } function onHoverIn(event) { const ticker = findSignalTicker(event.target); if (ticker) startLiveHover(ticker); } function onHoverOut(event) { const ticker = findSignalTicker(event.target); if (!ticker) return; const next = event.relatedTarget; if (next instanceof Node && ticker.contains(next)) return; stopLiveHover(); } function onClick(event) { const ticker = findClickableTicker(event.target); if (!ticker) return; const card = ticker.closest(".trade-card, .signal-card"); if (!card) return; event.preventDefault(); event.stopPropagation(); openSpreadRadar(card); } let domFeaturesStarted = false; function initDomFeatures() { if (domFeaturesStarted) return; if (!document.body) { setTimeout(initDomFeatures, 20); return; } domFeaturesStarted = true; injectStyle(); ensureOrderControl(); startInlineLiveSpreads(); document.addEventListener("click", onClick, true); document.addEventListener("mouseover", onHoverIn, true); document.addEventListener("mouseout", onHoverOut, true); window.addEventListener("resize", () => applyOrderPosition(document.getElementById(ORDER_CONTROL_ID), getStoredOrderPosition()), { passive: true }); window.addEventListener("beforeunload", stopInlineLiveSpreads, { once: true }); } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", initDomFeatures, { once: true }); setTimeout(initDomFeatures, 0); } else { initDomFeatures(); } })();