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 && (
)}
{/* 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 }) => (
))}
{/* 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)}%
)}
>
)}
)
}