/* global React */ /* Vault Control — Analytics page, two tabs (2026-07-21): * COMPARE — pick 2–8 strategies: overlaid equity, head-to-head metrics, correlation. * ANALYTICS — the whole book: class filter + legend toggles over four panels. * Charts are the in-house SVG primitives (charts.jsx) — theme-aware, labeled lines, * hover readouts. (bokeh/seaborn are Python-side and can't run in this static page.) */ const { Card: CpCard } = window.BraveAlphaCapitalDesignSystem_c4b179; const { LineChart: CpLine, GroupedBars: CpGBars, Histogram: CpHist, Legend: CpLegend } = window; const { FMT: CpF, toneOf: cpTone, DecayTag: CpDecay, InfoDot: CpInfo, DeployTag: CpDeployTag, venueOf: cpVenueOf, depsBySid: cpDepsBySid } = window; const { assetClass: cpAssetClass, ClassChips: CpClassChips, classCounts: cpClassCounts } = window; const CP = window.VC; const CMP_METRICS = [ { key: "totalReturn", label: "Total return · all history", fmt: (v) => CpF.pct(v), best: "max", perf: true }, { key: "base100", label: "Portfolio (base 100)", fmt: (v) => v.toFixed(1), best: "max" }, { key: "trades", label: "Trades", fmt: (v) => String(v), best: null }, { key: "winRate", label: "Win rate", fmt: (v) => v.toFixed(1) + "%", best: "max" }, { key: "avgWin", label: "Avg win", fmt: (v) => CpF.pct(v, 2), best: "max", perf: true }, { key: "avgLoss", label: "Avg loss", fmt: (v) => CpF.pct(v, 2), best: "max", perf: true }, { key: "pf", label: "Profit factor", fmt: (v) => CpF.num(v), best: "max" }, { key: "sharpe", label: "Sharpe (ann.)", fmt: (v) => CpF.num(v), best: "max" }, { key: "mdd", label: "Max drawdown", fmt: (v) => v.toFixed(1) + "%", best: "max" }, { key: "avgDurH", label: "Avg trade time", fmt: (v) => CpF.dur(v), best: null }, { key: "expectancy", label: "Expectancy", fmt: (v) => CpF.pct(v, 2), best: "max", perf: true }, { key: "composite", label: "Composite score", fmt: (v) => String(v), best: "max", strong: true }, ]; function cpReg(vault, sid) { return (vault.registry || []).find((r) => r.id === sid) || {}; } function cpStats(sid) { return CP.registryStatsFor(sid); } function cpBacktests(sid) { return CP.backtestingTradesFor(sid); } function ComparisonTable({ vault, ids, mode, pinned, setPinned, onOpen }) { const cols = ids.map((id) => ({ r: cpReg(vault, id), id, s: cpStats(id) })).filter((c) => c.s); const bestByMetric = {}; for (const m of CMP_METRICS) { if (!m.best) continue; const vals = cols.map((c) => c.s[m.key]).filter((v) => v != null && isFinite(v)); bestByMetric[m.key] = m.best === "max" ? Math.max(...vals) : Math.min(...vals); } if (!cols.length) return No trade history yet.; const grid = `188px ` + cols.map(() => `minmax(132px, 1fr)`).join(" "); // deploymentsAll, not deployments — the latter is vault-scoped server-side, so every // non-vault strategy (US equity on hl main, metals on paper) rendered as "REGISTRY". const depSet = cpDepsBySid(vault.deploymentsAll || vault.deployments || []); return (
Metric
{cols.map((c) => { const isPin = pinned === c.id; const dep = depSet[c.id]; return (
{c.r.name || c.id}
{c.id} · {c.r.symbol || ""} · {c.r.timeframe || ""} {c.r.leverage || ""}
); })}
{CMP_METRICS.map((m, ri) => (
{m.label}
{cols.map((c) => { const v = c.s[m.key]; const isBest = m.best && bestByMetric[m.key] != null && Math.abs(v - bestByMetric[m.key]) < 1e-9 && cols.length > 1; const isPin = pinned === c.id; let color = "var(--ink)"; if (m.perf) color = cpTone(v); if (isBest) color = "var(--green)"; return (
{m.fmt(v)}
); })}
))}
Decay
{cols.map((c) => (
))}
); } const HIST_EDGES = [-1000, -12, -9, -6, -3, 0, 3, 6, 9, 12, 1000]; const HIST_LABELS = ["<−12", "−12:−9", "−9:−6", "−6:−3", "−3:0", "0:3", "3:6", "6:9", "9:12", ">12"]; const MAX_SEL = 8; // ---- strategy picker chips (shared by both tabs) ----------------------------- function PickerChips({ regs, sel, toggleSel, depSet }) { return (
{regs.length === 0 && No strategies in this asset class.} {regs.map((r) => { const on = sel.includes(r.id); return ( ); })}
); } // =========================================================================== // COMPARE tab — a few strategies, side by side // =========================================================================== function CompareTab({ vault, mode, openStrategy }) { const regsAll = (vault.registry || []).filter((r) => /^id\d+$/.test(r.id || "") && (!r.status || r.status === "active" || r.status === "paused")); const [cls, setCls] = React.useState("all"); const regs = cls === "all" ? regsAll : regsAll.filter((r) => cpAssetClass(r.asset) === cls); const depSet = cpDepsBySid(vault.deploymentsAll || vault.deployments || []); const [sel, setSel] = React.useState(() => { const dep = (vault.deploymentsAll || vault.deployments || []).map((d) => d.strategy_id); return Array.from(new Set(dep)).slice(0, 4); }); const [pinned, setPinned] = React.useState(null); const [loading, setLoading] = React.useState(false); const [, setTick] = React.useState(0); function toggleSel(id) { setSel((cur) => cur.includes(id) ? cur.filter((x) => x !== id) : (cur.length >= MAX_SEL ? [...cur.slice(1), id] : [...cur, id])); } React.useEffect(() => { const need = sel.filter((id) => !CP.hasRegistryBacktesting(id)); if (!need.length) return; setLoading(true); CP.ensureRegistryBacktesting(need).then(() => { setLoading(false); setTick((t) => t + 1); }).catch(() => setLoading(false)); }, [sel]); const shown = sel.filter((id) => CP.hasRegistryBacktesting(id) && cpStats(id)); const label = (sid) => (((cpReg(vault, sid).symbol) || "") + " " + sid).trim(); const overlay = shown.map((sid) => ({ id: sid, color: CP.colorOf(sid), label: label(sid), data: cpStats(sid).equity })); let corr = null; if (shown.length >= 2) { const buckets = shown.map((id) => CP.weeklyBucketsFromTrades(cpBacktests(id))); corr = shown.map((_, a) => shown.map((__, b) => { if (a === b) return { v: 1 }; const wk = {}; Object.keys(buckets[a]).forEach((k) => { wk[k] = 1; }); Object.keys(buckets[b]).forEach((k) => { wk[k] = 1; }); const ks = Object.keys(wk); if (ks.length < 8) return { v: null }; const xs = ks.map((k) => buckets[a][k] || 0), ys = ks.map((k) => buckets[b][k] || 0); return { v: CP.pearson(xs, ys) }; })); } return (

Pick strategies

up to {MAX_SEL} · updates as you pick · {sel.length} selected{loading ? " · loading ledgers…" : ""}
{overlay.length >= 2 && (

Equity curves overlaid (base 100)

({ id: sid, color: CP.colorOf(sid), label: sid + " · " + (cpReg(vault, sid).name || "") }))} small /> v.toFixed(0)} hover />
)} {shown.length >= 2 && (
Head-to-head comparison

Best value in each row is green. For drawdown and average loss, “best” means least negative. Figures compound backtest + paper + live fills; the strategy detail splits them per source.

)} {shown.length < 2 && Pick at least two strategies to compare.} {corr && (

Weekly-return correlation

Pearson on weekly PnL sums (0-filled inactive weeks) · needs ≥8 overlapping weeks. Low or negative correlation between strategies is what makes a basket more than the sum of its parts.

{shown.map((id) => )} {shown.map((ida, a) => ( {shown.map((idb, b) => { const v = corr[a][b].v; const col = v == null ? "var(--muted)" : a === b ? "var(--muted)" : v > 0.5 ? "var(--red)" : v < 0 ? "var(--green)" : "var(--ink)"; return ; })} ))}
{id}
{ida} 0.5 || v < 0) && a !== b ? 600 : 400 }}>{v == null ? "—" : v.toFixed(2)}
)}
); } // =========================================================================== // ANALYTICS tab — the whole book at once, legend-toggled // =========================================================================== function AnalyticsTab({ vault, mode, openStrategy }) { const [hidden, setHidden] = React.useState(() => new Set()); const [cls, setCls] = React.useState("all"); const [loading, setLoading] = React.useState(false); const [, setTick] = React.useState(0); const toggle = (id) => setHidden((h) => { const n = new Set(h); n.has(id) ? n.delete(id) : n.add(id); return n; }); const allRegs = (vault.registry || []).filter((r) => /^id\d+$/.test(r.id || "") && (!r.status || r.status === "active" || r.status === "paused")); React.useEffect(() => { const ids = allRegs.map((r) => r.id).filter((id) => !CP.hasRegistryBacktesting(id)); if (!ids.length) return; setLoading(true); CP.ensureRegistryBacktesting(ids).then(() => { setLoading(false); setTick((t) => t + 1); }).catch(() => setLoading(false)); }, [vault]); const scoped = cls === "all" ? allRegs : allRegs.filter((r) => cpAssetClass(r.asset) === cls); const withStats = scoped.map((r) => r.id).filter((sid) => CP.hasRegistryBacktesting(sid) && cpStats(sid)); const label = (sid) => (((cpReg(vault, sid).symbol) || "") + " " + sid).trim(); const filterBar = (
Every strategy — filter by class, click a legend item to hide/show its line {withStats.length} shown{loading ? " · loading…" : ""}
({ id: sid, color: CP.colorOf(sid), label: sid + " · " + ((cpReg(vault, sid).symbol) || "") }))} hidden={hidden} onToggle={toggle} small />
); if (!withStats.length) return (
{filterBar} {loading ? "Loading strategy ledgers…" : "No strategy has trade history in this asset class."}
); const equitySeries = withStats.map((sid) => ({ id: sid, color: CP.colorOf(sid), label: label(sid), data: cpStats(sid).equity })); const months = Array.from(new Set(withStats.flatMap((sid) => cpBacktests(sid).filter((t) => t.exit).map((t) => t.exit.slice(0, 7))))).sort(); const monthlySeries = withStats.map((sid) => { const mp = CP.monthlyPnlFromTrades(cpBacktests(sid)); return { id: sid, color: CP.colorOf(sid), data: months.map((m) => mp[m] ?? 0) }; }); const rollSeries = withStats.map((sid) => ({ id: sid, color: CP.colorOf(sid), label: label(sid), data: CP.rollingWinRateFromTrades(cpBacktests(sid), 10) })); const histSeries = withStats.map((sid) => ({ id: sid, color: CP.colorOf(sid), data: CP.pnlHistogram(cpStats(sid).ordered, HIST_EDGES) })); const panel = (title, sub, chart) => (

{title}

{sub}

{chart}
); return (
{filterBar}
{panel("Compound strategy growth", "Per-strategy compounded equity, base 100 — lines named at their endpoints, hover for exact values.",
); } // ---- page shell: the two tabs ------------------------------------------------ function Analytics({ vault, mode, openStrategy }) { const [tab, setTab] = React.useState("compare"); return (
{[["compare", "Compare"], ["analytics", "Analytics"]].map(([id, lb]) => ( ))} {tab === "compare" ? "a few strategies, side by side" : "the whole book at once"}
{tab === "compare" ? : }
); } Object.assign(window, { Analytics, ComparisonTable });