import { useState } from 'react' import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query' import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceLine, LineChart, Line, ResponsiveContainer, Cell, } from 'recharts' import { ShieldAlert, TrendingDown, Activity, AlertTriangle, Info, Play, Clock, Database } from 'lucide-react' import clsx from 'clsx' // ─── API calls ─────────────────────────────────────────────────────────────── async function fetchLatest() { const r = await fetch('/api/var/latest') if (!r.ok) throw new Error(`Backend unreachable (${r.status})`) return r.json() } async function runNow(params: { confidence: number; horizon_days: number; lookback_days: number; default_iv: number }) { const qs = new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString() const r = await fetch(`/api/var/run-now?${qs}`, { method: 'POST' }) if (!r.ok) { const txt = await r.text() throw new Error(txt) } return r.json() } async function fetchSnapshots() { const r = await fetch('/api/var/snapshots?limit=10') if (!r.ok) return { snapshots: [] } return r.json() } // ─── Sub-components ─────────────────────────────────────────────────────────── function MetricCard({ label, method, var1d, varNd, cvar, varEur, cvarEur, horizon, stressed = false, colorClass, }: { label: string; method: string; var1d: number; varNd: number; cvar: number varEur: number; cvarEur: number; horizon: number; stressed?: boolean; colorClass: string }) { const pct = (v: number) => `${v > 0 ? '+' : ''}${v.toFixed(3)}%` const eur = (v: number) => `${v < 0 ? '' : '+'}${v.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €` const displayed = horizon === 1 ? var1d : varNd const displayedEur = horizon === 1 ? varEur : varEur * Math.sqrt(horizon) return (
{label} {stressed && Stressed ×1.5}
{method}
VaR {horizon}d
{pct(displayed)}
{eur(displayedEur)}
CVaR (Expected Shortfall)
{pct(cvar)}
{eur(cvarEur)}
) } function HistoTooltip({ active, payload }: any) { if (!active || !payload?.length) return null return (
Ret: {payload[0].payload.x.toFixed(3)}%
Obs: {payload[0].payload.count}
) } function RollingTooltip({ active, payload, label }: any) { if (!active || !payload?.length) return null return (
{label}
VaR: {payload[0]?.value?.toFixed(3)}%
) } // ─── Empty state ────────────────────────────────────────────────────────────── function IdleState({ onCompute, computing }: { onCompute: () => void; computing: boolean }) { return (

No VaR snapshot available

The VaR calculation does not run automatically to avoid unnecessary network calls. Click "Compute now" or configure the scheduler in Settings.

) } // ─── Main page ──────────────────────────────────────────────────────────────── export default function VaRAnalysis() { const qc = useQueryClient() const [confidence, setConfidence] = useState(0.95) const [horizon, setHorizon] = useState(1) const [lookback, setLookback] = useState(252) const [iv, setIv] = useState(0.20) const [activeSnap, setActiveSnap] = useState(null) // override from history // Load last snapshot from DB on mount (no heavy compute) const { data: latestData, isLoading: loadingLatest, error: latestError } = useQuery({ queryKey: ['var-latest'], queryFn: fetchLatest, staleTime: 30_000, retry: 1, }) const { data: snapshotsData } = useQuery({ queryKey: ['var-snapshots'], queryFn: fetchSnapshots, staleTime: 60_000, retry: 1, }) // Button-triggered compute + save const { mutate: compute, isPending: computing, error: computeError } = useMutation({ mutationFn: () => runNow({ confidence, horizon_days: horizon, lookback_days: lookback, default_iv: iv }), onSuccess: () => { qc.invalidateQueries({ queryKey: ['var-latest'] }) qc.invalidateQueries({ queryKey: ['var-snapshots'] }) setActiveSnap(null) }, }) // The displayed result: manual history pick OR latest from DB const displayedResult = activeSnap ?? latestData?.snapshot?.full_result const displayedMeta = activeSnap ? null : latestData?.snapshot const varData = displayedResult?.var const portfolio = displayedResult?.portfolio const histogram: any[] = displayedResult?.histogram ?? [] const rolling: any[] = displayedResult?.rolling_var ?? [] const positions: any[] = displayedResult?.positions ?? [] const backtest = displayedResult?.backtest const histVarThreshold = varData?.historical?.var_1d_pct ?? 0 const snapshots: any[] = snapshotsData?.snapshots ?? [] const backendDown = latestError && String(latestError).includes('Backend') return (
{/* Header bar */}

VaR Analysis — Value at Risk

Delta Black-Scholes approach {displayedMeta && ( · snapshot from {displayedMeta.computed_at?.slice(0, 16).replace('T', ' ')} UTC {displayedMeta.data_source === 'simulated' && ( ⚠ simulated data )} )}

{/* Controls */}
{/* Confidence */}
Confidence
{[0.90, 0.95, 0.99].map(c => ( ))}
{/* Horizon */}
Horizon
{[1, 5, 10, 21].map(h => ( ))}
{/* Lookback */}
Lookback
{[63, 126, 252].map(l => ( ))}
{/* IV */}
Default IV
{[0.15, 0.20, 0.30, 0.40].map(v => ( ))}
{/* Compute button */}
{/* Backend down notice */} {backendDown && (
Backend unreachable — check that the FastAPI server is running on port 8000.
)} {/* Compute error */} {computeError && (
{String(computeError)}
)} {/* History picker */} {snapshots.length > 1 && (
History: {snapshots.slice(1).map((s: any) => ( ))}
)} {/* Empty / loading state */} {loadingLatest && !displayedResult && (
Loading latest snapshot…
)} {!loadingLatest && !displayedResult && !backendDown && ( compute()} computing={computing} /> )} {/* ── Results ── */} {varData && portfolio && ( <> {/* Portfolio summary strip */}
{[ { label: 'Positions', val: String(portfolio.n_positions) }, { label: 'Notionnel', val: `${portfolio.total_notional_eur.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €` }, { label: 'Confidence', val: `${portfolio.confidence_pct}%` }, { label: 'Lookback', val: `${portfolio.lookback_days}d` }, { label: 'Source', val: portfolio.data_source === 'simulated' ? '⚠ Simulated' : '✓ Live' }, ].map(({ label, val }) => (
{label}
{val}
))}
{/* 3 VaR method cards */}
{/* Charts */}
{/* Histogram */}

Return Distribution

red line = hist. VaR
{histogram.length > 0 ? ( } /> {histogram.map((e: any, i: number) => ( ))} ) : (
No data
)}
{/* Rolling VaR */}

Rolling VaR 95% (30d window)

{rolling.length > 0 ? ( d.slice(5)} interval={Math.floor(rolling.length / 5)} /> } /> ) : (
Insufficient history
)}
{/* Positions + Kupiec */}
{/* Positions */}

Positions & Deltas

{positions.map((p: any, i: number) => (
{p.ticker} {p.strategy} {p.pattern &&
{p.pattern}
}
0 ? 'text-emerald-400' : 'text-red-400')}> Δ {p.delta > 0 ? '+' : ''}{p.delta.toFixed(3)} {p.notional.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €
))}
{/* Kupiec backtest */} {backtest && (

Backtest — Kupiec Test

{[ { label: 'Observations', val: String(backtest.n_observations) }, { label: 'VaR Violations', val: String(backtest.n_breaches) }, { label: 'Actual rate', val: `${backtest.breach_rate_pct}%`, color: backtest.kupiec_ok ? 'text-emerald-400' : 'text-red-400' }, { label: 'Expected rate', val: `${backtest.expected_breach_rate_pct}%`, color: 'text-slate-300' }, ].map(({ label, val, color }) => (
{label} {val}
))}
{backtest.kupiec_ok ? '✓ Model validated — violations within tolerance' : '✗ Excess violations — model underestimates risk'}
Kupiec rule: actual rate ≤ {(backtest.expected_breach_rate_pct * 2).toFixed(1)}%
)}
)}
) }