import { useMemo, useState } from 'react' import { useQuery } from '@tanstack/react-query' import { useGeoRiskScore, useAllQuotes, useCalendar, usePortfolioSummary, useLastScores, useAllPatterns, useMacroRegime, useTradeMtm, useRiskDashboard, useGeoNews, useSimPortfolioRisk, useCycleStatus, } 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 = { energy: '⛽', metals: '🥇', agriculture: '🌾', equities: '📈', indices: '📊', forex: '💱', crypto: '₿', rates: '📉', } const ASSET_COLORS: Record = { energy: '#f97316', metals: '#eab308', agriculture: '#22c55e', indices: '#3b82f6', equities: '#06b6d4', forex: '#8b5cf6', rates: '#64748b', unknown: '#334155', } const REGIME_LABELS: Record = { 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 (
) } 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: 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' ) 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 = {} 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] ?? {} // 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]) // Top impactful news const topNews = useMemo(() => [...(geoNews ?? [])] .sort((a, b) => b.impact_score - a.impact_score) .slice(0, 3) , [geoNews]) 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 const addedDate = p.created_at ? p.created_at.slice(0, 10) : null return (
{p.name} {addedDate && {addedDate}}
{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 */} {(() => { const trades: any[] = mtmTrades 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 const closedTrades = trades.filter((t: any) => t.status === 'closed') const openTrades = trades.filter((t: any) => t.status !== 'closed') // Use capital_invested if set, otherwise entry_price as proxy cost const totalCapital = trades.reduce((s: number, t: any) => s + (t.capital_invested ?? t.entry_price ?? 0), 0) const totalProfit = withPnl.reduce((s: number, t: any) => s + ((t.capital_invested ?? t.entry_price ?? 0) * t.pnl_pct / 100), 0) const isEstimated = trades.some((t: any) => t.capital_invested == null && t.entry_price != null) const targetHit = trades.filter((t: any) => t.alert_type === 'target_reached').length const stopHit = trades.filter((t: any) => t.alert_type === 'stop_loss').length 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 return (
📊 P&L
{pnlView === 'simulated' ? ( <>
= 0 ? 'text-emerald-400' : 'text-red-400')}> {avgPnl !== null ? `${avgPnl >= 0 ? '+' : ''}${avgPnl.toFixed(1)}%` : '—'}
{trades.length} trades · {winners}✓{' '} {losers}✗
{targetHit > 0 &&
🎯 {targetHit} cible{targetHit > 1 ? 's' : ''}
} {stopHit > 0 &&
⛔ {stopHit} stop
}
{withPnl.length} pricés
Investi{isEstimated ? ' ~' : ''}
{totalCapital > 0 ? `${totalCapital.toFixed(0)}€` : '—'}
P&L €
= 0 ? 'text-emerald-400' : 'text-red-400')}> {totalCapital > 0 ? `${totalProfit >= 0 ? '+' : ''}${totalProfit.toFixed(0)}€` : '—'}
) : (
= 0 ? 'text-emerald-400' : 'text-red-400')}> {pfPnlPct !== null ? `${pfPnlPct >= 0 ? '+' : ''}${pfPnlPct.toFixed(1)}%` : '—'}
{pf?.open_positions ?? 0} positions ouvertes
{pfInvest != null &&
{pfInvest.toFixed(0)}€ investi
} {pfPnl != null &&
= 0 ? 'text-emerald-400' : 'text-red-400')}> {pfPnl >= 0 ? '+' : ''}{pfPnl.toFixed(0)}€
} {pfReal != null && pfReal !== 0 &&
= 0 ? 'text-emerald-600' : 'text-red-600')}> réal. {pfReal >= 0 ? '+' : ''}{pfReal.toFixed(0)}€
}
)} {/* 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']} />
) })()} ) })()} {/* 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 = risk?.concentration ?? {} const sortedClasses = Object.entries(concentration) .sort(([, a], [, b]) => (b as any).pct - (a as any).pct) .slice(0, 5) return (
🛡️ Risque
0 ? 'text-red-400' : 'text-emerald-400')}> {alertCount > 0 ? `${alertCount} alerte${alertCount > 1 ? 's' : ''}` : 'OK'}
{sortedClasses.length > 0 && (
{sortedClasses.map(([cls, data]: [string, any]) => (
{cls}
{data.bullish > 0 && (
)} {data.bearish > 0 && (
)}
{data.pct}%
))}
haussier baissier {conflictCount > 0 && {conflictCount} conflit{conflictCount > 1 ? 's' : ''}}
)} {sortedClasses.length === 0 && (
{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)}%` : '—'}
))}
) })()} ) })()} {/* Dernier Cycle — bilan complet */} {(() => { const last = lastCycle 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` const patternsAdded: number = last?.patterns_added ?? 0 // Only count trades whose pattern was added THIS cycle (not older patterns re-scored) const cyclePatternIds = new Set(cyclePatterns.map((p: any) => p.id)) const cycleTradesFromNewPatterns = cycleTrades.filter((t: any) => cyclePatternIds.has(t.pattern_id)) const tradesLogged: number = cycleTradesFromNewPatterns.length const tradesNotLogged: number = Math.max(0, patternsAdded - tradesLogged) const tradesClosed = cycleTradesFromNewPatterns.filter((t: any) => t.status === 'closed').length const commentary = last?.commentary ? (() => { try { return typeof last.commentary === 'string' ? JSON.parse(last.commentary) : last.commentary } catch { return null } })() : null return (
🔄 Dernier Cycle
{elapsedStr}
{last ? ( <>
+{patternsAdded}
patterns ajoutés
+{tradesLogged}
trades ajoutés
{tradesNotLogged > 0 && (
{tradesNotLogged} non-loggé{tradesNotLogged > 1 ? 's' : ''}
)}
{tradesLogged}
trades loggés
{tradesClosed}
fermés
{commentary?.commentary && (
{commentary.commentary.slice(0, 100)}…
)} ) : (
Aucun cycle enregistré
)} ) })()} {/* 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 (
🌐 Régime Macro
{macroInfo ? `${macroInfo.emoji} ${macroInfo.label}` : '—'}
{ranked.length > 0 && (
{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 (
{REGIME_LABELS[key] ?? key}
{score}
) })}
)} {/* Key macro gauges */} {Object.keys(gauges).length > 0 && (
{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 (
{label} {fmt(val, chg)} {h}
) })}
)} {/* Trigger signals for dominant regime */} {reasons.length > 0 && (
{reasons.slice(0, 4).map((r: string, i: number) => ( {r} ))}
)} ) })()}
{/* ── Command Center: Intelligence & Contexte ── */}
{/* Top 5 trades loggés */} {(() => { const top5 = [...mtmTrades] .sort((a, b) => (b.pnl_pct ?? -999) - (a.pnl_pct ?? -999)) .slice(0, 5) return (
🏆 Top Trades
{top5.length > 0 ? (
{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 (
{i + 1} {isBear ? '🐻' : '🐂'} {t.underlying ?? '—'} {t.strategy ?? ''} {entryDate && {entryDate}} {pnl !== null ? ( = 0 ? 'text-emerald-400' : 'text-red-400')}> {pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}% ) : ( )}
) })}
) : (
Aucun trade loggé
)} ) })()} {/* Trades du dernier cycle — détaillé */} {(() => { // Build a map: pattern_id → trade for quick lookup const tradeByPattern: Record = {} for (const t of cycleTrades) { if (t.pattern_id) tradeByPattern[t.pattern_id] = t } // Show cyclePatterns with their trade status (loggé or not) const rows = cyclePatterns.length > 0 ? cyclePatterns : cycleTrades.map((t: any) => ({ id: t.pattern_id, name: t.pattern_name })) return (
📥 Trades du cycle
{rows.length > 0 ? (
{rows.slice(0, 3).map((p: any, i: number) => { const t = tradeByPattern[p.id] const logged = !!t const ev: number | null = t?.ev_net ?? t?.ev_at_entry ?? null const score: number | null = t?.score_at_entry ?? scoreMap[p.id]?.score ?? null const pnl: number | null = t?.pnl_pct ?? null const isBear = t?.strategy?.toLowerCase().includes('put') || t?.strategy?.toLowerCase().includes('bear') return (
{logged ? ( <> {/* Trade row — primary */}
{isBear ? '🐻' : '🐂'} {t.underlying ?? '—'} {t.strategy ?? ''} {ev !== null && ( = 0.5 ? 'text-emerald-400' : ev >= 0 ? 'text-yellow-400' : 'text-red-400')}> EV{ev >= 0 ? '+' : ''}{ev.toFixed(2)} )} {score !== null && ( {score} )} {pnl !== null && ( = 0 ? 'text-emerald-400' : 'text-red-400')}> {pnl >= 0 ? '+' : ''}{pnl.toFixed(1)}% )}
{/* Pattern name — secondary */}
{p.name}
) : ( <> {/* Suggested trade from scoreMap — primary */} {(() => { const sp = scoreMap[p.id] const rec = sp?.recommended_trade ?? sp?.trade_rankings?.[0] const underlying = rec?.underlying ?? rec?.ticker ?? null const strategy = rec?.strategy ?? rec?.trade_type ?? null const evVal: number | null = rec?.ev_net ?? rec?.ev ?? null const isBearS = strategy?.toLowerCase().includes('put') || strategy?.toLowerCase().includes('bear') return ( <>
{isBearS ? '🐻' : '🐂'} {underlying ?? '—'} {strategy ?? 'trade suggéré'} {evVal !== null && ( EV{evVal >= 0 ? '+' : ''}{evVal.toFixed(2)} )} {score !== null && ( {score} )} non loggé
{p.name}
) })()} )}
) })}
) : (
{lastCycle ? 'Aucun pattern ajouté ce cycle' : 'En attente du prochain cycle'}
)} ) })()} {/* Patterns du dernier cycle */} {(() => { return (
⭐ Pattern du cycle
{cyclePatterns.length > 0 ? (
{cyclePatterns.map((p, i) => { const sp = scoreMap[p.id] const score = sp?.score ?? null const ticker = sp?.recommended_trade?.underlying ?? null return (
{i + 1}
{p.name}
{ticker &&
{ticker}
}
{score !== null ? ( {score} ) : ( )}
) })}
) : (
{lastCycle ? 'Aucun pattern ajouté ce cycle' : 'Aucun scoré — lancer le scoring IA'}
)} ) })()} {/* Top news impactantes */} {(() => { return (
Top News
{topNews.length > 0 ? (
{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 (
{i + 1}

{n.title}

{impact}
) })}
) : (
Chargement des news…
)} ) })()}
{/* Markets mini overview */}
{['energy', 'metals', 'indices', 'forex'].map(cls => (
{assetEmoji[cls]} {cls}
{allQuotes?.[cls]?.map(q => ) ?? (
Chargement...
)}
))}
) }