feat: page VaR Analyse avec approche delta Black-Scholes
- Service var_service.py : calcul VaR Historique / Paramétrique / Monte Carlo stressé (vol ×1.5) + CVaR par méthode, deltas BS par position, fallback synthétique si yfinance indisponible - Router /api/var/compute : paramètres confidence, horizon, lookback, IV défaut - Page VaRAnalysis.tsx : cartes métriques %, montants EUR, histogramme retours, VaR glissante 30j, tableau positions + deltas, backtest Kupiec pass/fail - Route /var + nav sidebar « VaR Analyse » Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -17,6 +17,7 @@ import Analytics from './pages/Analytics'
|
||||
import AnalyticsAdvanced from './pages/AnalyticsAdvanced'
|
||||
import RiskDashboard from './pages/RiskDashboard'
|
||||
import SystemLogs from './pages/SystemLogs'
|
||||
import VaRAnalysis from './pages/VaRAnalysis'
|
||||
import { useCycleWatcher } from './hooks/useApi'
|
||||
|
||||
function GlobalWatcher() {
|
||||
@@ -48,6 +49,7 @@ export default function App() {
|
||||
<Route path="/analytics" element={<Analytics />} />
|
||||
<Route path="/analytics-advanced" element={<AnalyticsAdvanced />} />
|
||||
<Route path="/risk" element={<RiskDashboard />} />
|
||||
<Route path="/var" element={<VaRAnalysis />} />
|
||||
<Route path="/logs" element={<SystemLogs />} />
|
||||
</Routes>
|
||||
</main>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { NavLink } from 'react-router-dom'
|
||||
import {
|
||||
LayoutDashboard, Globe, BarChart2, FlaskConical,
|
||||
History, Calendar, TrendingUp, Zap, DollarSign, Settings, BrainCircuit, Activity, BookOpen, FileBarChart, Brain, ShieldAlert, Microscope, ScrollText
|
||||
History, Calendar, TrendingUp, Zap, DollarSign, Settings, BrainCircuit, Activity, BookOpen, FileBarChart, Brain, ShieldAlert, Microscope, ScrollText, Gauge
|
||||
} from 'lucide-react'
|
||||
import { useGeoRiskScore, useAiStatus, usePortfolioSummary } from '../../hooks/useApi'
|
||||
import clsx from 'clsx'
|
||||
@@ -20,6 +20,7 @@ const nav = [
|
||||
{ to: '/analytics', icon: FlaskConical, label: 'Analytics' },
|
||||
{ to: '/analytics-advanced', icon: Microscope, label: 'Analytics Avancées' },
|
||||
{ to: '/risk', icon: ShieldAlert, label: 'Risk Dashboard' },
|
||||
{ to: '/var', icon: Gauge, label: 'VaR Analyse' },
|
||||
{ to: '/backtest', icon: History, label: 'Backtest' },
|
||||
{ to: '/calendar', icon: Calendar, label: 'Calendrier' },
|
||||
{ to: '/logs', icon: ScrollText, label: 'Logs Système' },
|
||||
|
||||
475
frontend/src/pages/VaRAnalysis.tsx
Normal file
475
frontend/src/pages/VaRAnalysis.tsx
Normal file
@@ -0,0 +1,475 @@
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import {
|
||||
BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceLine,
|
||||
LineChart, Line, ResponsiveContainer, Cell,
|
||||
} from 'recharts'
|
||||
import { ShieldAlert, TrendingDown, Activity, AlertTriangle, Info, RefreshCw } from 'lucide-react'
|
||||
import clsx from 'clsx'
|
||||
|
||||
// ─── API ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
async function fetchVar(params: Record<string, string | number>) {
|
||||
const qs = new URLSearchParams(Object.entries(params).map(([k, v]) => [k, String(v)])).toString()
|
||||
const r = await fetch(`http://localhost:8000/api/var/compute?${qs}`)
|
||||
if (!r.ok) throw new Error(await r.text())
|
||||
return r.json()
|
||||
}
|
||||
|
||||
// ─── Sub-components ───────────────────────────────────────────────────────────
|
||||
|
||||
function MetricCard({
|
||||
label, method, var1d, varNd, cvar, horizon, stressed = false, color,
|
||||
}: {
|
||||
label: string
|
||||
method: string
|
||||
var1d: number
|
||||
varNd: number
|
||||
cvar: number
|
||||
horizon: number
|
||||
stressed?: boolean
|
||||
color: string
|
||||
}) {
|
||||
const pct = (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(3)}%`
|
||||
const isLoss = (v: number) => v < 0
|
||||
return (
|
||||
<div className={clsx('bg-dark-800 rounded-xl border p-4', `border-${color}-700/40`)}>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<span className={clsx('text-xs font-semibold uppercase tracking-wider', `text-${color}-400`)}>{label}</span>
|
||||
{stressed && (
|
||||
<span className="text-xs bg-orange-900/30 text-orange-400 border border-orange-700/40 px-2 py-0.5 rounded">
|
||||
Stressed ×1.5
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-500 mb-1">{method}</div>
|
||||
<div className="space-y-2">
|
||||
<div className="flex justify-between items-baseline">
|
||||
<span className="text-slate-400 text-xs">VaR 1J ({horizon === 1 ? '1j' : `${horizon}j`})</span>
|
||||
<span className={clsx('text-lg font-bold', isLoss(var1d) ? 'text-red-400' : 'text-emerald-400')}>
|
||||
{pct(horizon === 1 ? var1d : varNd)}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-baseline">
|
||||
<span className="text-slate-400 text-xs">CVaR (ES)</span>
|
||||
<span className={clsx('font-semibold text-sm', isLoss(cvar) ? 'text-orange-400' : 'text-slate-300')}>
|
||||
{pct(cvar)}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ControlBar({
|
||||
confidence, setConfidence,
|
||||
horizon, setHorizon,
|
||||
lookback, setLookback,
|
||||
iv, setIv,
|
||||
loading, onRefresh,
|
||||
}: {
|
||||
confidence: number
|
||||
setConfidence: (v: number) => void
|
||||
horizon: number
|
||||
setHorizon: (v: number) => void
|
||||
lookback: number
|
||||
setLookback: (v: number) => void
|
||||
iv: number
|
||||
setIv: (v: number) => void
|
||||
loading: boolean
|
||||
onRefresh: () => void
|
||||
}) {
|
||||
return (
|
||||
<div className="flex flex-wrap gap-3 items-end">
|
||||
{/* Confidence */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 block mb-1">Confiance</label>
|
||||
<div className="flex gap-1">
|
||||
{[0.90, 0.95, 0.99].map(c => (
|
||||
<button
|
||||
key={c}
|
||||
onClick={() => setConfidence(c)}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded text-xs font-semibold transition-colors',
|
||||
confidence === c
|
||||
? 'bg-blue-600 text-white'
|
||||
: 'bg-dark-700 text-slate-400 hover:text-white'
|
||||
)}
|
||||
>
|
||||
{(c * 100).toFixed(0)}%
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Horizon */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 block mb-1">Horizon</label>
|
||||
<div className="flex gap-1">
|
||||
{[1, 5, 10, 21].map(h => (
|
||||
<button
|
||||
key={h}
|
||||
onClick={() => setHorizon(h)}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded text-xs font-semibold transition-colors',
|
||||
horizon === h
|
||||
? 'bg-violet-600 text-white'
|
||||
: 'bg-dark-700 text-slate-400 hover:text-white'
|
||||
)}
|
||||
>
|
||||
{h}j
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Lookback */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 block mb-1">Historique</label>
|
||||
<div className="flex gap-1">
|
||||
{[63, 126, 252].map(l => (
|
||||
<button
|
||||
key={l}
|
||||
onClick={() => setLookback(l)}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded text-xs font-semibold transition-colors',
|
||||
lookback === l
|
||||
? 'bg-emerald-700 text-white'
|
||||
: 'bg-dark-700 text-slate-400 hover:text-white'
|
||||
)}
|
||||
>
|
||||
{l === 63 ? '3M' : l === 126 ? '6M' : '1A'}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* IV */}
|
||||
<div>
|
||||
<label className="text-xs text-slate-500 block mb-1">IV par défaut</label>
|
||||
<div className="flex gap-1">
|
||||
{[0.15, 0.20, 0.30, 0.40].map(v => (
|
||||
<button
|
||||
key={v}
|
||||
onClick={() => setIv(v)}
|
||||
className={clsx(
|
||||
'px-3 py-1.5 rounded text-xs font-semibold transition-colors',
|
||||
iv === v
|
||||
? 'bg-amber-600 text-white'
|
||||
: 'bg-dark-700 text-slate-400 hover:text-white'
|
||||
)}
|
||||
>
|
||||
{(v * 100).toFixed(0)}%
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={onRefresh}
|
||||
disabled={loading}
|
||||
className={clsx(
|
||||
'flex items-center gap-1.5 px-3 py-1.5 rounded text-xs font-semibold transition-colors',
|
||||
'bg-slate-700 text-slate-300 hover:bg-slate-600'
|
||||
)}
|
||||
>
|
||||
<RefreshCw className={clsx('w-3.5 h-3.5', loading && 'animate-spin')} />
|
||||
Recalcul
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Custom tooltip for histogram ─────────────────────────────────────────────
|
||||
|
||||
function HistoTooltip({ active, payload }: any) {
|
||||
if (!active || !payload?.length) return null
|
||||
const { x, count } = payload[0].payload
|
||||
return (
|
||||
<div className="bg-dark-800 border border-slate-700 rounded px-3 py-2 text-xs">
|
||||
<div className="text-slate-300">Ret: <span className="font-bold">{x.toFixed(3)}%</span></div>
|
||||
<div className="text-slate-400">Obs: {count}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function RollingTooltip({ active, payload, label }: any) {
|
||||
if (!active || !payload?.length) return null
|
||||
return (
|
||||
<div className="bg-dark-800 border border-slate-700 rounded px-3 py-2 text-xs">
|
||||
<div className="text-slate-500 mb-1">{label}</div>
|
||||
<div className="text-red-400 font-semibold">VaR: {payload[0]?.value?.toFixed(3)}%</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// ─── Main page ────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function VaRAnalysis() {
|
||||
const [confidence, setConfidence] = useState(0.95)
|
||||
const [horizon, setHorizon] = useState(1)
|
||||
const [lookback, setLookback] = useState(252)
|
||||
const [iv, setIv] = useState(0.20)
|
||||
const [refreshKey, setRefreshKey] = useState(0)
|
||||
|
||||
const params = useMemo(() => ({
|
||||
confidence,
|
||||
horizon_days: horizon,
|
||||
lookback_days: lookback,
|
||||
default_iv: iv,
|
||||
}), [confidence, horizon, lookback, iv, refreshKey]) // eslint-disable-line
|
||||
|
||||
const { data, isLoading, error } = useQuery({
|
||||
queryKey: ['var-compute', params],
|
||||
queryFn: () => fetchVar(params),
|
||||
staleTime: 60_000,
|
||||
})
|
||||
|
||||
const varData = data?.var
|
||||
const portfolio = data?.portfolio
|
||||
const histogram: { x: number; count: number }[] = data?.histogram ?? []
|
||||
const rolling: { date: string; var_95: number }[] = data?.rolling_var ?? []
|
||||
const positions: any[] = data?.positions ?? []
|
||||
const backtest = data?.backtest
|
||||
|
||||
// Color histogram bars: red if x < VaR threshold
|
||||
const histVarThreshold = varData?.historical?.var_1d_pct ?? 0
|
||||
|
||||
return (
|
||||
<div className="p-6 space-y-6 max-w-screen-xl mx-auto">
|
||||
{/* Header */}
|
||||
<div className="flex items-start justify-between flex-wrap gap-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3 mb-1">
|
||||
<ShieldAlert className="w-6 h-6 text-red-400" />
|
||||
<h1 className="text-xl font-bold text-white">Analyse VaR — Value at Risk</h1>
|
||||
</div>
|
||||
<p className="text-slate-400 text-sm">
|
||||
Approche delta Black-Scholes · {portfolio ? `${portfolio.n_positions} positions · Notionnel ${portfolio.total_notional_eur.toLocaleString('fr-FR')} EUR` : '—'}
|
||||
{portfolio?.data_source === 'simulated' && (
|
||||
<span className="ml-2 text-amber-400 text-xs">⚠ Données simulées (marché indisponible)</span>
|
||||
)}
|
||||
</p>
|
||||
</div>
|
||||
<ControlBar
|
||||
confidence={confidence} setConfidence={setConfidence}
|
||||
horizon={horizon} setHorizon={setHorizon}
|
||||
lookback={lookback} setLookback={setLookback}
|
||||
iv={iv} setIv={setIv}
|
||||
loading={isLoading}
|
||||
onRefresh={() => setRefreshKey(k => k + 1)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Error state */}
|
||||
{error && (
|
||||
<div className="bg-red-900/20 border border-red-700/40 rounded-xl p-4 text-red-400 flex items-center gap-2">
|
||||
<AlertTriangle className="w-4 h-4 shrink-0" />
|
||||
<span>{String(error)}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data?.error && (
|
||||
<div className="bg-amber-900/20 border border-amber-700/40 rounded-xl p-4 text-amber-400 flex items-center gap-2">
|
||||
<Info className="w-4 h-4 shrink-0" />
|
||||
<span>{data.error}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* VaR metric cards */}
|
||||
{varData && (
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<MetricCard
|
||||
label="Historique"
|
||||
method="Percentile empirique"
|
||||
var1d={varData.historical.var_1d_pct}
|
||||
varNd={varData.historical.var_nd_pct}
|
||||
cvar={varData.historical.cvar_pct}
|
||||
horizon={horizon}
|
||||
color="blue"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Paramétrique"
|
||||
method="Distribution normale"
|
||||
var1d={varData.parametric.var_1d_pct}
|
||||
varNd={varData.parametric.var_nd_pct}
|
||||
cvar={varData.parametric.cvar_pct}
|
||||
horizon={horizon}
|
||||
color="violet"
|
||||
/>
|
||||
<MetricCard
|
||||
label="Monte Carlo"
|
||||
method={`${(confidence * 100).toFixed(0)}% · vol stressée ×1.5`}
|
||||
var1d={varData.monte_carlo.var_1d_pct}
|
||||
varNd={varData.monte_carlo.var_nd_pct}
|
||||
cvar={varData.monte_carlo.cvar_pct}
|
||||
horizon={horizon}
|
||||
stressed
|
||||
color="orange"
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* EUR amounts */}
|
||||
{varData && portfolio && (
|
||||
<div className="grid grid-cols-3 md:grid-cols-6 gap-3">
|
||||
{[
|
||||
{ label: 'VaR Hist. 1J', val: varData.historical.var_1d_eur, color: 'blue' },
|
||||
{ label: 'CVaR Hist.', val: varData.historical.cvar_eur, color: 'blue' },
|
||||
{ label: 'VaR Param. 1J', val: varData.parametric.var_1d_eur, color: 'violet' },
|
||||
{ label: 'CVaR Param.', val: varData.parametric.cvar_eur, color: 'violet' },
|
||||
{ label: 'VaR MC Stressed', val: varData.monte_carlo.var_1d_eur, color: 'orange' },
|
||||
{ label: 'CVaR MC', val: varData.monte_carlo.cvar_eur, color: 'orange' },
|
||||
].map(({ label, val, color }) => (
|
||||
<div key={label} className={clsx('bg-dark-800 rounded-lg border p-3', `border-${color}-700/30`)}>
|
||||
<div className="text-xs text-slate-500 mb-1">{label}</div>
|
||||
<div className={clsx('font-bold text-sm', val < 0 ? 'text-red-400' : 'text-emerald-400')}>
|
||||
{val < 0 ? '' : '+'}{val.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Charts row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Returns distribution histogram */}
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<Activity className="w-4 h-4 text-slate-400" />
|
||||
<h3 className="text-sm font-semibold text-white">Distribution des Retours</h3>
|
||||
<span className="text-xs text-slate-500 ml-auto">— trait rouge = VaR hist.</span>
|
||||
</div>
|
||||
{histogram.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<BarChart data={histogram} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||||
<XAxis dataKey="x" tick={{ fontSize: 10, fill: '#94a3b8' }} />
|
||||
<YAxis tick={{ fontSize: 10, fill: '#94a3b8' }} />
|
||||
<Tooltip content={<HistoTooltip />} />
|
||||
<ReferenceLine x={histVarThreshold} stroke="#f87171" strokeDasharray="4 2" strokeWidth={2} />
|
||||
<Bar dataKey="count" radius={[2, 2, 0, 0]}>
|
||||
{histogram.map((entry, i) => (
|
||||
<Cell key={i} fill={entry.x < histVarThreshold ? '#ef4444' : '#3b82f6'} fillOpacity={0.75} />
|
||||
))}
|
||||
</Bar>
|
||||
</BarChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[220px] flex items-center justify-center text-slate-600 text-sm">
|
||||
{isLoading ? 'Calcul en cours…' : 'Aucune donnée'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Rolling VaR */}
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<TrendingDown className="w-4 h-4 text-red-400" />
|
||||
<h3 className="text-sm font-semibold text-white">VaR 95% Glissante (fenêtre 30J)</h3>
|
||||
</div>
|
||||
{rolling.length > 0 ? (
|
||||
<ResponsiveContainer width="100%" height={220}>
|
||||
<LineChart data={rolling} margin={{ top: 4, right: 4, left: -20, bottom: 0 }}>
|
||||
<CartesianGrid strokeDasharray="3 3" stroke="#334155" />
|
||||
<XAxis
|
||||
dataKey="date"
|
||||
tick={{ fontSize: 9, fill: '#94a3b8' }}
|
||||
tickFormatter={d => d.slice(5)} // MM-DD
|
||||
interval={Math.floor(rolling.length / 6)}
|
||||
/>
|
||||
<YAxis tick={{ fontSize: 10, fill: '#94a3b8' }} unit="%" />
|
||||
<Tooltip content={<RollingTooltip />} />
|
||||
<ReferenceLine y={0} stroke="#475569" />
|
||||
<Line
|
||||
type="monotone"
|
||||
dataKey="var_95"
|
||||
stroke="#f87171"
|
||||
dot={false}
|
||||
strokeWidth={2}
|
||||
/>
|
||||
</LineChart>
|
||||
</ResponsiveContainer>
|
||||
) : (
|
||||
<div className="h-[220px] flex items-center justify-center text-slate-600 text-sm">
|
||||
{isLoading ? 'Calcul en cours…' : 'Historique insuffisant'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bottom row: positions + backtest */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* Positions deltas */}
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
|
||||
<h3 className="text-sm font-semibold text-white mb-3">Positions & Deltas</h3>
|
||||
{positions.length === 0 ? (
|
||||
<p className="text-slate-600 text-sm">{isLoading ? 'Chargement…' : 'Aucune position'}</p>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{positions.map((p, i) => (
|
||||
<div key={i} className="flex items-center justify-between text-xs py-1.5 border-b border-slate-700/30 last:border-0">
|
||||
<div className="min-w-0 flex-1">
|
||||
<span className="text-white font-semibold">{p.ticker}</span>
|
||||
<span className="text-slate-400 ml-2">{p.strategy}</span>
|
||||
{p.pattern && <div className="text-slate-600 truncate">{p.pattern}</div>}
|
||||
</div>
|
||||
<div className="flex items-center gap-4 shrink-0 ml-4">
|
||||
<span className={clsx(
|
||||
'font-mono font-semibold',
|
||||
Math.abs(p.delta) < 0.05 ? 'text-slate-500'
|
||||
: p.delta > 0 ? 'text-emerald-400' : 'text-red-400'
|
||||
)}>
|
||||
Δ {p.delta > 0 ? '+' : ''}{p.delta.toFixed(3)}
|
||||
</span>
|
||||
<span className="text-slate-500 font-mono">{p.notional.toLocaleString('fr-FR', { maximumFractionDigits: 0 })} €</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Backtest Kupiec */}
|
||||
{backtest && (
|
||||
<div className="bg-dark-800 border border-slate-700/40 rounded-xl p-4">
|
||||
<h3 className="text-sm font-semibold text-white mb-3">Backtest — Test de Kupiec</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-400 text-sm">Observations</span>
|
||||
<span className="text-white font-semibold">{backtest.n_observations}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-400 text-sm">Violations VaR</span>
|
||||
<span className="text-white font-semibold">{backtest.n_breaches}</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-400 text-sm">Taux réel</span>
|
||||
<span className={clsx('font-semibold', backtest.kupiec_ok ? 'text-emerald-400' : 'text-red-400')}>
|
||||
{backtest.breach_rate_pct}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-slate-400 text-sm">Taux attendu</span>
|
||||
<span className="text-slate-300">{backtest.expected_breach_rate_pct}%</span>
|
||||
</div>
|
||||
<div className={clsx(
|
||||
'flex items-center gap-2 p-3 rounded-lg text-sm',
|
||||
backtest.kupiec_ok
|
||||
? 'bg-emerald-900/20 border border-emerald-700/40 text-emerald-400'
|
||||
: 'bg-red-900/20 border border-red-700/40 text-red-400'
|
||||
)}>
|
||||
{backtest.kupiec_ok
|
||||
? '✓ Modèle validé — violations dans la tolérance'
|
||||
: '✗ Excès de violations — modèle sous-estime le risque'}
|
||||
</div>
|
||||
<div className="text-xs text-slate-600 mt-1">
|
||||
Règle : taux réel ≤ 2× taux attendu ({backtest.expected_breach_rate_pct * 2}%)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user