feat: overhaul paper run detail page summary UI

- Key metrics row: trades replayed, closed positions, total PnL, win rate
  shown as stat cards at the top
- Run Details card: data range start/end alongside engine start/end times
- Instrument statistics table: numbers formatted to readable decimals,
  overflow guard for barter bug remnants (>1e15 shows --), PnL coloured
  green/red, pnl_return % column added
- Asset Balances table: shows final total/free balance per asset
- Raw Result JSON accordion collapsed by default (was open)
- Mode badge added to header
- displayMetric replaced by typed extractNumber + formatMetric/formatPnl/
  formatPct helpers that cap and format values sensibly

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 13:30:43 +02:00
parent 3808474937
commit a1ab827e1c

View File

@@ -23,7 +23,7 @@ const STATUS_BADGE: Record<PaperRun['status'], { bg: string; text?: string }> =
cancelled: { bg: 'secondary' },
};
function formatTimestamp(iso: string | null): string {
function formatTimestamp(iso: string | null | undefined): string {
if (!iso) return '--';
return new Date(iso).toLocaleString();
}
@@ -38,30 +38,67 @@ function durationBetween(start: string | null, end: string | null): string {
return `${mins}m ${remSecs}s`;
}
/** Try to extract a displayable value from a potentially nested metric. */
function displayMetric(value: unknown): string {
if (value === null || value === undefined) return '--';
if (typeof value === 'number') return value === 0 ? '--' : String(value);
/** Extract a numeric value from barter's nested metric wrappers. */
function extractNumber(value: unknown): number | null {
if (value === null || value === undefined) return null;
if (typeof value === 'number') return value;
if (typeof value === 'string') {
const num = Number(value);
return !isNaN(num) && num === 0 ? '--' : value;
const n = parseFloat(value);
return isNaN(n) ? null : n;
}
if (typeof value === 'object') {
// Wrapper types may serialize with a nested value field
const obj = value as Record<string, unknown>;
for (const key of ['value', '0', 'ratio', 'rate']) {
if (key in obj) return displayMetric(obj[key]);
if (key in obj) {
const inner = extractNumber(obj[key]);
if (inner !== null) return inner;
}
}
}
return '--';
return null;
}
interface ResultSummary {
time_engine_start?: string;
time_engine_end?: string;
instruments?: Record<string, TearSheetData>;
assets?: Record<string, unknown>;
backtest_metadata?: { position_count?: number };
function formatMetric(value: unknown, decimals = 4): string {
const n = extractNumber(value);
if (n === null || n === 0) return '--';
// Detect suspiciously large absolute values (barter bug remnants)
if (Math.abs(n) > 1e15) return '--';
return n.toFixed(decimals);
}
function formatPct(value: unknown): string {
const n = extractNumber(value);
if (n === null || n === 0) return '--';
return `${(n * 100).toFixed(2)}%`;
}
function formatPnl(value: unknown): string {
const n = extractNumber(value);
if (n === null || n === 0) return '--';
if (Math.abs(n) > 1e15) return '--';
const sign = n >= 0 ? '+' : '';
return `${sign}${n.toFixed(4)}`;
}
interface BacktestMetadata {
trade_count?: number;
position_count?: number;
data_range_start?: string;
data_range_end?: string;
}
interface Balance {
free?: unknown;
total?: unknown;
}
interface AssetEntry {
exchange?: string;
asset?: string;
tear_sheet?: {
balance_end?: Balance;
drawdown_max?: unknown;
};
}
interface TearSheetData {
@@ -72,6 +109,15 @@ interface TearSheetData {
win_rate?: unknown;
profit_factor?: unknown;
pnl_drawdown_max?: unknown;
pnl_return?: unknown;
}
interface ResultSummary {
time_engine_start?: string;
time_engine_end?: string;
instruments?: Record<string, TearSheetData>;
assets?: AssetEntry[];
backtest_metadata?: BacktestMetadata;
}
export default function PaperRunDetailPage() {
@@ -79,7 +125,7 @@ export default function PaperRunDetailPage() {
const { data: run, isLoading, isError, error } = usePaperRun(id!);
const cancelMutation = useCancelPaperRun();
const isComplete = run?.status === 'complete';
const { data: positions } = usePaperRunPositions(isComplete ? id : undefined, 500);
const { data: positionsData } = usePaperRunPositions(isComplete ? id : undefined, 500);
if (isLoading) {
return (
@@ -101,6 +147,7 @@ export default function PaperRunDetailPage() {
const badge = STATUS_BADGE[run.status];
const summary = run.result_summary as ResultSummary | null;
const meta = summary?.backtest_metadata;
return (
<Container>
@@ -120,7 +167,8 @@ export default function PaperRunDetailPage() {
)}
{run.status}
</Badge>
<code>{run.id}</code>
<Badge bg="secondary" className="me-2">{run.mode}</Badge>
<code style={{ fontSize: '0.85em' }}>{run.id}</code>
</div>
{run.status === 'queued' && (
<Button
@@ -143,12 +191,12 @@ export default function PaperRunDetailPage() {
<div>{formatTimestamp(run.finishes_at)}</div>
</Col>
<Col sm={3}>
<small className="text-muted">Worker</small>
<div>{run.worker_id ? <code>{run.worker_id}</code> : '--'}</div>
<small className="text-muted">Duration</small>
<div>{durationBetween(run.starts_at, run.finishes_at)}</div>
</Col>
<Col sm={3}>
<small className="text-muted">Created</small>
<div>{formatTimestamp(run.created_at)}</div>
<small className="text-muted">Worker</small>
<div>{run.worker_id ? <code style={{ fontSize: '0.8em' }}>{run.worker_id}</code> : '--'}</div>
</Col>
</Row>
<Row>
@@ -190,69 +238,184 @@ export default function PaperRunDetailPage() {
{/* Result summary for completed runs */}
{run.status === 'complete' && summary && (
<>
{/* Key metrics row */}
<Row className="mb-3">
{meta?.trade_count != null && (
<Col sm={3}>
<Card className="h-100 text-center">
<Card.Body className="py-3">
<div style={{ fontSize: '1.6em', fontWeight: 600 }}>
{meta.trade_count.toLocaleString()}
</div>
<small className="text-muted">Trades Replayed</small>
</Card.Body>
</Card>
</Col>
)}
{positionsData != null && (
<Col sm={3}>
<Card className="h-100 text-center">
<Card.Body className="py-3">
<div style={{ fontSize: '1.6em', fontWeight: 600 }}>
{positionsData.total.toLocaleString()}
</div>
<small className="text-muted">Closed Positions</small>
</Card.Body>
</Card>
</Col>
)}
{summary.instruments && Object.values(summary.instruments).map((sheet, i) => {
const pnl = extractNumber(sheet.pnl);
if (pnl === null || Math.abs(pnl) > 1e15) return null;
const isPositive = pnl >= 0;
return (
<Col sm={3} key={i}>
<Card className="h-100 text-center">
<Card.Body className="py-3">
<div style={{ fontSize: '1.6em', fontWeight: 600, color: isPositive ? '#198754' : '#dc3545' }}>
{formatPnl(sheet.pnl)}
</div>
<small className="text-muted">Total PnL</small>
</Card.Body>
</Card>
</Col>
);
})}
{summary.instruments && Object.values(summary.instruments).map((sheet, i) => {
const wr = extractNumber(sheet.win_rate);
if (wr === null) return null;
return (
<Col sm={3} key={i}>
<Card className="h-100 text-center">
<Card.Body className="py-3">
<div style={{ fontSize: '1.6em', fontWeight: 600 }}>
{formatPct(sheet.win_rate)}
</div>
<small className="text-muted">Win Rate</small>
</Card.Body>
</Card>
</Col>
);
})}
</Row>
{/* Data range + engine times */}
<Card className="mb-3">
<Card.Header>Summary</Card.Header>
<Card.Header>Run Details</Card.Header>
<Card.Body>
<Row>
<Col sm={6}>
{meta?.data_range_start && (
<Col sm={3}>
<small className="text-muted">Data Start</small>
<div>{formatTimestamp(meta.data_range_start)}</div>
</Col>
)}
{meta?.data_range_end && (
<Col sm={3}>
<small className="text-muted">Data End</small>
<div>{formatTimestamp(meta.data_range_end)}</div>
</Col>
)}
<Col sm={3}>
<small className="text-muted">Engine Start</small>
<div>{formatTimestamp(summary.time_engine_start ?? null)}</div>
<div>{formatTimestamp(summary.time_engine_start)}</div>
</Col>
<Col sm={6}>
<Col sm={3}>
<small className="text-muted">Engine End</small>
<div>{formatTimestamp(summary.time_engine_end ?? null)}</div>
<div>{formatTimestamp(summary.time_engine_end)}</div>
</Col>
</Row>
</Card.Body>
</Card>
{positions && positions.positions.length > 0 && (
{/* Equity curve */}
{positionsData && positionsData.positions.length > 0 && (
<Card className="mb-3">
<Card.Header>
Equity Curve
<small className="text-muted ms-2">
{positions.total.toLocaleString()} closed position{positions.total !== 1 ? 's' : ''}
{positions.sampled < positions.total && (
<span className="ms-1">(showing {positions.sampled.toLocaleString()} sampled)</span>
{positionsData.total.toLocaleString()} closed position{positionsData.total !== 1 ? 's' : ''}
{positionsData.sampled < positionsData.total && (
<span className="ms-1">(showing {positionsData.sampled.toLocaleString()} sampled)</span>
)}
</small>
</Card.Header>
<Card.Body className="pt-3 pb-2 px-2">
<EquityCurveChart positions={positions.positions} />
<EquityCurveChart positions={positionsData.positions} />
</Card.Body>
</Card>
)}
{/* Instrument statistics */}
{summary.instruments && Object.keys(summary.instruments).length > 0 && (
<Card className="mb-3">
<Card.Header>Instruments</Card.Header>
<Card.Header>Instrument Statistics</Card.Header>
<Card.Body className="p-0">
<Table striped bordered hover size="sm" className="mb-0">
<thead>
<tr>
<th>Instrument</th>
<th>PnL</th>
<th>Sharpe</th>
<th>Sortino</th>
<th>Calmar</th>
<th>Return</th>
<th>Win Rate</th>
<th>Profit Factor</th>
<th>Sharpe</th>
<th>Sortino</th>
<th>Max Drawdown</th>
</tr>
</thead>
<tbody>
{Object.entries(summary.instruments).map(([name, sheet]) => (
<tr key={name}>
<td><code>{name}</code></td>
<td>{displayMetric(sheet.pnl)}</td>
<td>{displayMetric(sheet.sharpe_ratio)}</td>
<td>{displayMetric(sheet.sortino_ratio)}</td>
<td>{displayMetric(sheet.calmar_ratio)}</td>
<td>{displayMetric(sheet.win_rate)}</td>
<td>{displayMetric(sheet.profit_factor)}</td>
<td>{displayMetric(sheet.pnl_drawdown_max)}</td>
</tr>
))}
{Object.entries(summary.instruments).map(([name, sheet]) => {
const pnl = extractNumber(sheet.pnl);
const pnlColor = pnl == null || Math.abs(pnl) > 1e15
? undefined
: pnl >= 0 ? '#198754' : '#dc3545';
return (
<tr key={name}>
<td><code>{name}</code></td>
<td style={{ color: pnlColor }}>{formatPnl(sheet.pnl)}</td>
<td>{formatPct(sheet.pnl_return)}</td>
<td>{formatPct(sheet.win_rate)}</td>
<td>{formatMetric(sheet.profit_factor, 3)}</td>
<td>{formatMetric(sheet.sharpe_ratio, 3)}</td>
<td>{formatMetric(sheet.sortino_ratio, 3)}</td>
<td style={{ color: '#dc3545' }}>{formatPnl(sheet.pnl_drawdown_max)}</td>
</tr>
);
})}
</tbody>
</Table>
</Card.Body>
</Card>
)}
{/* Asset balances */}
{summary.assets && summary.assets.length > 0 && (
<Card className="mb-3">
<Card.Header>Asset Balances</Card.Header>
<Card.Body className="p-0">
<Table striped bordered hover size="sm" className="mb-0">
<thead>
<tr>
<th>Exchange</th>
<th>Asset</th>
<th>Total</th>
<th>Free</th>
</tr>
</thead>
<tbody>
{summary.assets.map((entry, i) => {
const total = extractNumber(entry.tear_sheet?.balance_end?.total);
const free = extractNumber(entry.tear_sheet?.balance_end?.free);
return (
<tr key={i}>
<td>{entry.exchange ?? '--'}</td>
<td><code>{entry.asset ?? '--'}</code></td>
<td>{total != null ? total.toFixed(4) : '--'}</td>
<td>{free != null ? free.toFixed(4) : '--'}</td>
</tr>
);
})}
</tbody>
</Table>
</Card.Body>