From ce53f97b5b6d261a73217dd26276faa7d442e83a Mon Sep 17 00:00:00 2001 From: Capucine Gest Date: Mon, 8 Jun 2026 20:12:18 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20redesign=20OIS=20block=20=E2=80=94=20SO?= =?UTF-8?q?FR=20live=20data=20primary,=20analyst=20(IL)=20secondary?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Section 1 (SOFR/OIS live, prominent): - Large current rate → year-end implied + Δbps in bold colored type - Probability chart across all meetings - Per-meeting table: date, HOLD/HIKE?/HIKE, prob%, implied rate - Peak meeting highlighted in amber Section 2 (G. Dellamotta / InvestingLive, below): - Analyst year-end bps + next-meeting direction (from IL article) - Article date shown - Weekly delta arrows (vs previous article) — lowered bps threshold to 5bps Add ILCurrent type to CBRatePath to carry analyst absolute values server-side. Co-Authored-By: Claude Sonnet 4.6 --- components/CurrencyCard.tsx | 203 ++++++++++++++++++++++++------------ lib/rateprobability.ts | 21 +++- 2 files changed, 154 insertions(+), 70 deletions(-) diff --git a/components/CurrencyCard.tsx b/components/CurrencyCard.tsx index f4e50de..c8603f7 100644 --- a/components/CurrencyCard.tsx +++ b/components/CurrencyCard.tsx @@ -679,78 +679,145 @@ export default function CurrencyCard({ {/* ════ APERÇU ════════════════════════════════════════════════════ */} {activeTab === "overview" && ( <> - {/* OIS mini-chart */} - {ratePath && ratePath.meetings.length > 0 && ( -
-
- OIS · Probabilités de move - au {ratePath.asOf} -
- - ({ - label: m.label, - prob: m.probMovePct, - }))} - margin={{ top: 2, right: 0, left: 0, bottom: 0 }} - > - - - [`${v.toFixed(0)}%`, "Probabilité"]} - /> - - - {ratePath.peakMeeting && ( - <> -
- - Pic : {ratePath.peakMeeting.label} - - - {ratePath.peakMeeting.probMovePct.toFixed(0)}% {ratePath.peakMeeting.probIsCut ? "Cut" : "Hike"} - - {ratePath.yearEndImplied !== null && (() => { - const bps = Math.round((ratePath.yearEndImplied! - ratePath.currentRate) * 100); - const cls = bps < 0 ? "text-sky-400 font-bold" : bps > 0 ? "text-red-400 font-bold" : "text-slate-500"; - return ( - - {bps > 0 ? "+" : ""}{bps}bps fin an - - ); - })()} + {/* ── OIS block ───────────────────────────────────────────── */} + {ratePath && ratePath.meetings.length > 0 && (() => { + const yearEnd2026 = `${new Date().getFullYear()}-12-31`; + const meets2026 = ratePath.meetings.filter(m => m.dateIso <= yearEnd2026); + const bpsYE = ratePath.yearEndImplied !== null + ? Math.round((ratePath.yearEndImplied - ratePath.currentRate) * 100) : null; + const bpsCls = bpsYE === null ? "text-slate-400" + : bpsYE < 0 ? "text-sky-400" : bpsYE > 0 ? "text-red-400" : "text-slate-400"; + + return ( +
+ + {/* ── SECTION 1 : SOFR / OIS live ─────────────────────── */} +
+ {/* header */} +
+
+ OIS · SOFR Futures + LIVE +
+ au {ratePath.asOf}
- {/* ── Flèches de tendance vs article IL semaine précédente ── */} - {ratePath.ilDelta && (Math.abs(ratePath.ilDelta.probDelta) >= 3 || Math.abs(ratePath.ilDelta.bpsDelta) >= 10) && ( -
- - vs sem. préc. : - - +
+
Taux actuel
+
{ratePath.currentRate.toFixed(2)}%
+
+
+
+
Fin d'an (Dec)
+
+ {ratePath.yearEndImplied?.toFixed(2) ?? "—"}% +
+
+ {bpsYE !== null && ( +
+
Δ bps
+
+ {bpsYE > 0 ? "+" : ""}{bpsYE} + bps +
+
+ )} +
+ + {/* chart probabilités */} + + ({ label: m.label, prob: m.probMovePct }))} + margin={{ top: 2, right: 0, left: 0, bottom: 0 }} + > + + + [`${v.toFixed(0)}%`, "Prob. move"]} /> - {ratePath.ilDelta.bpsDelta !== 0 && ( - - )} + + + + {/* tableau des réunions 2026 */} + {meets2026.length > 0 && ( +
+ {meets2026.map(m => { + const isPeak = m.dateIso === ratePath.peakMeeting?.dateIso; + const dir = m.probMovePct < 20 ? "HOLD" + : m.probIsCut ? (m.probMovePct >= 50 ? "CUT" : "CUT?") + : (m.probMovePct >= 50 ? "HIKE" : "HIKE?"); + const dirCls = m.probMovePct < 20 ? "text-slate-600" + : m.probIsCut ? "text-sky-400" : "text-red-400"; + return ( +
+ + {isPeak && }{m.label} + + {dir} + = 20 ? "text-slate-300" : "text-slate-600"}`}> + {m.probMovePct.toFixed(0)}% + + {m.impliedRate.toFixed(3)}% +
+ ); + })}
)} - - )} -
- )} +
+ + {/* ── SECTION 2 : Analyste G. Dellamotta ──────────────── */} + {ratePath.ilCurrent && ( +
+
+ Analyste · G. Dellamotta + {ratePath.ilCurrent.articleDate} +
+
+ {/* bps year-end analyste */} +
+
Fin d'an (IL)
+
+ {ratePath.ilCurrent.isCut ? "" : "+"}{ratePath.ilCurrent.bpsYearEnd} + bps +
+
+ {/* prob next meeting analyste */} +
+
Proch. réunion
+
+ {ratePath.ilCurrent.isNoChange + ? {ratePath.ilCurrent.probPct.toFixed(0)}% Hold + : + {ratePath.ilCurrent.probPct.toFixed(0)}% {ratePath.ilCurrent.isCut ? "Cut" : "Hike"} + + } +
+
+ {/* delta hebdo */} + {ratePath.ilDelta && (Math.abs(ratePath.ilDelta.probDelta) >= 3 || Math.abs(ratePath.ilDelta.bpsDelta) >= 5) && ( +
+
vs sem. préc.
+
+ + +
+
+ )} +
+
+ )} +
+ ); + })()} {/* Divergence / convergence signal — expandable */} {divergenceSignal !== "neutral" && ( diff --git a/lib/rateprobability.ts b/lib/rateprobability.ts index aef5924..7e0f402 100644 --- a/lib/rateprobability.ts +++ b/lib/rateprobability.ts @@ -24,13 +24,22 @@ export interface ILWeeklyDelta { prevDate: string; // date de l'article de référence (semaine précédente) } +export interface ILCurrent { + bpsYearEnd: number; // bps fin d'an selon l'article IL courant + probPct: number; // probabilité de move à la prochaine réunion (IL) + isNoChange: boolean; // l'analyste anticipe un statu quo + isCut: boolean; // l'analyste anticipe une baisse + articleDate: string; // date de publication de l'article (YYYY-MM-DD) +} + export interface CBRatePath { currency: Currency; asOf: string; // "2026-05-31" currentRate: number; meetings: RateProbMeeting[]; peakMeeting: RateProbMeeting | null; // réunion avec proba max de mouvement - yearEndImplied: number | null; // taux impliqué à la dernière réunion connue + yearEndImplied: number | null; // taux impliqué à la dernière réunion connue (SOFR) + ilCurrent?: ILCurrent; // valeurs absolues de l'article IL courant ilDelta?: ILWeeklyDelta; // delta vs article IL semaine précédente } @@ -256,7 +265,15 @@ export async function fetchAllCBPaths(): Promise { }; } - data[ccy] = { ...path, yearEndImplied, ...(ilDelta ? { ilDelta } : {}) }; + const ilCurrent: ILCurrent = { + bpsYearEnd: ilEntry.bpsYearEnd, + probPct: ilEntry.nextMeetingProbPct, + isNoChange: ilEntry.nextMeetingIsNoChange, + isCut: !ilEntry.nextMeetingIsHike && !ilEntry.nextMeetingIsNoChange, + articleDate: ilEntry.publishedDate, + }; + + data[ccy] = { ...path, yearEndImplied, ilCurrent, ...(ilDelta ? { ilDelta } : {}) }; } return data;