fix: TvChart async=false (document.currentScript fix), YieldsTab dark theme + compact layout

This commit is contained in:
caty21
2026-06-28 11:28:45 +02:00
parent cb52cfad1a
commit d34dd8765e
2 changed files with 88 additions and 242 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ function mountEmbedWidget(
const script = document.createElement("script"); const script = document.createElement("script");
script.type = "text/javascript"; script.type = "text/javascript";
script.async = true; script.async = false; // false = exécution ordonnée → document.currentScript correctement défini
script.src = scriptSrc; script.src = scriptSrc;
script.text = JSON.stringify(config); script.text = JSON.stringify(config);
wrapper.appendChild(script); wrapper.appendChild(script);
+87 -241
View File
@@ -1,7 +1,5 @@
"use client"; "use client";
// YieldsTab — Rendements souverains 10Y : classement, opportunités carry, divergences
import { useMemo } from "react"; import { useMemo } from "react";
import { TrendingUp, TrendingDown, Minus, ArrowRight } from "lucide-react"; import { TrendingUp, TrendingDown, Minus, ArrowRight } from "lucide-react";
@@ -25,19 +23,13 @@ const CCY_FLAGS: Record<string, string> = {
AUD: "🇦🇺", CAD: "🇨🇦", CHF: "🇨🇭", NZD: "🇳🇿", AUD: "🇦🇺", CAD: "🇨🇦", CHF: "🇨🇭", NZD: "🇳🇿",
}; };
const CCY_COUNTRY: Record<string, string> = {
USD: "États-Unis", EUR: "Zone Euro", GBP: "Royaume-Uni",
JPY: "Japon", AUD: "Australie", CAD: "Canada",
CHF: "Suisse", NZD: "Nvl-Zélande",
};
function fmt(n: number | null, dec = 3): string { function fmt(n: number | null, dec = 3): string {
return n !== null ? n.toFixed(dec) + "%" : "—"; return n !== null ? n.toFixed(dec) + "%" : "—";
} }
function deltaColor(d: number | null) { function deltaColor(d: number | null) {
if (d === null || d === 0) return "text-gray-400"; if (d === null || d === 0) return "text-slate-500";
return d > 0 ? "text-green-600" : "text-red-600"; return d > 0 ? "text-emerald-400" : "text-red-400";
} }
function deltaIcon(d: number | null) { function deltaIcon(d: number | null) {
@@ -45,12 +37,11 @@ function deltaIcon(d: number | null) {
return d > 0 ? <TrendingUp size={10} /> : <TrendingDown size={10} />; return d > 0 ? <TrendingUp size={10} /> : <TrendingDown size={10} />;
} }
// Niveau d'écart en bps → label d'opportunité function carryStrength(bps: number): { label: string; color: string } {
function carryLabel(bps: number): { label: string; color: string } { if (bps >= 400) return { label: "Très fort", color: "text-emerald-400 bg-emerald-500/15 border-emerald-500/30" };
if (bps >= 400) return { label: "Très fort", color: "text-emerald-700 bg-emerald-50 border-emerald-200" }; if (bps >= 200) return { label: "Fort", color: "text-green-400 bg-green-500/15 border-green-500/30" };
if (bps >= 200) return { label: "Fort", color: "text-green-700 bg-green-50 border-green-200" }; if (bps >= 100) return { label: "Modéré", color: "text-yellow-400 bg-yellow-500/15 border-yellow-500/30" };
if (bps >= 100) return { label: "Modéré", color: "text-yellow-700 bg-yellow-50 border-yellow-200" }; return { label: "Faible", color: "text-slate-400 bg-slate-700/30 border-slate-600/30" };
return { label: "Faible", color: "text-gray-600 bg-gray-50 border-gray-200" };
} }
// ── Component ───────────────────────────────────────────────────────────────── // ── Component ─────────────────────────────────────────────────────────────────
@@ -58,7 +49,7 @@ function carryLabel(bps: number): { label: string; color: string } {
export default function YieldsTab({ yieldsData, fxDayPct }: Props) { export default function YieldsTab({ yieldsData, fxDayPct }: Props) {
if (!yieldsData) { if (!yieldsData) {
return ( return (
<div className="flex items-center justify-center h-48 text-gray-400 text-sm"> <div className="flex items-center justify-center h-48 text-slate-500 text-sm">
Chargement des rendements Chargement des rendements
</div> </div>
); );
@@ -66,7 +57,6 @@ export default function YieldsTab({ yieldsData, fxDayPct }: Props) {
const { yields, dayDeltas = {} } = yieldsData; const { yields, dayDeltas = {} } = yieldsData;
// Filtre les devises avec données, tri décroissant
const sorted = useMemo(() => { const sorted = useMemo(() => {
return (Object.entries(yields) as [string, number | null][]) return (Object.entries(yields) as [string, number | null][])
.filter(([, v]) => v !== null) .filter(([, v]) => v !== null)
@@ -75,16 +65,14 @@ export default function YieldsTab({ yieldsData, fxDayPct }: Props) {
if (sorted.length === 0) { if (sorted.length === 0) {
return ( return (
<div className="flex items-center justify-center h-48 text-gray-400 text-sm"> <div className="flex items-center justify-center h-48 text-slate-500 text-sm">
Données indisponibles Données indisponibles
</div> </div>
); );
} }
const maxYield = sorted[0][1]; const maxYield = sorted[0][1];
const minYield = sorted[sorted.length - 1][1];
// Top carry pairs (toutes combinaisons triées par écart décroissant)
const carryPairs = useMemo(() => { const carryPairs = useMemo(() => {
const pairs: { long: string; short: string; bps: number }[] = []; const pairs: { long: string; short: string; bps: number }[] = [];
for (let i = 0; i < sorted.length; i++) { for (let i = 0; i < sorted.length; i++) {
@@ -97,73 +85,37 @@ export default function YieldsTab({ yieldsData, fxDayPct }: Props) {
return pairs.sort((a, b) => b.bps - a.bps); return pairs.sort((a, b) => b.bps - a.bps);
}, [sorted]); }, [sorted]);
const topCarry = carryPairs.slice(0, 6); const topCarry = carryPairs.slice(0, 4);
const tightPairs = [...carryPairs].sort((a, b) => a.bps - b.bps).slice(0, 4);
// Divergences yield/FX : yield et devise vont dans des directions opposées
// Yield ↑ + devise ↓ → obligations vendues MAIS devise sous pression = signal de stress
// Yield ↓ + devise ↑ → obligations achetées MAIS devise forte = safe-haven ou anomalie
// Seuils : yield ≥ 2bp (0.02%) ET FX ≥ 0.1% pour filtrer le bruit
const divergences = useMemo(() => {
const YIELD_THRESHOLD = 0.02; // 2 basis points minimum
const FX_THRESHOLD = 0.1; // 0.1% FX move minimum
const result: { ccy: string; yieldDelta: number; fxPct: number; type: "stress" | "safehaven" }[] = [];
for (const [ccy] of sorted) {
const yd = dayDeltas[ccy] ?? null;
const fx = fxDayPct?.[ccy] ?? null;
if (yd === null || fx === null) continue;
if (Math.abs(yd) < YIELD_THRESHOLD || Math.abs(fx) < FX_THRESHOLD) continue;
const yieldUp = yd > 0;
const fxUp = fx > 0;
if (yieldUp && !fxUp) {
// Yield monte, devise baisse → signal de stress / pression vendeuse sur dettes
result.push({ ccy, yieldDelta: yd, fxPct: fx, type: "stress" });
} else if (!yieldUp && fxUp) {
// Yield baisse, devise monte → safe-haven ou anomalie (ex: JPY, CHF)
result.push({ ccy, yieldDelta: yd, fxPct: fx, type: "safehaven" });
}
}
return result.sort((a, b) => Math.abs(b.yieldDelta) + Math.abs(b.fxPct) - (Math.abs(a.yieldDelta) + Math.abs(a.fxPct)));
}, [sorted, dayDeltas, fxDayPct]);
// Movers du jour (triés par amplitude de variation)
const movers = useMemo(() => { const movers = useMemo(() => {
return sorted return sorted
.map(([ccy]) => ({ ccy, d: dayDeltas[ccy] ?? 0 })) .map(([ccy, yld]) => ({ ccy, yld, d: dayDeltas[ccy] ?? 0 }))
.filter(x => Math.abs(x.d) >= 0.001) .filter(x => Math.abs(x.d) >= 0.001)
.sort((a, b) => Math.abs(b.d) - Math.abs(a.d)); .sort((a, b) => Math.abs(b.d) - Math.abs(a.d));
}, [sorted, dayDeltas]); }, [sorted, dayDeltas]);
return ( return (
<div className="space-y-5"> <div className="space-y-4">
{/* ── Header ──────────────────────────────────────────────────────────── */} {/* ── Header ──────────────────────────────────────────────────────────── */}
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<div> <div>
<h2 className="text-sm font-semibold text-gray-800"> <h2 className="text-sm font-semibold text-slate-200">Rendements Souverains 10Y</h2>
Rendements Souverains 10Y <p className="text-[10px] text-slate-600 mt-0.5">Trading Economics · EUR = Bund allemand</p>
</h2>
<p className="text-[10px] text-gray-400 mt-0.5">
Source : Trading Economics (données du jour) · EUR = Bund allemand
</p>
</div> </div>
<div className="text-right text-[10px] text-gray-400"> <div className="flex items-center gap-3 text-[10px]">
<span className="font-semibold text-green-600">{sorted[0][0]} {fmt(maxYield)} max</span> <span className="text-emerald-400 font-semibold">{sorted[0][0]} {fmt(maxYield)} max</span>
<span className="mx-2 text-gray-300">|</span> <span className="text-slate-700">|</span>
<span className="font-semibold text-blue-600">{sorted.at(-1)![0]} {fmt(minYield)} min</span> <span className="text-sky-400 font-semibold">{sorted.at(-1)![0]} {fmt(sorted.at(-1)![1])} min</span>
</div> </div>
</div> </div>
{/* ── Classement barres horizontales ──────────────────────────────────── */} {/* ── Classement Yield 10Y ────────────────────────────────────────────── */}
<div className="bg-white border border-gray-100 rounded-xl p-4"> <div className="bg-slate-900 border border-slate-800 rounded-xl p-4">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-3"> <p className="text-[9px] font-semibold text-slate-500 uppercase tracking-widest mb-3">
Classement Yield 10Y Classement Yield 10Y
</p> </p>
<div className="space-y-2"> <div className="space-y-2.5">
{sorted.map(([ccy, yld], i) => { {sorted.map(([ccy, yld], i) => {
const pct = maxYield > 0 ? (yld / maxYield) * 100 : 0; const pct = maxYield > 0 ? (yld / maxYield) * 100 : 0;
const delta = dayDeltas[ccy] ?? null; const delta = dayDeltas[ccy] ?? null;
@@ -172,41 +124,28 @@ export default function YieldsTab({ yieldsData, fxDayPct }: Props) {
const barColor = isMax const barColor = isMax
? "bg-emerald-500" ? "bg-emerald-500"
: isMin : isMin
? "bg-blue-400" ? "bg-sky-400"
: yld > 3.5 : yld > 3.5 ? "bg-green-500"
? "bg-green-400" : yld > 2 ? "bg-yellow-500"
: yld > 2 : "bg-sky-500";
? "bg-yellow-400"
: "bg-blue-300";
return ( return (
<div key={ccy} className="flex items-center gap-3"> <div key={ccy} className="flex items-center gap-3">
{/* Devise */} <div className="w-24 flex items-center gap-1.5 shrink-0">
<div className="w-24 flex items-center gap-1.5 flex-shrink-0">
<span className="text-sm">{CCY_FLAGS[ccy]}</span> <span className="text-sm">{CCY_FLAGS[ccy]}</span>
<span className="text-xs font-semibold text-gray-700 w-7">{ccy}</span> <span className="text-[11px] font-semibold text-slate-300 w-7">{ccy}</span>
{isMax && ( {isMax && <span className="text-[8px] font-medium bg-emerald-500/20 text-emerald-400 rounded-full px-1.5">MAX</span>}
<span className="text-[9px] font-medium bg-emerald-100 text-emerald-700 rounded-full px-1">MAX</span> {isMin && <span className="text-[8px] font-medium bg-sky-500/20 text-sky-400 rounded-full px-1.5">MIN</span>}
)}
{isMin && (
<span className="text-[9px] font-medium bg-blue-100 text-blue-700 rounded-full px-1">MIN</span>
)}
</div> </div>
{/* Barre */} <div className="flex-1 bg-slate-700/40 rounded-full h-2 overflow-hidden">
<div className="flex-1 bg-gray-100 rounded-full h-2.5 overflow-hidden"> <div className={`h-full rounded-full transition-all duration-500 ${barColor}`} style={{ width: `${pct}%` }} />
<div
className={`h-full rounded-full transition-all duration-500 ${barColor}`}
style={{ width: `${pct}%` }}
/>
</div> </div>
{/* Yield */} <div className="w-16 text-right text-[11px] font-mono font-semibold text-slate-200">
<div className="w-16 text-right text-xs font-mono font-semibold text-gray-800">
{fmt(yld)} {fmt(yld)}
</div> </div>
{/* Delta jour */}
<div className={`w-16 text-right text-[10px] font-mono flex items-center justify-end gap-0.5 ${deltaColor(delta)}`}> <div className={`w-16 text-right text-[10px] font-mono flex items-center justify-end gap-0.5 ${deltaColor(delta)}`}>
{deltaIcon(delta)} {deltaIcon(delta)}
{delta !== null ? (delta >= 0 ? "+" : "") + delta.toFixed(3) : "—"} {delta !== null ? (delta >= 0 ? "+" : "") + delta.toFixed(3) : "—"}
@@ -217,179 +156,86 @@ export default function YieldsTab({ yieldsData, fxDayPct }: Props) {
</div> </div>
</div> </div>
{/* ── Deux colonnes : Top Carry + Spreads Étroits ─────────────────────── */} {/* ── Carry Trade + Movers ────────────────────────────────────────────── */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
{/* Top carry */} {/* Top Carry — compact */}
<div className="bg-white border border-gray-100 rounded-xl p-4"> <div className="bg-slate-900 border border-slate-800 rounded-xl p-4">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-3"> <p className="text-[9px] font-semibold text-slate-500 uppercase tracking-widest mb-3">
🚀 Top Carry Écarts les plus larges 🚀 Top Carry Trade
</p> </p>
<div className="space-y-2"> <div className="space-y-2">
{topCarry.map(({ long, short, bps }) => { {topCarry.map(({ long, short, bps }, i) => {
const { label, color } = carryLabel(bps); const { label, color } = carryStrength(bps);
return ( return (
<div key={`${long}${short}`} className="flex items-center justify-between gap-2"> <div key={`${long}${short}`} className="flex items-center gap-2">
<div className="flex items-center gap-1.5"> {i === 0 && <span className="text-[8px] text-amber-400 font-bold w-3"></span>}
<span className="text-xs">{CCY_FLAGS[long]}</span> {i !== 0 && <span className="text-[8px] text-slate-700 w-3">{i + 1}</span>}
<span className="text-xs font-semibold text-gray-700">{long}</span> <span className="text-[11px]">{CCY_FLAGS[long]}</span>
<ArrowRight size={10} className="text-gray-300" /> <span className="text-[10px] font-semibold text-slate-300">{long}</span>
<span className="text-xs">{CCY_FLAGS[short]}</span> <ArrowRight size={9} className="text-slate-600 shrink-0" />
<span className="text-xs font-semibold text-gray-700">{short}</span> <span className="text-[11px]">{CCY_FLAGS[short]}</span>
</div> <span className="text-[10px] font-semibold text-slate-300">{short}</span>
<div className="flex items-center gap-2"> <span className="ml-auto text-[10px] font-mono font-bold text-slate-200">+{bps}bp</span>
<span className="text-xs font-mono font-semibold text-gray-800"> <span className={`text-[8px] font-medium border rounded-full px-1.5 py-0.5 shrink-0 ${color}`}>
+{bps} bps {label}
</span> </span>
<span className={`text-[9px] font-medium border rounded-full px-1.5 py-0.5 ${color}`}>
{label}
</span>
</div>
</div> </div>
); );
})} })}
</div> </div>
<p className="text-[9px] text-gray-400 mt-3 border-t border-gray-50 pt-2"> <p className="text-[8px] text-slate-700 mt-3 pt-2 border-t border-slate-800">
Long = devise avec yield le plus élevé · Court = yield le plus faible. Long haut yield · Short bas yield · Emprunter la devise bon marché
Carry = emprunter la devise bon marché pour acheter la devise chère.
</p> </p>
</div> </div>
{/* Spreads étroits */} {/* Movers du jour — renforcé */}
<div className="bg-white border border-gray-100 rounded-xl p-4"> <div className="bg-slate-900 border border-slate-800 rounded-xl p-4">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-3"> <p className="text-[9px] font-semibold text-slate-500 uppercase tracking-widest mb-3">
🔄 Convergence Spreads les plus étroits 📊 Movers du Jour Yield 10Y
</p>
<div className="space-y-2">
{tightPairs.map(({ long, short, bps }) => (
<div key={`${long}${short}`} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5">
<span className="text-xs">{CCY_FLAGS[long]}</span>
<span className="text-xs font-semibold text-gray-700">{long}</span>
<span className="text-[9px] text-gray-400">/</span>
<span className="text-xs">{CCY_FLAGS[short]}</span>
<span className="text-xs font-semibold text-gray-700">{short}</span>
</div>
<div className="flex items-center gap-2">
<span className="text-xs font-mono font-semibold text-gray-600">
{bps} bps
</span>
<span className="text-[9px] font-medium border rounded-full px-1.5 py-0.5 bg-purple-50 text-purple-700 border-purple-200">
Range
</span>
</div>
</div>
))}
</div>
<p className="text-[9px] text-gray-400 mt-3 border-t border-gray-50 pt-2">
Spreads étroits = peu d'avantage de taux entre les deux devises.
La paire est davantage guidée par d'autres facteurs (sentiment, données macro).
</p>
</div>
</div>
{/* ── Divergences + Movers du jour ────────────────────────────────────── */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Divergences yield/FX */}
<div className="bg-white border border-gray-100 rounded-xl p-4">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1">
Divergences Yield / FX
</p>
<p className="text-[9px] text-gray-400 mb-3">
Yield et devise vont dans des sens opposés signal d'anomalie ou de stress
</p>
{!fxDayPct ? (
<p className="text-xs text-gray-400 italic">Données FX non disponibles.</p>
) : divergences.length === 0 ? (
<p className="text-xs text-gray-400 italic">
Aucune divergence — yield et devise évoluent de façon cohérente ce jour.
</p>
) : (
<>
<div className="space-y-2.5">
{divergences.map(({ ccy, yieldDelta, fxPct, type }) => (
<div key={ccy} className="flex items-center justify-between gap-2">
<div className="flex items-center gap-1.5 shrink-0">
<span className="text-xs">{CCY_FLAGS[ccy]}</span>
<span className="text-xs font-semibold text-gray-700 w-7">{ccy}</span>
</div>
<div className="flex items-center gap-2 flex-1 justify-end flex-wrap">
{/* Yield delta */}
<span className={`text-[10px] font-mono font-semibold flex items-center gap-0.5 ${deltaColor(yieldDelta)}`}>
{deltaIcon(yieldDelta)}
<span className="text-[9px] text-gray-400 mr-0.5">yield</span>
{yieldDelta >= 0 ? "+" : ""}{yieldDelta.toFixed(3)}%
</span>
<span className="text-gray-300 text-[10px]">vs</span>
{/* FX delta */}
<span className={`text-[10px] font-mono font-semibold flex items-center gap-0.5 ${deltaColor(fxPct)}`}>
{deltaIcon(fxPct)}
<span className="text-[9px] text-gray-400 mr-0.5">FX</span>
{fxPct >= 0 ? "+" : ""}{fxPct.toFixed(2)}%
</span>
{/* Badge type */}
<span className={`text-[9px] border rounded-full px-1.5 py-0.5 shrink-0 ${
type === "stress"
? "bg-red-50 text-red-700 border-red-200"
: "bg-blue-50 text-blue-700 border-blue-200"
}`}>
{type === "stress" ? "⚠ stress dette" : "🛡 safe-haven"}
</span>
</div>
</div>
))}
</div>
<p className="text-[9px] text-gray-400 mt-3 border-t border-gray-50 pt-2">
Stress dette : yield ↑ + devise ↓ (obligations vendues, devise sous pression).<br />
Safe-haven : yield ↓ + devise ↑ (capitaux attirés malgré taux bas).
</p>
</>
)}
</div>
{/* Movers du jour */}
<div className="bg-white border border-gray-100 rounded-xl p-4">
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-3">
📊 Movers du jour (variation yield)
</p> </p>
{movers.length === 0 ? ( {movers.length === 0 ? (
<p className="text-xs text-gray-400 italic">Pas de mouvement significatif ce jour.</p> <p className="text-[11px] text-slate-600 italic">Aucun mouvement significatif.</p>
) : ( ) : (
<div className="space-y-2"> <div className="space-y-2.5">
{movers.map(({ ccy, d }, i) => { {movers.map(({ ccy, yld, d }, i) => {
const maxDelta = Math.max(...movers.map(m => Math.abs(m.d))); const maxAbs = Math.max(...movers.map(m => Math.abs(m.d)));
const barW = maxDelta > 0 ? (Math.abs(d) / maxDelta) * 100 : 0; const barW = maxAbs > 0 ? (Math.abs(d) / maxAbs) * 100 : 0;
const isTop = i === 0;
return ( return (
<div key={ccy} className="flex items-center gap-3"> <div key={ccy} className="space-y-0.5">
<div className="w-20 flex items-center gap-1.5 flex-shrink-0"> <div className="flex items-center gap-2">
<span className="text-xs">{CCY_FLAGS[ccy]}</span> <span className="text-[11px]">{CCY_FLAGS[ccy]}</span>
<span className="text-xs font-semibold text-gray-700">{ccy}</span> <span className={`text-[10px] font-semibold ${isTop ? "text-slate-100" : "text-slate-300"}`}>
{i === 0 && ( {ccy}
<span className="text-[9px] bg-orange-100 text-orange-700 rounded-full px-1">1er</span> </span>
{isTop && (
<span className="text-[8px] bg-orange-500/20 text-orange-400 rounded-full px-1.5">1er</span>
)} )}
<span className="ml-auto text-[9px] font-mono text-slate-500">{fmt(yld)}</span>
<span className={`text-[10px] font-mono font-bold w-16 text-right ${deltaColor(d)}`}>
{d >= 0 ? "+" : ""}{d.toFixed(3)}%
</span>
</div> </div>
<div className="flex-1 bg-gray-100 rounded-full h-1.5 overflow-hidden"> <div className="flex items-center gap-1.5 pl-5">
<div <div className="flex-1 bg-slate-800 rounded-full h-1 overflow-hidden">
className={`h-full rounded-full ${d > 0 ? "bg-green-400" : "bg-red-400"}`} <div
style={{ width: `${barW}%` }} className={`h-full rounded-full ${d > 0 ? "bg-emerald-500" : "bg-red-500"}`}
/> style={{ width: `${barW}%` }}
</div> />
<div className={`w-16 text-right text-[10px] font-mono font-semibold ${deltaColor(d)}`}> </div>
{d >= 0 ? "+" : ""}{d.toFixed(3)}%
</div> </div>
</div> </div>
); );
})} })}
</div> </div>
)} )}
<p className="text-[9px] text-gray-400 mt-3 border-t border-gray-50 pt-2"> <p className="text-[8px] text-slate-700 mt-3 pt-2 border-t border-slate-800">
Variation du rendement 10Y par rapport à la clôture de la veille. Variation vs clôture veille · Yield = signal hawkish / pression vendeuse
Un yield qui monte = signal hawkish / pression vendeuse sur l'obligataire.
</p> </p>
</div> </div>
</div>
</div>
</div> </div>
); );
} }