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