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
+66
View File
@@ -72,3 +72,69 @@ export function formatDateTime(d: string): string {
hour12: true,
})}`;
}
// ── Metric display helpers ─────────────────────────────────────────────────
// Shared by the Signals backtest/paper-trade panels. Dashboard and
// OpenTradesPanel deliberately still carry their own copies — migrating them is
// a separate change, not drive-by scope.
/** R-multiple with an explicit sign. e.g. 1.2 → "+1.20R", null → "—" */
export function fmtR(v: number | null | undefined): string {
if (v === null || v === undefined) return '—';
return `${v > 0 ? '+' : ''}${v.toFixed(2)}R`;
}
/** e.g. 12.34 → "12.3%" */
export function fmtPct(v: number | null | undefined): string {
return v === null || v === undefined ? '—' : `${v.toFixed(1)}%`;
}
/** e.g. 12.34 → "+12.3%" */
export function fmtSignedPct(v: number | null | undefined): string {
if (v === null || v === undefined) return '—';
return `${v > 0 ? '+' : ''}${v.toFixed(1)}%`;
}
/** Always rendered negative, whatever sign the source uses. 17.3 → "-17.3%" */
export function fmtDrawdown(v: number | null | undefined): string {
return v === null || v === undefined ? '—' : `-${Math.abs(v).toFixed(1)}%`;
}
/** e.g. 15.3 → "15.3d" */
export function fmtDays(v: number | null | undefined): string {
return v === null || v === undefined ? '—' : `${v.toFixed(1)}d`;
}
/** Unitless ratios — Sharpe, Sortino, Calmar, Gain/Pain, profit factor. */
export function fmtRatio(v: number | null | undefined): string {
return v === null || v === undefined ? '—' : v.toFixed(2);
}
/**
* Bare amount, no currency symbol and no sign. e.g. 1234.5 → "1,234.50"
* Kept separate from fmtSignedMoney on purpose — they are not interchangeable.
*/
export function fmtMoney(v: number | null | undefined): string {
if (v === null || v === undefined) return '—';
return v.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
}
/**
* Signed currency, using U+2212 for negatives. e.g. -12.3 → "$12.30"
* Use wherever a value can go negative and the unit is money.
*/
export function fmtSignedMoney(v: number | null | undefined): string {
if (v === null || v === undefined) return '—';
return `${v >= 0 ? '+' : ''}$${Math.abs(v).toFixed(2)}`;
}
/** Green above zero, red below, neutral at zero or null. */
export function rColor(v: number | null | undefined): string {
if (v === null || v === undefined) return 'text-gray-400';
if (v > 0) return 'text-emerald-400';
if (v < 0) return 'text-red-400';
return 'text-gray-300';
}