/* ===== ArbitrajZ — Dashboard app ===== */
/* ---- tiny helpers ---- */
function pad(n) { return n < 10 ? "0" + n : "" + n; }
function fmtClock(d) { return pad(d.getHours()) + ":" + pad(d.getMinutes()); }
function StabilityMeter({ level }) {
// High = 3 bars, Med = 2, Low = 1
const n = level === "High" ? 3 : level === "Med" ? 2 : 1;
return (
{[0, 1, 2].map((i) => (
))}
);
}
/* ---- info tooltip ---- */
function InfoTip({ text }) {
const [pos, setPos] = useState(null);
const ref = useRef(null);
function show() {
if (!ref.current) return;
const r = ref.current.getBoundingClientRect();
setPos({ top: r.bottom + 7, left: r.left + r.width / 2 });
}
return (
setPos(null)}
onClick={(e) => { e.stopPropagation(); pos ? setPos(null) : show(); }}>
i
{pos && (
{text}
)}
);
}
/* ---- top bar ---- */
function TopBar({ scanning, exchanges }) {
const NAV = [
{ label: "Dashboard", href: "/" },
{ label: "Opportunities", href: "/opportunities.html" },
{ label: "Alerts" },
{ label: "Help", href: "/help.html" },
];
const [active, setActive] = useState("Dashboard");
const lastScanRaw = window.ARBZ.SUMMARY.lastScan;
const scanDisplay = lastScanRaw ? fmtClock(new Date(lastScanRaw)) : "--:--";
return (
);
}
/* ---- summary metric cards ---- */
function MetricCard({ label, children, accent, sub, tip }) {
return (
{label}{tip && }
{children}
{sub &&
{sub}
}
);
}
function Summary({ s }) {
return (
{s.cleanSignalsTotal}
{s.bestApr.toFixed(1)}%
{s.pairsScanned.toLocaleString()}
High
);
}
/* ---- one opportunity ---- */
function OppRow({ o, open, onToggle }) {
const stable = o.stable;
const exLeg = `${o.base}\u2011PERP`;
return (
{/* pair */}
{o.base} / {o.quote}
{o.exchange}
delta-neutral · spot + perp short
{/* net APR */}
+{o.netApr.toFixed(1)}%
net APR · 7d
{/* sparkline — hero */}
7d funding —{" "}
{stable ? "flat, stays positive" : "choppy, dips negative"}
{/* risk badge */}
{stable ? "stable" : "volatile"}
· {o.negCount} neg
7-day funding flips
{/* secondary stats */}
breakeven {o.breakevenH}h
24h vol ${o.volM}M
{open &&
}
);
}
function OppDetail({ o, exLeg }) {
const avg = o.series.reduce((a, b) => a + b, 0) / o.series.length;
const stable = o.stable;
return (
LONG
{o.base}/{o.quote} spot
{o.exchange} · collects nothing, hedges price
delta-neutral
SHORT
{exLeg} perp
{o.exchange} · earns funding every 8h
= 0 ? "+" : "") + o.curFunding.toFixed(4) + "%"} tone={o.curFunding >= 0 ? "pos" : "neg"} sub="per 8h" />
= 0 ? "+" : "") + avg.toFixed(4) + "%"} tone="pos" sub="per 8h" />
{stable ? "✓" : "!"}
{stable
? `Funding held positive across all 21 intervals this week. Carry has been steady — this is the kind of clean, boring signal we surface first.`
: `Headline APR looks comparable, but funding flipped negative ${o.negCount}× this week. Each flip means you paid instead of earned. Size down or skip — the 7d average can hide a bumpy ride.`}
Set funding alert
window.open(pairUrl(o), "_blank")}>Open pair
refreshed just now
);
}
function pairUrl(o) {
const base = (o.base || "").toUpperCase();
const ex = (o.exchange || "").toLowerCase();
if (ex === "bybit") return "https://www.bybit.com/trade/usdt/" + base + "USDT";
if (ex === "binance") return "https://www.binance.com/en/futures/" + base + "USDT";
if (ex === "okx") return "https://www.okx.com/trade-swap/" + base.toLowerCase() + "-usdt-swap";
if (ex === "gate") return "https://www.gate.io/futures/USDT/" + base + "_USDT";
return "https://www.google.com/search?q=" + encodeURIComponent(base + " USDT perpetual " + (o.exchange || ""));
}
function Stat({ k, v, sub, tone }) {
return (
);
}
function ActionBtn({ children, primary, onAction }) {
const [hit, setHit] = useState(false);
return (
{ setHit(true); if (onAction) onAction(); setTimeout(() => setHit(false), 1100); }}>
{hit ? (primary ? "Alert set ✓" : "Opening…") : children}
);
}
/* ---- opportunities section ---- */
function Opportunities({ opps }) {
const [sort, setSort] = useState("stability");
const [stableOnly, setStableOnly] = useState(false);
const [openId, setOpenId] = useState("apex");
const [refreshIn, setRefreshIn] = useState(60);
useEffect(() => {
const t = setInterval(() => setRefreshIn((r) => (r <= 1 ? 60 : r - 1)), 1000);
return () => clearInterval(t);
}, []);
const list = useMemo(() => {
let xs = opps.slice();
if (stableOnly) xs = xs.filter((o) => o.stable);
if (sort === "stability") xs.sort((a, b) => a.negCount - b.negCount || b.netApr - a.netApr);
else xs.sort((a, b) => b.netApr - a.netApr);
return xs;
}, [opps, sort, stableOnly]);
return (
Opportunities
{list.length} live
setStableOnly((v) => !v)}>
Stable only
Sort
setSort("stability")}>Stability
setSort("apr")}>Net APR
sorted by {sort === "stability" ? "stability" : "net APR"} · auto-refresh in {refreshIn}s
{list.map((o) => (
setOpenId((id) => (id === o.id ? null : o.id))} />
))}
{list.length === 0 && (
No stable signals match the filter right now.
)}
);
}
/* ---- root ---- */
function App() {
const { OPPS, SUMMARY } = window.ARBZ;
const [scanning, setScanning] = useState(false);
// periodic "re-scan" pulse — feels alive without being noisy
useEffect(() => {
const t = setInterval(() => {
setScanning(true);
setTimeout(() => { setScanning(false); }, 1500);
}, 18000);
return () => clearInterval(t);
}, []);
return (
);
}
/* Re-randare la fiecare refresh de date din data.js (fetch async) */
const __arbzRoot = ReactDOM.createRoot(document.getElementById("root"));
window.__ARBZ_RENDER = function () { __arbzRoot.render( ); };
window.__ARBZ_RENDER();