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, BacktestPortfolioPolicy } from '../../lib/types'; function fmtR(v: number | null | undefined): string { if (v === null || v === undefined) return '—'; return `${v > 0 ? '+' : ''}${v.toFixed(2)}R`; } function fmtPct(v: number | null): string { return v === null ? '—' : `${v.toFixed(1)}%`; } function fmtMoney(v: number | null | undefined): string { if (v === null || v === undefined) return '—'; return v.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); } function fmtSignedPct(v: number | null | undefined): string { if (v === null || v === undefined) return '—'; return `${v > 0 ? '+' : ''}${v.toFixed(1)}%`; } function fmtDays(v: number | null | undefined): string { return v === null || v === undefined ? '—' : `${v.toFixed(1)}d`; } function fmtRPerDay(v: number | null | undefined): string { if (v === null || v === undefined) return '—'; return `${v > 0 ? '+' : ''}${v.toFixed(3)}R`; } 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', }; const ABLATION_LABELS: Record = { all_floors: 'All floors (current gate)', no_confidence_floor: 'Without confidence floor', no_rr_floor: 'Without R:R floor', no_neutral_exclusion: 'Without NEUTRAL exclusion', momentum_only: 'Momentum only (no floors)', }; const POLICY_LABELS: Record = { target: 'S/R target exit', hold: 'Hold to horizon', }; // Prefer the net-of-costs number when the report carries it; older cached // reports (pre-cost model) fall back to gross. function netOrGross(r: { avg_r: number | null; net_avg_r?: number | null }): number | null { return r.net_avg_r ?? r.avg_r; } // 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)} {fmtR(b.net_avg_r ?? null)} {fmtR(b.best_r)} {fmtR(b.worst_r)} {fmtDays(b.avg_hold_days)} {fmtRPerDay(b.net_r_per_day)} ); } export function BacktestPanel() { const { data: report, isLoading } = useBacktestReport(); const queryClient = useQueryClient(); const toast = useToast(); const bestTimeAvgR = report?.time_exit_sweep && report.time_exit_sweep.length > 0 ? Math.max(...report.time_exit_sweep.map((r) => netOrGross(r) ?? -Infinity)) : null; const sim = report?.portfolio_sim ?? null; 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.params.cost_per_side_pct != null && ( <> · net assumes {report.params.cost_per_side_pct}%/side costs )}

{report.recommendation && report.recommendation.items.length > 0 && (

What this backtest recommends

{report.recommendation.headline && (

{report.recommendation.headline}

)}
    {report.recommendation.items.map((item) => (
  • {item.text}
  • ))}
{report.recommendation.note && (

{report.recommendation.note}

)}
)}
{report.overall_qualified.median_net_r != null && ( )} {report.overall_qualified.profit_factor != null && ( 1 ? 'text-emerald-400' : 'text-red-400'} sub="qualified · net wins / net losses" /> )} {report.overall_qualified.net_avg_r_ex_top5 != null && ( )}
{report.by_direction.long && } {report.by_direction.short && }
Set Setups Wins Losses Expired Hit Rate Avg R Net Avg R Best R Worst R Avg Hold Net R/d
{/* 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 Net 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.net_avg_r ?? null)} {fmtR(row.total_r)}
)} {report.gate_ablation && report.gate_ablation.length > 0 && (

Gate ablation — which floors earn their keep

{report.gate_ablation_note ?? 'Each row re-qualifies the same candidates at the current momentum cutoff with one floor removed (long-only throughout).'}

{report.gate_ablation.map((row) => ( ))}
Variant Setups Hit Rate Avg R Net Avg R Total R Hold Net Avg R Hold Total R
{ABLATION_LABELS[row.variant] ?? row.variant} {row.total} {fmtPct(row.hit_rate)} {fmtR(row.avg_r)} {fmtR(row.net_avg_r ?? null)} {fmtR(row.total_r)} {fmtR(row.hold_net_avg_r ?? null)} {fmtR(row.hold_total_r ?? null)}
)} {report.time_exit_sweep && report.time_exit_sweep.length > 0 && (

Time-based exit

Buy at detection, keep the initial ATR stop, and exit at the{' '} day-N close — no target, no trailing. This is the classic cross-sectional momentum implementation (hold ~a month, re-rank).{' '} Win Rate = share closed in profit. ★ = best net avg R.

{report.time_exit_sweep.map((row) => { const best = netOrGross(row) != null && netOrGross(row) === bestTimeAvgR; return ( ); })}
Hold Setups Profitable Win Rate Avg R Net Avg R Total R Best R Worst R Avg Hold Net R/d
{best && } {row.hold_days}d {row.total} {row.wins} {fmtPct(row.win_rate)} {fmtR(row.avg_r)} {fmtR(row.net_avg_r ?? null)} {fmtR(row.total_r)} {fmtR(row.best_r)} {fmtR(row.worst_r)} {fmtDays(row.avg_hold_days)} {fmtRPerDay(row.net_r_per_day)}
)} {sim && sim.policies.length > 0 && (

Portfolio simulation

{sim.note ?? 'One capital-constrained book over the qualified setups.'}{' '} Start {fmtMoney(sim.params.starting_capital)} · max {sim.params.max_positions} positions ·{' '} {sim.params.risk_per_trade_pct}% risk/trade · {sim.params.notional_cap_pct}% notional cap ·{' '} {sim.params.cost_per_side_pct}%/side costs · {sim.policies[0].start_date} → {sim.policies[0].end_date}

{sim.policies.map((p) => ( ))} {( [ ['Final equity', (p) => fmtMoney(p.final_equity), (p) => rColor(p.final_equity - p.starting_capital)], ['Total return', (p) => fmtSignedPct(p.total_return_pct), (p) => rColor(p.total_return_pct)], ['SPY return (same window)', (p) => fmtSignedPct(p.spy_return_pct), () => 'text-gray-300'], ['CAGR', (p) => fmtSignedPct(p.cagr_pct), (p) => rColor(p.cagr_pct)], ['Max drawdown', (p) => `−${p.max_drawdown_pct.toFixed(1)}%`, () => 'text-amber-400'], ['Sharpe (daily, annualized)', (p) => (p.sharpe === null ? '—' : p.sharpe.toFixed(2)), () => 'text-gray-200'], ['Trades', (p) => String(p.trades), () => 'text-gray-300'], ['Win rate', (p) => fmtPct(p.win_rate), () => 'text-gray-200'], ['Avg P&L / trade', (p) => fmtMoney(p.avg_trade_pnl), (p) => rColor(p.avg_trade_pnl)], ['Best / worst trade', (p) => `${fmtR(p.best_trade_r)} / ${fmtR(p.worst_trade_r)}`, () => 'text-gray-300'], ['Avg holding time', (p) => fmtDays(p.avg_hold_days), () => 'text-gray-300'], [ 'Per-year returns', (p) => p.yearly_returns && p.yearly_returns.length > 0 ? p.yearly_returns .map((y) => `${y.year} ${fmtSignedPct(y.return_pct)}`) .join(' · ') : '—', () => 'text-gray-300', ], ['Entries skipped (book full)', (p) => String(p.skipped_book_full), () => 'text-gray-500'], ] as [string, (p: BacktestPortfolioPolicy) => string, (p: BacktestPortfolioPolicy) => string][] ).map(([label, fmt, color]) => ( {sim.policies.map((p) => ( ))} ))}
Metric {POLICY_LABELS[p.policy] ?? p.policy}
{label} {fmt(p)}
)} {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}

)}
); }