mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-07-27 20:37:45 +00:00
eb416be780
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>
224 lines
8.3 KiB
TypeScript
224 lines
8.3 KiB
TypeScript
"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>
|
||
);
|
||
}
|