feat: onglet Scénario — étend le simulateur à l'EUR et au GBP

Recherché un équivalent Atlanta Fed MPT (options-implied probability) pour
l'EUR et le GBP : n'existe pas. La BoE a explicitement arrêté de publier ses
propres fonctions de densité de probabilité par options (short sterling) ;
pour l'EUR, seule la recherche académique calcule ça depuis les options
Euribor, aucune institution ne publie un jeu de données téléchargeable comme
le fait l'Atlanta Fed. Le seul outil CME équivalent (€STRWatch) est un
simulateur scénario→prix, pas un extracteur prix→probabilité — donc la même
famille que l'onglet Scénario (CME SOFRWatch), pas que l'onglet Atlanta Fed.

Étendu /api/scenario-spreads et ScenarioTab.tsx en conséquence (sélecteur de
devise USD/EUR/GBP) :
  EUR : €STR ≈ DFR + (€STR − DFR)          [rate_decisions.json: MRO − 15bps]
  GBP : SONIA ≈ Bank Rate + (SONIA − Bank Rate)
Séries FRED : ECBESTRVOLWGTTRMDMNRT (€STR), IUDSOIA (SONIA).

Fix méthodologique découvert en testant l'EUR : la fenêtre de calcul du spread
était de 60 jours, ce qui chevauchait la hausse ECB du 17 juin 2026 et
mélangeait ancien/nouveau régime (spread €STR−DFR calculé à -26bps au lieu de
~-7bps réel). Réduit à ~2 semaines (10 observations) pour les 3 devises,
assez court pour rester dans le régime de taux courant tout en étant une
vraie moyenne réalisée plutôt qu'un point unique bruité.

Vérifié en direct : USD 3.64% (SOFR), EUR 2.18% (€STR), GBP 3.73% (SONIA) —
tous cohérents avec les valeurs réelles du jour. Sélecteur de devise et clics
sur le constructeur de scénario fonctionnels pour les 3 devises.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
caty21
2026-07-08 23:29:32 +02:00
co-authored by Claude Sonnet 5
parent fc899fb912
commit 187ca12658
4 changed files with 231 additions and 106 deletions
+124 -45
View File
@@ -1,15 +1,18 @@
// app/api/scenario-spreads/route.ts // app/api/scenario-spreads/route.ts
// Spreads EFFR/SOFR utilisés par l'onglet Scénario (simulateur type CME // Spreads utilisés par l'onglet Scénario (simulateur type CME SOFRWatch /
// SOFRWatch) pour traduire une fourchette cible Fed hypothétique en niveau de // €STRWatch) pour traduire un taux directeur hypothétique en taux de
// SOFR implicite : SOFR ≈ lowerBound + (EFFR lowerBound) + (SOFR EFFR). // référence overnight implicite :
// USD : SOFR ≈ borne basse Fed + (EFFR borne basse) + (SOFR EFFR)
// EUR : €STR ≈ DFR + (€STR DFR)
// GBP : SONIA ≈ Bank Rate + (SONIA Bank Rate)
// //
// CME SOFRWatch calcule ces deux spreads à partir des prix des futures SOFR // CME calcule ces spreads à partir de prix de futures/options (SR1ZQ pour
// 1 mois vs Fed Funds (SR1ZQ), des données de marché propriétaires. On les // SOFR ; pas d'équivalent public pour €STR/SONIA). On les approxime tous avec
// approxime ici avec la moyenne réalisée historique EFFR/SOFR (FRED, gratuit), // la moyenne réalisée historique (FRED, gratuit) — un proxy raisonnable tant
// ce qui est un proxy raisonnable tant que la Fed n'a pas bougé récemment // que le taux directeur n'a pas bougé récemment, mais pas un prix
// (spread stable), mais reste un proxy — pas un prix forward-looking. // forward-looking.
import { NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import rateDecisionsData from "@/data/rate_decisions.json"; import rateDecisionsData from "@/data/rate_decisions.json";
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -31,52 +34,128 @@ async function fredObs(seriesId: string, apiKey: string, limit = 60): Promise<Fr
} catch { return []; } } catch { return []; }
} }
export interface ScenarioSpreads { function avgSpread(ref: FredObs[], target: number | null): number | null {
asOf: string; if (target === null || !ref.length) return null;
lowerBound: number | null; return ref.reduce((s, o) => s + (o.value - target), 0) / ref.length;
upperBound: number | null;
effrSpreadBps: number | null; // EFFR lowerBound, moyenne réalisée
sofrEffrSpreadBps: number | null; // SOFR EFFR, moyenne réalisée (apparié par date)
windowDays: number;
note: string;
} }
export async function GET() { export interface ScenarioSpread {
const key = process.env.FRED_API_KEY; label: string; // "EFFR borne basse", "€STR DFR", "SONIA Bank Rate"…
if (!key) return NextResponse.json({ error: "FRED_API_KEY missing" }, { status: 500 }); bps: number | null;
}
const [effr, sofr] = await Promise.all([ export interface ScenarioSpreads {
fredObs("EFFR", key, 60), currency: "USD" | "EUR" | "GBP";
fredObs("SOFR", key, 60), asOf: string;
]); targetRate: number | null; // borne basse (USD), DFR (EUR), Bank Rate (GBP)
if (!effr.length || !sofr.length) { targetLabel: string;
return NextResponse.json({ error: "Données FRED EFFR/SOFR indisponibles" }, { status: 502 }); refRateLabel: string; // "SOFR" | "€STR" | "SONIA"
} // Offset fixe pour convertir l'impliedRate déjà exposé par /api/rate-probabilities
// (convention borne haute pour USD, MRO pour EUR, Bank Rate direct pour GBP)
// vers la même échelle que targetRate — permet au front de comparer le
// scénario utilisateur au marché sans dupliquer les conventions par devise.
marketConversionOffset: number;
spreads: ScenarioSpread[];
windowDays: number;
note: string;
}
const rateDec = (rateDecisionsData as Array<{ decisions: Record<string, { current: number }> }>)[0]; type RateDecisions = Array<{ decisions: Record<string, { current: number }> }>;
// Fenêtre courte (≈2 semaines ouvrées) plutôt que longue : une fenêtre large
// (60j) peut chevaucher un changement de taux directeur récent et fausser la
// moyenne en mélangeant l'ancien et le nouveau régime (constaté sur l'EUR :
// la BCE a bougé le 17 juin 2026, une fenêtre 60j donnait un spread €STR-DFR
// de -26bps au lieu des ~-7bps réels une fois restreint à l'après-hausse).
const SPREAD_WINDOW = 10;
async function buildUsd(key: string): Promise<ScenarioSpreads | { error: string }> {
const [effr, sofr] = await Promise.all([fredObs("EFFR", key, SPREAD_WINDOW), fredObs("SOFR", key, SPREAD_WINDOW)]);
if (!effr.length || !sofr.length) return { error: "Données FRED EFFR/SOFR indisponibles" };
const rateDec = (rateDecisionsData as RateDecisions)[0];
const upperBound = rateDec?.decisions?.USD?.current ?? null; const upperBound = rateDec?.decisions?.USD?.current ?? null;
const lowerBound = upperBound !== null ? parseFloat((upperBound - 0.25).toFixed(2)) : null; const lowerBound = upperBound !== null ? parseFloat((upperBound - 0.25).toFixed(2)) : null;
const effrSpread = lowerBound !== null const effrSpread = avgSpread(effr, lowerBound);
? effr.reduce((s, o) => s + (o.value - lowerBound), 0) / effr.length
: null;
const effrByDate = new Map(effr.map(o => [o.date, o.value])); const effrByDate = new Map(effr.map(o => [o.date, o.value]));
const pairedDiffs = sofr const pairedDiffs = sofr.filter(o => effrByDate.has(o.date)).map(o => o.value - effrByDate.get(o.date)!);
.filter(o => effrByDate.has(o.date)) const sofrEffrSpread = pairedDiffs.length ? pairedDiffs.reduce((s, d) => s + d, 0) / pairedDiffs.length : null;
.map(o => o.value - effrByDate.get(o.date)!);
const sofrEffrSpread = pairedDiffs.length
? pairedDiffs.reduce((s, d) => s + d, 0) / pairedDiffs.length
: null;
const data: ScenarioSpreads = { return {
currency: "USD",
asOf: new Date().toISOString().slice(0, 10), asOf: new Date().toISOString().slice(0, 10),
lowerBound, targetRate: lowerBound,
upperBound, targetLabel: "Fourchette Fed (borne basse)",
effrSpreadBps: effrSpread !== null ? Math.round(effrSpread * 100) : null, refRateLabel: "SOFR",
sofrEffrSpreadBps: sofrEffrSpread !== null ? Math.round(sofrEffrSpread * 100) : null, marketConversionOffset: -0.25, // impliedRate (borne haute) -> borne basse
spreads: [
{ label: "EFFR borne basse", bps: effrSpread !== null ? Math.round(effrSpread * 100) : null },
{ label: "SOFR EFFR", bps: sofrEffrSpread !== null ? Math.round(sofrEffrSpread * 100) : null },
],
windowDays: effr.length, windowDays: effr.length,
note: "Spreads = moyennes réalisées historiques EFFR/SOFR (FRED, fenêtre glissante ~60j) — approximation du spread forward-looking SR1ZQ utilisé par CME SOFRWatch (données de marché propriétaires non accessibles gratuitement).", note: "Spreads = moyennes réalisées historiques EFFR/SOFR (FRED, fenêtre glissante ~2 semaines) — approximation du spread forward-looking SR1ZQ utilisé par CME SOFRWatch (données de marché propriétaires non accessibles gratuitement).",
}; };
return NextResponse.json(data, { headers: { "Cache-Control": "s-maxage=21600, stale-while-revalidate=43200" } }); }
async function buildEur(key: string): Promise<ScenarioSpreads | { error: string }> {
const estr = await fredObs("ECBESTRVOLWGTTRMDMNRT", key, SPREAD_WINDOW);
if (!estr.length) return { error: "Données FRED €STR indisponibles" };
const rateDec = (rateDecisionsData as RateDecisions)[0];
const mro = rateDec?.decisions?.EUR?.current ?? null; // rate_decisions.json stocke le MRO
const dfr = mro !== null ? parseFloat((mro - 0.15).toFixed(2)) : null; // corridor ECB: MRO = DFR+15bps
const estrSpread = avgSpread(estr, dfr);
return {
currency: "EUR",
asOf: new Date().toISOString().slice(0, 10),
targetRate: dfr,
targetLabel: "DFR (taux de dépôt BCE)",
refRateLabel: "€STR",
marketConversionOffset: -0.15, // impliedRate (MRO) -> DFR
spreads: [
{ label: "€STR DFR", bps: estrSpread !== null ? Math.round(estrSpread * 100) : null },
],
windowDays: estr.length,
note: "Spread = moyenne réalisée historique €STR/DFR (FRED, fenêtre glissante ~2 semaines) — approximation ; pas d'équivalent public d'un spread forward-looking type SR1ZQ pour l'EUR.",
};
}
async function buildGbp(key: string): Promise<ScenarioSpreads | { error: string }> {
const sonia = await fredObs("IUDSOIA", key, SPREAD_WINDOW);
if (!sonia.length) return { error: "Données FRED SONIA indisponibles" };
const rateDec = (rateDecisionsData as RateDecisions)[0];
const bankRate = rateDec?.decisions?.GBP?.current ?? null;
const soniaSpread = avgSpread(sonia, bankRate);
return {
currency: "GBP",
asOf: new Date().toISOString().slice(0, 10),
targetRate: bankRate,
targetLabel: "Bank Rate (BoE)",
refRateLabel: "SONIA",
marketConversionOffset: 0, // impliedRate déjà en convention Bank Rate directe
spreads: [
{ label: "SONIA Bank Rate", bps: soniaSpread !== null ? Math.round(soniaSpread * 100) : null },
],
windowDays: sonia.length,
note: "Spread = moyenne réalisée historique SONIA/Bank Rate (FRED, fenêtre glissante ~2 semaines) — approximation ; la BoE a arrêté de publier ses propres fonctions de densité de probabilité implicites par options (short sterling), donc pas de spread forward-looking public disponible.",
};
}
export async function GET(req: NextRequest) {
const key = process.env.FRED_API_KEY;
if (!key) return NextResponse.json({ error: "FRED_API_KEY missing" }, { status: 500 });
const currency = (req.nextUrl.searchParams.get("currency") ?? "USD").toUpperCase();
const builder = currency === "EUR" ? buildEur : currency === "GBP" ? buildGbp : buildUsd;
const result = await builder(key);
if ("error" in result) return NextResponse.json(result, { status: 502 });
return NextResponse.json(result, { headers: { "Cache-Control": "s-maxage=21600, stale-while-revalidate=43200" } });
} }
+105 -59
View File
@@ -1,19 +1,21 @@
"use client"; "use client";
// components/ScenarioTab.tsx // components/ScenarioTab.tsx
// Simulateur "scénario → prix implicite", façon CME SOFRWatch — logique // Simulateur "scénario → prix implicite", façon CME SOFRWatch / €STRWatch —
// INVERSÉE par rapport aux autres onglets Taux (qui déduisent une probabilité // logique INVERSÉE par rapport aux autres onglets Taux (qui déduisent une
// depuis les prix de marché) : ici, l'utilisateur choisit lui-même un scénario // probabilité depuis les prix de marché) : ici, l'utilisateur choisit
// de hikes/cuts à chaque réunion FOMC, et l'outil en déduit où devrait se // lui-même un scénario de hikes/cuts à chaque réunion de banque centrale, et
// situer le SOFR implicite si ce scénario se réalisait. // 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 cible Fed → SOFR (méthodologie CME SOFRWatch, en 2 spreads) : // Lien taux directeur → taux de référence overnight (voir /api/scenario-spreads) :
// 1. EFFR borne basse de la fourchette cible (observé) // USD : SOFR borne basse Fed + (EFFR borne basse) + (SOFR EFFR)
// 2. SOFR EFFR (observé) // EUR : €STR ≈ DFR + (€STR DFR)
// SOFR implicite ≈ borne basse + spread 1 + spread 2 // GBP : SONIA ≈ Bank Rate + (SONIA Bank Rate)
// CME calcule ces spreads à partir des prix forward des futures SOFR 1 mois // CME calcule ces spreads (USD) à partir des prix forward des futures SOFR
// vs Fed Funds (SR1ZQ) — données de marché propriétaires. On les approxime // 1 mois vs Fed Funds (SR1ZQ) — données de marché propriétaires, pas
// ici avec la moyenne réalisée historique (FRED, gratuit) : voir /api/scenario-spreads. // 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 { useEffect, useState, useMemo } from "react";
import { RefreshCw, RotateCcw, Info } from "lucide-react"; import { RefreshCw, RotateCcw, Info } from "lucide-react";
@@ -21,49 +23,65 @@ import {
LineChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid, Legend, LineChart, Line, ResponsiveContainer, Tooltip, XAxis, YAxis, CartesianGrid, Legend,
} from "recharts"; } from "recharts";
interface ScenarioSpreads { type ScenarioCcy = "USD" | "EUR" | "GBP";
asOf: string;
lowerBound: number | null; interface ScenarioSpread {
upperBound: number | null; label: string;
effrSpreadBps: number | null; bps: number | null;
sofrEffrSpreadBps: number | null;
windowDays: number;
note: string;
} }
interface UsdMeeting { interface ScenarioSpreads {
dateIso: string; currency: ScenarioCcy;
label: string; asOf: string;
impliedRate: number; // convention upper bound, cf. rate_decisions.json 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 STEPS = [-50, -25, 0, 25, 50] as const;
const CCY_META: Record<ScenarioCcy, { flag: string; bank: string; meetingLabel: string }> = {
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 { function fmtDate(iso: string): string {
const d = new Date(iso + "T12:00:00Z"); const d = new Date(iso + "T12:00:00Z");
return d.toLocaleDateString("fr-FR", { month: "short", day: "numeric" }); return d.toLocaleDateString("fr-FR", { month: "short", day: "numeric" });
} }
export default function ScenarioTab() { export default function ScenarioTab() {
const [currency, setCurrency] = useState<ScenarioCcy>("USD");
const [spreads, setSpreads] = useState<ScenarioSpreads | null>(null); const [spreads, setSpreads] = useState<ScenarioSpreads | null>(null);
const [meetings, setMeetings] = useState<UsdMeeting[]>([]); const [meetings, setMeetings] = useState<Meeting[]>([]);
const [deltas, setDeltas] = useState<Record<string, number>>({}); const [deltas, setDeltas] = useState<Record<string, number>>({});
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null); const [error, setError] = useState<string | null>(null);
const load = async () => { const load = async (ccy: ScenarioCcy) => {
setLoading(true); setLoading(true);
setError(null); setError(null);
try { try {
const [spreadsRes, rpRes] = await Promise.all([ const [spreadsRes, rpRes] = await Promise.all([
fetch("/api/scenario-spreads", { cache: "no-store" }).then(r => r.json()), fetch(`/api/scenario-spreads?currency=${ccy}`, { cache: "no-store" }).then(r => r.json()),
fetch("/api/rate-probabilities", { 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); if (spreadsRes?.error) throw new Error(spreadsRes.error);
setSpreads(spreadsRes as ScenarioSpreads); setSpreads(spreadsRes as ScenarioSpreads);
const usd = rpRes?.data?.USD; const entry = rpRes?.data?.[ccy];
const ms: UsdMeeting[] = (usd?.meetings ?? []).map((m: { dateIso: string; label: string; impliedRate: number }) => ({ const ms: Meeting[] = (entry?.meetings ?? []).map((m: { dateIso: string; label: string; impliedRate: number }) => ({
dateIso: m.dateIso, label: m.label, impliedRate: m.impliedRate, dateIso: m.dateIso, label: m.label, impliedRate: m.impliedRate,
})); }));
setMeetings(ms); setMeetings(ms);
@@ -74,59 +92,84 @@ export default function ScenarioTab() {
} }
}; };
useEffect(() => { load(); }, []); useEffect(() => { setDeltas({}); load(currency); }, [currency]);
const setDelta = (dateIso: string, bps: number) => setDeltas(prev => ({ ...prev, [dateIso]: bps })); const setDelta = (dateIso: string, bps: number) => setDeltas(prev => ({ ...prev, [dateIso]: bps }));
const resetAll = () => setDeltas({}); const resetAll = () => setDeltas({});
const chartData = useMemo(() => { const totalSpreadBps = useMemo(
if (!spreads || spreads.lowerBound === null || !meetings.length) return []; () => spreads ? spreads.spreads.reduce((s, x) => s + (x.bps ?? 0), 0) : null,
const spreadTotal = (spreads.effrSpreadBps ?? 0) / 100 + (spreads.sofrEffrSpreadBps ?? 0) / 100; [spreads]
);
let cumulLower = spreads.lowerBound; 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 => { return meetings.map(m => {
const delta = deltas[m.dateIso] ?? 0; const delta = deltas[m.dateIso] ?? 0;
cumulLower = parseFloat((cumulLower + delta / 100).toFixed(4)); cumulTarget = parseFloat((cumulTarget + delta / 100).toFixed(4));
const scenarioSofr = parseFloat((cumulLower + spreadTotal).toFixed(4)); const scenarioRef = parseFloat((cumulTarget + spreadTotal).toFixed(4));
// Marché (OIS) : impliedRate suit la convention upper bound → on // Marché (OIS) : impliedRate suit la convention propre à /api/rate-probabilities
// convertit en borne basse pour rester sur la même échelle SOFR. // (borne haute pour USD, MRO pour EUR, Bank Rate direct pour GBP) → on la
const marketLower = parseFloat((m.impliedRate - 0.25).toFixed(4)); // convertit vers la même échelle que targetRate avant d'ajouter le spread.
const marketSofr = parseFloat((marketLower + spreadTotal).toFixed(4)); const marketTarget = parseFloat((m.impliedRate + spreads.marketConversionOffset).toFixed(4));
const marketRef = parseFloat((marketTarget + spreadTotal).toFixed(4));
return { return {
label: m.label, label: m.label,
dateIso: m.dateIso, dateIso: m.dateIso,
scenario: scenarioSofr, scenario: scenarioRef,
marche: marketSofr, marche: marketRef,
cumulBps: Math.round((cumulLower - spreads.lowerBound!) * 100), cumulBps: Math.round((cumulTarget - spreads.targetRate!) * 100),
}; };
}); });
}, [spreads, meetings, deltas]); }, [spreads, meetings, deltas, totalSpreadBps]);
const spreadTotalBps = spreads ? (spreads.effrSpreadBps ?? 0) + (spreads.sofrEffrSpreadBps ?? 0) : null; const currentImpliedRef = spreads?.targetRate !== null && totalSpreadBps !== null && spreads
const currentImpliedSofr = spreads?.lowerBound !== null && spreadTotalBps !== null && spreads ? parseFloat((spreads.targetRate + totalSpreadBps / 100).toFixed(2))
? parseFloat((spreads.lowerBound + spreadTotalBps / 100).toFixed(2))
: null; : null;
const meta = CCY_META[currency];
return ( return (
<div className="space-y-3"> <div className="space-y-3">
<div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-3"> <div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-3">
<div className="flex items-center justify-between mb-1"> <div className="flex items-center justify-between mb-2 flex-wrap gap-2">
<h2 className="text-sm font-semibold text-slate-200">Scénario simulateur taux SOFR implicite</h2> <h2 className="text-sm font-semibold text-slate-200">Scénario simulateur taux {spreads?.refRateLabel ?? "taux"} implicite</h2>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<button onClick={resetAll} className="flex items-center gap-1 text-[10px] text-slate-400 hover:text-slate-200 border border-slate-800 hover:border-slate-600 rounded-md px-2 py-1 transition-colors"> <button onClick={resetAll} className="flex items-center gap-1 text-[10px] text-slate-400 hover:text-slate-200 border border-slate-800 hover:border-slate-600 rounded-md px-2 py-1 transition-colors">
<RotateCcw size={11} /> Réinitialiser <RotateCcw size={11} /> Réinitialiser
</button> </button>
<button onClick={load} disabled={loading} className="flex items-center gap-1 text-[10px] text-slate-400 hover:text-slate-200 border border-slate-800 hover:border-slate-600 rounded-md px-2 py-1 transition-colors disabled:opacity-50"> <button onClick={() => load(currency)} disabled={loading} className="flex items-center gap-1 text-[10px] text-slate-400 hover:text-slate-200 border border-slate-800 hover:border-slate-600 rounded-md px-2 py-1 transition-colors disabled:opacity-50">
<RefreshCw size={11} className={loading ? "animate-spin" : ""} /> Rafraîchir <RefreshCw size={11} className={loading ? "animate-spin" : ""} /> Rafraîchir
</button> </button>
</div> </div>
</div> </div>
{/* Sélecteur de devise */}
<div className="flex gap-1.5 mb-2">
{(Object.keys(CCY_META) as ScenarioCcy[]).map(ccy => (
<button
key={ccy}
onClick={() => setCurrency(ccy)}
className={`flex items-center gap-1.5 text-[11px] font-semibold px-3 py-1 rounded-full border transition-colors ${
currency === ccy
? "bg-slate-700 border-slate-500 text-white"
: "border-slate-700/60 text-slate-400 hover:text-white"
}`}
>
<span>{CCY_META[ccy].flag}</span> {ccy} <span className="text-slate-500 font-normal">· {CCY_META[ccy].bank}</span>
</button>
))}
</div>
<p className="text-[10px] text-slate-600 leading-relaxed"> <p className="text-[10px] text-slate-600 leading-relaxed">
Logique inversée par rapport aux autres onglets Taux (qui déduisent une probabilité depuis les prix de marché) : 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&apos;outil déduit devrait se situer le SOFR choisissez vous-même un scénario de hikes/cuts à chaque {meta.meetingLabel}, l&apos;outil déduit devrait se situer
implicite si ce scénario se réalisait façon CME SOFRWatch. le {spreads?.refRateLabel ?? "taux de référence"} implicite si ce scénario se réalisait façon CME SOFRWatch / STRWatch.
</p> </p>
</div> </div>
@@ -144,20 +187,23 @@ export default function ScenarioTab() {
<div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-3"> <div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-3">
<div className="flex items-center gap-1.5 mb-2"> <div className="flex items-center gap-1.5 mb-2">
<Info size={11} className="text-slate-600" /> <Info size={11} className="text-slate-600" />
<span className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Fourchette Fed actuelle SOFR</span> <span className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">{spreads.targetLabel} {spreads.refRateLabel}</span>
</div> </div>
<div className="flex items-center gap-4 flex-wrap text-[11px]"> <div className="flex items-center gap-4 flex-wrap text-[11px]">
<span className="text-slate-400">Fourchette : <span className="text-slate-200 font-semibold font-mono">{spreads.lowerBound?.toFixed(2)}{spreads.upperBound?.toFixed(2)}%</span></span> <span className="text-slate-400">{spreads.targetLabel} : <span className="text-slate-200 font-semibold font-mono">{spreads.targetRate?.toFixed(2)}%</span></span>
<span className="text-slate-400">Spread EFFRborne basse : <span className="text-amber-400 font-semibold font-mono">{spreads.effrSpreadBps! >= 0 ? "+" : ""}{spreads.effrSpreadBps}bps</span></span> {spreads.spreads.map(s => (
<span className="text-slate-400">Spread SOFREFFR : <span className="text-sky-400 font-semibold font-mono">{spreads.sofrEffrSpreadBps! >= 0 ? "+" : ""}{spreads.sofrEffrSpreadBps}bps</span></span> <span key={s.label} className="text-slate-400">
<span className="text-slate-400">SOFR implicite actuel : <span className="text-slate-100 font-bold font-mono">{currentImpliedSofr?.toFixed(2)}%</span></span> Spread {s.label} : <span className="text-amber-400 font-semibold font-mono">{s.bps !== null ? `${s.bps >= 0 ? "+" : ""}${s.bps}bps` : "—"}</span>
</span>
))}
<span className="text-slate-400">{spreads.refRateLabel} implicite actuel : <span className="text-slate-100 font-bold font-mono">{currentImpliedRef?.toFixed(2)}%</span></span>
</div> </div>
<p className="text-[9px] text-slate-700 mt-2 leading-snug">{spreads.note}</p> <p className="text-[9px] text-slate-700 mt-2 leading-snug">{spreads.note}</p>
</div> </div>
{/* Constructeur de scénario */} {/* Constructeur de scénario */}
<div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-3"> <div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-3">
<span className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Votre scénario par réunion FOMC</span> <span className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">Votre scénario par {meta.meetingLabel}</span>
<div className="mt-2 space-y-1.5"> <div className="mt-2 space-y-1.5">
{meetings.map(m => { {meetings.map(m => {
const delta = deltas[m.dateIso] ?? 0; const delta = deltas[m.dateIso] ?? 0;
@@ -189,7 +235,7 @@ export default function ScenarioTab() {
{/* Chart comparatif */} {/* Chart comparatif */}
<div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-3"> <div className="bg-slate-900 border border-slate-800 rounded-xl px-4 py-3">
<span className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">SOFR implicite votre scénario vs marché (OIS)</span> <span className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">{spreads.refRateLabel} implicite votre scénario vs marché (OIS)</span>
<ResponsiveContainer width="100%" height={260}> <ResponsiveContainer width="100%" height={260}>
<LineChart data={chartData} margin={{ top: 12, right: 16, left: 0, bottom: 0 }}> <LineChart data={chartData} margin={{ top: 12, right: 16, left: 0, bottom: 0 }}>
<CartesianGrid stroke="#1e293b" strokeDasharray="3 3" /> <CartesianGrid stroke="#1e293b" strokeDasharray="3 3" />
+1 -1
View File
@@ -1 +1 @@
{"ts":1783535815952,"data":{"current":{"NZD":{"currency":"NZD","nextMeetingProbPct":82,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":62,"publishedDate":"2026-07-03"},"USD":{"currency":"USD","nextMeetingProbPct":17,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":30,"publishedDate":"2026-07-03"},"EUR":{"currency":"EUR","nextMeetingProbPct":28,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":24,"publishedDate":"2026-07-03"},"JPY":{"currency":"JPY","nextMeetingProbPct":1,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":20,"publishedDate":"2026-07-03"},"GBP":{"currency":"GBP","nextMeetingProbPct":7,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":18,"publishedDate":"2026-07-03"},"AUD":{"currency":"AUD","nextMeetingProbPct":15,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":10,"publishedDate":"2026-07-03"},"CAD":{"currency":"CAD","nextMeetingProbPct":9,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":8,"publishedDate":"2026-07-03"},"CHF":{"currency":"CHF","nextMeetingProbPct":4,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":5,"publishedDate":"2026-07-03"}},"prev":{"NZD":{"currency":"NZD","nextMeetingProbPct":62,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":55,"publishedDate":"2026-06-26"},"USD":{"currency":"USD","nextMeetingProbPct":30,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":31,"publishedDate":"2026-06-26"},"EUR":{"currency":"EUR","nextMeetingProbPct":31,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":27,"publishedDate":"2026-06-26"},"GBP":{"currency":"GBP","nextMeetingProbPct":14,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":20,"publishedDate":"2026-06-26"},"JPY":{"currency":"JPY","nextMeetingProbPct":3,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":19,"publishedDate":"2026-06-26"},"CAD":{"currency":"CAD","nextMeetingProbPct":4,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":18,"publishedDate":"2026-06-26"},"AUD":{"currency":"AUD","nextMeetingProbPct":13,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":8,"publishedDate":"2026-06-26"},"CHF":{"currency":"CHF","nextMeetingProbPct":6,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":5,"publishedDate":"2026-06-26"}},"prevDate":"2026-06-26"}} {"ts":1783543879291,"data":{"current":{"NZD":{"currency":"NZD","nextMeetingProbPct":82,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":62,"publishedDate":"2026-07-03"},"USD":{"currency":"USD","nextMeetingProbPct":17,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":30,"publishedDate":"2026-07-03"},"EUR":{"currency":"EUR","nextMeetingProbPct":28,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":24,"publishedDate":"2026-07-03"},"JPY":{"currency":"JPY","nextMeetingProbPct":1,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":20,"publishedDate":"2026-07-03"},"GBP":{"currency":"GBP","nextMeetingProbPct":7,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":18,"publishedDate":"2026-07-03"},"AUD":{"currency":"AUD","nextMeetingProbPct":15,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":10,"publishedDate":"2026-07-03"},"CAD":{"currency":"CAD","nextMeetingProbPct":9,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":8,"publishedDate":"2026-07-03"},"CHF":{"currency":"CHF","nextMeetingProbPct":4,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":5,"publishedDate":"2026-07-03"}},"prev":{"NZD":{"currency":"NZD","nextMeetingProbPct":62,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":55,"publishedDate":"2026-06-26"},"USD":{"currency":"USD","nextMeetingProbPct":30,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":31,"publishedDate":"2026-06-26"},"EUR":{"currency":"EUR","nextMeetingProbPct":31,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":27,"publishedDate":"2026-06-26"},"GBP":{"currency":"GBP","nextMeetingProbPct":14,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":20,"publishedDate":"2026-06-26"},"JPY":{"currency":"JPY","nextMeetingProbPct":3,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":19,"publishedDate":"2026-06-26"},"CAD":{"currency":"CAD","nextMeetingProbPct":4,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":18,"publishedDate":"2026-06-26"},"AUD":{"currency":"AUD","nextMeetingProbPct":13,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":8,"publishedDate":"2026-06-26"},"CHF":{"currency":"CHF","nextMeetingProbPct":6,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":5,"publishedDate":"2026-06-26"}},"prevDate":"2026-06-26"}}
+1 -1
View File
File diff suppressed because one or more lines are too long