From bcd1a3a0d50bc13a0fd6710d04d162b78d9a5790 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Sat, 20 Jun 2026 06:41:17 +0200 Subject: [PATCH] feat: courbe PnL historique + VaR dans les cards Dashboard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Card PnL : area chart historique depuis pnl_snapshots (gradient couleur selon PnL positif/négatif, axes date + %, tooltip) - Card Risque : bloc VaR 95% (Historique / CVaR / Monte Carlo ×1.5) depuis le dernier var_snapshot, avec horodatage du calcul Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/pages/Dashboard.tsx | 88 +++++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/frontend/src/pages/Dashboard.tsx b/frontend/src/pages/Dashboard.tsx index eaa4a66..c090f57 100644 --- a/frontend/src/pages/Dashboard.tsx +++ b/frontend/src/pages/Dashboard.tsx @@ -1,4 +1,5 @@ import { useMemo, useState } from 'react' +import { useQuery } from '@tanstack/react-query' import { useGeoRiskScore, useAllQuotes, useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime, @@ -11,7 +12,10 @@ import clsx from 'clsx' import type { Quote } from '../types' import { format } from 'date-fns' import { fr } from 'date-fns/locale' -import { RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer } from 'recharts' +import { + RadarChart, PolarGrid, PolarAngleAxis, Radar, ResponsiveContainer, + AreaChart, Area, XAxis, YAxis, Tooltip, CartesianGrid, +} from 'recharts' import { scoreColor } from '../components/TradeIdeas' const riskGauge = (score: number) => { @@ -88,6 +92,20 @@ export default function Dashboard() { const { data: cycleStatusData } = useCycleStatus() const { data: geoNews } = useGeoNews() + // Historical PnL curve + latest VaR snapshot + const { data: pnlHistoryData } = useQuery({ + queryKey: ['dash-pnl-history'], + queryFn: () => fetch('/api/var/pnl/snapshots?limit=200').then(r => r.ok ? r.json() : { snapshots: [] }), + staleTime: 120_000, + retry: 1, + }) + const { data: latestVarData } = useQuery({ + queryKey: ['dash-var-latest'], + queryFn: () => fetch('/api/var/latest').then(r => r.ok ? r.json() : { snapshot: null }), + staleTime: 120_000, + retry: 1, + }) + const [pnlView, setPnlView] = useState<'simulated' | 'portfolio'>(() => (localStorage.getItem('dash_pnl_view') as any) ?? 'simulated' ) @@ -400,6 +418,46 @@ export default function Dashboard() { )} + {/* PnL historical sparkline */} + {(() => { + const snaps: any[] = [...(pnlHistoryData?.snapshots ?? [])].reverse() + if (snaps.length < 2) return null + const chartData = snaps.map(s => ({ + date: s.snapped_at?.slice(5, 10), + pnl: s.total_pnl_pct ?? 0, + })) + const minVal = Math.min(...chartData.map(d => d.pnl)) + const maxVal = Math.max(...chartData.map(d => d.pnl)) + const isPositive = chartData[chartData.length - 1]?.pnl >= 0 + const strokeColor = isPositive ? '#34d399' : '#f87171' + const gradId = 'pnlGrad' + return ( +
+
Courbe PnL historique ({snaps.length} pts)
+ + + + + + + + + + + + [`${v >= 0 ? '+' : ''}${Number(v).toFixed(3)}%`, 'PnL']} + /> + + + +
+ ) + })()} ) })()} @@ -457,6 +515,34 @@ export default function Dashboard() { {openCount > 0 ? `${openCount} positions` : 'Aucune position'} )} + {/* Latest VaR snapshot */} + {(() => { + const snap = latestVarData?.snapshot + if (!snap) return null + const hv = snap.hist_var_1d_pct + const cv = snap.hist_cvar_pct + const mv = snap.mc_var_1d_pct + const ts = snap.computed_at?.slice(0, 16).replace('T', ' ') + return ( +
+
VaR 95% · {ts} UTC
+
+ {[ + { label: 'Hist.', val: hv, color: 'text-blue-400' }, + { label: 'CVaR', val: cv, color: 'text-orange-400' }, + { label: 'MC×1.5', val: mv, color: 'text-red-400' }, + ].map(({ label, val, color }) => ( +
+
{label}
+
+ {val != null ? `${val >= 0 ? '+' : ''}${val.toFixed(2)}%` : '—'} +
+
+ ))} +
+
+ ) + })()} ) })()}