// Charts — pure SVG, no external libs. const { useMemo: useMemoC, useEffect: useEffectC, useRef: useRefC, useState: useStateC } = React; // ============================================================ // Sparkline (line + optional area fill) // ============================================================ function Sparkline({ data, w = 120, h = 32, color = "#19E5A6", fill = true, strokeW = 1.5, ariaLabel }) { const path = useMemoC(() => { if (!data || data.length < 2) return { line: "", area: "" }; const min = Math.min(...data), max = Math.max(...data); const span = max - min || 1; const pts = data.map((v, i) => [ (i / (data.length - 1)) * w, h - 2 - ((v - min) / span) * (h - 4), ]); const line = pts.map(([x, y], i) => (i === 0 ? `M${x},${y}` : `L${x},${y}`)).join(" "); const area = `${line} L${w},${h} L0,${h} Z`; return { line, area }; }, [data, w, h]); const id = useMemoC(() => "spg-" + Math.random().toString(36).slice(2, 8), []); return ( {fill && } ); } // ============================================================ // Drawdown gauge (half-arc) // ============================================================ function DrawdownGauge({ value, max, size = 180 }) { const pct = Math.min(1, value / max); const w = size, h = size * 0.62; const cx = w / 2, cy = h - 8; const r = w / 2 - 14; const startA = Math.PI, endA = 0; const arc = (a0, a1) => { const x0 = cx + r * Math.cos(a0), y0 = cy + r * Math.sin(a0); const x1 = cx + r * Math.cos(a1), y1 = cy + r * Math.sin(a1); const large = Math.abs(a1 - a0) > Math.PI ? 1 : 0; const sweep = a1 > a0 ? 1 : 0; return `M${x0},${y0} A${r},${r} 0 ${large} ${sweep} ${x1},${y1}`; }; const angAt = (p) => startA - (startA - endA) * p; const color = pct < 0.5 ? "var(--profit)" : pct < 0.8 ? "var(--warn)" : "var(--loss)"; return ( {[0.25, 0.5, 0.75].map((t) => { const a = angAt(t); const x0 = cx + (r - 14) * Math.cos(a), y0 = cy + (r - 14) * Math.sin(a); const x1 = cx + (r - 4) * Math.cos(a), y1 = cy + (r - 4) * Math.sin(a); return ; })} ); } // ============================================================ // Equity curve with benchmark overlay + hover // ============================================================ function EquityCurve({ portfolio, benchmark, h = 280, accent = "#00D4AA", yLabel = (v) => "$" + Math.round(v / 1000) + "k" }) { const wrapRef = useRefC(null); const [w, setW] = useStateC(800); const [hover, setHover] = useStateC(null); useEffectC(() => { if (!wrapRef.current) return; const ro = new ResizeObserver(([e]) => setW(Math.max(400, e.contentRect.width))); ro.observe(wrapRef.current); return () => ro.disconnect(); }, []); const pad = { t: 18, r: 18, b: 26, l: 56 }; const innerW = w - pad.l - pad.r; const innerH = h - pad.t - pad.b; const all = [...portfolio, ...(benchmark || [])]; const min = Math.min(...all); const max = Math.max(...all); const span = max - min || 1; const n = portfolio.length; const toXY = (v, i) => [ pad.l + (i / (n - 1)) * innerW, pad.t + innerH - ((v - min) / span) * innerH, ]; const linePath = (arr) => arr.map((v, i) => { const [x, y] = toXY(v, i); return (i === 0 ? "M" : "L") + x.toFixed(1) + "," + y.toFixed(1); }).join(" "); const areaPath = (arr) => linePath(arr) + ` L${pad.l + innerW},${pad.t + innerH} L${pad.l},${pad.t + innerH} Z`; const yTicks = Array.from({ length: 5 }, (_, i) => min + (span * i) / 4); const onMove = (e) => { const rect = e.currentTarget.getBoundingClientRect(); const x = e.clientX - rect.left; const idx = Math.round(((x - pad.l) / innerW) * (n - 1)); if (idx >= 0 && idx < n) setHover(idx); else setHover(null); }; return (
setHover(null)} style={{ display: "block", cursor: "crosshair" }}> {yTicks.map((v, i) => { const y = pad.t + innerH - ((v - min) / span) * innerH; return ( {yLabel(v)} ); })} {benchmark && } {["6mo", "5mo", "4mo", "3mo", "2mo", "1mo", "Now"].map((lab, i) => { const x = pad.l + (i / 6) * innerW; return {lab}; })} {hover != null && (() => { const [hx, hy] = toXY(portfolio[hover], hover); return ( {benchmark && (() => { const [, by] = toXY(benchmark[hover], hover); return ; })()} ); })()} {hover != null && (() => { const x = pad.l + (hover / (n - 1)) * innerW; const left = Math.min(w - 180, Math.max(0, x + 12)); return (
{Math.round((1 - hover/(n-1)) * 6 * 30)}d ago
Equity ${portfolio[hover].toFixed(0)}
{benchmark && (
BTC HODL ${benchmark[hover].toFixed(0)}
)}
); })()}
); } // ============================================================ // Portfolio sparkline (medium size, no axis) // ============================================================ function PortfolioSpark({ data, accent = "#00D4AA", w = 320, h = 80 }) { const min = Math.min(...data), max = Math.max(...data); const span = max - min || 1; const pts = data.map((v, i) => [ (i / (data.length - 1)) * w, h - 4 - ((v - min) / span) * (h - 8), ]); const line = pts.map(([x, y], i) => (i === 0 ? `M${x},${y}` : `L${x},${y}`)).join(" "); const area = `${line} L${w},${h} L0,${h} Z`; return ( ); } // ============================================================ // Daily P&L bar chart (gain/loss bars + win rate dots) // ============================================================ function DailyPnLBars({ data, h = 200 }) { const wrapRef = useRefC(null); const [w, setW] = useStateC(800); useEffectC(() => { if (!wrapRef.current) return; const ro = new ResizeObserver(([e]) => setW(Math.max(300, e.contentRect.width))); ro.observe(wrapRef.current); return () => ro.disconnect(); }, []); const pad = { t: 14, r: 16, b: 24, l: 50 }; const iw = w - pad.l - pad.r; const ih = h - pad.t - pad.b; const max = Math.max(...data.map((d) => Math.abs(d.pnl))); const zero = pad.t + ih / 2; const halfH = ih / 2; const barW = (iw / data.length) * 0.7; const step = iw / data.length; return (
{/* y ticks */} {[-max, -max/2, 0, max/2, max].map((v, i) => { const y = zero - (v / max) * halfH; return ( {(v >= 0 ? "+" : "−") + "$" + Math.abs(Math.round(v))} ); })} {data.map((d, i) => { const cx = pad.l + i * step + step / 2; const x = cx - barW / 2; const barH = Math.abs(d.pnl / max) * halfH; const y = d.pnl >= 0 ? zero - barH : zero; const fill = d.pnl >= 0 ? "var(--profit)" : "var(--loss)"; return ( {d.d === 0 ? "Today" : Math.abs(d.d) + "d ago"} · {(d.pnl >= 0 ? "+" : "−") + "$" + Math.abs(d.pnl).toFixed(2)} · {d.trades} trades · {Math.round(d.wr*100)}% wr {i % 2 === 0 && ( {d.d === 0 ? "today" : Math.abs(d.d) + "d"} )} ); })}
); } // ============================================================ // Latency histogram (signal → execution) // ============================================================ function LatencyHistogram({ samples, h = 140 }) { const wrapRef = useRefC(null); const [w, setW] = useStateC(400); useEffectC(() => { if (!wrapRef.current) return; const ro = new ResizeObserver(([e]) => setW(Math.max(200, e.contentRect.width))); ro.observe(wrapRef.current); return () => ro.disconnect(); }, []); // Bin samples into 8 buckets const max = 1000; const buckets = Array(8).fill(0); samples.forEach((v) => { const b = Math.min(7, Math.floor((v / max) * 8)); buckets[b]++; }); const peak = Math.max(...buckets, 1); const pad = { t: 8, r: 8, b: 24, l: 8 }; const iw = w - pad.l - pad.r; const ih = h - pad.t - pad.b; const barW = iw / buckets.length; return (
{buckets.map((b, i) => { const barH = (b / peak) * ih; const x = pad.l + i * barW; const y = pad.t + ih - barH; const ms = i * (max / 8); const fill = ms < 500 ? "var(--profit)" : ms < 1000 ? "var(--warn)" : "var(--loss)"; return ( {ms}–{ms + max/8}ms · {b} signals {Math.round(ms)} ); })}
); } // ============================================================ // Signal funnel — vertical bars showing dropoff // ============================================================ function SignalFunnel({ stages }) { const max = Math.max(...stages.map((s) => s.value)); return (
{stages.map((s, i) => { const pct = (s.value / max) * 100; return (
{s.label} {s.value}{s.suffix || ""}
); })}
); } // ============================================================ // Hourly heatmap — 24 cells // ============================================================ function HourlyHeatmap({ data, h = 60 }) { const max = Math.max(...data.map((d) => Math.abs(d.pnl))); return (
{data.map((d, i) => { const intensity = Math.abs(d.pnl) / max; const color = d.pnl >= 0 ? `rgba(25,229,166,${0.10 + intensity * 0.75})` : `rgba(242,63,92,${0.10 + intensity * 0.75})`; return (
0.5 ? "rgba(0,0,0,0.7)" : "var(--text-3)", }} title={`${d.hour.toString().padStart(2,"0")}:00 UTC · ${d.pnl >= 0 ? "+" : "−"}$${Math.abs(d.pnl).toFixed(0)} · ${d.trades} trades`}> {i % 3 === 0 ? d.hour.toString().padStart(2, "0") : ""}
); })}
); } // ============================================================ // Monthly returns heatmap (backtest) // ============================================================ function MonthlyHeatmap({ data }) { const months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; const all = data.flatMap((r) => r.vals).filter((v) => v != null); const max = Math.max(...all.map(Math.abs)); return (
{months.map((m) => )} {data.map((row) => { const ytd = row.vals.filter((v) => v != null).reduce((a, b) => a + b, 0); return ( {row.vals.map((v, i) => { if (v == null) return ); })} ); })}
{m}YTD
{row.year}; const intensity = Math.abs(v) / max; const bg = v >= 0 ? `rgba(25,229,166,${0.12 + intensity * 0.7})` : `rgba(242,63,92,${0.12 + intensity * 0.7})`; return ( 0.45 ? "rgba(0,0,0,0.78)" : "var(--text)", }}>{v > 0 ? "+" : ""}{v.toFixed(1)}= 0 ? "var(--profit)" : "var(--loss)", background: "rgba(255,255,255,0.04)", borderRadius: 3, padding: "0 6px", }}>{ytd > 0 ? "+" : ""}{ytd.toFixed(1)}%
); } // ============================================================ // P&L distribution histogram (trade outcomes) // ============================================================ function PnLHistogram({ trades, h = 160 }) { const wrapRef = useRefC(null); const [w, setW] = useStateC(400); useEffectC(() => { if (!wrapRef.current) return; const ro = new ResizeObserver(([e]) => setW(Math.max(200, e.contentRect.width))); ro.observe(wrapRef.current); return () => ro.disconnect(); }, []); const vals = trades.filter((t) => t.pnl != null).map((t) => t.pnl); const min = Math.min(...vals, -120); const max = Math.max(...vals, 120); const n = 12; const buckets = Array(n).fill(0); vals.forEach((v) => { const b = Math.max(0, Math.min(n - 1, Math.floor(((v - min) / (max - min)) * n))); buckets[b]++; }); const peak = Math.max(...buckets, 1); const pad = { t: 8, r: 8, b: 24, l: 8 }; const iw = w - pad.l - pad.r; const ih = h - pad.t - pad.b; const barW = iw / n; return (
{buckets.map((b, i) => { const barH = (b / peak) * ih; const x = pad.l + i * barW; const y = pad.t + ih - barH; const v0 = min + (i / n) * (max - min); const fill = v0 >= 0 ? "var(--profit)" : "var(--loss)"; return ( {Math.round(v0)} – {Math.round(v0 + (max-min)/n)} · {b} trades ); })} −${Math.round(Math.abs(min))} $0 +${Math.round(max)}
); } // ============================================================ // Log volume line chart (last 24h, hourly buckets) // ============================================================ function LogVolumeChart({ data, h = 80, accent = "#00D4AA" }) { const wrapRef = useRefC(null); const [w, setW] = useStateC(400); useEffectC(() => { if (!wrapRef.current) return; const ro = new ResizeObserver(([e]) => setW(Math.max(200, e.contentRect.width))); ro.observe(wrapRef.current); return () => ro.disconnect(); }, []); const max = Math.max(...data, 1); const pad = { t: 6, r: 6, b: 14, l: 6 }; const iw = w - pad.l - pad.r; const ih = h - pad.t - pad.b; const barW = iw / data.length; return (
{data.map((v, i) => { const barH = (v / max) * ih; return ( ); })} 24h ago now
); } Object.assign(window, { Sparkline, DrawdownGauge, EquityCurve, PortfolioSpark, DailyPnLBars, LatencyHistogram, SignalFunnel, HourlyHeatmap, MonthlyHeatmap, PnLHistogram, LogVolumeChart, });