import { useMemo } from 'react' import { useGeoRiskScore, useAllQuotes, useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime, useTradeMtm, useRiskDashboard, useSimPortfolioRisk, useCycleStatus, useKnowledgeState, } from '../hooks/useApi' import { Clock, Globe, ShieldAlert, ArrowUpRight, Brain } 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 } 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 = { energy: '⛽', metals: '🥇', agriculture: '🌾', equities: '📈', indices: '📊', forex: '💱', crypto: '₿', rates: '📉', } function QuoteRow({ q }: { q: Quote }) { if (!q.price) return null const pos = q.change_pct >= 0 return (
{q.name || q.symbol}
{q.price.toFixed(2)}
{pos ? '+' : ''}{q.change_pct.toFixed(2)}%
) } 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: riskDashboard } = useRiskDashboard() const { data: simRisk } = useSimPortfolioRisk() const { data: cycleStatusData } = useCycleStatus() const { data: knowledgeState } = useKnowledgeState() const allPatterns: any[] = allPatternsData ?? [] const scoreMap = useMemo(() => { const map: Record = {} 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 = sc.asset_bias?.[dom] ?? {} return { dominant: dom, label: m.label, color: m.color, emoji: m.emoji, assetBias } }, [macroData]) const gauge = riskScore ? riskGauge(riskScore.score) : null const radarData = riskScore?.breakdown ? Object.entries(riskScore.breakdown).map(([k, v]) => ({ subject: k.replace('_', ' '), score: v })) : [] return (
{/* Header */}

Cockpit GeoOptions

{format(new Date(), "EEEE d MMMM yyyy · HH:mm", { locale: fr })}

{portfolio && portfolio.open_positions > 0 && (
= 0 ? 'text-emerald-400' : 'text-red-400')}> Portfolio: {portfolio.unrealized_pnl >= 0 ? '+' : ''}{portfolio.unrealized_pnl?.toFixed(0)}€
)}
Live
{/* Risk concentration banner */} {(riskDashboard as any)?.concentration_alerts?.length > 0 && (
{((riskDashboard as any).concentration_alerts as any[]).slice(0, 2).map((alert: any, i: number) => (
{alert.message} Risk Dashboard →
))}
)} {/* Top row */}
{/* Geo Risk */}
Risque Géopolitique
News géo
{riskLoading ? (
) : riskScore && gauge ? ( <>
{riskScore.score}
{gauge.label}
{riskScore.top_risks?.map(([cat, val]) => (
{(cat as string).replace('_', ' ')} {Math.round((val as number) * 100)}%
))}
) :
Backend requis
}
{/* Radar */}
Cartographie risques
{radarData.length > 0 ? ( ) :
Chargement...
}
{/* Patterns — AI scores */}
Scores patterns {allPatterns.length > 0 && {allPatterns.length} patterns}
{allPatterns.length === 0 ? (
Backend requis
) : ( [...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 return (
{p.name} {bestEff !== null ? ( {bestEff} ) : ( )}
) }) )}
{/* Calendrier */}
Prochains catalyseurs
{calendar?.slice(0, 4).map((ev, i) => (
{'!'.repeat(ev.importance === 'high' ? 3 : ev.importance === 'medium' ? 2 : 1)}
{ev.title}
{ev.date?.slice(0, 10)}
))}
{/* ── Command Center: Résumé Opérationnel ── */}
{/* PnL Simulé */} {(() => { const trades: any[] = (tradeMtmData as any)?.trades ?? [] const withPnl = trades.filter((t: any) => t.pnl_pct != null) const avgPnl = withPnl.length ? withPnl.reduce((s: number, t: any) => s + t.pnl_pct, 0) / withPnl.length : null const winners = withPnl.filter((t: any) => t.pnl_pct > 0).length const losers = withPnl.filter((t: any) => t.pnl_pct < 0).length return (
📊 PnL Simulé
= 0 ? 'text-emerald-400' : 'text-red-400')}> {avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
{trades.length} trades · {winners}✓{' '} {losers}✗
) })()} {/* Risque Simulé */} {(() => { 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 return (
🛡️ Risque Simulé
0 ? 'text-red-400' : 'text-emerald-400')}> {alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
{openCount} positions {conflictCount > 0 && · {conflictCount} conflit{conflictCount > 1 ? 's' : ''}}
) })()} {/* Dernier Cycle */} {(() => { const last = (cycleStatusData as any)?.last_cycle const ts = last?.ts ? new Date(last.ts.endsWith('Z') ? last.ts : last.ts + 'Z') : null const elapsed = ts ? Math.round((Date.now() - ts.getTime()) / 60_000) : null const elapsedStr = elapsed === null ? '—' : elapsed < 60 ? `${elapsed}min` : elapsed < 1440 ? `${Math.round(elapsed / 60)}h` : `${Math.round(elapsed / 1440)}j` return (
🔄 Dernier Cycle
{elapsedStr}
{last ? `${last.patterns_added ?? 0} loggés · géo ${last.geo_score ?? '—'}` : 'Aucun cycle enregistré'}
) })()} {/* Régime Macro */} {(() => { const dom = macroInfo?.dominant const colorClass = dom === 'growth' ? 'text-emerald-400' : dom === 'stagflation' || dom === 'recession' ? 'text-red-400' : dom === 'deflation' ? 'text-blue-300' : 'text-slate-400' return (
🌐 Régime Macro
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
{macroInfo ? Object.entries(macroInfo.assetBias).slice(0, 2).map(([k, v]) => `${k}→${v}`).join(' · ') : 'Chargement...'}
) })()}
{/* ── Command Center: Intelligence & Contexte ── */}
{/* Super Contexte IA */} {(() => { const state = (knowledgeState as any)?.state const synthesis = state?.synthesis const insights = (synthesis?.regime_insights?.length ?? 0) + (synthesis?.pattern_insights?.length ?? 0) + (synthesis?.recurring_mistakes?.length ?? 0) const priority = synthesis?.strategic_priorities?.[0] const excerpt = typeof priority === 'string' ? priority : typeof state?.narrative === 'string' ? state.narrative.slice(0, 90) : null return (
🧠 Super Contexte
{state ? `${insights} insights actifs` : 'Aucune synthèse'}
{excerpt ?? 'Lancer une synthèse dans Super Contexte'}
) })()} {/* Trades du dernier cycle */} {(() => { const last = (cycleStatusData as any)?.last_cycle const added: number = last?.patterns_added ?? 0 const scored: number = last?.patterns_scored ?? 0 const geoScore: number | null = last?.geo_score ?? null const mtmTrades: any[] = (tradeMtmData as any)?.trades ?? [] const recent = mtmTrades.slice(0, 3) return (
📥 Trades du cycle
+{added}
{recent.length > 0 ? (
{recent.map((t: any) => (
{t.underlying ?? '—'} {t.direction === 'bearish' ? '🐻' : '🐂'}
))}
) : (
{scored > 0 ? `${scored} scorés` : 'Aucun cycle enregistré'} {geoScore !== null && ` · géo ${geoScore}`}
)} ) })()} {/* Meilleur Pattern scoré */} {(() => { const best = [...allPatterns] .map(p => ({ p, sp: scoreMap[p.id] })) .filter(x => x.sp?.score != null) .sort((a, b) => (b.sp.score ?? 0) - (a.sp.score ?? 0))[0] const score = best?.sp?.score ?? null const name = best?.p?.name ?? null const ticker = best?.sp?.recommended_trade?.underlying ?? null return (
⭐ Meilleur Pattern
{score ?? '—'}
{name ?? 'Aucun scoré'}{ticker ? ` · ${ticker}` : ''}
) })()} {/* Patterns Actifs */} {(() => { const total = allPatterns.length const scored = allPatterns.filter(p => scoreMap[p.id]).length const unscored = total - scored return (
📋 Patterns Actifs
{total}
{scored} scorés {unscored > 0 && · {unscored} à scorer}
) })()}
{/* Markets mini overview */}
{['energy', 'metals', 'indices', 'forex'].map(cls => (
{assetEmoji[cls]} {cls}
{allQuotes?.[cls]?.map(q => ) ?? (
Chargement...
)}
))}
) }