From 34382c72ddf5360fa5d74d90203836d9dfa18944 Mon Sep 17 00:00:00 2001 From: caty21 Date: Mon, 29 Jun 2026 12:17:48 +0200 Subject: [PATCH] feat(CurrencyCard): apply uncommitted CurrencyCard refactor from previous session Co-Authored-By: Claude Sonnet 4.6 --- components/CurrencyCard.tsx | 585 ++++++++++++++++++++++++++---------- 1 file changed, 418 insertions(+), 167 deletions(-) diff --git a/components/CurrencyCard.tsx b/components/CurrencyCard.tsx index 02a8ea8..5e5dd79 100644 --- a/components/CurrencyCard.tsx +++ b/components/CurrencyCard.tsx @@ -5,7 +5,7 @@ import { motion, AnimatePresence } from "framer-motion"; import { TrendingUp, TrendingDown, Minus, Loader2, Database, BarChart2, Activity, Target, Zap, Eye, Layers, - ChevronRight, ArrowUpRight, ArrowDownRight, AlertTriangle, Info, + ChevronRight, ArrowUpRight, ArrowDownRight, AlertTriangle, Info, Settings, X, ExternalLink, } from "lucide-react"; import { AreaChart, Area, LineChart, Line, BarChart, Bar, Cell, @@ -62,6 +62,9 @@ interface Props { onCardTabChange?: (id: "overview" | "mispricing" | "focus") => void; syncSignauxSlide?: "ois" | "cot" | "sent"; onSignauxSlideChange?: (id: "ois" | "cot" | "sent") => void; + syncOisChartTab?: "curve" | "implied" | "scenarios"; + onOisChartTabChange?: (id: "curve" | "implied" | "scenarios") => void; + isLoading?: boolean; } type Tab = "overview" | "mispricing" | "focus"; @@ -334,12 +337,157 @@ function SignalBar({ strength, direction }: { strength: number; direction: Signa ); } +// ─── Sources popup ──────────────────────────────────────────────────────────── + +const TE_COUNTRY: Record = { + USD:"united-states", EUR:"euro-area", GBP:"united-kingdom", JPY:"japan", + CHF:"switzerland", CAD:"canada", AUD:"australia", NZD:"new-zealand", +}; +const IC_SLUG: Record = { + USD:"fed-rate-monitor", EUR:"ecb-rate-monitor", GBP:"boe-rate-monitor", + JPY:"boj-rate-monitor", CAD:"boc-rate-monitor", AUD:"rba-rate-monitor", + NZD:"rbnz-rate-monitor", CHF:"snb-rate-monitor", +}; +const CB_NAME: Record = { + USD:"Fed (FOMC)", EUR:"BCE", GBP:"BoE MPC", JPY:"BoJ", + CHF:"SNB", CAD:"BoC", AUD:"RBA", NZD:"RBNZ", +}; + +function SourcesPopup({ currency, onClose }: { currency: string; onClose: () => void }) { + const country = TE_COUNTRY[currency] ?? currency.toLowerCase(); + const icSlug = IC_SLUG[currency]; + const cbName = CB_NAME[currency] ?? currency; + + const sections: Array<{ title: string; icon: string; sources: Array<{ label: string; url: string; note?: string }> }> = [ + { + title: "OIS · Futures (onglet Signaux)", + icon: "📡", + sources: [ + ...(currency === "USD" ? [{ label: "CME FedWatch — contrats SOFR", url: "https://www.cmegroup.com/markets/interest-rates/cme-fedwatch-tool.html", note: "Probabilités Fed par réunion · source primaire USD" }] : []), + ...(icSlug ? [{ label: `Investing.com — ${cbName} Rate Monitor`, url: `https://www.investing.com/central-banks/${icSlug}`, note: "OIS implicites par réunion" }] : []), + { label: "InvestingLive — Giuseppe Dellamotta", url: "https://investinglive.com", note: "Articles hebdo : variation bps fin d'an (fallback)" }, + ], + }, + { + title: "COT CFTC (onglet Signaux)", + icon: "📊", + sources: [ + { label: "CFTC — TFF Report (Traders in Financial Futures)", url: "https://www.cftc.gov/MarketReports/CommitmentsofTraders/index.htm", note: "Leveraged Money (HF/CTAs) + Asset Managers · publié chaque vendredi" }, + ], + }, + { + title: "Sentiment retail (onglet DXM)", + icon: "🔄", + sources: [ + { label: "Myfxbook — Community Outlook", url: "https://www.myfxbook.com/community/outlook", note: "Positions longues/courtes retail en temps réel" }, + ], + }, + { + title: "Politique monétaire (Aperçu → Mon.)", + icon: "🏦", + sources: [ + { label: `Trading Economics — ${country} interest rate`, url: `https://tradingeconomics.com/${country}/interest-rate`, note: `Taux directeur ${cbName} actuel` }, + { label: "FRED — Federal Reserve Economic Data", url: "https://fred.stlouisfed.org", note: "Spreads crédit HY/IG (BAMLH0A0HYM2, BAMLC0A0CM)" }, + ], + }, + { + title: "Inflation (Aperçu → Infl.)", + icon: "📈", + sources: [ + { label: `Trading Economics — ${country} inflation`, url: `https://tradingeconomics.com/${country}/inflation-cpi`, note: "CPI, core CPI, PPI · scraping HTML" }, + ], + }, + { + title: "Croissance & PMI (Aperçu → Cro.)", + icon: "📉", + sources: [ + { label: `Trading Economics — ${country} GDP`, url: `https://tradingeconomics.com/${country}/gdp-growth`, note: "PIB, PMI manufacturier & services" }, + ], + }, + { + title: "Emploi (Aperçu → Empl.)", + icon: "👷", + sources: [ + { label: `Trading Economics — ${country} employment`, url: `https://tradingeconomics.com/${country}/employment-change`, note: "Variation emploi, chômage, NFP (USD), JOLTS, ADP" }, + ], + }, + { + title: "Rendements obligataires (Aperçu → Mon.)", + icon: "🔢", + sources: [ + { label: `Trading Economics — ${country} government bond`, url: `https://tradingeconomics.com/${country}/government-bond-yield`, note: "Taux 10Y souverain · mise à jour horaire" }, + ], + }, + ]; + + return ( +
+
+
e.stopPropagation()} + > + {/* Header */} +
+
+
Sources des données · {currency}
+
Toutes les données utilisées dans cette carte
+
+ +
+ + {/* Sections */} +
+ {sections.map(sec => ( +
+
+ {sec.icon} {sec.title} +
+
+ {sec.sources.map(src => ( +
+ +
+ + {src.label} + + {src.note && ( +

{src.note}

+ )} +
+
+ ))} +
+
+ ))} +
+
+
+ ); +} + // ─── OIS Enhanced Block ─────────────────────────────────────────────────────── // Bloc OIS enrichi : summary (Current Rate → Expected, Next Meeting, Most Likely/Alt) // + 3 onglets graphiques : Rate Curve / Implied Points / Scénarios -function OISEnhancedBlock({ ratePath }: { ratePath: CBRatePath }) { - const [chartTab, setChartTab] = useState<"curve" | "implied" | "scenarios">("curve"); +function OISEnhancedBlock({ ratePath, syncChartTab, onChartTabChange }: { + ratePath: CBRatePath; + syncChartTab?: "curve" | "implied" | "scenarios"; + onChartTabChange?: (id: "curve" | "implied" | "scenarios") => void; +}) { + const [localChartTab, setLocalChartTab] = useState<"curve" | "implied" | "scenarios">("curve"); + const chartTab = syncChartTab ?? localChartTab; + const setChartTab = (id: "curve" | "implied" | "scenarios") => { + setLocalChartTab(id); + onChartTabChange?.(id); + }; const { currentRate, meetings, yearEndImplied, ilDelta, ilCurrent, prevMeetings, prevWeekDate } = ratePath; if (!meetings.length) return null; @@ -402,91 +550,79 @@ function OISEnhancedBlock({ ratePath }: { ratePath: CBRatePath }) { return (
- {/* ── Summary header ────────────────────────────────────────────────── */} -
- {/* Title */} -
-
- OIS · Futures - LIVE + {/* ── Summary compact ───────────────────────────────────────────────── */} +
+ {/* Ligne 1 : titre + date */} +
+
+ OIS · Futures + LIVE
au {ratePath.asOf}
- {/* 3-column rates */} -
-
-
Current Rate
-
{currentRate.toFixed(2)}%
-
-
-
Expected
-
{yearEndImplied?.toFixed(2) ?? "—"}%
-
-
-
Next Meeting
-
{m0.label}
-
-
- - {/* Expected Move + Change bps + Δ fin an */} -
-
-
Expected Move
-
- {moveIcon}{moveLabel} -
-
-
-
Change (bps)
-
- {expectedBps > 0 ? "+" : ""}{expectedBps.toFixed(2)} -
+ {/* Ligne 2 : taux actuel → résultat attendu prochaine réunion + Δ fin an */} +
+ {currentRate.toFixed(2)}% + +
+ {moveIcon} {moveLabel} + {m0.label} + {isMoveExpected && ( + + ({m0.changeBps > 0 ? "+" : ""}{Math.round(m0.changeBps)}bps) + + )}
{bpsYE !== null && ( -
-
Δ fin an
-
- {bpsYE > 0 ? "+" : ""}{bpsYE}bps +
+
+ {bpsYE > 0 ? "+" : ""}{bpsYE}bps
+
fin an
)}
- {/* Most Likely / Alternative probability bars */} -
-
- Most Likely - {mlIsMove ? moveIcon : "="} - {mlRate.toFixed(2)}% -
-
-
- {Math.round(mlProb)}% + {/* Ligne 3 : barre proba scénario principal */} +
+ + {mlIsMove ? moveLabel : "Hold"} + +
+
-
- Alternative - {altIsMove ? moveIcon : "="} - {altRate.toFixed(2)}% -
+ {Math.round(mlProb)}% + {mlRate.toFixed(2)}% +
+ {/* Scénario alternatif seulement si proba ≥ 15% */} + {altProb >= 15 && ( +
+ {altIsMove ? moveLabel : "Hold"} +
- {Math.round(altProb)}% + {Math.round(altProb)}% + {altRate.toFixed(2)}%
-
+ )}
{/* ── Chart section ─────────────────────────────────────────────────── */}
{/* Tab buttons */}
- {([ { id: "curve" as const, label: "Rate Curve" }, { id: "implied" as const, label: "Implied Pts" }, { id: "scenarios" as const, label: "Scénarios" } ]).map(t => ( + {([ + { id: "curve" as const, label: "Rate Path" }, + { id: "implied" as const, label: "Implied Pts" }, + { id: "scenarios" as const, label: "Scénarios" }, + ]).map(t => (
- {/* Chart 1 — Implied Interest Rate Curve */} + {/* Chart 1 — Implied Rate Path (step line) */} {chartTab === "curve" && (
- {/* Légende visuelle */} -
- Courbe de taux implicite -
- - - Actuel + {/* Légende */} +
+ + + Actuel + + {hasPrevCurve && ( + + + {prevCurveLabel ? `Sem. préc. (${prevCurveLabel})` : "Sem. préc."} - - - {hasPrevCurve && prevCurveLabel ? `Sem. préc. (${prevCurveLabel})` : hasPrevCurve ? "Sem. préc." : "Sem. préc. (indispo)"} - -
+ )} + {!hasPrevCurve && ( + Sem. préc. indispo + )} + {/* dashed reference = current rate floor */} + + + Taux actuel +
- + v.toFixed(2)} /> + [`${v.toFixed(3)}%`, name === "current" ? "Actuel" : "Sem. préc."]} + content={({ label, payload }) => { + if (!payload?.length) return null; + return ( +
+

{label}

+ {(payload as {dataKey: string; value: number; stroke: string}[]).map(p => ( +

+ {p.dataKey === "current" ? "●" : "···"} {(p.value as number).toFixed(3)}% +

+ ))} +
+ ); + }} /> - + {hasPrevCurve && ( - + )}
@@ -587,7 +740,13 @@ function OISEnhancedBlock({ ratePath }: { ratePath: CBRatePath }) { {/* Chart 3 — Scénarios : taux implicite par réunion (barres horizontales) */} {chartTab === "scenarios" && (
-
Taux implicite par réunion
+
+ Taux implicite par réunion +
+ Taux prévu + Proba mvt +
+
{(() => { const maxR2 = Math.max(...scenariosData.map(d => d.rate), currentRate); @@ -646,6 +805,7 @@ export default function CurrencyCard({ currency, expectations, yields, sentiment, cot, ratePath, onDivergenceUpdate, calEvents, macroSection, syncMacroSlide, onMacroSlideChange, syncCardTab, onCardTabChange, syncSignauxSlide, onSignauxSlideChange, + syncOisChartTab, onOisChartTabChange, isLoading = false, }: Props) { const meta = CURRENCY_META[currency]; @@ -671,6 +831,7 @@ export default function CurrencyCard({ const [showCotInfo, setShowCotInfo] = useState(false); const [showAllMeetings, setShowAllMeetings] = useState(false); const [showYield10Y, setShowYield10Y] = useState(false); + const [showSources, setShowSources] = useState(false); const [showRecentNews, setShowRecentNews] = useState(false); const [showUpcoming, setShowUpcoming] = useState(false); const [sliderBlock, setSliderBlock] = useState("ois"); @@ -1328,9 +1489,19 @@ export default function CurrencyCard({ ))}
+
+ {/* Sources popup */} + {showSources && setShowSources(false)} />} + {/* ── Tab content ─────────────────────────────────────────────────────── */}
@@ -1505,7 +1676,7 @@ export default function CurrencyCard({ { id: "ois", label: "OIS" }, ]; if (cot) sigTabs.push({ id: "cot", label: "COT" }); - if (sentiment) sigTabs.push({ id: "sent", label: "Sentiment" }); + if (sentiment) sigTabs.push({ id: "sent", label: "DXM" }); return (
{sigTabs.length > 1 && ( @@ -1515,7 +1686,11 @@ export default function CurrencyCard({ ))}
)} -
0 ? "h-[460px]" : "h-[310px]"} overflow-hidden rounded-xl transition-all duration-300`}> +
0 && ( - + )} {/* OIS — état indisponible */} {signauxSlide === "ois" && (!ratePath || !ratePath.meetings.length) && ( -
- -
-

Données OIS indisponibles

-

Cache actualisé par GitHub Actions (24–48h)

+ isLoading ? ( +
+ {/* Header skeleton */} +
+
+
+
+ {/* Tab buttons skeleton */} +
+ {[48, 40, 44].map(w => ( +
+ ))} +
+ {/* Rate curve rows */} + {[70, 85, 60, 78, 65, 72].map((w, i) => ( +
+
+
+
+
+ ))}
-
+ ) : ( +
+ +
+

Données OIS indisponibles

+

Cache actualisé par GitHub Actions (24–48h)

+
+
+ ) )} {/* COT */} @@ -1598,96 +1801,144 @@ export default function CurrencyCard({

AM (Asset Managers) — fonds pension, souverains, assurances. Prennent des positions pour couvrir des expositions réelles (hedging). Leur flux pilote la devise à moyen terme.

HF (Hedge Funds / CTAs) — spéculation directionnelle à court terme. Réactifs aux catalyseurs macro. Retournements rapides possibles.

-

Lire : Net = longs − shorts · Δ sem = variation vs semaine précédente · L% = part de longs dans le total. Un HF net short avec des longs qui augmentent (Δ L↑) signale un potentiel retournement haussier.

+

Lire : Δ Net HF = variation du net (longs−shorts) vs sem. précédente → chiffre clé de la pression spéculative. L / S = variation de chaque jambe séparément. Ex : L↑ + S↑ = les deux côtés s'accumulent ; Δ Net négatif = pression baissière nette qui s'intensifie.

)} - {/* ② Qui pilote la devise ? */} -
- {amDominates - ? <>AM pilote ({amPct}%) · hedge {cot.amNet > 0 ? "haussier" : "baissier"} — institutionnel / carry - : <>HF pilote ({hfPct}%) · spéculation {hfIsShort ? "baissière" : "haussière"} - } -
+ {/* ② Deux cartes côte à côte : NET en gros + DELTA bien visible */} +
- {/* ③ Évolution hebdomadaire — le delta est la vraie lecture */} -
-
Évolution cette semaine
- - {/* AM delta */} -
- AM (hedge) - - 0 ? "text-emerald-400" : "text-red-400"}`}> - {cot.amNet > 0 ? "LONG" : "SHORT"} {dFmt(cot.amNet)} - - {cot.amNetDelta !== null && ( - 0 ? "text-emerald-400" : cot.amNetDelta < 0 ? "text-red-400" : "text-slate-500"}`}> - Δ{dFmt(cot.amNetDelta)}{cot.amNetDelta > 0 ? "↑" : cot.amNetDelta < 0 ? "↓" : ""} - - )} - + {/* Carte AM */} +
+
+ AM · hedge {amDominates ? "▶ pilote" : ""} +
+ {/* NET — très grand */} +
0 ? "text-emerald-400" : "text-red-400"}`}> + {cot.amNet > 0 ? "LONG" : "SHORT"} +
+
0 ? "text-emerald-400" : "text-red-400"}`}> + {dFmt(cot.amNet)} +
+ {/* DELTA AM — L/S séparés comme HF */} +
+
Δ cette semaine
+
+ {cot.amLongsDelta != null && Number.isFinite(cot.amLongsDelta) ? ( +
0 ? "text-emerald-400" : "text-red-400"}`}> + L {cot.amLongsDelta > 0 ? "↑" : "↓"}{Math.abs(cot.amLongsDelta / 1000).toFixed(1)}k +
+ ) : cot.amNetDelta != null && Number.isFinite(cot.amNetDelta) ? ( +
0 ? "text-emerald-400" : "text-red-400"}`}> + net {cot.amNetDelta > 0 ? "↑" : "↓"}{Math.abs(cot.amNetDelta / 1000).toFixed(1)}k +
+ ) :
} + {cot.amShortsDelta != null && Number.isFinite(cot.amShortsDelta) && ( +
0 ? "text-red-400" : "text-emerald-400"}`}> + S {cot.amShortsDelta > 0 ? "↑" : "↓"}{Math.abs(cot.amShortsDelta / 1000).toFixed(1)}k +
+ )} +
+
- {/* HF delta — Δlongs / Δshorts séparés : c'est ici que se lit l'accumulation */} -
- HF (spécu) - - 0 ? "text-emerald-400" : "text-red-400"}`}> - {cot.net > 0 ? "LONG" : "SHORT"} {dFmt(cot.net)} - - + {/* Carte HF */} +
+
+ HF · spécu {!amDominates ? "▶ pilote" : ""} +
+ {/* NET — très grand */} +
0 ? "text-emerald-400" : "text-red-400"}`}> + {cot.net > 0 ? "LONG" : "SHORT"} +
+
0 ? "text-emerald-400" : "text-red-400"}`}> + {dFmt(cot.net)} +
+ {/* DELTA — Longs / Shorts séparés + delta net */} +
+
Δ cette semaine
+ {/* Delta net HF — le chiffre le plus utile */} + {cot.netDelta != null && Number.isFinite(cot.netDelta) && ( +
0 ? "text-emerald-400" : "text-red-400"}`}> + Δ Net {cot.netDelta > 0 ? "+" : ""}{(cot.netDelta / 1000).toFixed(1)}k +
+ )} +
{cot.longsDelta !== null && ( - 0 ? "text-emerald-400" : "text-red-400"}> - L {cot.longsDelta > 0 ? "+" : ""}{(cot.longsDelta/1000).toFixed(1)}k{cot.longsDelta > 0 ? "↑" : "↓"} - +
0 ? "text-emerald-400" : "text-red-400"}`}> + L {cot.longsDelta > 0 ? "↑" : "↓"}{Math.abs(cot.longsDelta / 1000).toFixed(1)}k +
)} {cot.shortsDelta !== null && ( - 0 ? "text-red-400" : "text-emerald-400"}> - S {cot.shortsDelta > 0 ? "+" : ""}{(cot.shortsDelta/1000).toFixed(1)}k{cot.shortsDelta > 0 ? "↑" : "↓"} - +
0 ? "text-red-400" : "text-emerald-400"}`}> + S {cot.shortsDelta > 0 ? "↑" : "↓"}{Math.abs(cot.shortsDelta / 1000).toFixed(1)}k +
)} - - +
+
- {/* ④ Barre de dominance + signal de convergence */} -
-
-
-
+ {/* ③ Barre de dominance */} +
+
+
+
-
+
AM {amPct}% HF {hfPct}%
- - {/* Signal de convergence : est-ce que les deux groupes convergent ? */} - {(() => { - const amBull = cot.amNet > 0; - const convergeBull = amBull && (hfLongsGrowing || hfShortsReducing); - const convergeBear = !amBull && (hfShortsGrowing || hfLongsReducing); - const hfFlipping = hfIsShort && hfLongsGrowing && hfShortsReducing; - const txt = hfFlipping - ? `↻ HF retournement potentiel — longs ↑ & shorts ↓ malgré position short` - : convergeBull - ? `▲ Convergence haussière — AM long + HF ${hfTrend}` - : convergeBear - ? `▼ Convergence baissière — AM short + HF ${hfTrend}` - : `→ Divergence — AM ${cot.amNet > 0 ? "long" : "short"} / HF ${hfTrend}`; - const cls = hfFlipping || convergeBull - ? "text-emerald-400/80 bg-emerald-500/5 border-emerald-500/15" - : convergeBear - ? "text-red-400/80 bg-red-500/5 border-red-500/15" - : "text-slate-400 bg-slate-800/30 border-slate-700/20"; - return ( -
- {txt} -
- ); - })()}
+ + {/* ④ Verdict — la phrase qui résume le mouvement */} + {(() => { + const amBull = cot.amNet > 0; + let verdict = ""; + let sub = ""; + let cls = ""; + + if (hfIsShort && hfLongsGrowing && hfShortsReducing) { + verdict = "↻ Majorité vend — mais de + en + achètent"; + sub = "HF couvre ses shorts ET accumule des longs → retournement potentiel"; + cls = "text-emerald-400 border-emerald-500/25 bg-emerald-500/8"; + } else if (hfIsShort && hfLongsGrowing) { + verdict = "▲ Majorité vend — longs HF en hausse"; + sub = "Accumulation : surveiller si les shorts commencent à baisser"; + cls = "text-emerald-400/80 border-emerald-500/20 bg-emerald-500/5"; + } else if (hfIsShort && hfShortsReducing) { + verdict = "▲ Shorts HF se réduisent — pression baissière s'allège"; + sub = "Signal de couverture — retournement possible si longs suivent"; + cls = "text-emerald-400/70 border-emerald-500/15 bg-emerald-500/5"; + } else if (!hfIsShort && amBull) { + verdict = "▲▲ Convergence haussière — AM + HF alignés"; + sub = "AM long (hedging) et HF long (spécu) → signal fort"; + cls = "text-emerald-400 border-emerald-500/25 bg-emerald-500/8"; + } else if (hfIsShort && !amBull) { + verdict = "▼▼ Convergence baissière — AM + HF alignés"; + sub = "AM short (hedging) et HF short (spécu) → signal fort"; + cls = "text-red-400 border-red-500/25 bg-red-500/8"; + } else if (!hfIsShort && hfShortsGrowing && hfLongsReducing) { + verdict = "▼ Majorité achète — mais distribution en cours"; + sub = "HF réduit ses longs ET renforce ses shorts → retournement potentiel"; + cls = "text-red-400 border-red-500/25 bg-red-500/8"; + } else if (!hfIsShort && hfShortsGrowing) { + verdict = "▼ Majorité achète — shorts HF en hausse"; + sub = "Signal de distribution — surveiller la réduction des longs"; + cls = "text-red-400/80 border-red-500/20 bg-red-500/5"; + } else { + verdict = "→ Flux mixtes AM / HF"; + sub = `AM ${amBull ? "long" : "short"} · HF ${hfIsShort ? "short" : "long"} · pas de signal directionnel clair`; + cls = "text-slate-400 border-slate-700/30 bg-slate-800/20"; + } + + return ( +
+
{verdict}
+
{sub}
+
+ ); + })()}
); })()}