Files
signal-platform/frontend/src/components/signals/MyTradesPanel.tsx
T
dennisthiessenandClaude Opus 5 13a984a84d 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>
2026-08-11 22:09:23 +02:00

113 lines
6.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo } from 'react';
import { Link } from 'react-router-dom';
import { usePaperTrades } from '../../hooks/usePaperTrades';
import { tradePnl } from '../../lib/paperTrade';
import { formatPrice, fmtR, fmtSignedMoney, rColor } from '../../lib/format';
import { Section } from '../ui/Section';
import { Callout } from '../ui/Callout';
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 } {
switch (reason) {
case 'stop': return { label: 'Stop', cls: 'text-red-400' };
case 'trailing': return { label: 'Trail', cls: 'text-amber-400' };
case 'target': return { label: 'Target', cls: 'text-emerald-400' };
case 'time': return { label: 'Time', cls: 'text-gray-400' };
case 'manual': return { label: 'Manual', cls: 'text-blue-300' };
default: return { label: '—', cls: 'text-gray-500' };
}
}
export function MyTradesPanel() {
const { data: closed, isLoading } = usePaperTrades('closed');
const stats = useMemo(() => {
const rows = (closed ?? []).map((t) => ({ t, p: tradePnl(t) }));
const rs = rows.map((r) => r.p?.r).filter((r): r is number => r != null);
const pnls = rows.map((r) => r.p?.pnl ?? 0);
const alphas = rows.map((r) => r.t.alpha_usd).filter((a): a is number => a != null);
const wins = pnls.filter((p) => p > 0).length;
const losses = pnls.filter((p) => p < 0).length;
const decided = wins + losses;
return {
total: rows.length,
wins,
losses,
hitRate: decided ? (wins / decided) * 100 : null,
avgR: rs.length ? rs.reduce((a, b) => a + b, 0) / rs.length : null,
totalR: rs.length ? rs.reduce((a, b) => a + b, 0) : null,
totalPnl: pnls.reduce((a, b) => a + b, 0),
totalAlpha: alphas.length ? alphas.reduce((a, b) => a + b, 0) : null,
rows,
};
}, [closed]);
if (isLoading) return null;
return (
<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
the stop or target (or when you sell).
</Callout>
) : (
<div className="space-y-4">
<div className="grid gap-3 sm:grid-cols-2 lg:grid-cols-5">
<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">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-white/[0.06] text-left text-xs uppercase tracking-wider text-gray-500">
<th className="px-4 py-2.5">Ticker</th>
<th className="px-4 py-2.5">Dir</th>
<th className="px-4 py-2.5 text-right">Entry</th>
<th className="px-4 py-2.5 text-right">Exit Px</th>
<th className="px-4 py-2.5 text-right">P&L</th>
<th className="px-4 py-2.5 text-right">R</th>
<th className="px-4 py-2.5 text-right">Alpha</th>
<th className="px-4 py-2.5">Reason</th>
<th className="px-4 py-2.5 text-right">Closed</th>
</tr>
</thead>
<tbody>
{stats.rows.map(({ t, p }) => (
<tr key={t.id} className="border-b border-white/[0.04] hover:bg-white/[0.03]">
<td className="px-4 py-2.5">
<Link to={`/ticker/${t.symbol}`} className="font-medium text-blue-300 hover:text-blue-200">{t.symbol}</Link>
</td>
<td className="px-4 py-2.5">
<span className={`num text-[10px] font-semibold uppercase ${t.direction === 'long' ? 'text-emerald-400' : 'text-red-400'}`}>{t.direction}</span>
</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 ? 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}
</span>
</td>
<td className="num px-4 py-2.5 text-right text-gray-500">{t.closed_at ? new Date(t.closed_at).toLocaleDateString() : '—'}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</Section>
);
}