/* 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}
setPinned(isPin ? null : c.id)} title="Pin column" style={{ background: "none", border: "none", cursor: "pointer", color: isPin ? "var(--gold)" : "var(--line)", fontSize: 14, lineHeight: 1, padding: 0, flex: "none" }}>{isPin ? "★" : "☆"}
{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) => (
onOpen(c.id)} style={{ fontFamily: "var(--font-body)", fontSize: 12.5, fontWeight: 600, color: "var(--ink)", background: "none", border: "none", borderBottom: "1.5px solid var(--gold)", padding: 0, cursor: "pointer", whiteSpace: "nowrap" }}>Detail →
))}
);
}
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 (
toggleSel(r.id)} style={{
display: "inline-flex", alignItems: "center", gap: 7, fontFamily: "var(--font-mono)", fontSize: 12.5, padding: "7px 12px", borderRadius: 100, cursor: "pointer",
background: on ? "var(--btn-ink)" : "var(--surface)", color: on ? "#fff" : "var(--ink-soft)", border: `1px solid ${on ? "var(--btn-ink)" : "var(--line)"}`,
}}>
{r.id} · {r.symbol}
{depSet[r.id] && {cpVenueOf(depSet[r.id].account_id)} }
);
})}
);
}
// ===========================================================================
// 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) => {id} )}
{shown.map((ida, a) => (
{ida}
{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 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.",
v.toFixed(0)} hover />)}
{panel("Monthly P&L %", "Compounded return within each calendar month, per strategy.",
v.toFixed(0) + "%"} />)}
{panel("Rolling win rate", "10-trade trailing window, chronological — a falling line is early decay.",
v.toFixed(0)} xLabels={(i) => "T" + (i + 10)} hover />)}
{panel("Trade P&L distribution", "Count of trades by P&L bucket (%).",
)}
);
}
// ---- page shell: the two tabs ------------------------------------------------
function Analytics({ vault, mode, openStrategy }) {
const [tab, setTab] = React.useState("compare");
return (
{[["compare", "Compare"], ["analytics", "Analytics"]].map(([id, lb]) => (
setTab(id)} style={{ fontFamily: "var(--font-body)", fontSize: 14, fontWeight: 600, padding: "10px 18px", background: "none", border: "none", borderBottom: "2.5px solid " + (tab === id ? "var(--gold)" : "transparent"), color: tab === id ? "var(--ink)" : "var(--muted)", cursor: "pointer" }}>{lb}
))}
{tab === "compare" ? "a few strategies, side by side" : "the whole book at once"}
{tab === "compare"
?
:
}
);
}
Object.assign(window, { Analytics, ComparisonTable });