Files
forex-dashboard/components/DriversBar.tsx
T
Capucine Gest eb416be780 feat: calendrier économique TE+Investing, OIS SNB, yields 10Y, taux directeurs corrects
Calendrier économique
- Nouvelles sources : TE HTML scraping (483 events/2 sem.) + Investing.com en co-primaires
- Dédupe déterministe par clé currency+category+date (±1j pour policy_rate)
- Filtre catégorie "other", filtre discours CB (fix doublon TE vs Investing)
- CHF/Switzerland via page séparée TE (hors G20)
- Fenêtre 14 jours avec ?startDate=&endDate= passés à TE
- ForexFactory+FRED en secours uniquement si les deux scrapers tombent à vide

Probabilités OIS
- rateprobability.com (7 CBs : FED/ECB/BOJ/BOE/BOC/RBA/RBNZ)
- investinglive.com (Dellamotta) : scan URL-date 14 jours → couvre SNB/CHF manquant
- SNB : 3% probabilité hausse au 19 juin 2026 (source : IL article 2026-05-29)
- rate_expectations.json MAJ : RBNZ 79% hike (corrigé depuis 70% no-change stale)
- Dédup ±1 jour pour policy_rate (fix doublon SNB TE-18/06 vs IL-19/06)

Taux directeurs
- USD : scrapeTeRate() = 3.75% (fin midpoint FRED 3.625%)
- AUD prev : 4.10% (corrigé depuis 4.60%)
- NZD/CHF/JPY/CAD : cohérents avec TE officiel

Yields 10Y souverains
- Nouvelle lib lib/tebonds.ts : scrape TE bonds, données du jour, cache 1h
- Remplace FRED IRLTLT01XXM156N mensuel (JPY/CHF/AUD/NZD avaient 1 mois de retard)
- 8 devises + spread vs USD calculé automatiquement
- /api/yields et /api/drivers utilisent la même lib

Drivers globaux
- BTC : ticker/price Binance (plus précis que 24hr lastPrice)
- HY Spread cache : 86400s → 3600s (données FRED du jour)
- DriversBar : suppression affichage direct US 10Y, tooltip Crb 2-10 affiche US10Y+US2Y

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-01 23:16:18 +02:00

224 lines
8.3 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useRef, useCallback } from "react";
import { createPortal } from "react-dom";
import type { DriverData } from "@/lib/types";
interface Props { drivers: DriverData }
// ── Helpers ───────────────────────────────────────────────────────────────────
function fmt(v: number | null, dec: number, unit = "") {
if (v === null) return "—";
return `${v.toLocaleString("fr-FR", { minimumFractionDigits: dec, maximumFractionDigits: dec })}${unit}`;
}
function DeltaTag({ delta, pct = false, dec = 2 }: { delta: number | null; pct?: boolean; dec?: number }) {
if (delta === null) return null;
const pos = delta > 0;
const neg = delta < 0;
const color = pos ? "text-emerald-600" : neg ? "text-red-500" : "text-gray-400";
const arrow = pos ? "▲" : neg ? "▼" : "▬";
return (
<span className={`text-[10px] font-medium tabular-nums ml-0.5 ${color}`}>
{arrow}{Math.abs(delta).toFixed(dec)}{pct ? "%" : ""}
</span>
);
}
// ── Tooltip via portal (évite le clipping de overflow-x-auto) ─────────────────
interface TooltipState { x: number; y: number }
function TooltipLabel({ label, tooltip }: { label: string; tooltip: string }) {
const [tip, setTip] = useState<TooltipState | null>(null);
const ref = useRef<HTMLSpanElement>(null);
const show = useCallback(() => {
if (!ref.current) return;
const r = ref.current.getBoundingClientRect();
setTip({ x: r.left + r.width / 2, y: r.top });
}, []);
const hide = useCallback(() => setTip(null), []);
return (
<>
<span
ref={ref}
onMouseEnter={show}
onMouseLeave={hide}
className="flex items-center gap-0.5 cursor-help whitespace-nowrap"
>
<span className="text-[9px] text-gray-400 leading-tight">{label}</span>
<span className="w-3 h-3 flex-shrink-0 rounded-full border border-gray-300 text-gray-400 text-[8px] flex items-center justify-center leading-none select-none">
i
</span>
</span>
{tip && typeof document !== "undefined" && createPortal(
<div
className="fixed z-[9999] bg-gray-800 text-white text-[10px] rounded px-2.5 py-2 w-60 leading-snug shadow-xl pointer-events-none text-left"
style={{
left: tip.x,
top: tip.y - 8,
transform: "translate(-50%, -100%)",
}}
>
{tooltip}
{/* Flèche en bas */}
<span
className="absolute top-full left-1/2 -translate-x-1/2 border-4 border-transparent border-t-gray-800"
style={{ marginTop: 0 }}
/>
</div>,
document.body,
)}
</>
);
}
/** Un indicateur : label · valeur · delta — hauteur fixe garantie */
function M({
label, value, dec = 2, unit = "", delta, deltaPct, deltaDec, tooltip,
}: {
label: string;
value: number | null;
dec?: number;
unit?: string;
delta?: number | null;
deltaPct?: boolean;
deltaDec?: number;
tooltip?: string;
}) {
return (
<div className="flex flex-col items-center text-center flex-shrink-0">
{/* Ligne 1 — label */}
<div className="h-4 flex items-center justify-center">
{tooltip
? <TooltipLabel label={label} tooltip={tooltip} />
: <span className="text-[9px] text-gray-400 whitespace-nowrap leading-tight">{label}</span>
}
</div>
{/* Ligne 2 — valeur */}
<div className="h-5 flex items-center justify-center">
<span className={`text-sm font-semibold tabular-nums ${value === null ? "text-gray-300" : "text-gray-800"}`}>
{fmt(value, dec, unit)}
</span>
</div>
{/* Ligne 3 — delta (slot réservé = alignement garanti) */}
<div className="h-4 flex items-center justify-center">
{delta !== undefined && delta !== null && (
<DeltaTag delta={delta} pct={deltaPct} dec={deltaDec ?? dec} />
)}
</div>
</div>
);
}
/** Séparateur vertical */
function VSep() {
return <div className="w-px bg-gray-100 self-stretch mx-2 flex-shrink-0" />;
}
// ── Composant principal ───────────────────────────────────────────────────────
export default function DriversBar({ drivers }: Props) {
const {
vix, vixDelta,
sp500, sp500ChangePct,
btc, btcChange24h,
hySpread, igSpread,
us10y, us2y, curveSlope,
gold, goldDelta,
silver, silverDelta,
brent, brentDelta,
wti, wtiDelta,
} = drivers as DriverData & { dxy?: number | null };
const dxy = (drivers as DriverData & { dxy?: number | null }).dxy ?? null;
const riskOff = (vix ?? 0) > 25 || (hySpread ?? 0) > 500;
return (
<div className="mb-4 bg-white border border-gray-200 rounded-xl px-5 py-3">
{/* En-tête */}
<div className="flex items-center justify-between mb-2">
<span className="text-[9px] font-semibold text-gray-400 uppercase tracking-widest">
Drivers globaux
</span>
{riskOff && (
<span className="text-[9px] font-medium bg-red-50 text-red-600 border border-red-200 rounded-full px-2 py-0.5">
Risk-Off
</span>
)}
</div>
{/* ── Ligne unique — tous les indicateurs ── */}
<div className="flex items-start gap-4 overflow-x-auto pb-0.5">
{/* Sentiment / Risk-On */}
<M
label="VIX" value={vix} dec={1} delta={vixDelta} deltaDec={1}
tooltip="Delta = clôture actuelle clôture précédente issue de Yahoo Finance (métrique intraday/close de session)."
/>
<M
label="S&P 500" value={sp500} dec={0} delta={sp500ChangePct} deltaPct deltaDec={2}
tooltip="Changement = clôture actuelle clôture précédente de Yahoo Finance. Le % est calculé sur la clôture précédente."
/>
<M
label="Bitcoin" value={btc} dec={0} unit=" $" delta={btcChange24h} deltaPct deltaDec={2}
tooltip="Variation 24h issue de Binance / CoinGecko via le ticker 24h (priceChangePercent)."
/>
<VSep />
{/* Crédit */}
<M
label="HY Spread" value={hySpread} dec={0} unit=" bps"
tooltip="High Yield spread : écart entre les obligations d'entreprises à haut risque (< BBB) et les Treasuries US. >500 bps = signal risk-off fort."
/>
<M
label="IG Spread" value={igSpread} dec={0} unit=" bps"
tooltip="Investment Grade spread : écart entre les obligations bien notées (BBB+) et les Treasuries US. Mesure le coût du crédit des grandes entreprises."
/>
<VSep />
{/* Taux & FX */}
<M
label="DXY" value={dxy} dec={2}
delta={(drivers as DriverData & { dxyDelta?: number | null }).dxyDelta ?? null}
deltaDec={2}
tooltip="ICE Dollar Index Futures (DX=F) via Yahoo Finance — temps réel, cache 5 min. Delta = clôture actuelle clôture précédente."
/>
<M
label="Crb 2-10" value={curveSlope} dec={0} unit=" bps"
tooltip={`Spread US 10Y US 2Y. Positif = courbe normale. Négatif = courbe inversée (signal récessif historiquement).\nUS 10Y: ${us10y != null ? us10y.toFixed(4) + "%" : "N/A"} | US 2Y: ${us2y != null ? us2y.toFixed(2) + "%" : "N/A"}`}
/>
<VSep />
{/* Commodités */}
<M
label="Or $/oz" value={gold} dec={0} delta={goldDelta} deltaDec={1}
tooltip="Delta intraday = close open issue de Stooq (métaux précieux)."
/>
<M
label="Argent $/oz" value={silver} dec={2} delta={silverDelta} deltaDec={2}
tooltip="Delta intraday = close open issue de Stooq (métaux précieux)."
/>
<M
label="Brent $/b" value={brent} dec={1} delta={brentDelta} deltaDec={1}
tooltip="Delta intraday = close open issue de Stooq (pétrole Brent)."
/>
<M
label="WTI $/b" value={wti} dec={1} delta={wtiDelta} deltaDec={1}
tooltip="Delta intraday = close open issue de Stooq (pétrole WTI)."
/>
</div>
</div>
);
}