mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-25 02:18:07 +00:00
feat: redesign OIS block — SOFR live data primary, analyst (IL) secondary
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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
764070a63a
commit
ce53f97b5b
+135
-68
@@ -679,78 +679,145 @@ export default function CurrencyCard({
|
|||||||
{/* ════ APERÇU ════════════════════════════════════════════════════ */}
|
{/* ════ APERÇU ════════════════════════════════════════════════════ */}
|
||||||
{activeTab === "overview" && (
|
{activeTab === "overview" && (
|
||||||
<>
|
<>
|
||||||
{/* OIS mini-chart */}
|
{/* ── OIS block ───────────────────────────────────────────── */}
|
||||||
{ratePath && ratePath.meetings.length > 0 && (
|
{ratePath && ratePath.meetings.length > 0 && (() => {
|
||||||
<div className="bg-slate-800/40 rounded-xl border border-slate-700/30 p-3">
|
const yearEnd2026 = `${new Date().getFullYear()}-12-31`;
|
||||||
<div className="flex items-center justify-between mb-2">
|
const meets2026 = ratePath.meetings.filter(m => m.dateIso <= yearEnd2026);
|
||||||
<span className="text-[10px] text-slate-500 uppercase tracking-wider font-semibold">OIS · Probabilités de move</span>
|
const bpsYE = ratePath.yearEndImplied !== null
|
||||||
<span className="text-[10px] text-slate-600">au {ratePath.asOf}</span>
|
? Math.round((ratePath.yearEndImplied - ratePath.currentRate) * 100) : null;
|
||||||
</div>
|
const bpsCls = bpsYE === null ? "text-slate-400"
|
||||||
<ResponsiveContainer width="100%" height={48}>
|
: bpsYE < 0 ? "text-sky-400" : bpsYE > 0 ? "text-red-400" : "text-slate-400";
|
||||||
<LineChart
|
|
||||||
data={ratePath.meetings.map(m => ({
|
return (
|
||||||
label: m.label,
|
<div className="rounded-xl border border-slate-700/30 overflow-hidden">
|
||||||
prob: m.probMovePct,
|
|
||||||
}))}
|
{/* ── SECTION 1 : SOFR / OIS live ─────────────────────── */}
|
||||||
margin={{ top: 2, right: 0, left: 0, bottom: 0 }}
|
<div className="bg-slate-800/40 p-3">
|
||||||
>
|
{/* header */}
|
||||||
<Line type="monotone" dataKey="prob" stroke="#f59e0b" strokeWidth={1.5} dot={{ r: 2, fill: "#f59e0b" }} />
|
<div className="flex items-center justify-between mb-3">
|
||||||
<XAxis dataKey="label" tick={{ fontSize: 8, fill: "#64748b" }} axisLine={false} tickLine={false} />
|
<div className="flex items-center gap-2">
|
||||||
<Tooltip
|
<span className="text-[10px] text-slate-400 uppercase tracking-wider font-bold">OIS · SOFR Futures</span>
|
||||||
contentStyle={{ background: "#0f172a", border: "1px solid #334155", borderRadius: 6, fontSize: 10 }}
|
<span className="text-[8px] font-bold px-1.5 py-0.5 rounded bg-emerald-500/15 text-emerald-400 border border-emerald-500/25">LIVE</span>
|
||||||
labelStyle={{ color: "#94a3b8" }}
|
</div>
|
||||||
itemStyle={{ color: "#f59e0b" }}
|
<span className="text-[9px] text-slate-600">au {ratePath.asOf}</span>
|
||||||
formatter={(v: number) => [`${v.toFixed(0)}%`, "Probabilité"]}
|
|
||||||
/>
|
|
||||||
</LineChart>
|
|
||||||
</ResponsiveContainer>
|
|
||||||
{ratePath.peakMeeting && (
|
|
||||||
<>
|
|
||||||
<div className="flex items-center justify-between mt-1 text-[10px]">
|
|
||||||
<span className="text-slate-600">
|
|
||||||
Pic : <span className="text-slate-400">{ratePath.peakMeeting.label}</span>
|
|
||||||
</span>
|
|
||||||
<span className={ratePath.peakMeeting.probIsCut ? "text-sky-400 font-bold" : "text-red-400 font-bold"}>
|
|
||||||
{ratePath.peakMeeting.probMovePct.toFixed(0)}% {ratePath.peakMeeting.probIsCut ? "Cut" : "Hike"}
|
|
||||||
</span>
|
|
||||||
{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 (
|
|
||||||
<span className={cls} title={`Taux actuel: ${ratePath.currentRate.toFixed(2)}% → fin an: ${ratePath.yearEndImplied!.toFixed(2)}%`}>
|
|
||||||
{bps > 0 ? "+" : ""}{bps}bps fin an
|
|
||||||
</span>
|
|
||||||
);
|
|
||||||
})()}
|
|
||||||
</div>
|
</div>
|
||||||
{/* ── 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) && (
|
{/* taux actuel → fin d'an */}
|
||||||
<div className="flex items-center gap-2 mt-0.5 text-[9px] text-slate-600">
|
<div className="flex items-center gap-3 mb-3">
|
||||||
<span title={`vs article du ${ratePath.ilDelta.prevDate}`}>
|
<div>
|
||||||
vs sem. préc. :
|
<div className="text-[9px] text-slate-600 mb-0.5">Taux actuel</div>
|
||||||
</span>
|
<div className="text-[18px] font-bold text-slate-200 tabular-nums">{ratePath.currentRate.toFixed(2)}%</div>
|
||||||
<RpArrow
|
</div>
|
||||||
delta={ratePath.ilDelta.probDelta}
|
<div className="text-slate-700 text-lg">→</div>
|
||||||
isBearishIfPositive={ratePath.ilDelta.isCut}
|
<div>
|
||||||
suffix="%"
|
<div className="text-[9px] text-slate-600 mb-0.5">Fin d'an (Dec)</div>
|
||||||
strongT={10}
|
<div className={`text-[18px] font-bold tabular-nums ${bpsCls}`}>
|
||||||
modT={3}
|
{ratePath.yearEndImplied?.toFixed(2) ?? "—"}%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{bpsYE !== null && (
|
||||||
|
<div className={`ml-auto text-right`}>
|
||||||
|
<div className="text-[9px] text-slate-600 mb-0.5">Δ bps</div>
|
||||||
|
<div className={`text-[22px] font-black tabular-nums ${bpsCls}`}>
|
||||||
|
{bpsYE > 0 ? "+" : ""}{bpsYE}
|
||||||
|
<span className="text-[11px] font-semibold ml-0.5">bps</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* chart probabilités */}
|
||||||
|
<ResponsiveContainer width="100%" height={44}>
|
||||||
|
<LineChart
|
||||||
|
data={ratePath.meetings.map(m => ({ label: m.label, prob: m.probMovePct }))}
|
||||||
|
margin={{ top: 2, right: 0, left: 0, bottom: 0 }}
|
||||||
|
>
|
||||||
|
<Line type="monotone" dataKey="prob" stroke="#f59e0b" strokeWidth={1.5} dot={{ r: 2, fill: "#f59e0b" }} />
|
||||||
|
<XAxis dataKey="label" tick={{ fontSize: 8, fill: "#64748b" }} axisLine={false} tickLine={false} />
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{ background: "#0f172a", border: "1px solid #334155", borderRadius: 6, fontSize: 10 }}
|
||||||
|
labelStyle={{ color: "#94a3b8" }}
|
||||||
|
itemStyle={{ color: "#f59e0b" }}
|
||||||
|
formatter={(v: number) => [`${v.toFixed(0)}%`, "Prob. move"]}
|
||||||
/>
|
/>
|
||||||
{ratePath.ilDelta.bpsDelta !== 0 && (
|
</LineChart>
|
||||||
<RpArrow
|
</ResponsiveContainer>
|
||||||
delta={ratePath.ilDelta.bpsDelta}
|
|
||||||
isBearishIfPositive={true}
|
{/* tableau des réunions 2026 */}
|
||||||
suffix="bps"
|
{meets2026.length > 0 && (
|
||||||
strongT={25}
|
<div className="mt-2 space-y-0.5">
|
||||||
modT={10}
|
{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 (
|
||||||
|
<div key={m.dateIso} className={`flex items-center justify-between text-[10px] px-1 rounded ${isPeak ? "bg-amber-500/10" : ""}`}>
|
||||||
|
<span className={isPeak ? "text-amber-300 font-semibold" : "text-slate-500"}>
|
||||||
|
{isPeak && <span className="mr-1">●</span>}{m.label}
|
||||||
|
</span>
|
||||||
|
<span className={`font-bold tabular-nums ${dirCls}`}>{dir}</span>
|
||||||
|
<span className={`tabular-nums ${m.probMovePct >= 20 ? "text-slate-300" : "text-slate-600"}`}>
|
||||||
|
{m.probMovePct.toFixed(0)}%
|
||||||
|
</span>
|
||||||
|
<span className="text-slate-600 tabular-nums">{m.impliedRate.toFixed(3)}%</span>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</>
|
</div>
|
||||||
)}
|
|
||||||
</div>
|
{/* ── SECTION 2 : Analyste G. Dellamotta ──────────────── */}
|
||||||
)}
|
{ratePath.ilCurrent && (
|
||||||
|
<div className="border-t border-slate-700/40 bg-slate-900/40 p-3">
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<span className="text-[9px] text-slate-500 uppercase tracking-wider font-semibold">Analyste · G. Dellamotta</span>
|
||||||
|
<span className="text-[9px] text-slate-700">{ratePath.ilCurrent.articleDate}</span>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center gap-4">
|
||||||
|
{/* bps year-end analyste */}
|
||||||
|
<div>
|
||||||
|
<div className="text-[9px] text-slate-600 mb-0.5">Fin d'an (IL)</div>
|
||||||
|
<div className={`text-[15px] font-bold tabular-nums ${
|
||||||
|
ratePath.ilCurrent.isCut ? "text-sky-400"
|
||||||
|
: ratePath.ilCurrent.isNoChange ? "text-slate-400"
|
||||||
|
: "text-red-400"
|
||||||
|
}`}>
|
||||||
|
{ratePath.ilCurrent.isCut ? "" : "+"}{ratePath.ilCurrent.bpsYearEnd}
|
||||||
|
<span className="text-[10px] font-medium ml-0.5">bps</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* prob next meeting analyste */}
|
||||||
|
<div>
|
||||||
|
<div className="text-[9px] text-slate-600 mb-0.5">Proch. réunion</div>
|
||||||
|
<div className="text-[12px] font-semibold text-slate-300 tabular-nums">
|
||||||
|
{ratePath.ilCurrent.isNoChange
|
||||||
|
? <span className="text-slate-500">{ratePath.ilCurrent.probPct.toFixed(0)}% Hold</span>
|
||||||
|
: <span className={ratePath.ilCurrent.isCut ? "text-sky-400" : "text-red-400"}>
|
||||||
|
{ratePath.ilCurrent.probPct.toFixed(0)}% {ratePath.ilCurrent.isCut ? "Cut" : "Hike"}
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* delta hebdo */}
|
||||||
|
{ratePath.ilDelta && (Math.abs(ratePath.ilDelta.probDelta) >= 3 || Math.abs(ratePath.ilDelta.bpsDelta) >= 5) && (
|
||||||
|
<div className="ml-auto">
|
||||||
|
<div className="text-[9px] text-slate-600 mb-0.5">vs sem. préc.</div>
|
||||||
|
<div className="flex items-center gap-1.5">
|
||||||
|
<RpArrow delta={ratePath.ilDelta.probDelta} isBearishIfPositive={ratePath.ilDelta.isCut} suffix="%" strongT={10} modT={3} />
|
||||||
|
<RpArrow delta={ratePath.ilDelta.bpsDelta} isBearishIfPositive={true} suffix="bps" strongT={25} modT={5} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
|
||||||
{/* Divergence / convergence signal — expandable */}
|
{/* Divergence / convergence signal — expandable */}
|
||||||
{divergenceSignal !== "neutral" && (
|
{divergenceSignal !== "neutral" && (
|
||||||
|
|||||||
+19
-2
@@ -24,13 +24,22 @@ export interface ILWeeklyDelta {
|
|||||||
prevDate: string; // date de l'article de référence (semaine précédente)
|
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 {
|
export interface CBRatePath {
|
||||||
currency: Currency;
|
currency: Currency;
|
||||||
asOf: string; // "2026-05-31"
|
asOf: string; // "2026-05-31"
|
||||||
currentRate: number;
|
currentRate: number;
|
||||||
meetings: RateProbMeeting[];
|
meetings: RateProbMeeting[];
|
||||||
peakMeeting: RateProbMeeting | null; // réunion avec proba max de mouvement
|
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
|
ilDelta?: ILWeeklyDelta; // delta vs article IL semaine précédente
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,7 +265,15 @@ export async function fetchAllCBPaths(): Promise<RateProbData> {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return data;
|
||||||
|
|||||||
Reference in New Issue
Block a user