feat: sharpen the event study — more events, fair baseline, per-event view
The first run gave only 2 events (N=2 is anecdote, not evidence) and an unfairly weak coincident baseline, so the +42d lead couldn't be trusted. This makes the measurement meaningful: - More, cleaner events: default drawdown threshold 15%→10%, and dedup switched from "recover to the high" to a rising-edge + cooldown (40d), so distinct drawdowns each register instead of merging. - Fair comparison: each indicator now warns at its OWN 80th percentile instead of a shared absolute 60, removing the artifact that muted the coincident baseline. - Per-event breakdown (date · depth · breadth lead · coincident lead) so a median over a tiny sample can't hide an apples-to-oranges comparison — you see whether both warned on the same drawdown. - Surface precision/recall (best row) + base rate per indicator — the honest edge read, not just lead time. Re-run the Event Study job to regenerate the cached report in the new shape. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -23,6 +23,7 @@ import type {
|
||||
RegimeFundamentals,
|
||||
EventStudyReport,
|
||||
EventStudyLeadStats,
|
||||
EventStudyPerEvent,
|
||||
} from '../lib/types';
|
||||
|
||||
const BAND_STYLES: Record<RegimeBand, { text: string; bar: string; ring: string; label: string }> = {
|
||||
@@ -285,7 +286,22 @@ function Sparkline({ values, color = '#60a5fa', height = 28 }: { values: number[
|
||||
);
|
||||
}
|
||||
|
||||
function pctLabel(v: number | null): string {
|
||||
return v == null ? '—' : `${Math.round(v * 100)}%`;
|
||||
}
|
||||
|
||||
function leadLabel(v: number | null): string {
|
||||
return v == null ? 'missed' : `${v}d`;
|
||||
}
|
||||
|
||||
function bestPr(stats: EventStudyLeadStats) {
|
||||
const rows = stats.signal.rows.filter((r) => r.precision != null && r.recall != null && r.recall > 0);
|
||||
if (!rows.length) return null;
|
||||
return rows.reduce((a, b) => ((b.precision ?? 0) > (a.precision ?? 0) ? b : a));
|
||||
}
|
||||
|
||||
function LeadStat({ label, stats, highlight }: { label: string; stats: EventStudyLeadStats; highlight?: boolean }) {
|
||||
const pr = bestPr(stats);
|
||||
return (
|
||||
<div className={`rounded-lg border px-3 py-2 ${highlight ? 'border-blue-400/30 bg-blue-400/[0.06]' : 'border-white/[0.06] bg-white/[0.02]'}`}>
|
||||
<div className="text-xs text-gray-500">{label}</div>
|
||||
@@ -293,8 +309,46 @@ function LeadStat({ label, stats, highlight }: { label: string; stats: EventStud
|
||||
{stats.median_lead_days != null ? `${stats.median_lead_days}d lead` : 'no signal'}
|
||||
</div>
|
||||
<div className="text-[11px] text-gray-600">
|
||||
{stats.events_with_signal}/{stats.events_total} events warned
|
||||
{stats.events_with_signal}/{stats.events_total} warned
|
||||
{stats.warn_threshold != null ? ` · warn ≥ ${Math.round(stats.warn_threshold)}` : ''}
|
||||
</div>
|
||||
{pr && (
|
||||
<div className="text-[11px] text-gray-600">
|
||||
best P {pctLabel(pr.precision)} · R {pctLabel(pr.recall)} @ {pr.threshold}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PerEventTable({ rows }: { rows: EventStudyPerEvent[] }) {
|
||||
return (
|
||||
<div className="overflow-x-auto rounded-lg border border-white/[0.06]">
|
||||
<table className="w-full text-xs">
|
||||
<thead>
|
||||
<tr className="border-b border-white/[0.06] text-left uppercase tracking-wider text-gray-500">
|
||||
<th className="px-3 py-2 font-medium">Drawdown</th>
|
||||
<th className="px-3 py-2 text-right font-medium">Depth</th>
|
||||
<th className="px-3 py-2 text-right font-medium">Breadth lead</th>
|
||||
<th className="px-3 py-2 text-right font-medium">Coincident lead</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{rows.map((e) => {
|
||||
const earlier = e.breadth_lead != null && (e.coincident_lead == null || e.breadth_lead > e.coincident_lead);
|
||||
return (
|
||||
<tr key={e.date} className="border-b border-white/[0.03] last:border-0">
|
||||
<td className="px-3 py-2 num text-gray-300">{e.date}</td>
|
||||
<td className="px-3 py-2 text-right num text-gray-400">{e.depth_pct}%</td>
|
||||
<td className={`px-3 py-2 text-right num ${earlier ? 'text-emerald-400' : 'text-gray-300'}`}>
|
||||
{leadLabel(e.breadth_lead)}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right num text-gray-300">{leadLabel(e.coincident_lead)}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -325,6 +379,12 @@ function EventStudyBody({ report }: { report: EventStudyReport }) {
|
||||
{lead >= 0 ? 'earlier' : 'later'} than the coincident baseline.
|
||||
</p>
|
||||
)}
|
||||
{report.per_event && report.per_event.length > 0 && (
|
||||
<div className="space-y-1.5">
|
||||
<div className="text-[11px] uppercase tracking-wider text-gray-500">Per drawdown (same events, both indicators)</div>
|
||||
<PerEventTable rows={report.per_event} />
|
||||
</div>
|
||||
)}
|
||||
{recent.length > 1 && (
|
||||
<div className="flex flex-wrap items-end gap-6">
|
||||
<div>
|
||||
|
||||
Reference in New Issue
Block a user