"use client"; // components/ScenarioTab.tsx // Simulateur "scénario → prix implicite", façon CME SOFRWatch / €STRWatch — // logique INVERSÉE par rapport aux autres onglets Taux (qui déduisent une // probabilité depuis les prix de marché) : ici, l'utilisateur choisit // lui-même un scénario de hikes/cuts à chaque réunion de banque centrale, et // l'outil en déduit où devrait se situer le taux de référence overnight // implicite si ce scénario se réalisait. // // Lien taux directeur → taux de référence overnight (voir /api/scenario-spreads) : // USD : SOFR ≈ borne basse Fed + (EFFR − borne basse) + (SOFR − EFFR) // EUR : €STR ≈ DFR + (€STR − DFR) // GBP : SONIA ≈ Bank Rate + (SONIA − Bank Rate) // CME calcule ces spreads (USD) à partir des prix forward des futures SOFR // 1 mois vs Fed Funds (SR1−ZQ) — données de marché propriétaires, pas // d'équivalent public pour €STR/SONIA. On les approxime partout avec la // moyenne réalisée historique (FRED, gratuit, fenêtre ~2 semaines). import { useEffect, useState, useMemo } from "react"; import { RefreshCw, RotateCcw, Info } from "lucide-react"; import { LineChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid, Legend, } from "recharts"; type ScenarioCcy = "USD" | "EUR" | "GBP"; interface ScenarioSpread { label: string; bps: number | null; } interface ScenarioSpreads { currency: ScenarioCcy; asOf: string; targetRate: number | null; targetLabel: string; refRateLabel: string; marketConversionOffset: number; spreads: ScenarioSpread[]; windowDays: number; note: string; } interface Meeting { dateIso: string; label: string; impliedRate: number; } const STEPS = [-50, -25, 0, 25, 50] as const; const CCY_META: Record = { USD: { flag: "🇺🇸", bank: "Fed (FOMC)", meetingLabel: "réunion FOMC" }, EUR: { flag: "🇪🇺", bank: "BCE", meetingLabel: "Conseil des gouverneurs" }, GBP: { flag: "🇬🇧", bank: "BoE (MPC)", meetingLabel: "réunion MPC" }, }; function fmtDate(iso: string): string { const d = new Date(iso + "T12:00:00Z"); return d.toLocaleDateString("fr-FR", { month: "short", day: "numeric" }); } export default function ScenarioTab() { const [currency, setCurrency] = useState("USD"); const [spreads, setSpreads] = useState(null); const [meetings, setMeetings] = useState([]); const [deltas, setDeltas] = useState>({}); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const load = async (ccy: ScenarioCcy) => { setLoading(true); setError(null); try { const [spreadsRes, rpRes] = await Promise.all([ fetch(`/api/scenario-spreads?currency=${ccy}`, { cache: "no-store" }).then(r => r.json()), fetch("/api/rate-probabilities", { cache: "no-store" }).then(r => r.json()), ]); if (spreadsRes?.error) throw new Error(spreadsRes.error); setSpreads(spreadsRes as ScenarioSpreads); const entry = rpRes?.data?.[ccy]; const ms: Meeting[] = (entry?.meetings ?? []).map((m: { dateIso: string; label: string; impliedRate: number }) => ({ dateIso: m.dateIso, label: m.label, impliedRate: m.impliedRate, })); setMeetings(ms); } catch (e) { setError(e instanceof Error ? e.message : "Erreur de chargement"); } finally { setLoading(false); } }; useEffect(() => { setDeltas({}); load(currency); }, [currency]); const setDelta = (dateIso: string, bps: number) => setDeltas(prev => ({ ...prev, [dateIso]: bps })); const resetAll = () => setDeltas({}); const totalSpreadBps = useMemo( () => spreads ? spreads.spreads.reduce((s, x) => s + (x.bps ?? 0), 0) : null, [spreads] ); const chartData = useMemo(() => { if (!spreads || spreads.targetRate === null || totalSpreadBps === null || !meetings.length) return []; const spreadTotal = totalSpreadBps / 100; let cumulTarget = spreads.targetRate; return meetings.map(m => { const delta = deltas[m.dateIso] ?? 0; cumulTarget = parseFloat((cumulTarget + delta / 100).toFixed(4)); const scenarioRef = parseFloat((cumulTarget + spreadTotal).toFixed(4)); // Marché (OIS) : impliedRate suit la convention propre à /api/rate-probabilities // (borne haute pour USD, MRO pour EUR, Bank Rate direct pour GBP) → on la // convertit vers la même échelle que targetRate avant d'ajouter le spread. const marketTarget = parseFloat((m.impliedRate + spreads.marketConversionOffset).toFixed(4)); const marketRef = parseFloat((marketTarget + spreadTotal).toFixed(4)); return { label: m.label, dateIso: m.dateIso, scenario: scenarioRef, marche: marketRef, cumulBps: Math.round((cumulTarget - spreads.targetRate!) * 100), }; }); }, [spreads, meetings, deltas, totalSpreadBps]); const currentImpliedRef = spreads?.targetRate !== null && totalSpreadBps !== null && spreads ? parseFloat((spreads.targetRate + totalSpreadBps / 100).toFixed(2)) : null; const meta = CCY_META[currency]; return (

Scénario — simulateur taux → {spreads?.refRateLabel ?? "taux"} implicite

{/* Sélecteur de devise */}
{(Object.keys(CCY_META) as ScenarioCcy[]).map(ccy => ( ))}

Logique inversée par rapport aux autres onglets Taux (qui déduisent une probabilité depuis les prix de marché) : choisissez vous-même un scénario de hikes/cuts à chaque {meta.meetingLabel}, l'outil déduit où devrait se situer le {spreads?.refRateLabel ?? "taux de référence"} implicite si ce scénario se réalisait — façon CME SOFRWatch / €STRWatch.

{error && (
{error}
)} {loading && !spreads ? (
Chargement…
) : spreads && meetings.length > 0 ? ( <> {/* Spreads utilisés */}
{spreads.targetLabel} → {spreads.refRateLabel}
{spreads.targetLabel} : {spreads.targetRate?.toFixed(2)}% {spreads.spreads.map(s => ( Spread {s.label} : {s.bps !== null ? `${s.bps >= 0 ? "+" : ""}${s.bps}bps` : "—"} ))} {spreads.refRateLabel} implicite actuel : {currentImpliedRef?.toFixed(2)}%

{spreads.note}

{/* Constructeur de scénario */}
Votre scénario — par {meta.meetingLabel}
{meetings.map(m => { const delta = deltas[m.dateIso] ?? 0; return (
{fmtDate(m.dateIso)}
{STEPS.map(s => ( ))}
); })}
{/* Chart comparatif */}
{spreads.refRateLabel} implicite — votre scénario vs marché (OIS) `${v.toFixed(2)}%`} domain={["dataMin - 0.1", "dataMax + 0.1"]} /> { if (!payload?.length) return null; const items = payload as { dataKey: string; value: number; color: string }[]; return (

{label}

{items.map(it => (

{it.dataKey === "scenario" ? "Votre scénario" : "Marché (OIS)"} : {it.value.toFixed(3)}%

))}
); }} /> ( {value === "scenario" ? "Votre scénario" : "Marché (OIS)"} )} />
) : (
Données indisponibles pour le moment.
)}
); }