From a1ab827e1c6c80d1f44b5322a44473e5498c92d6 Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Sat, 28 Feb 2026 13:30:43 +0200 Subject: [PATCH] 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 --- dashboard/src/pages/PaperRunDetailPage.tsx | 259 +++++++++++++++++---- 1 file changed, 211 insertions(+), 48 deletions(-) diff --git a/dashboard/src/pages/PaperRunDetailPage.tsx b/dashboard/src/pages/PaperRunDetailPage.tsx index 2015140..d8f82f0 100644 --- a/dashboard/src/pages/PaperRunDetailPage.tsx +++ b/dashboard/src/pages/PaperRunDetailPage.tsx @@ -23,7 +23,7 @@ const STATUS_BADGE: Record = 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; 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; - assets?: Record; - 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; + 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 ( @@ -120,7 +167,8 @@ export default function PaperRunDetailPage() { )} {run.status} - {run.id} + {run.mode} + {run.id} {run.status === 'queued' && (