Files
OpenFin/frontend/src/pages/Dashboard.tsx
OpenSquared fff0c15316 fix: P&L Réalisé dashboard — source useClosedTrades au lieu de mtmTrades
get_trade_entry_prices filtre WHERE status='open', les trades fermés n'y
apparaissaient jamais. Réalisé branche maintenant sur useClosedTrades(90)
avec calcul EUR = capital * pnl_realized% / 100 et affichage avg %.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 11:34:07 +02:00

993 lines
51 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMemo, useState } from 'react'
import { useQuery } from '@tanstack/react-query'
import {
useGeoRiskScore, useAllQuotes,
useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime,
useTradeMtm, useRiskDashboard, useGeoNews,
useSimPortfolioRisk, useCycleStatus, useClosedTrades,
} from '../hooks/useApi'
import { Clock, Globe, ShieldAlert, ArrowUpRight, Brain, Newspaper } from 'lucide-react'
import { Link } from 'react-router-dom'
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,
AreaChart, Area, XAxis, YAxis, Tooltip, CartesianGrid,
} from 'recharts'
import { scoreColor } from '../components/TradeIdeas'
const riskGauge = (score: number) => {
if (score < 25) return { color: 'text-emerald-400', bg: 'bg-emerald-500', label: 'FAIBLE' }
if (score < 50) return { color: 'text-yellow-400', bg: 'bg-yellow-500', label: 'MODÉRÉ' }
if (score < 75) return { color: 'text-orange-400', bg: 'bg-orange-500', label: 'ÉLEVÉ' }
return { color: 'text-red-400', bg: 'bg-red-500', label: 'EXTRÊME' }
}
const assetEmoji: Record<string, string> = {
energy: '⛽', metals: '🥇', agriculture: '🌾', equities: '📈',
indices: '📊', forex: '💱', crypto: '₿', rates: '📉',
}
const ASSET_COLORS: Record<string, string> = {
energy: '#f97316', metals: '#eab308', agriculture: '#22c55e',
indices: '#3b82f6', equities: '#06b6d4', forex: '#8b5cf6',
rates: '#64748b', unknown: '#334155',
}
const REGIME_LABELS: Record<string, string> = {
goldilocks: 'Goldilocks', desinflation: 'Désinflation', stagflation: 'Stagflation',
recession: 'Récession', crise_liquidite: 'Crise liq.', reflation: 'Reflation',
soft_landing: 'Soft Landing', inflation_shock: 'Infl. Choc', incertain: 'Incertain',
}
function ViewToggle({ value, onChange }: { value: 'simulated' | 'portfolio'; onChange: (v: 'simulated' | 'portfolio') => void }) {
const click = (v: 'simulated' | 'portfolio') => (e: React.MouseEvent) => {
e.preventDefault(); e.stopPropagation(); onChange(v)
}
return (
<div className="flex gap-0.5 bg-dark-800 rounded p-0.5 text-[9px]">
<button onClick={click('simulated')}
className={clsx('px-1.5 py-0.5 rounded transition-colors',
value === 'simulated' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
Simulé
</button>
<button onClick={click('portfolio')}
className={clsx('px-1.5 py-0.5 rounded transition-colors',
value === 'portfolio' ? 'bg-blue-600 text-white' : 'text-slate-600 hover:text-slate-400')}>
Portfolio
</button>
</div>
)
}
function QuoteRow({ q }: { q: Quote }) {
if (!q.price) return null
const pos = q.change_pct >= 0
return (
<div className="flex items-center justify-between py-1.5 border-b border-slate-700/20 last:border-0">
<span className="text-xs text-white truncate max-w-[140px]">{q.name || q.symbol}</span>
<div className="text-right ml-2">
<div className="text-xs text-white font-mono">{q.price.toFixed(2)}</div>
<div className={clsx('text-xs font-mono', pos ? 'positive' : 'negative')}>
{pos ? '+' : ''}{q.change_pct.toFixed(2)}%
</div>
</div>
</div>
)
}
export default function Dashboard() {
const { data: riskScore, isLoading: riskLoading } = useGeoRiskScore()
const { data: allQuotes } = useAllQuotes()
const { data: calendar } = useCalendar()
const { data: portfolio } = usePortfolioSummary()
const { data: lastScoresData } = useLastScores()
const { data: allPatternsData } = useAllPatterns()
const { data: macroData } = useMacroRegime()
const { data: tradeMtmData } = useTradeMtm(30)
const { data: closedTradesData } = useClosedTrades(90)
const { data: riskDashboard } = useRiskDashboard()
const { data: simRisk } = useSimPortfolioRisk()
const { data: cycleStatusData } = useCycleStatus()
const { data: geoNews } = useGeoNews()
// Cycle history (last 5 for "Derniers Cycles" card)
const { data: cycleHistoryData } = useQuery({
queryKey: ['dash-cycle-history'],
queryFn: () => fetch('/api/cycle/history?limit=5').then(r => r.ok ? r.json() : { runs: [] }),
staleTime: 60_000,
retry: 1,
})
// 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'
)
const [riskView, setRiskView] = useState<'simulated' | 'portfolio'>(() =>
(localStorage.getItem('dash_risk_view') as any) ?? 'simulated'
)
const setPnlViewPersist = (v: 'simulated' | 'portfolio') => {
setPnlView(v); localStorage.setItem('dash_pnl_view', v)
}
const setRiskViewPersist = (v: 'simulated' | 'portfolio') => {
setRiskView(v); localStorage.setItem('dash_risk_view', v)
}
const allPatterns: any[] = allPatternsData ?? []
const scoreMap = useMemo(() => {
const map: Record<string, any> = {}
for (const sp of (lastScoresData?.scored_patterns ?? [])) {
if (sp.pattern_id) map[sp.pattern_id] = sp
}
return map
}, [lastScoresData])
const macroInfo = useMemo(() => {
if (!macroData?.scenarios) return null
const sc = macroData.scenarios
const dom = sc.dominant ?? 'incertain'
const m = sc.meta?.[dom] ?? { label: dom, color: '#94a3b8', emoji: '?' }
const assetBias: Record<string, string> = sc.asset_bias?.[dom] ?? {}
// Top 3 scenario scores (excluding dominant, sorted desc)
const ranked: [string, number][] = (sc.ranked ?? []).slice(0, 4)
return { dominant: dom, label: m.label, color: m.color, emoji: m.emoji, assetBias, ranked }
}, [macroData])
const gauge = riskScore ? riskGauge(riskScore.score) : null
const radarData = riskScore?.breakdown
? Object.entries(riskScore.breakdown).map(([k, v]) => ({ subject: k.replace('_', ' '), score: v }))
: []
// Cycle trades: use scoring_run_id (which differs from cycle run_id)
const lastCycle = (cycleStatusData as any)?.last_cycle ?? null
const scoringRunId: string | null = lastCycle?.scoring_run_id ?? null
const mtmTrades: any[] = (tradeMtmData as any)?.trades ?? []
const cycleTrades = scoringRunId ? mtmTrades.filter((t: any) => t.run_id === scoringRunId) : []
// Patterns from the last cycle only (filter by created_at >= cycle started_at)
const cyclePatterns = useMemo(() => {
const started = lastCycle?.started_at ?? null
if (!started) return []
return [...allPatterns]
.filter(p => p.created_at && p.created_at >= started)
.sort((a, b) => (scoreMap[b.id]?.score ?? -1) - (scoreMap[a.id]?.score ?? -1))
.slice(0, 4)
}, [allPatterns, scoreMap, lastCycle])
// Trades grouped by run_id for cycle history card
const tradesByRun = useMemo(() => {
const map: Record<string, { logged: number; closed: number }> = {}
for (const t of mtmTrades) {
if (!t.run_id) continue
if (!map[t.run_id]) map[t.run_id] = { logged: 0, closed: 0 }
map[t.run_id].logged++
if (t.status === 'closed') map[t.run_id].closed++
}
return map
}, [mtmTrades])
// Top impactful news
const topNews = useMemo(() =>
[...(geoNews ?? [])]
.sort((a, b) => b.impact_score - a.impact_score)
.slice(0, 3)
, [geoNews])
return (
<div className="p-6 space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-xl font-bold text-white">Cockpit GeoOptions</h1>
<p className="text-xs text-slate-500 mt-0.5">
{format(new Date(), "EEEE d MMMM yyyy · HH:mm", { locale: fr })}
</p>
</div>
<div className="flex items-center gap-3">
{portfolio && portfolio.open_positions > 0 && (
<div className={clsx('text-sm font-bold', portfolio.unrealized_pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
Portfolio: {portfolio.unrealized_pnl >= 0 ? '+' : ''}{portfolio.unrealized_pnl?.toFixed(0)}
</div>
)}
<div className="flex items-center gap-2 text-xs text-slate-500">
<div className="w-2 h-2 rounded-full bg-emerald-400 animate-pulse"></div>
Live
</div>
</div>
</div>
{/* Risk concentration banner */}
{(riskDashboard as any)?.concentration_alerts?.length > 0 && (
<div className="space-y-1.5">
{((riskDashboard as any).concentration_alerts as any[]).slice(0, 2).map((alert: any, i: number) => (
<div key={i} className={clsx('flex items-center gap-2 text-xs px-3 py-2 rounded border', {
'bg-red-900/20 border-red-700/30 text-red-300': alert.level === 'high',
'bg-amber-900/20 border-amber-700/30 text-amber-300': alert.level === 'warning',
})}>
<ShieldAlert className="w-3 h-3 shrink-0" />
{alert.message}
<a href="/risk" className="ml-auto text-slate-500 hover:text-slate-300 underline">Risk Dashboard </a>
</div>
))}
</div>
)}
{/* Top row */}
<div className="grid grid-cols-4 gap-4">
{/* Geo Risk */}
<div className="card col-span-1">
<div className="flex items-center justify-between mb-2">
<div className="section-title flex items-center gap-1 mb-0">
<Globe className="w-3 h-3" /> Risque Géopolitique
</div>
<Link to="/geo" className="flex items-center gap-0.5 text-[10px] text-slate-600 hover:text-slate-300 transition-colors">
News géo <ArrowUpRight className="w-2.5 h-2.5" />
</Link>
</div>
{riskLoading ? (
<div className="animate-pulse h-16 bg-dark-600 rounded"></div>
) : riskScore && gauge ? (
<>
<div className={clsx('text-5xl font-bold', gauge.color)}>{riskScore.score}</div>
<div className={clsx('text-sm font-semibold mt-1', gauge.color)}>{gauge.label}</div>
<div className="mt-3 bg-dark-700 rounded-full h-2">
<div className={clsx('h-2 rounded-full', gauge.bg)} style={{ width: `${riskScore.score}%` }} />
</div>
<div className="mt-2 space-y-1">
{riskScore.top_risks?.map(([cat, val]) => (
<div key={cat} className="flex justify-between text-xs">
<span className="text-slate-500 capitalize">{(cat as string).replace('_', ' ')}</span>
<span className="text-slate-300">{Math.round((val as number) * 100)}%</span>
</div>
))}
</div>
</>
) : <div className="text-slate-500 text-xs">Backend requis</div>}
</div>
{/* Radar */}
<div className="card col-span-1">
<div className="section-title">Cartographie risques</div>
{radarData.length > 0 ? (
<ResponsiveContainer width="100%" height={160}>
<RadarChart data={radarData}>
<PolarGrid stroke="#1e2d4d" />
<PolarAngleAxis dataKey="subject" tick={{ fill: '#64748b', fontSize: 9 }} />
<Radar dataKey="score" stroke="#3b82f6" fill="#3b82f6" fillOpacity={0.25} />
</RadarChart>
</ResponsiveContainer>
) : <div className="h-40 flex items-center justify-center text-slate-600 text-xs">Chargement...</div>}
</div>
{/* Patterns — AI scores */}
<div className="card col-span-1 overflow-y-auto max-h-64">
<div className="section-title flex items-center gap-1">
<Brain className="w-3 h-3" /> Scores patterns
{allPatterns.length > 0 && <span className="text-slate-600 text-xs ml-auto font-normal">{allPatterns.length} patterns</span>}
</div>
<div className="space-y-1.5 mt-1">
{allPatterns.length === 0 ? (
<div className="text-slate-600 text-xs">Backend requis</div>
) : (
[...allPatterns]
.sort((a, b) => {
const spA = scoreMap[a.id], spB = scoreMap[b.id]
const effA = spA ? Math.max(...[0, ...(spA.trade_rankings ?? []).map((r: any) => (spA.score ?? 0) + (r.score_delta ?? 0))]) : -1
const effB = spB ? Math.max(...[0, ...(spB.trade_rankings ?? []).map((r: any) => (spB.score ?? 0) + (r.score_delta ?? 0))]) : -1
return effB - effA
})
.map(p => {
const sp = scoreMap[p.id]
const rawScore = sp?.score ?? null
const bestEff = sp
? Math.max(rawScore ?? 0, ...(sp.trade_rankings ?? []).map((r: any) =>
Math.max(0, Math.min(100, (rawScore ?? 0) + (r.score_delta ?? 0)))))
: null
const addedDate = p.created_at ? p.created_at.slice(0, 10) : null
return (
<div key={p.id} className="flex items-center gap-2">
<div className="flex-1 min-w-0">
<span className="text-xs text-slate-300 truncate block">{p.name}</span>
{addedDate && <span className="text-[9px] text-slate-700 font-mono">{addedDate}</span>}
</div>
{bestEff !== null ? (
<span className={clsx('text-xs font-bold shrink-0', scoreColor(bestEff))}>{bestEff}</span>
) : (
<span className="text-xs text-slate-600 shrink-0"></span>
)}
</div>
)
})
)}
</div>
</div>
{/* Calendrier */}
<div className="card col-span-1 space-y-3">
<div className="section-title flex items-center gap-1"><Clock className="w-3 h-3" /> Prochains catalyseurs</div>
<div className="space-y-1.5">
{calendar?.slice(0, 4).map((ev, i) => (
<div key={i} className="flex items-center gap-2 text-xs">
<span className={clsx('badge', {
'badge-red': ev.importance === 'high',
'badge-yellow': ev.importance === 'medium',
'badge-blue': ev.importance === 'low',
})}>
{'!'.repeat(ev.importance === 'high' ? 3 : ev.importance === 'medium' ? 2 : 1)}
</span>
<div className="min-w-0">
<div className="text-white line-clamp-1">{ev.title}</div>
<div className="text-slate-600">{ev.date?.slice(0, 10)}</div>
</div>
</div>
))}
</div>
</div>
</div>
{/* ── Command Center: Résumé Opérationnel ── */}
<div className="grid grid-cols-4 gap-3">
{/* PnL */}
{(() => {
// Unrealized — open trades only (get_trade_entry_prices excludes closed)
const openTrades = mtmTrades
const openWithPnl = openTrades.filter((t: any) => t.pnl_pct != null)
const openCapital = openTrades.reduce((s: number, t: any) => s + (t.capital_invested ?? t.entry_price ?? 0), 0)
const openProfit = openWithPnl.reduce((s: number, t: any) => s + ((t.capital_invested ?? t.entry_price ?? 0) * t.pnl_pct / 100), 0)
const openPnlPct = openCapital > 0 ? openProfit / openCapital * 100 : null
const openWinners = openWithPnl.filter((t: any) => t.pnl_pct > 0).length
const openLosers = openWithPnl.filter((t: any) => t.pnl_pct < 0).length
const targetHit = openTrades.filter((t: any) => t.alert_type === 'target_reached').length
const stopHit = openTrades.filter((t: any) => t.alert_type === 'stop_loss').length
// Realized — from dedicated closed-trades endpoint (pnl_realized in %, capital for EUR)
const closedTrades = (closedTradesData as any)?.trades ?? []
const closedCapital = closedTrades.reduce((s: number, t: any) => s + (t.capital_invested ?? t.entry_price ?? 0), 0)
const closedProfitEur = closedTrades.reduce((s: number, t: any) => {
const cap = t.capital_invested ?? t.entry_price ?? 0
return s + (cap * (t.pnl_realized ?? 0) / 100)
}, 0)
const closedWithPnl = closedTrades.filter((t: any) => t.pnl_realized != null)
const avgClosedPct = closedWithPnl.length > 0
? closedWithPnl.reduce((s: number, t: any) => s + t.pnl_realized, 0) / closedWithPnl.length
: null
const closedWinners = closedTrades.filter((t: any) => (t.pnl_realized ?? 0) > 0).length
const closedLosers = closedTrades.filter((t: any) => (t.pnl_realized ?? 0) < 0).length
const isEstimated = openTrades.some((t: any) => t.capital_invested == null && t.entry_price != null)
const totalCapital = openCapital + closedCapital
const totalProfit = openProfit + closedProfitEur
const pf = portfolio as any
const pfPnlPct = pf?.unrealized_pnl_pct ?? null
const pfPnl = pf?.unrealized_pnl ?? null
const pfInvest = pf?.total_invested ?? null
const pfReal = pf?.realized_pnl ?? null
const pfNet = pf?.net_pnl ?? null
return (
<Link to="/journal" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1.5">
<span className="section-title mb-0 text-[10px]">📊 P&L</span>
<div className="flex items-center gap-1.5">
<ViewToggle value={pnlView} onChange={setPnlViewPersist} />
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
</div>
{/* ── Side-by-side: Unrealized | Realized ── */}
<div className="grid grid-cols-2 gap-2 flex-shrink-0">
{/* Left: Unrealized */}
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30">
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Ouvertes</div>
{pnlView === 'simulated' ? (
<>
<div className={clsx('text-xl font-bold font-mono leading-none',
openPnlPct === null ? 'text-slate-600' : openPnlPct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{openPnlPct !== null ? `${openPnlPct >= 0 ? '+' : ''}${openPnlPct.toFixed(1)}%` : '—'}
</div>
<div className={clsx('text-xs font-mono mt-0.5',
openProfit >= 0 ? 'text-emerald-500' : 'text-red-400')}>
{openCapital > 0 ? `${openProfit >= 0 ? '+' : ''}${openProfit.toFixed(0)}` : '—'}
</div>
<div className="text-[10px] text-slate-500 mt-1">
{openTrades.length} trades · <span className="text-emerald-500">{openWinners}</span>{' '}
<span className="text-red-400">{openLosers}</span>
</div>
</>
) : (
<>
<div className={clsx('text-xl font-bold font-mono leading-none',
pfPnlPct === null ? 'text-slate-600' : pfPnlPct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : '—'}
</div>
<div className={clsx('text-xs font-mono mt-0.5', (pfPnl ?? 0) >= 0 ? 'text-emerald-500' : 'text-red-400')}>
{pfPnl != null ? `${pfPnl >= 0 ? '+' : ''}${pfPnl.toFixed(0)}` : '—'}
</div>
<div className="text-[10px] text-slate-500 mt-1">
{pf?.open_positions ?? 0} positions
</div>
</>
)}
{(targetHit > 0 || stopHit > 0) && (
<div className="mt-1 text-[9px] space-x-1">
{targetHit > 0 && <span className="text-emerald-400">🎯{targetHit}</span>}
{stopHit > 0 && <span className="text-red-400">{stopHit}</span>}
</div>
)}
</div>
{/* Right: Realized */}
<div className="bg-dark-700/40 rounded px-2.5 py-2 border border-slate-700/30">
<div className="text-[9px] text-slate-600 uppercase tracking-wide mb-1">Réalisé</div>
{pnlView === 'simulated' ? (
<>
<div className={clsx('text-xl font-bold font-mono leading-none',
avgClosedPct === null ? 'text-slate-600'
: avgClosedPct >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{avgClosedPct !== null
? `${avgClosedPct >= 0 ? '+' : ''}${avgClosedPct.toFixed(2)}%`
: closedTrades.length === 0 ? '—' : '+0.00%'}
</div>
<div className={clsx('text-xs font-mono mt-0.5',
closedProfitEur >= 0 ? 'text-emerald-500' : 'text-red-400')}>
{closedCapital > 0
? `${closedProfitEur >= 0 ? '+' : ''}${closedProfitEur.toFixed(0)}`
: closedTrades.length > 0 ? 'sans capital' : ''}
</div>
<div className="text-[10px] text-slate-500 mt-1">
{closedTrades.length} fermés · <span className="text-emerald-500">{closedWinners}</span>{' '}
<span className="text-red-400">{closedLosers}</span>
</div>
</>
) : (
<>
<div className={clsx('text-xl font-bold font-mono leading-none',
pfReal == null || pfReal === 0 ? 'text-slate-600'
: pfReal > 0 ? 'text-emerald-400' : 'text-red-400')}>
{pfReal != null && pfReal !== 0
? `${pfReal >= 0 ? '+' : ''}${pfReal.toFixed(0)}`
: '—'}
</div>
{pfNet != null && (
<div className={clsx('text-xs font-mono mt-0.5', pfNet >= 0 ? 'text-emerald-500/70' : 'text-red-400/70')}>
net {pfNet >= 0 ? '+' : ''}{pfNet.toFixed(0)}
</div>
)}
<div className="text-[10px] text-slate-500 mt-1">
{pf?.closed_positions ?? 0} fermées
</div>
</>
)}
</div>
</div>
{/* Total line */}
<div className="mt-1.5 flex items-center justify-between text-[10px] flex-shrink-0">
<span className="text-slate-600">
Investi{isEstimated ? ' ~' : ''} {pnlView === 'simulated'
? (totalCapital > 0 ? `${totalCapital.toFixed(0)}` : '—')
: (pfInvest != null ? `${pfInvest.toFixed(0)}` : '—')}
</span>
<span className={clsx('font-mono font-bold',
(pnlView === 'simulated' ? totalProfit : (pfNet ?? 0)) >= 0 ? 'text-emerald-400' : 'text-red-400')}>
Total {pnlView === 'simulated'
? (totalCapital > 0 ? `${totalProfit >= 0 ? '+' : ''}${totalProfit.toFixed(0)}` : '—')
: (pfNet != null ? `${pfNet >= 0 ? '+' : ''}${pfNet.toFixed(0)}` : '—')}
</span>
</div>
{/* 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 (
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30">
<div className="text-[9px] text-slate-600 mb-0.5">Courbe PnL historique ({snaps.length} pts)</div>
<ResponsiveContainer width="100%" height={58}>
<AreaChart data={chartData} margin={{ top: 2, right: 0, left: -38, bottom: 0 }}>
<defs>
<linearGradient id={gradId} x1="0" y1="0" x2="0" y2="1">
<stop offset="5%" stopColor={strokeColor} stopOpacity={0.25} />
<stop offset="95%" stopColor={strokeColor} stopOpacity={0} />
</linearGradient>
</defs>
<CartesianGrid strokeDasharray="3 3" stroke="#1e293b" vertical={false} />
<XAxis dataKey="date" tick={{ fontSize: 8, fill: '#475569' }}
interval={Math.max(0, Math.floor(chartData.length / 5) - 1)} />
<YAxis domain={[minVal - 0.1, maxVal + 0.1]} tick={{ fontSize: 8, fill: '#475569' }} unit="%" />
<Tooltip
contentStyle={{ background: '#0f172a', border: '1px solid #334155', borderRadius: 6, fontSize: 10, padding: '4px 8px' }}
labelStyle={{ color: '#64748b' }}
formatter={(v: any) => [`${v >= 0 ? '+' : ''}${Number(v).toFixed(3)}%`, 'PnL']}
/>
<Area type="monotone" dataKey="pnl" stroke={strokeColor} strokeWidth={1.5}
fill={`url(#${gradId})`} dot={false} />
</AreaChart>
</ResponsiveContainer>
</div>
)
})()}
</Link>
)
})()}
{/* Risk */}
{(() => {
const risk = simRisk as any
const alertCount: number = risk?.alerts?.length ?? 0
const conflictCount: number = risk?.conflicts?.length ?? 0
const openCount: number = risk?.open_count ?? 0
const concentration: Record<string, any> = risk?.concentration ?? {}
const sortedClasses = Object.entries(concentration)
.sort(([, a], [, b]) => (b as any).pct - (a as any).pct)
.slice(0, 5)
return (
<Link to="/risk" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🛡 Risque</span>
<div className="flex items-center gap-1.5">
<ViewToggle value={riskView} onChange={setRiskViewPersist} />
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
</div>
<div className={clsx('text-lg font-bold mt-0.5', alertCount > 0 ? 'text-red-400' : 'text-emerald-400')}>
{alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
</div>
{sortedClasses.length > 0 && (
<div className="mt-2 space-y-1">
{sortedClasses.map(([cls, data]: [string, any]) => (
<div key={cls} className="flex items-center gap-1.5">
<span className="text-[9px] text-slate-500 w-12 truncate capitalize">{cls}</span>
<div className="flex-1 h-2 bg-dark-700 rounded-full overflow-hidden flex">
{data.bullish > 0 && (
<div className="h-full rounded-l-full" title={`${data.bullish} haussier`}
style={{ width: `${(data.bullish / openCount) * 100}%`, background: ASSET_COLORS[cls] ?? '#3b82f6', opacity: 0.9 }} />
)}
{data.bearish > 0 && (
<div className="h-full rounded-r-full" title={`${data.bearish} baissier`}
style={{ width: `${(data.bearish / openCount) * 100}%`, background: '#ef4444', opacity: 0.7 }} />
)}
</div>
<span className="text-[9px] text-slate-600 w-7 text-right font-mono">{data.pct}%</span>
</div>
))}
<div className="flex items-center gap-1 text-[9px] text-slate-700 pt-0.5">
<span className="inline-block w-2 h-1.5 rounded-sm bg-blue-500/60"></span>haussier
<span className="inline-block w-2 h-1.5 rounded-sm bg-red-500/60 ml-1"></span>baissier
{conflictCount > 0 && <span className="ml-auto text-red-400">{conflictCount} conflit{conflictCount > 1 ? 's' : ''}</span>}
</div>
</div>
)}
{sortedClasses.length === 0 && (
<div className="text-[10px] text-slate-600 mt-1">
{openCount > 0 ? `${openCount} positions` : 'Aucune position'}
</div>
)}
{/* 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 (
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30">
<div className="text-[9px] text-slate-600 mb-1">VaR 95% · {ts} UTC</div>
<div className="grid grid-cols-3 gap-1">
{[
{ 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 }) => (
<div key={label} className="bg-dark-700/60 rounded px-2 py-1 text-center">
<div className="text-[8px] text-slate-600 mb-0.5">{label}</div>
<div className={clsx('text-xs font-bold font-mono', color)}>
{val != null ? `${val >= 0 ? '+' : ''}${val.toFixed(2)}%` : '—'}
</div>
</div>
))}
</div>
</div>
)
})()}
</Link>
)
})()}
{/* Derniers Cycles — liste */}
{(() => {
const runs: any[] = cycleHistoryData?.runs ?? []
return (
<Link to="/journal" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-2">
<span className="section-title mb-0 text-[10px]">🔄 Derniers Cycles</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{/* Column headers */}
<div className="flex items-center gap-1 mb-1 pb-1 border-b border-slate-700/30">
<span className="text-[8px] text-slate-700 w-20 shrink-0">Date</span>
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Pat.</span>
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Log.</span>
<span className="text-[8px] text-slate-700 w-8 shrink-0 text-right">Clos</span>
<span className="text-[8px] text-slate-700 flex-1 truncate">Régime</span>
</div>
{runs.length > 0 ? (
<div className="space-y-2.5 flex-1 overflow-hidden">
{runs.map((r: any, i: number) => {
const dt = r.started_at ? new Date(r.started_at.endsWith('Z') ? r.started_at : r.started_at + 'Z') : null
const dateStr = dt ? `${String(dt.getDate()).padStart(2,'0')}/${String(dt.getMonth()+1).padStart(2,'0')} ${String(dt.getHours()).padStart(2,'0')}:${String(dt.getMinutes()).padStart(2,'0')}` : '—'
const pat: number = r.patterns_added ?? 0
const tStats = tradesByRun[r.run_id] ?? { logged: 0, closed: 0 }
const regime = r.dominant_regime ? (REGIME_LABELS[r.dominant_regime] ?? r.dominant_regime) : '—'
const ok = r.status === 'completed'
return (
<div key={r.run_id ?? i} className="flex items-center gap-1">
<span className="text-[9px] text-slate-400 font-mono w-20 shrink-0">{dateStr}</span>
<span className={clsx('text-[9px] font-mono font-bold w-8 shrink-0 text-right', pat > 0 ? 'text-blue-400' : 'text-slate-600')}>
+{pat}
</span>
<span className={clsx('text-[9px] font-mono w-8 shrink-0 text-right', tStats.logged > 0 ? 'text-emerald-400' : 'text-slate-600')}>
{tStats.logged}
</span>
<span className={clsx('text-[9px] font-mono w-8 shrink-0 text-right', tStats.closed > 0 ? 'text-slate-400' : 'text-slate-600')}>
{tStats.closed}
</span>
<span className="text-[8px] text-slate-600 flex-1 truncate">{regime}</span>
<span className={clsx('w-1.5 h-1.5 rounded-full shrink-0', ok ? 'bg-emerald-500' : 'bg-slate-700')} />
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun cycle enregistré</div>
)}
</Link>
)
})()}
{/* Régime Macro */}
{(() => {
const dom = macroInfo?.dominant
const colorClass = dom === 'growth' || dom === 'goldilocks' || dom === 'soft_landing' ? 'text-emerald-400'
: dom === 'stagflation' || dom === 'recession' || dom === 'crise_liquidite' ? 'text-red-400'
: dom === 'deflation' || dom === 'desinflation' ? 'text-blue-300'
: dom === 'inflation_shock' ? 'text-orange-400'
: 'text-slate-400'
const ranked = macroInfo?.ranked ?? []
// Key gauges from macroData
const gauges = (macroData as any)?.gauges ?? {}
const reasons: string[] = dom ? ((macroData as any)?.scenarios?.reasons?.[dom] ?? []) : []
// 5 key indicators that explain the regime
const KEY_GAUGES = [
{
key: 'vix', label: 'VIX',
color: (v: number) => v < 18 ? 'text-emerald-400' : v < 25 ? 'text-yellow-400' : 'text-red-400',
fmt: (v: number) => v.toFixed(1),
hint: (v: number) => v < 18 ? '↓ risque bas' : v < 25 ? '↑ alerte' : '↑ crise',
},
{
key: 'spx_vs_200d', label: 'S&P/200j',
color: (v: number) => v >= 0 ? 'text-emerald-400' : 'text-red-400',
fmt: (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(1)}%`,
hint: (v: number) => v >= 5 ? 'bull fort' : v >= 0 ? 'bull mod.' : 'bear',
},
{
key: 'slope_10y3m', label: 'Pente 10Y-3M',
color: (v: number) => v >= 0.5 ? 'text-emerald-400' : v >= 0 ? 'text-yellow-400' : 'text-red-400',
fmt: (v: number) => `${v >= 0 ? '+' : ''}${v.toFixed(2)}`,
hint: (v: number) => v >= 0.5 ? 'normale' : v >= 0 ? 'plate' : 'inversée',
},
{
key: 'copper', label: 'Cuivre',
color: (v: number, chg: number) => chg >= 0 ? 'text-emerald-400' : 'text-red-400',
fmt: (v: number, chg: number) => `${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%`,
hint: (v: number, chg: number) => chg >= 0 ? '↑ croissance' : '↓ ralentis.',
},
{
key: 'gold', label: 'Or',
color: (v: number, chg: number) => chg >= 1 ? 'text-amber-400' : chg <= -1 ? 'text-emerald-400' : 'text-slate-400',
fmt: (v: number, chg: number) => `${chg >= 0 ? '+' : ''}${chg.toFixed(2)}%`,
hint: (v: number, chg: number) => chg >= 1 ? '↑ refuge' : chg <= -1 ? '↓ risk-on' : '≈ neutre',
},
]
return (
<Link to="/macro" className="card flex flex-col h-[288px] overflow-hidden hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🌐 Régime Macro</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
<div className={clsx('text-base font-bold mt-1', colorClass)}>
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
</div>
{ranked.length > 0 && (
<div className="mt-1.5 space-y-1">
{ranked.slice(0, 4).map(([key, score]: [string, number]) => {
const maxScore = ranked[0]?.[1] ?? 1
const pct = Math.round((score / Math.max(maxScore, 1)) * 100)
const isDom = key === dom
return (
<div key={key} className="flex items-center gap-1.5">
<span className={clsx('text-[9px] w-16 truncate', isDom ? 'text-slate-200 font-semibold' : 'text-slate-500')}>
{REGIME_LABELS[key] ?? key}
</span>
<div className="flex-1 h-1.5 bg-dark-700 rounded-full overflow-hidden">
<div
className={clsx('h-full rounded-full transition-all', isDom ? 'bg-blue-500' : 'bg-slate-600')}
style={{ width: `${pct}%` }}
/>
</div>
<span className={clsx('text-[9px] font-mono w-5 text-right', isDom ? 'text-blue-400' : 'text-slate-600')}>
{score}
</span>
</div>
)
})}
</div>
)}
{/* Key macro gauges */}
{Object.keys(gauges).length > 0 && (
<div className="mt-1.5 pt-1.5 border-t border-slate-700/30 space-y-0.5">
{KEY_GAUGES.map(({ key, label, color, fmt, hint }) => {
const g = gauges[key]
if (!g) return null
const val: number = g.value ?? 0
const chg: number = g.change_pct ?? 0
const c = color(val, chg)
const h = hint(val, chg)
return (
<div key={key} className="flex items-center gap-1.5">
<span className="text-[9px] text-slate-600 w-16 shrink-0">{label}</span>
<span className={clsx('text-[9px] font-mono font-semibold shrink-0', c)}>
{fmt(val, chg)}
</span>
<span className="text-[8px] text-slate-700 truncate">{h}</span>
</div>
)
})}
</div>
)}
{/* Trigger signals for dominant regime */}
{reasons.length > 0 && (
<div className="mt-1 flex flex-wrap gap-1">
{reasons.slice(0, 4).map((r: string, i: number) => (
<span key={i} className="text-[8px] bg-blue-900/20 text-blue-400/80 border border-blue-800/30 px-1.5 py-0.5 rounded">
{r}
</span>
))}
</div>
)}
</Link>
)
})()}
</div>
{/* ── Command Center: Intelligence & Contexte ── */}
<div className="grid grid-cols-4 gap-3">
{/* Top 5 trades loggés */}
{(() => {
const top5 = [...mtmTrades]
.sort((a, b) => (b.pnl_pct ?? -999) - (a.pnl_pct ?? -999))
.slice(0, 5)
return (
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">🏆 Top Trades</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{top5.length > 0 ? (
<div className="space-y-1.5 mt-1">
{top5.map((t: any, i: number) => {
const pnl: number | null = t.pnl_pct ?? null
const isBear = t.strategy?.toLowerCase().includes('put') || t.strategy?.toLowerCase().includes('bear')
const entryDate = t.entry_date ? t.entry_date.slice(0, 10) : null
return (
<div key={i} className="flex items-center gap-1.5 text-[10px]">
<span className="text-[9px] text-slate-700 font-mono w-3 shrink-0">{i + 1}</span>
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
<span className="font-mono text-slate-200 font-bold shrink-0">{t.underlying ?? '—'}</span>
<span className="text-slate-500 truncate flex-1 text-[9px]">{t.strategy ?? ''}</span>
{entryDate && <span className="text-[8px] text-slate-700 shrink-0 font-mono">{entryDate}</span>}
{pnl !== null ? (
<span className={clsx('font-mono font-bold shrink-0', pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
</span>
) : (
<span className="text-slate-700 shrink-0 text-[9px]"></span>
)}
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
)}
</Link>
)
})()}
{/* Derniers trades loggés */}
{(() => {
const recent = [...mtmTrades]
.filter((t: any) => t.entry_date)
.sort((a: any, b: any) => (b.entry_date ?? '').localeCompare(a.entry_date ?? ''))
.slice(0, 3)
return (
<Link to="/journal" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]">📥 Derniers trades loggés</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{recent.length > 0 ? (
<div className="space-y-2.5 mt-1">
{recent.map((t: any, i: number) => {
const pnl: number | null = t.pnl_pct ?? null
const isBear = t.strategy?.toLowerCase().includes('put') || t.strategy?.toLowerCase().includes('bear')
const entryDate = t.entry_date ? t.entry_date.slice(0, 10) : null
const score: number | null = t.score_at_entry ?? null
return (
<div key={i}>
<div className="flex items-center gap-1.5 text-[10px]">
<span className="shrink-0">{isBear ? '🐻' : '🐂'}</span>
<span className="font-mono text-slate-200 font-bold w-10 truncate shrink-0">{t.underlying ?? '—'}</span>
<span className="text-slate-500 truncate flex-1 text-[9px]">{t.strategy ?? ''}</span>
{score !== null && (
<span className={clsx('font-mono font-bold shrink-0 text-[9px]', scoreColor(score))}>{score}</span>
)}
{pnl !== null ? (
<span className={clsx('font-mono font-bold shrink-0', pnl >= 0 ? 'text-emerald-400' : 'text-red-400')}>
{pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}%
</span>
) : (
<span className="text-slate-700 shrink-0 text-[9px]">en cours</span>
)}
</div>
{entryDate && (
<div className="text-[8px] text-slate-700 mt-0.5 ml-4 font-mono">{entryDate} · {t.pattern_name ? <span className="italic">{t.pattern_name}</span> : ''}</div>
)}
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun trade loggé</div>
)}
</Link>
)
})()}
{/* Derniers Patterns ajoutés */}
{(() => {
const recent = [...allPatterns]
.filter((p: any) => p.created_at)
.sort((a: any, b: any) => (b.created_at ?? '').localeCompare(a.created_at ?? ''))
.slice(0, 3)
return (
<Link to="/patterns" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px]"> Derniers Patterns ajoutés</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{recent.length > 0 ? (
<div className="space-y-2.5 mt-1">
{recent.map((p: any, i: number) => {
const sp = scoreMap[p.id]
const score: number | null = sp?.score ?? null
const ticker: string | null = sp?.recommended_trade?.underlying ?? null
const dateStr = p.created_at ? p.created_at.slice(0, 10) : null
return (
<div key={p.id}>
<div className="flex items-center gap-1.5">
<span className="text-[9px] text-slate-700 w-3 shrink-0 font-mono">{i + 1}</span>
<div className="flex-1 min-w-0">
<div className="text-[10px] text-slate-300 truncate leading-tight">{p.name}</div>
</div>
{score !== null ? (
<span className={clsx('text-sm font-bold font-mono shrink-0', scoreColor(score))}>{score}</span>
) : (
<span className="text-[9px] text-slate-700 shrink-0"></span>
)}
</div>
<div className="text-[8px] text-slate-700 mt-0.5 ml-4 font-mono">
{ticker && <span className="text-slate-500">{ticker}</span>}
{ticker && dateStr && ' · '}
{dateStr}
</div>
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Aucun pattern enregistré</div>
)}
</Link>
)
})()}
{/* Top news impactantes */}
{(() => {
return (
<Link to="/geo" className="card block hover:border-slate-600/60 transition-all cursor-pointer">
<div className="flex items-center justify-between mb-1">
<span className="section-title mb-0 text-[10px] flex items-center gap-1">
<Newspaper className="w-2.5 h-2.5" /> Top News
</span>
<ArrowUpRight className="w-3 h-3 text-slate-600" />
</div>
{topNews.length > 0 ? (
<div className="space-y-2 mt-1">
{topNews.map((n, i) => {
const impact = Math.round(n.impact_score * 100)
const impactColor = impact >= 70 ? 'text-red-400' : impact >= 40 ? 'text-orange-400' : 'text-yellow-400'
const barColor = impact >= 70 ? 'bg-red-500' : impact >= 40 ? 'bg-orange-500' : 'bg-yellow-500'
return (
<div key={i} className="space-y-0.5">
<div className="flex items-start gap-1.5">
<span className="text-[9px] text-slate-700 font-mono mt-0.5 shrink-0">{i + 1}</span>
<p className="text-[10px] text-slate-300 leading-tight line-clamp-2 flex-1">{n.title}</p>
<span className={clsx('text-[10px] font-bold font-mono shrink-0', impactColor)}>{impact}</span>
</div>
<div className="ml-3 bg-dark-700 rounded-full h-0.5">
<div className={clsx('h-0.5 rounded-full', barColor)} style={{ width: `${impact}%` }} />
</div>
</div>
)
})}
</div>
) : (
<div className="text-[10px] text-slate-600 mt-1">Chargement des news</div>
)}
</Link>
)
})()}
</div>
{/* Markets mini overview */}
<div className="grid grid-cols-4 gap-3">
{['energy', 'metals', 'indices', 'forex'].map(cls => (
<div key={cls} className="card">
<div className="section-title">{assetEmoji[cls]} {cls}</div>
{allQuotes?.[cls]?.map(q => <QuoteRow key={q.symbol} q={q} />) ?? (
<div className="text-xs text-slate-600">Chargement...</div>
)}
</div>
))}
</div>
</div>
)
}