import { useState } from 'react' import { useQuery } from '@tanstack/react-query' import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, ReferenceLine, } from 'recharts' import { History, TrendingUp, TrendingDown, Plus, Minus, RefreshCw, ArrowRight, AlertTriangle } from 'lucide-react' import clsx from 'clsx' // ─── API ───────────────────────────────────────────────────────────────────── async function fetchSnapshots() { const r = await fetch('/api/var/pnl/snapshots?limit=200') if (!r.ok) throw new Error(`${r.status}`) return r.json() } async function fetchSnapshotDetail(id: number) { const r = await fetch(`/api/var/pnl/snapshots/${id}`) if (!r.ok) throw new Error(`${r.status}`) return r.json() } async function fetchDiff(a: number, b: number) { const r = await fetch(`/api/var/pnl/diff?a=${a}&b=${b}`) if (!r.ok) throw new Error(`${r.status}`) return r.json() } // ─── Helpers ───────────────────────────────────────────────────────────────── const fmt = (v: number | null | undefined, digits = 2) => v == null ? '—' : `${v >= 0 ? '+' : ''}${v.toFixed(digits)}%` const fmtEur = (v: number | null | undefined) => v == null ? '—' : `${v < 0 ? '' : '+'}${v.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €` const fmtDate = (s: string) => s?.replace('T', ' ').slice(0, 16) function DeltaBadge({ v, unit = '%' }: { v: number; unit?: string }) { if (Math.abs(v) < 0.001) return ≈ 0 return ( 0 ? 'text-emerald-400' : 'text-red-400')}> {v > 0 ? '▲' : '▼'} {Math.abs(v).toFixed(unit === '%' ? 3 : 0)}{unit} ) } // ─── Snapshot list item ─────────────────────────────────────────────────────── function SnapRow({ snap, selectedA, selectedB, onSelectA, onSelectB, }: { snap: any; selectedA: number | null; selectedB: number | null onSelectA: (id: number) => void; onSelectB: (id: number) => void }) { const isA = selectedA === snap.id const isB = selectedB === snap.id const pnl = snap.total_pnl_pct ?? 0 return (
{/* Date */} {fmtDate(snap.snapped_at)} {/* Positions */} {snap.n_open} pos. {/* PnL */} = 0 ? 'text-emerald-400' : 'text-red-400')}> {fmt(pnl)} = 0 ? 'text-emerald-400/60' : 'text-red-400/60')}> {fmtEur(snap.total_pnl_eur)} {/* Actions */}
) } // ─── Diff view ─────────────────────────────────────────────────────────────── function DiffView({ diffData }: { diffData: any }) { const { snapshot_a: sa, snapshot_b: sb, portfolio_delta: pd, new_positions, closed_positions, changed_positions } = diffData return (
{/* Portfolio delta header */}
A {fmtDate(sa.snapped_at)} {sa.macro_regime && · {sa.macro_regime}}
B {fmtDate(sb.snapped_at)} {sb.macro_regime && · {sb.macro_regime}}
Δ PnL %
{fmt(sa.total_pnl_pct)} → {fmt(sb.total_pnl_pct)}
Δ PnL €
{fmtEur(sa.total_pnl_eur)} → {fmtEur(sb.total_pnl_eur)}
Δ Capital
{pd.positions_opened > 0 && (
+{pd.positions_opened}
opened
)} {pd.positions_closed > 0 && (
-{pd.positions_closed}
closed
)}
{/* New positions */} {new_positions.length > 0 && (

New positions ({new_positions.length})

{new_positions.map((p: any) => (
{p.ticker} {p.strategy}
Entry: {p.entry_price?.toFixed(2) ?? '—'} = 0 ? 'text-emerald-400' : 'text-red-400')}> {fmt(p.pnl_pct)}
))}
)} {/* Closed positions */} {closed_positions.length > 0 && (

Closed positions ({closed_positions.length})

{closed_positions.map((p: any) => (
{p.ticker} {p.strategy}
Last val.: {fmt(p.pnl_pct)} = 0 ? 'text-emerald-400' : 'text-red-400')}> {fmtEur(p.pnl_eur)}
))}
)} {/* Changed positions */} {changed_positions.length > 0 && (

Position changes ({changed_positions.length})

{changed_positions.map((p: any) => ( ))}
Position PnL A PnL B Δ PnL % Δ PnL €
{p.ticker} {p.strategy} = 0 ? 'text-emerald-400/70' : 'text-red-400/70')}> {fmt(p.pnl_pct_a)} = 0 ? 'text-emerald-400' : 'text-red-400')}> {fmt(p.pnl_pct_b)}
)}
) } // ─── Snapshot detail ────────────────────────────────────────────────────────── function SnapshotDetail({ snapId }: { snapId: number }) { const { data, isLoading } = useQuery({ queryKey: ['pnl-snap-detail', snapId], queryFn: () => fetchSnapshotDetail(snapId), staleTime: 300_000, }) if (isLoading) return
Loading…
if (!data) return null const trades: any[] = data.trades_snapshot ?? [] const macro = data.macro_regime return (

{fmtDate(data.snapped_at)} UTC

{macro?.dominant && ( Regime: {macro.dominant} )}
Capital {(data.total_capital_eur ?? 0).toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €
PnL = 0 ? 'text-emerald-400' : 'text-red-400')}> {fmt(data.total_pnl_pct)} · {fmtEur(data.total_pnl_eur)}
{data.n_open} open · {data.n_closed} closed
{trades.length > 0 ? ( {trades.map((t: any, i: number) => ( ))}
Ticker Strategy Entry Current price PnL % PnL €
{t.ticker} {t.strategy} {t.entry_price?.toFixed(2) ?? '—'} {t.current_price?.toFixed(2) ?? '—'} = 0 ? 'text-emerald-400' : 'text-red-400')}> {fmt(t.pnl_pct)} = 0 ? 'text-emerald-400/70' : 'text-red-400/70')}> {fmtEur(t.pnl_eur)}
) : (
No positions in this snapshot
)}
) } // ─── PnL sparkline tooltip ──────────────────────────────────────────────────── function SparkTooltip({ active, payload, label }: any) { if (!active || !payload?.length) return null return (
{label}
= 0 ? 'text-emerald-400' : 'text-red-400')}> PnL: {fmt(payload[0].value)}
) } // ─── Main page ──────────────────────────────────────────────────────────────── export default function PositionHistory() { const [selectedA, setSelectedA] = useState(null) const [selectedB, setSelectedB] = useState(null) const [viewMode, setViewMode] = useState<'detail' | 'diff'>('detail') const [activeDetail, setActiveDetail] = useState(null) const { data, isLoading, error, refetch } = useQuery({ queryKey: ['pnl-snapshots-all'], queryFn: fetchSnapshots, staleTime: 60_000, }) const { data: diffData, isLoading: diffLoading, error: diffError } = useQuery({ queryKey: ['pnl-diff', selectedA, selectedB], queryFn: () => fetchDiff(selectedA!, selectedB!), enabled: selectedA != null && selectedB != null && selectedA !== selectedB, staleTime: 60_000, }) const snapshots: any[] = data?.snapshots ?? [] // Build sparkline data (chronological) const sparkData = [...snapshots].reverse().map(s => ({ date: s.snapped_at?.slice(5, 16).replace('T', ' '), pnl: s.total_pnl_pct ?? 0, })) const handleSelectA = (id: number) => { setSelectedA(id) setViewMode('diff') } const handleSelectB = (id: number) => { setSelectedB(id) setViewMode('diff') } const canDiff = selectedA != null && selectedB != null && selectedA !== selectedB return (
{/* Header */}

Position History

{snapshots.length} PnL snapshot{snapshots.length !== 1 ? 's' : ''} recorded {snapshots.length > 0 && ` · from ${fmtDate(snapshots[snapshots.length - 1]?.snapped_at)} to ${fmtDate(snapshots[0]?.snapped_at)}`}

{canDiff && (
)} {(selectedA || selectedB) && ( )}
{/* Error */} {error && (
{String(error)}
)} {/* Selection banner */} {(selectedA || selectedB) && (
Diff selection: {selectedA ? ( A {fmtDate(snapshots.find(s => s.id === selectedA)?.snapped_at ?? '')} ) : A: not selected} {selectedB ? ( B {fmtDate(snapshots.find(s => s.id === selectedB)?.snapped_at ?? '')} ) : B: not selected} {canDiff && ( )}
)} {/* Empty state */} {!isLoading && snapshots.length === 0 && (
No PnL snapshots available

Enable the PnL Scheduler in Configuration or trigger a snapshot manually from the Configuration → Auto-Cycle & Logging page.

)} {snapshots.length > 0 && (
{/* Left: timeline list */}
{/* Mini sparkline */} {sparkData.length > 2 && (
Portfolio PnL over time
} />
)} {/* Snapshot list */}
{snapshots.length} snapshots — click for detail · A/B to diff
{snapshots.map(s => (
{ setActiveDetail(s.id); setViewMode('detail') }}> { handleSelectA(id); setActiveDetail(null) }} onSelectB={id => { handleSelectB(id); setActiveDetail(null) }} />
))}
{/* Right: detail or diff */}
{viewMode === 'diff' && canDiff && ( diffLoading ?
Computing diff…
: diffError ?
{String(diffError)}
: diffData ? : null )} {viewMode === 'detail' && activeDetail && ( )} {viewMode === 'detail' && !activeDetail && !canDiff && (
Click a snapshot to see the detail
or select A and B to compare two valuations
)} {viewMode === 'diff' && !canDiff && (
Select two snapshots (A and B) from the list to view the diff
)}
)}
) }