+ {/* Score global */}
+
+
+ {pct !== null ? `${pct}%` : '—'}
+
+
+ {correct}/{total} nœuds corrects
+
+
+
+ {/* Détail par nœud */}
+
+ {Object.entries(nodes).map(([k, n]) => (
+
+
+ {statusIcon(n.status)}
+
+ {nodeLabels[k] ?? k}
+
+ {n.pred > 0 ? '+' : ''}{typeof n.pred === 'number'
+ ? (Math.abs(n.pred) > 10 ? Math.round(n.pred)+'p' : n.pred.toFixed(2))
+ : '?'}
+
+ {n.act !== null && (
+
+ → {n.act > 0 ? '+' : ''}{typeof n.act === 'number'
+ ? (Math.abs(n.act) > 10 ? Math.round(n.act)+'p' : n.act.toFixed(2))
+ : '?'}
+
+ )}
+
+ ))}
+
+
+ )
+}
+
+// ── Liste d'événements (colonne gauche) ────────────────────────────────────────
+
+const SERIES_OPTS = [
+ { value: '', label: 'Toutes séries' },
+ { value: 'CPIAUCSL', label: 'CPI US' },
+ { value: 'CPILFESL', label: 'Core CPI US' },
+ { value: 'PAYEMS', label: 'NFP' },
+ { value: 'FEDFUNDS', label: 'Fed Funds' },
+ { value: 'DFF', label: 'Fed Funds D' },
+ { value: 'ECBDFR', label: 'ECB Rate' },
+ { value: 'DFII10', label: 'Taux réel US' },
+]
+
+function EventList({ events, selectedId, onSelect, loading }: {
+ events: EventSummary[]; selectedId: number | null
+ onSelect: (e: EventSummary) => void; loading: boolean
+}) {
+ const [filter, setFilter] = useState('')
+
+ const filtered = useMemo(() =>
+ filter ? events.filter(e => e.series_id === filter) : events,
+ [events, filter]
+ )
+
+ const LeakDot = ({ leak }: { leak?: string | null }) => {
+ if (!leak || leak === 'unknown') return null
+ return (
+
+
+
+
{filtered.length} événements
+
+
+
+ {loading && (
+
+
+ Chargement…
+
+ )}
+ {!loading && filtered.map(ev => {
+ const isSelected = ev.id === selectedId
+ const surprise = (ev.actual_value ?? 0) - (ev.forecast_value ?? ev.previous_value ?? 0)
+ const isPos = surprise > 0
+ const side = ev.cfg?.side ?? 'US'
+ // Direction effet EUR : US hawkish = rouge (baisse EUR), EU hawkish = vert
+ const surpriseCol = side === 'US'
+ ? (isPos ? 'text-rose-400' : 'text-emerald-400')
+ : (isPos ? 'text-emerald-400' : 'text-rose-400')
+
+ return (
+
+ )
+ })}
+ {!loading && filtered.length === 0 && (
+
Aucun événement trouvé
+ )}
+
+
+ )
+}
+
+// ── Panneau stats (colonne droite) ─────────────────────────────────────────────
+
+function StatsPanel({ analysis, onAnalyze, analyzing }: {
+ analysis: Analysis | null; onAnalyze: () => void; analyzing: boolean
+}) {
+ if (!analysis && !analyzing) return (
+
+
+ {/* Score d'activation */}
+
+
+ {/* Pips prédit vs observé */}
+
+
EUR/USD — pips
+
+ {[
+ { label: 'Prédit', val: model.total_pips, unit: 'p' },
+ { label: 'Observé', val: drift.post_pips, unit: 'p' },
+ ].map(it => (
+
+
{it.label}
+
5 ? 'text-emerald-400' : it.val < -5 ? 'text-rose-400' : 'text-slate-400')}>
+ {it.val !== null ? `${it.val > 0 ? '+' : ''}${it.val}p` : '—'}
+
+
+ ))}
+
+
+
+ {/* Drift pré/post */}
+
+
+
Drift pré/post
+ {drift.leak !== 'unknown' && (
+
+
+
+ Fuite {drift.leak}
+
+
+ )}
+
+
+ {drift.drift_ratio !== null && (
+
+ Ratio pré/post : {drift.drift_ratio.toFixed(2)}
+ {' '}(seuil fuite : 0.25 / 0.50)
+
+ )}
+ {drift.pre_pips === null && (
+
+ Détection de fuite nécessite données intraday (<55j)
+
+ )}
+
+
+ {/* Taux observés */}
+
+
Taux observés (Δ jour J)
+ {[
+ { label: 'US 10Y', pred: model.us_10y_delta, act: yields.actual_us10y },
+ { label: 'US 2Y', pred: model.us_2y_delta, act: yields.actual_us2y },
+ { label: 'Bund 10Y', pred: model.eu_10y_delta, act: yields.actual_eu10y },
+ ].map(r => {
+ const col = matchColor(r.pred, r.act)
+ return (
+
+
+
{r.label}
+
+ pred {r.pred >= 0 ? '+' : ''}{r.pred.toFixed(2)}%
+
+
+ act {r.act !== null ? `${r.act >= 0 ? '+' : ''}${r.act.toFixed(2)}%` : '—'}
+
+
+ )
+ })}
+
+
+ {/* Mini graphe prix */}
+
+
+ {/* Re-analyser */}
+
+
+
+ Analysé le {analysis.analyzed_at?.slice(0, 10) ?? '—'}
+
+
+ )
+}
+
+// ── Vue instrument — agrégat ───────────────────────────────────────────────────
+
+function InstrumentView({ summary, loading }: { summary: Summary | null; loading: boolean }) {
+ if (loading) return (
+
+ Aucune analyse disponible — analysez d'abord des événements dans la vue "Par événement".
+
+ )
+
+ const { by_series, by_node, drift_dist, n } = summary
+
+ const nodeLabels: Record