"use client"; // components/ScenarioTab.tsx // Simulateur "scénario → prix implicite", façon CME SOFRWatch — 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 FOMC, et l'outil en déduit où devrait se // situer le SOFR implicite si ce scénario se réalisait. // // Lien taux cible Fed → SOFR (méthodologie CME SOFRWatch, en 2 spreads) : // 1. EFFR − borne basse de la fourchette cible (observé) // 2. SOFR − EFFR (observé) // SOFR implicite ≈ borne basse + spread 1 + spread 2 // CME calcule ces spreads à partir des prix forward des futures SOFR 1 mois // vs Fed Funds (SR1−ZQ) — données de marché propriétaires. On les approxime // ici avec la moyenne réalisée historique (FRED, gratuit) : voir /api/scenario-spreads. import { useEffect, useState, useMemo } from "react"; import { RefreshCw, RotateCcw, Info } from "lucide-react"; import { LineChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid, Legend, } from "recharts"; interface ScenarioSpreads { asOf: string; lowerBound: number | null; upperBound: number | null; effrSpreadBps: number | null; sofrEffrSpreadBps: number | null; windowDays: number; note: string; } interface UsdMeeting { dateIso: string; label: string; impliedRate: number; // convention upper bound, cf. rate_decisions.json } const STEPS = [-50, -25, 0, 25, 50] as const; 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 [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 () => { setLoading(true); setError(null); try { const [spreadsRes, rpRes] = await Promise.all([ fetch("/api/scenario-spreads", { 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 usd = rpRes?.data?.USD; const ms: UsdMeeting[] = (usd?.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(() => { load(); }, []); const setDelta = (dateIso: string, bps: number) => setDeltas(prev => ({ ...prev, [dateIso]: bps })); const resetAll = () => setDeltas({}); const chartData = useMemo(() => { if (!spreads || spreads.lowerBound === null || !meetings.length) return []; const spreadTotal = (spreads.effrSpreadBps ?? 0) / 100 + (spreads.sofrEffrSpreadBps ?? 0) / 100; let cumulLower = spreads.lowerBound; return meetings.map(m => { const delta = deltas[m.dateIso] ?? 0; cumulLower = parseFloat((cumulLower + delta / 100).toFixed(4)); const scenarioSofr = parseFloat((cumulLower + spreadTotal).toFixed(4)); // Marché (OIS) : impliedRate suit la convention upper bound → on // convertit en borne basse pour rester sur la même échelle SOFR. const marketLower = parseFloat((m.impliedRate - 0.25).toFixed(4)); const marketSofr = parseFloat((marketLower + spreadTotal).toFixed(4)); return { label: m.label, dateIso: m.dateIso, scenario: scenarioSofr, marche: marketSofr, cumulBps: Math.round((cumulLower - spreads.lowerBound!) * 100), }; }); }, [spreads, meetings, deltas]); const spreadTotalBps = spreads ? (spreads.effrSpreadBps ?? 0) + (spreads.sofrEffrSpreadBps ?? 0) : null; const currentImpliedSofr = spreads?.lowerBound !== null && spreadTotalBps !== null && spreads ? parseFloat((spreads.lowerBound + spreadTotalBps / 100).toFixed(2)) : null; return (

Scénario — simulateur taux → SOFR implicite

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 réunion FOMC, l'outil déduit où devrait se situer le SOFR implicite si ce scénario se réalisait — façon CME SOFRWatch.

{error && (
{error}
)} {loading && !spreads ? (
Chargement…
) : spreads && meetings.length > 0 ? ( <> {/* Spreads utilisés */}
Fourchette Fed actuelle → SOFR
Fourchette : {spreads.lowerBound?.toFixed(2)}–{spreads.upperBound?.toFixed(2)}% Spread EFFR−borne basse : {spreads.effrSpreadBps! >= 0 ? "+" : ""}{spreads.effrSpreadBps}bps Spread SOFR−EFFR : {spreads.sofrEffrSpreadBps! >= 0 ? "+" : ""}{spreads.sofrEffrSpreadBps}bps SOFR implicite actuel : {currentImpliedSofr?.toFixed(2)}%

{spreads.note}

{/* Constructeur de scénario */}
Votre scénario — par réunion FOMC
{meetings.map(m => { const delta = deltas[m.dateIso] ?? 0; return (
{fmtDate(m.dateIso)}
{STEPS.map(s => ( ))}
); })}
{/* Chart comparatif */}
SOFR 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.
)}
); }