import { Callout } from '../ui/Callout'; import { fmtSignedPct } from '../../lib/format'; import type { BacktestCurvePoint, BacktestPortfolioMonitorRun } from '../../lib/types'; /** * Portfolio return vs S&P 500 for one monitor run. * * Hand-rolled SVG on purpose: two polylines and two axis rules do not justify a * charting dependency, and the shape is fixed. Lives in `signals/` rather than * `ui/` because it is typed to the backtest payload — generalising it for a * single caller would be the wrong trade. */ function curvePath( points: BacktestCurvePoint[], min: number, max: number, w: number, h: number, pad: number, startMs: number, endMs: number, ): string { if (points.length < 2) return ''; const span = Math.max(max - min, 1); const timeSpan = Math.max(endMs - startMs, 1); return points .map((p, i) => { const t = new Date(p.date).getTime(); const x = pad + ((t - startMs) / timeSpan) * (w - pad * 2); const value = p.return_pct ?? 0; const y = pad + (1 - (value - min) / span) * (h - pad * 2); return `${i === 0 ? 'M' : 'L'}${x.toFixed(1)},${y.toFixed(1)}`; }) .join(' '); } export function EquityCurveChart({ run }: { run: BacktestPortfolioMonitorRun }) { const portfolio = run.equity_curve ?? []; const benchmark = run.benchmark_curve ?? []; const values = [...portfolio, ...benchmark] .map((p) => p.return_pct) .filter((v): v is number => v !== null && v !== undefined); if (portfolio.length < 2 || values.length === 0) { return No equity curve points for this selection.; } const min = Math.min(0, ...values); const max = Math.max(0, ...values); const times = [...portfolio, ...benchmark] .map((p) => new Date(p.date).getTime()) .filter((v) => Number.isFinite(v)); if (times.length === 0) { return No dated equity curve points for this selection.; } const startMs = Math.min(...times); const endMs = Math.max(...times); const w = 720; const h = 240; const pad = 28; const portfolioPath = curvePath(portfolio, min, max, w, h, pad, startMs, endMs); const benchmarkPath = curvePath(benchmark, min, max, w, h, pad, startMs, endMs); const lastPortfolio = portfolio[portfolio.length - 1]?.return_pct ?? null; const lastBenchmark = benchmark[benchmark.length - 1]?.return_pct ?? run.spy_return_pct; return (

{run.label}

{run.start_date} - {run.end_date}

Portfolio {fmtSignedPct(lastPortfolio)} S&P 500 {fmtSignedPct(lastBenchmark)}
{benchmarkPath && ( )} {fmtSignedPct(max)} {fmtSignedPct(min)}
); }