feat: Atlanta Fed MPT (options SOFR) + libellé méthodologie Réunions USD

Nouveau sous-onglet "Atlanta Fed" dans le bloc OIS de la card USD, méthodologie
alternative aux 30-day Fed Fund Futures demandée : distribution du niveau de
taux Fed déduite des options sur futures SOFR 3 mois (CME), publiée
quotidiennement par la Fed d'Atlanta (Market Probability Tracker). Affiche
Cut/Hold/Hike, la fourchette de taux SOFR implicite (25e-75e percentile), et
la distribution complète par fourchette de 25bps pour la fenêtre trimestrielle
la plus proche.

- .github/scripts/fetch-atlanta-mpt.mjs : télécharge et parse mpt_histdata.xlsx
  (Atlanta Fed) avec un lecteur ZIP/OOXML maison — le paquet npm "xlsx" a des
  CVE critiques non patchées sur le registre public (SheetJS a arrêté d'y
  publier), donc pas de dépendance ajoutée pour ça.
- .github/workflows/fetch-atlanta-mpt.yml : quotidien (donnée mise à jour 1x/jour
  par l'Atlanta Fed), + déclenchement manuel.
- lib/atlantaFedMpt.ts + app/api/macro/route.ts : expose la donnée (USD only)
  via l'API macro existante.

Sous-onglet "Réunions" (USD) : ajout du libellé de méthodologie demandé —
"Probabilités = somme des % associés à chaque fourchette au-dessus/en-dessous
de la fourchette actuelle · Investing.com Fed Rate Monitor, calculées à partir
des 30-day Fed Fund Futures (CME)".

Vérifié en direct : les deux sous-onglets s'affichent et les chiffres
correspondent aux données brutes (72.4% hike / 1.1% cut / distribution
27.2-45.7-21.4-5.7% pour la fenêtre Sep 2026).

Note en marge (hors scope, découverte pendant les tests) : /api/expectations
prend ~24s à répondre, ce qui bloque l'affichage complet du dashboard
(Promise.allSettled attend les 8 fetches). À investiguer séparément.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
caty21
2026-07-08 22:05:35 +02:00
co-authored by Claude Sonnet 5
parent 8c6f96efd7
commit 274c7f534e
8 changed files with 650 additions and 9 deletions
+80 -7
View File
@@ -16,6 +16,7 @@ import { biasLabel, calcMacroScore } from "@/lib/scoring";
import { saveCache, loadCache, formatCacheDate } from "@/lib/localCache";
import type { Currency, BiasPhase, RateExpectation, MacroSection } from "@/lib/types";
import type { CBRatePath, ILWeeklyDelta } from "@/lib/rateprobability";
import type { AtlantaFedMpt } from "@/lib/atlantaFedMpt";
import type { SentimentEntry, CotEntry } from "@/lib/types";
import type { CalendarEvent } from "@/app/api/calendar/route";
import NarrativeButton from "./NarrativeButton";
@@ -48,6 +49,7 @@ interface MacroData {
indicators: Record<string, Ind | null>;
forecasts?: MacroForecasts | null;
moneySupplyM3?: MoneySupplyM3 | null;
atlantaFedMpt?: AtlantaFedMpt | null;
fetchedAt: string;
}
@@ -67,8 +69,8 @@ interface Props {
onCardTabChange?: (id: "overview" | "mispricing" | "focus") => void;
syncSignauxSlide?: "ois" | "cot" | "sent";
onSignauxSlideChange?: (id: "ois" | "cot" | "sent") => void;
syncOisChartTab?: "curve" | "probas" | "meetings";
onOisChartTabChange?: (id: "curve" | "probas" | "meetings") => void;
syncOisChartTab?: "curve" | "probas" | "meetings" | "atlanta";
onOisChartTabChange?: (id: "curve" | "probas" | "meetings" | "atlanta") => void;
isLoading?: boolean;
}
@@ -504,14 +506,15 @@ const STIR_INSTRUMENT: Partial<Record<string, { instrument: string; exchange: st
NZD: { instrument: "OIS NZD (swaps)", exchange: "ASX OTC / Bloomberg NDOIS1M", convention: "maturité exacte / réunion", note: "Pas de futures standardisés. RBNZ publie les probas dans ses MPS." },
};
function OISEnhancedBlock({ ratePath, syncChartTab, onChartTabChange }: {
function OISEnhancedBlock({ ratePath, syncChartTab, onChartTabChange, atlantaMpt }: {
ratePath: CBRatePath;
syncChartTab?: "curve" | "probas" | "meetings";
onChartTabChange?: (id: "curve" | "probas" | "meetings") => void;
syncChartTab?: "curve" | "probas" | "meetings" | "atlanta";
onChartTabChange?: (id: "curve" | "probas" | "meetings" | "atlanta") => void;
atlantaMpt?: AtlantaFedMpt | null;
}) {
const [localChartTab, setLocalChartTab] = useState<"curve" | "probas" | "meetings">("curve");
const [localChartTab, setLocalChartTab] = useState<"curve" | "probas" | "meetings" | "atlanta">("curve");
const chartTab = syncChartTab ?? localChartTab;
const setChartTab = (id: "curve" | "probas" | "meetings") => {
const setChartTab = (id: "curve" | "probas" | "meetings" | "atlanta") => {
setLocalChartTab(id);
onChartTabChange?.(id);
};
@@ -623,6 +626,7 @@ function OISEnhancedBlock({ ratePath, syncChartTab, onChartTabChange }: {
{ id: "curve" as const, label: "Courbe" },
{ id: "probas" as const, label: "Probabilités" },
{ id: "meetings" as const, label: "Réunions" },
...(ratePath.currency === "USD" && atlantaMpt ? [{ id: "atlanta" as const, label: "Atlanta Fed" }] : []),
]).map(t => (
<button
key={t.id}
@@ -811,9 +815,77 @@ function OISEnhancedBlock({ ratePath, syncChartTab, onChartTabChange }: {
);
})}
</div>
{ratePath.currency === "USD" && (
<p className="mt-1.5 text-[7px] text-slate-700 leading-snug">
Probabilités = somme des % associés à chaque fourchette de taux au-dessus/en-dessous de la fourchette actuelle · Investing.com Fed Rate Monitor, calculées à partir des 30-day Fed Fund Futures (CME).
</p>
)}
</div>
)}
{/* Atlanta Fed MPT — méthodologie alternative (options sur futures SOFR) */}
{chartTab === "atlanta" && atlantaMpt && (() => {
const front = atlantaMpt.windows[0];
if (!front) return null;
const holdPct = front.probHikePct !== null && front.probCutPct !== null
? Math.max(0, +(100 - front.probHikePct - front.probCutPct).toFixed(1))
: null;
return (
<div>
<div className="flex items-center justify-between mb-1.5">
<span className="text-[7px] text-slate-600 uppercase tracking-wider">Fenêtre SOFR 3M {front.windowStartIso}</span>
<span className="text-[7px] text-slate-700">au {atlantaMpt.asOf}</span>
</div>
<div className="flex h-[6px] rounded-full overflow-hidden bg-slate-700/20 mb-1.5">
{front.probCutPct !== null && front.probCutPct > 0 && (
<div className="h-full bg-sky-500/60" style={{ width: `${front.probCutPct}%` }} title={`Cut ${front.probCutPct}%`} />
)}
{holdPct !== null && holdPct > 0 && (
<div className="h-full bg-slate-500/40" style={{ width: `${holdPct}%` }} title={`Hold ${holdPct}%`} />
)}
{front.probHikePct !== null && front.probHikePct > 0 && (
<div className="h-full bg-red-500/60" style={{ width: `${front.probHikePct}%` }} title={`Hike ${front.probHikePct}%`} />
)}
</div>
<div className="flex items-center gap-3 text-[9px] mb-2">
<span className="text-sky-400 font-semibold">Cut {front.probCutPct?.toFixed(1) ?? "—"}%</span>
<span className="text-slate-400 font-semibold">Hold {holdPct?.toFixed(1) ?? "—"}%</span>
<span className="text-red-400 font-semibold">Hike {front.probHikePct?.toFixed(1) ?? "—"}%</span>
</div>
{front.rate25 !== null && front.rate75 !== null && (
<div className="text-[9px] text-slate-500 mb-2">
Taux SOFR composé implicite : <span className="text-slate-200 font-mono font-semibold">{(front.rate25 / 100).toFixed(2)}%</span> <span className="text-slate-200 font-mono font-semibold">{(front.rate75 / 100).toFixed(2)}%</span>
<span className="text-slate-600"> (25e75e percentile{front.rateMode !== null ? `, mode ${(front.rateMode / 100).toFixed(2)}%` : ""})</span>
</div>
)}
<span className="text-[7px] text-slate-600 uppercase tracking-wider">Distribution complète (fenêtre)</span>
<div className="space-y-[2px] mt-1">
{front.distribution.map(b => {
const m = b.rangeLabel.match(/(\d+)bps\s*-\s*(\d+)bps/);
const label = m ? `${(parseInt(m[1]) / 100).toFixed(2)}-${(parseInt(m[2]) / 100).toFixed(2)}%` : b.rangeLabel;
const isAnchor = b.rangeLabel === front.anchorRange;
return (
<div key={b.rangeLabel} className={`flex items-center gap-1.5 px-1 py-[2px] rounded ${isAnchor ? "bg-amber-500/8" : ""}`}>
<span className={`text-[8px] w-[70px] shrink-0 font-mono tabular-nums ${isAnchor ? "text-amber-300 font-bold" : "text-slate-500"}`}>{label}</span>
<div className="flex-1 bg-slate-700/20 rounded-full h-[4px] overflow-hidden">
<div className="h-full rounded-full bg-amber-500/50" style={{ width: `${b.probPct}%` }} />
</div>
<span className="text-[8px] text-slate-400 w-[32px] text-right tabular-nums shrink-0">{b.probPct.toFixed(1)}%</span>
</div>
);
})}
</div>
<p className="mt-1.5 text-[7px] text-slate-700 leading-snug">
Distribution déduite des options sur futures SOFR 3 mois (CME), taux SOFR composé sur la fenêtre méthodologie Atlanta Fed (Market Probability Tracker), alternative aux 30-day Fed Fund Futures. Mise à jour quotidienne.
</p>
</div>
);
})()}
</div>
{/* ── IL footer (analyste InvestingLive) + source STIR ───────────────── */}
@@ -1809,6 +1881,7 @@ export default function CurrencyCard({
ratePath={ratePath}
syncChartTab={syncOisChartTab}
onChartTabChange={onOisChartTabChange}
atlantaMpt={data?.atlantaFedMpt}
/>
)}
{/* OIS — état indisponible */}