mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-15 21:48:05 +00:00
feat: OISEnhancedBlock — rate curve, implied pts, scénarios charts per CB
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
a3d847ce59
commit
9da463a424
+1020
-534
File diff suppressed because it is too large
Load Diff
+24
-23
@@ -95,18 +95,16 @@ export default function DriversBar({ drivers }: Props) {
|
||||
const {
|
||||
vix, vixDelta,
|
||||
sp500, sp500ChangePct,
|
||||
btc, btcChange24h,
|
||||
btc, btcDeltaPct, btcChange24h,
|
||||
hySpread, igSpread,
|
||||
us10y, us2y, curveSlope,
|
||||
gold, goldDelta,
|
||||
silver, silverDelta,
|
||||
brent, brentDelta,
|
||||
wti, wtiDelta,
|
||||
gold, goldDeltaPct,
|
||||
silver, silverDeltaPct,
|
||||
brent, brentDelta, brentDeltaPct,
|
||||
wti, wtiDelta, wtiDeltaPct,
|
||||
} = drivers;
|
||||
|
||||
const dxy = (drivers as DriverData & { dxy?: number | null }).dxy ?? null;
|
||||
const dxyDelta = (drivers as DriverData & { dxyDelta?: number | null }).dxyDelta ?? null;
|
||||
const riskOff = (vix ?? 0) > 25 || (hySpread ?? 0) > 500;
|
||||
const riskOff = (vix ?? 0) > 25 || (hySpread ?? 0) > 500;
|
||||
|
||||
return (
|
||||
<div className="mb-4 bg-slate-950/60 border border-slate-800 rounded-xl p-3 space-y-2">
|
||||
@@ -133,9 +131,10 @@ export default function DriversBar({ drivers }: Props) {
|
||||
accent={(vix ?? 0) > 25 ? "red" : undefined}
|
||||
tooltip="Clôture actuelle − clôture précédente (Yahoo Finance)." />
|
||||
<Tile label="S&P 500" value={sp500} dec={0} delta={sp500ChangePct} deltaPct deltaDec={2}
|
||||
tooltip="% vs clôture précédente (Yahoo Finance)." />
|
||||
<Tile label="Bitcoin" value={btc} dec={0} unit=" $" delta={btcChange24h} deltaPct deltaDec={2}
|
||||
tooltip="Variation 24h (Binance / CoinGecko)." />
|
||||
tooltip="% vs clôture J-1 (Business Insider, cache 1 min). Fallback : Yahoo Finance." />
|
||||
<Tile label="BTC/USD" value={btc} dec={0} unit=" $"
|
||||
delta={btcDeltaPct ?? btcChange24h} deltaPct deltaDec={2}
|
||||
tooltip="% vs clôture J-1 (investing.com, cache 1 min). Fallback : Binance/CoinGecko 24h." />
|
||||
|
||||
{/* ── Crédit ────────────────────────────────────────────── */}
|
||||
<div className="w-px bg-slate-800 self-stretch mx-0.5 hidden sm:block" />
|
||||
@@ -146,11 +145,9 @@ export default function DriversBar({ drivers }: Props) {
|
||||
<Tile label="IG Spread" value={igSpread} dec={0} unit=" bps"
|
||||
tooltip="Investment Grade spread vs Treasuries US." />
|
||||
|
||||
{/* ── FX / Taux ─────────────────────────────────────────── */}
|
||||
{/* ── Taux ──────────────────────────────────────────────── */}
|
||||
<div className="w-px bg-slate-800 self-stretch mx-0.5 hidden sm:block" />
|
||||
<GroupLabel label="FX / Taux" />
|
||||
<Tile label="DXY" value={dxy} dec={2} delta={dxyDelta} deltaDec={2}
|
||||
tooltip="ICE Dollar Index Futures (DX=F) — Yahoo Finance, cache 5 min." />
|
||||
<GroupLabel label="Taux" />
|
||||
<Tile
|
||||
label="Crb 2-10" value={curveSlope} dec={0} unit=" bps"
|
||||
accent={(curveSlope ?? 0) < -50 ? "amber" : undefined}
|
||||
@@ -160,14 +157,18 @@ export default function DriversBar({ drivers }: Props) {
|
||||
{/* ── Commodités ────────────────────────────────────────── */}
|
||||
<div className="w-px bg-slate-800 self-stretch mx-0.5 hidden sm:block" />
|
||||
<GroupLabel label="Commodités" />
|
||||
<Tile label="Or $/oz" value={gold} dec={0} delta={goldDelta} deltaDec={1}
|
||||
tooltip="Delta intraday close−open (Stooq)." />
|
||||
<Tile label="Argent $/oz" value={silver} dec={2} delta={silverDelta}
|
||||
tooltip="Delta intraday close−open (Stooq)." />
|
||||
<Tile label="Brent $/b" value={brent} dec={1} delta={brentDelta} deltaDec={1}
|
||||
tooltip="Delta intraday close−open (Stooq)." />
|
||||
<Tile label="WTI $/b" value={wti} dec={1} delta={wtiDelta} deltaDec={1}
|
||||
tooltip="Delta intraday close−open (Stooq)." />
|
||||
<Tile label="Or $/oz" value={gold} dec={0}
|
||||
delta={goldDeltaPct} deltaPct deltaDec={2}
|
||||
tooltip="% vs clôture J-1 (Business Insider, cache 1 min). Fallback : Yahoo GC=F." />
|
||||
<Tile label="Argent $/oz" value={silver} dec={2}
|
||||
delta={silverDeltaPct} deltaPct deltaDec={2}
|
||||
tooltip="% vs clôture J-1 (Business Insider, cache 1 min). Fallback : Yahoo SI=F." />
|
||||
<Tile label="Brent $/b" value={brent} dec={1}
|
||||
delta={brentDeltaPct ?? brentDelta} deltaPct={brentDeltaPct !== null} deltaDec={2}
|
||||
tooltip={`% évolution vs clôture J-1 (abcbourse.com — Six Financial Information, temps réel).${brentDelta != null ? `\nDelta: ${brentDelta > 0 ? "+" : ""}${brentDelta.toFixed(2)} $` : ""}`} />
|
||||
<Tile label="WTI $/b" value={wti} dec={1}
|
||||
delta={wtiDeltaPct ?? wtiDelta} deltaPct={wtiDeltaPct !== null} deltaDec={2}
|
||||
tooltip={`% évolution vs clôture J-1 (Business Insider, cache 1 min). Fallback : Yahoo Finance CL=F.${wtiDelta != null ? `\nDelta: ${wtiDelta > 0 ? "+" : ""}${wtiDelta.toFixed(2)} $` : ""}`} />
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -93,8 +93,12 @@ export default function NewsTab({ items, loading, onRefresh }: Props) {
|
||||
if (priorityOnly && !isPriorityItem(item)) return false;
|
||||
if (filterCcy !== "ALL" && !item.impacts.some(i => i.ccy === filterCcy)) return false;
|
||||
if (filterCat !== "ALL" && !item.categories.includes(filterCat)) return false;
|
||||
if (filterDir !== "all" && filterCcy !== "ALL") {
|
||||
if (!item.impacts.some(i => i.ccy === filterCcy && i.direction === filterDir)) return false;
|
||||
if (filterDir !== "all") {
|
||||
if (filterCcy !== "ALL") {
|
||||
if (!item.impacts.some(i => i.ccy === filterCcy && i.direction === filterDir)) return false;
|
||||
} else {
|
||||
if (!item.impacts.some(i => i.direction === filterDir)) return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
@@ -251,8 +255,8 @@ export default function NewsTab({ items, loading, onRefresh }: Props) {
|
||||
{filterCcy !== "ALL" && ` · ${CCY_FLAGS[filterCcy]} ${filterCcy}`}
|
||||
{filterCat !== "ALL" && ` · ${filterCat}`}
|
||||
</span>
|
||||
{(filterCcy !== "ALL" || filterCat !== "ALL" || filterDir !== "all") && (
|
||||
<button onClick={() => { setFilterCcy("ALL"); setFilterCat("ALL"); setFilterDir("all"); }}
|
||||
{(filterCcy !== "ALL" || filterCat !== "ALL" || filterDir !== "all" || priorityOnly) && (
|
||||
<button onClick={() => { setFilterCcy("ALL"); setFilterCat("ALL"); setFilterDir("all"); setPriorityOnly(false); }}
|
||||
className="text-[10px] text-slate-600 hover:text-slate-400">
|
||||
Effacer filtres ×
|
||||
</button>
|
||||
|
||||
+66
-35
@@ -15,6 +15,7 @@ interface YieldsData {
|
||||
|
||||
interface Props {
|
||||
yieldsData: YieldsData | null;
|
||||
fxDayPct?: Record<string, number | null> | null;
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────────
|
||||
@@ -54,7 +55,7 @@ function carryLabel(bps: number): { label: string; color: string } {
|
||||
|
||||
// ── Component ─────────────────────────────────────────────────────────────────
|
||||
|
||||
export default function YieldsTab({ yieldsData }: Props) {
|
||||
export default function YieldsTab({ yieldsData, fxDayPct }: Props) {
|
||||
if (!yieldsData) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-48 text-gray-400 text-sm">
|
||||
@@ -99,21 +100,35 @@ export default function YieldsTab({ yieldsData }: Props) {
|
||||
const topCarry = carryPairs.slice(0, 6);
|
||||
const tightPairs = [...carryPairs].sort((a, b) => a.bps - b.bps).slice(0, 4);
|
||||
|
||||
// Divergences : devises dont la direction journalière est opposée à la majorité
|
||||
// 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 deltas = sorted.map(([ccy]) => ({ ccy, d: dayDeltas[ccy] ?? 0 }));
|
||||
const rising = deltas.filter(x => x.d > 0.001).length;
|
||||
const falling = deltas.filter(x => x.d < -0.001).length;
|
||||
const majority = rising >= falling ? "up" : "down";
|
||||
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" }[] = [];
|
||||
|
||||
return deltas
|
||||
.filter(x => {
|
||||
if (majority === "up" && x.d < -0.001) return true;
|
||||
if (majority === "down" && x.d > 0.001) return true;
|
||||
return false;
|
||||
})
|
||||
.sort((a, b) => Math.abs(b.d) - Math.abs(a.d));
|
||||
}, [sorted, dayDeltas]);
|
||||
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(() => {
|
||||
@@ -276,42 +291,58 @@ export default function YieldsTab({ yieldsData }: Props) {
|
||||
{/* ── Divergences + Movers du jour ────────────────────────────────────── */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
|
||||
{/* Divergences */}
|
||||
{/* 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-3">
|
||||
⚡ Divergences du jour
|
||||
<p className="text-[10px] font-semibold text-gray-400 uppercase tracking-widest mb-1">
|
||||
⚡ Divergences Yield / FX
|
||||
</p>
|
||||
{divergences.length === 0 ? (
|
||||
<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 notable — tous les yields bougent dans la même direction.
|
||||
Aucune divergence — yield et devise évoluent de façon cohérente ce jour.
|
||||
</p>
|
||||
) : (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
{divergences.map(({ ccy, d }) => (
|
||||
<div key={ccy} className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<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">{ccy}</span>
|
||||
<span className="text-[10px] text-gray-400">{CCY_COUNTRY[ccy]}</span>
|
||||
<span className="text-xs font-semibold text-gray-700 w-7">{ccy}</span>
|
||||
</div>
|
||||
<div className={`flex items-center gap-1 text-xs font-mono font-semibold ${deltaColor(d)}`}>
|
||||
{deltaIcon(d)}
|
||||
{d >= 0 ? "+" : ""}{d.toFixed(3)}%
|
||||
<span className={`ml-1 text-[9px] border rounded-full px-1.5 py-0.5 ${
|
||||
d > 0
|
||||
? "bg-green-50 text-green-700 border-green-200"
|
||||
: "bg-red-50 text-red-700 border-red-200"
|
||||
<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"
|
||||
}`}>
|
||||
{d > 0 ? "↑ contre tendance" : "↓ contre tendance"}
|
||||
{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">
|
||||
Ces devises bougent à contre-courant des autres ce jour.
|
||||
Signal potentiel de réévaluation ou d'actualité macro spécifique.
|
||||
Stress dette : yield ↑ + devise ↓ (obligations vendues, devise sous pression).<br />
|
||||
Safe-haven : yield ↓ + devise ↑ (capitaux attirés malgré taux bas).
|
||||
</p>
|
||||
</>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user