refactor(signals): split the Track Record tab and cut the backtest page down

One tab stacked three things that all called themselves a track record:
realized paper P&L, setup-outcome grading under the rejected take-profit model,
and the backtest portfolio simulation. Split into Setups | Paper Trades |
Backtest, one subject each. `track` stays the Paper Trades slug so the legacy
/performance redirect keeps working. The grading diagnostic and its Evaluate /
Reset controls go with Backtest, not Paper Trades — reset_track_record deletes
trade_setups, not paper trades.

BacktestPanel 439 -> 175 lines. Its run settings alone were 106 lines of
hand-rolled sr-only radio cards for two binary choices; they are now two
Dropdowns and a button on one wrapping row, with the per-option prose moved into
the existing explainer. The amber warnings survive as a conditional slot, so a
non-default choice still announces itself but the common path is silent.

The recommendation printed eight findings at equal weight, burying the verdict
in tuning detail. `topic` now splits them: production, benchmark and robustness
stay inline, gate/exit/cutoff collapse behind a disclosure, and any WARNING or
LAGS item is promoted out of the collapsed group regardless of topic. No topic
chips — every backend string already self-prefixes, so a chip would render
"GATE | Gate: ...".

Portfolio metrics are now two tiers: five headline tiles for what the book
returned, then a smaller labelled row for how good that return was (Sortino,
Calmar (MAR), Gain/Pain, Profit Factor $, EV/trade). Reports cached before those
metrics existed hide the second row rather than showing a half-populated line of
dashes.

Extracted EquityCurveChart, PortfolioMonitorPanel and BacktestRecommendationCard,
plus a StatTile primitive and shared formatters for the duplication in the files
this touched. DashboardPage and OpenTradesPanel deliberately keep their own
copies — migrating them is separate scope.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-11 22:09:23 +02:00
co-authored by Claude Opus 5
parent 442dc3f04b
commit 13a984a84d
10 changed files with 599 additions and 397 deletions
@@ -2,22 +2,10 @@ import { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { usePaperTrades } from '../../hooks/usePaperTrades';
import { tradePnl } from '../../lib/paperTrade';
import { formatPrice } from '../../lib/format';
import { formatPrice, fmtR, fmtSignedMoney, rColor } from '../../lib/format';
import { Section } from '../ui/Section';
import { Callout } from '../ui/Callout';
function money(v: number): string {
return `${v >= 0 ? '+' : ''}$${Math.abs(v).toFixed(2)}`;
}
function fmtR(v: number | null): string {
return v === null ? '—' : `${v > 0 ? '+' : ''}${v.toFixed(2)}R`;
}
function color(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';
}
import { StatTile } from '../ui/StatTile';
// How the trade was closed — useful context on real trades at almost no cost.
function reasonMeta(reason: string | null): { label: string; cls: string } {
@@ -31,18 +19,6 @@ function reasonMeta(reason: string | null): { label: string; cls: string } {
}
}
function Stat({ label, value, valueClass = 'text-gray-100', sub }: {
label: string; value: string; valueClass?: string; sub?: string;
}) {
return (
<div className="glass p-4">
<p className="section-index">{label}</p>
<p className={`num mt-1.5 text-2xl font-semibold ${valueClass}`}>{value}</p>
{sub && <p className="mt-1 text-xs text-gray-500">{sub}</p>}
</div>
);
}
export function MyTradesPanel() {
const { data: closed, isLoading } = usePaperTrades('closed');
@@ -70,7 +46,10 @@ export function MyTradesPanel() {
if (isLoading) return null;
return (
<Section title="My Trades" hint="your realized paper-trading results">
<Section
title="Closed Trades"
hint="realized paper-trading results — open positions are on the Dashboard"
>
{stats.total === 0 ? (
<Callout variant="empty">
No closed trades yet. Take setups as paper trades and theyll resolve here when price hits
@@ -79,11 +58,11 @@ export function MyTradesPanel() {
) : (
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
<Stat label="Hit Rate" value={stats.hitRate != null ? `${stats.hitRate.toFixed(1)}%` : '—'} sub={`${stats.wins}W / ${stats.losses}L`} />
<Stat label="Expectancy" value={fmtR(stats.avgR)} valueClass={color(stats.avgR)} sub="avg R per closed trade" />
<Stat label="Total R" value={fmtR(stats.totalR)} valueClass={color(stats.totalR)} sub={`${stats.total} closed`} />
<Stat label="Total P&L" value={money(stats.totalPnl)} valueClass={color(stats.totalPnl)} sub="realized, all closed" />
<Stat label="Alpha vs S&P 500" value={stats.totalAlpha != null ? money(stats.totalAlpha) : '—'} valueClass={color(stats.totalAlpha)} sub="realized vs buy-and-hold SPY" />
<StatTile label="Hit Rate" value={stats.hitRate != null ? `${stats.hitRate.toFixed(1)}%` : '—'} sub={`${stats.wins}W / ${stats.losses}L`} />
<StatTile label="Expectancy" value={fmtR(stats.avgR)} valueClass={rColor(stats.avgR)} sub="avg R per closed trade" />
<StatTile label="Total R" value={fmtR(stats.totalR)} valueClass={rColor(stats.totalR)} sub={`${stats.total} closed`} />
<StatTile label="Total P&L" value={fmtSignedMoney(stats.totalPnl)} valueClass={rColor(stats.totalPnl)} sub="realized, all closed" />
<StatTile label="Alpha vs S&P 500" value={stats.totalAlpha != null ? fmtSignedMoney(stats.totalAlpha) : '—'} valueClass={rColor(stats.totalAlpha)} sub="realized vs buy-and-hold SPY" />
</div>
<div className="glass overflow-x-auto">
@@ -112,9 +91,9 @@ export function MyTradesPanel() {
</td>
<td className="num px-4 py-2.5 text-right text-gray-300">{formatPrice(t.entry_price)}</td>
<td className="num px-4 py-2.5 text-right text-gray-300">{t.close_price != null ? formatPrice(t.close_price) : '—'}</td>
<td className={`num px-4 py-2.5 text-right font-semibold ${p ? color(p.pnl) : 'text-gray-500'}`}>{p ? money(p.pnl) : '—'}</td>
<td className={`num px-4 py-2.5 text-right ${p?.r != null ? color(p.r) : 'text-gray-500'}`}>{p?.r != null ? fmtR(p.r) : '—'}</td>
<td className={`num px-4 py-2.5 text-right ${t.alpha_pct != null ? color(t.alpha_pct) : 'text-gray-500'}`} title="Return vs. S&P 500 over the holding period">{t.alpha_pct != null ? `${t.alpha_pct >= 0 ? '+' : ''}${t.alpha_pct.toFixed(1)}%` : '—'}</td>
<td className={`num px-4 py-2.5 text-right font-semibold ${p ? rColor(p.pnl) : 'text-gray-500'}`}>{p ? fmtSignedMoney(p.pnl) : '—'}</td>
<td className={`num px-4 py-2.5 text-right ${p?.r != null ? rColor(p.r) : 'text-gray-500'}`}>{p?.r != null ? fmtR(p.r) : '—'}</td>
<td className={`num px-4 py-2.5 text-right ${t.alpha_pct != null ? rColor(t.alpha_pct) : 'text-gray-500'}`} title="Return vs. S&P 500 over the holding period">{t.alpha_pct != null ? `${t.alpha_pct >= 0 ? '+' : ''}${t.alpha_pct.toFixed(1)}%` : '—'}</td>
<td className="px-4 py-2.5">
<span className={`num text-[10px] font-semibold uppercase tracking-wider ${reasonMeta(t.close_reason).cls}`} title="How the trade was closed">
{reasonMeta(t.close_reason).label}