import { useMutation, useQueryClient } from '@tanstack/react-query'; import { useBacktestReport } from '../../hooks/useMarketRegime'; import { triggerJob } from '../../api/admin'; import { Button } from '../ui/Button'; import { Callout } from '../ui/Callout'; import { Disclosure } from '../ui/Disclosure'; import { Section } from '../ui/Section'; import { useToast } from '../ui/Toast'; import type { BacktestBucket } from '../../lib/types'; function fmtR(v: number | null): string { if (v === null) return '—'; return `${v > 0 ? '+' : ''}${v.toFixed(2)}R`; } function fmtPct(v: number | null): string { return v === null ? '—' : `${v.toFixed(1)}%`; } function rColor(v: number | null): string { if (v === null) return 'text-gray-400'; if (v > 0) return 'text-emerald-400'; if (v < 0) return 'text-red-400'; return 'text-gray-300'; } const SIGNAL_LABELS: Record = { mom_12_1: '12–1 month momentum', mom_6_1: '6–1 month momentum', mom_3_1: '3–1 month momentum', reversal_1m: '1-month reversal', trend_200: 'Price vs 200-day SMA', high_52w: 'Proximity to 52-week high', vol_6m: '6-month realized volatility', }; // An |IC| this large, with a consistent sign, is a real (if small) edge worth // building on; below it, ranking on the signal sorts essentially nothing. const IC_EDGE_THRESHOLD = 0.03; function icColor(v: number): string { if (Math.abs(v) < 0.02) return 'text-gray-400'; return v > 0 ? 'text-emerald-400' : 'text-red-400'; } function fmtSpread(v: number | null): string { if (v === null) return '—'; return `${v > 0 ? '+' : ''}${(v * 100).toFixed(2)}%`; } function timeAgo(iso: string): string { const mins = Math.floor((Date.now() - new Date(iso).getTime()) / 60_000); if (mins < 1) return 'just now'; if (mins < 60) return `${mins}m ago`; const hrs = Math.floor(mins / 60); if (hrs < 24) return `${hrs}h ago`; return `${Math.floor(hrs / 24)}d ago`; } function Stat({ label, value, valueClass = 'text-gray-100', sub }: { label: string; value: string; valueClass?: string; sub?: string; }) { return (

{label}

{value}

{sub &&

{sub}

}
); } function BucketRow({ label, b }: { label: string; b: BacktestBucket }) { return ( {label} {b.total} {b.wins} {b.losses} {b.expired} {fmtPct(b.hit_rate)} {fmtR(b.avg_r)} ); } export function BacktestPanel() { const { data: report, isLoading } = useBacktestReport(); const queryClient = useQueryClient(); const toast = useToast(); const run = useMutation({ mutationFn: () => triggerJob('backtest'), onSuccess: (res) => { if (res.status === 'triggered') { toast.addToast('success', 'Backtest started — results appear when it finishes (a minute or two).'); setTimeout(() => queryClient.invalidateQueries({ queryKey: ['backtest-report'] }), 8000); } else { toast.addToast('info', res.message || 'Could not start backtest'); } }, onError: () => toast.addToast('error', 'Failed to start backtest'), }); return (

At each weekly point in history, the setup is rebuilt using only data up to that day (no lookahead), then the actual following ~30 trading days decide its outcome. This shows how the current settings would have performed. Sentiment and fundamentals are held neutral (no point-in-time history), so this calibrates the price / support-resistance / probability machinery. ~6 months of data is roughly one market regime — read it as directional, not a guarantee.

{isLoading && Loading…} {!isLoading && !report && ( No backtest yet. Click “Run backtest” (or trigger it in Admin → Jobs) — it replays every ticker over history and takes a minute or two. )} {report && ( <>

Ran {timeAgo(report.generated_at)} · {report.tickers} tickers · {report.candidates} setups ({report.qualified} qualified) · weekly cadence, {report.params.horizon_days}-day horizon

{report.by_direction.long && } {report.by_direction.short && }
Set Setups Wins Losses Expired Hit Rate Avg R
{/* Guard on the new field so a stale cached report (pre-momentum, with min_expected_value rows) hides the sweep instead of crashing the whole page. Re-running the backtest repopulates it. */} {report.sweep && report.sweep.length > 0 && report.sweep[0].min_momentum_percentile != null && (

Momentum-percentile sweep

How many setups qualify — and how they perform — at each momentum-rank cutoff (floors held fixed). 80 = only the top 20% of the universe by 12-1 momentum each week; 0 = floors only. Lower = more trades, watch that expectancy holds. Your current setting is highlighted; set it in Admin → Settings → Activation.

{report.sweep.map((row) => { const current = Math.abs(row.min_momentum_percentile - report.min_momentum_percentile) < 0.001; return ( ); })}
Min momentum %ile Qualified Wins Losses Hit Rate Avg R Total R
{current && } {row.min_momentum_percentile.toFixed(0)} {row.total} {row.wins} {row.losses} {fmtPct(row.hit_rate)} {fmtR(row.avg_r)} {fmtR(row.total_r)}
)}

Probability calibration

Do targets we call “X% likely” actually hit that often? Realized below predicted = the model is over-confident.

{report.calibration.length === 0 ? ( Not enough resolved setups to calibrate. ) : (
{report.calibration.map((row) => { const over = row.realized_hit_rate < row.predicted_avg; return ( ); })}
Predicted Bucket Setups Avg Predicted Realized Hit Rate
{row.bucket} {row.n} {row.predicted_avg.toFixed(0)}% {row.realized_hit_rate.toFixed(0)}%
)}
{report.signal_eval && report.signal_eval.length > 0 && (

Signal edge (cross-sectional)

Does ranking the universe by a signal predict the forward {report.params.horizon_days}-day return? Mean IC is the rank correlation between signal and return, averaged over non-overlapping windows. |IC| ≳ {IC_EDGE_THRESHOLD} with a consistent sign (high IC>0 %) is a real, if small, edge; near 0 means it sorts nothing. Momentum skips the last month; reversal_1m is expected negative if the universe mean-reverts. Q5−Q1 is the top-minus-bottom-quintile forward return. Greyed rows have too few independent windows to trust — deepen history via the Data Backfill job.

{report.signal_eval.map((row) => { // Only trust the edge highlight when the IC rests on enough // independent windows; thin signals are dimmed, not starred. const edge = row.reliable && Math.abs(row.mean_ic) >= IC_EDGE_THRESHOLD; return ( ); })}
Signal Weeks Avg N Mean IC t-stat IC>0 % Q5−Q1 fwd
{edge && } {SIGNAL_LABELS[row.signal] ?? row.signal} {row.weeks} {row.avg_cross_section ?? '—'} {row.mean_ic.toFixed(3)} {row.ic_t_stat === null ? '—' : row.ic_t_stat.toFixed(2)} {fmtPct(row.ic_positive_pct)} {fmtSpread(row.mean_quintile_spread)}
{report.signal_eval_note && (

{report.signal_eval_note}

)}
)}

{report.note}

)}
); }