2026-05-27 13:18:45 +02:00
"use client" ;
import { useEffect , useState , useCallback } from "react" ;
2026-06-02 20:51:20 +02:00
import { motion , AnimatePresence } from "framer-motion" ;
import {
TrendingUp , TrendingDown , Minus , Loader2 , Database ,
BarChart2 , Activity , Target , Zap , Eye , Layers ,
ChevronRight , ArrowUpRight , ArrowDownRight , AlertTriangle ,
} from "lucide-react" ;
import {
AreaChart , Area , LineChart , Line , ResponsiveContainer ,
Tooltip , XAxis ,
} from "recharts" ;
2026-06-01 23:24:32 +02:00
import { CURRENCY_META , COUNTRY_PROFILES } from "@/lib/constants" ;
2026-06-02 20:51:20 +02:00
import { biasLabel , calcMacroScore } from "@/lib/scoring" ;
2026-05-28 22:58:27 +02:00
import { saveCache , loadCache , formatCacheDate } from "@/lib/localCache" ;
2026-05-27 13:18:45 +02:00
import type { Currency , BiasPhase , RateExpectation } from "@/lib/types" ;
2026-06-01 23:24:32 +02:00
import type { CBRatePath } from "@/lib/rateprobability" ;
import type { SentimentEntry , CotEntry } from "@/lib/types" ;
2026-05-27 13:18:45 +02:00
import NarrativeButton from "./NarrativeButton" ;
2026-06-02 20:51:20 +02:00
// ─── Types internes ───────────────────────────────────────────────────────────
interface Ind {
value : number | null ; prev : number | null ; surprise : number | null ;
trend : "up" | "down" | "flat" | null ; lastUpdated : string | null ;
consensus? : number | null ;
}
2026-06-01 23:24:32 +02:00
interface MacroForecasts {
cpi : number | null ; cpiSurprise : number | null ;
2026-06-02 20:51:20 +02:00
cpiCore : number | null ; cpiMoM : number | null ;
cpiCoreMoM : number | null ; ppiMoM : number | null ;
2026-06-01 23:24:32 +02:00
unemployment : number | null ; unemploymentSurprise : number | null ;
pmiMfg : number | null ; pmiMfgSurprise : number | null ;
pmiSvc : number | null ; pmiSvcSurprise : number | null ;
pmiComposite : number | null ; pmiCompositeSurprise : number | null ;
retailSales : number | null ; retailSalesSurprise : number | null ;
gdp : number | null ; gdpSurprise : number | null ;
employment : number | null ; employmentSurprise : number | null ;
}
2026-06-02 20:51:20 +02:00
interface MacroData {
currency : string ;
indicators : Record < string , Ind | null >;
forecasts? : MacroForecasts | null ;
fetchedAt : string ;
}
2026-05-27 13:18:45 +02:00
interface Props {
currency : Currency ;
expectations : Record < string , unknown > | null ;
yields : { yields : Record < string , number | null >; spreads : Record < string , number | null > } | null ;
2026-06-01 23:24:32 +02:00
sentiment : SentimentEntry | null ;
cot : CotEntry | null ;
ratePath : CBRatePath | null ;
2026-05-27 13:18:45 +02:00
onDivergenceUpdate : ( currency : Currency , score : number ) => void ;
}
2026-06-02 20:51:20 +02:00
type Tab = "overview" | "mispricing" | "focus" ;
type SignalDir = "bullish" | "bearish" | "neutral" | "warning" ;
// ─── Helpers de style ─────────────────────────────────────────────────────────
function phaseStyle ( p : BiasPhase ) {
if ( p === "tightening" ) return "bg-red-500/15 text-red-400 border-red-500/30" ;
if ( p === "easing" ) return "bg-sky-500/15 text-sky-400 border-sky-500/30" ;
if ( p === "hawkish_pause" ) return "bg-amber-500/15 text-amber-400 border-amber-500/30" ;
if ( p === "dovish_pause" ) return "bg-blue-500/15 text-blue-400 border-blue-500/30" ;
return "bg-slate-500/15 text-slate-400 border-slate-500/30" ;
}
function phaseLabel ( p : BiasPhase ) {
if ( p === "tightening" ) return "Resserrement" ;
if ( p === "easing" ) return "Assouplissement" ;
if ( p === "hawkish_pause" ) return "Pause Hawkish" ;
if ( p === "dovish_pause" ) return "Pause Dovish" ;
return "Transition" ;
}
function scoreDir ( score : number ) : SignalDir {
if ( score >= 3 ) return "bullish" ;
if ( score <= - 3 ) return "bearish" ;
return "neutral" ;
}
function sigColor ( d : SignalDir ) {
if ( d === "bullish" ) return "text-emerald-400" ;
if ( d === "bearish" ) return "text-red-400" ;
if ( d === "warning" ) return "text-amber-400" ;
return "text-slate-400" ;
}
function sigBg ( d : SignalDir ) {
if ( d === "bullish" ) return "bg-emerald-500/10 border-emerald-500/20" ;
if ( d === "bearish" ) return "bg-red-500/10 border-red-500/20" ;
if ( d === "warning" ) return "bg-amber-500/10 border-amber-500/20" ;
return "bg-slate-500/10 border-slate-500/20" ;
}
2026-05-27 13:18:45 +02:00
2026-06-02 20:51:20 +02:00
function sigBar ( d : SignalDir ) {
if ( d === "bullish" ) return "bg-emerald-500" ;
if ( d === "bearish" ) return "bg-red-500" ;
if ( d === "warning" ) return "bg-amber-500" ;
return "bg-slate-500" ;
}
function trendDir ( t : "up" | "down" | "flat" | null ) : SignalDir {
if ( t === "up" ) return "bullish" ;
if ( t === "down" ) return "bearish" ;
return "neutral" ;
}
// ─── Sous-composants ─────────────────────────────────────────────────────────
function MacroBlock ({ title , children } : { title : string ; children : React.ReactNode }) {
2026-06-01 23:24:32 +02:00
return (
2026-06-02 20:51:20 +02:00
< div className = "bg-slate-800/40 rounded-xl border border-slate-700/30 p-3 space-y-1.5" >
< div className = "text-[10px] text-slate-500 uppercase tracking-wider font-semibold mb-2" >{ title }</ div >
{ children }
2026-06-01 23:24:32 +02:00
</ div >
);
}
2026-06-02 20:51:20 +02:00
function IRow ({
label , ind , unit = "" , consensus , surpriseVsCons , tooltip , info , invertSurprise = false ,
} : {
label : string ; ind : Ind | null ; unit? : string ;
consensus? : number | null ; surpriseVsCons? : number | null ;
tooltip? : string | null ; info? : string | null ; invertSurprise? : boolean ;
2026-05-27 13:18:45 +02:00
}) {
const value = ind ? . value ?? null ;
const prev = ind ? . prev ?? null ;
2026-06-02 20:51:20 +02:00
const fmt = ( v : number | null ) => v !== null ? ` ${ v . toFixed ( 2 ) }${ unit } ` : "—" ;
2026-05-27 13:18:45 +02:00
const s = ind ? . surprise ?? null ;
2026-06-02 20:51:20 +02:00
const effS = invertSurprise && s !== null ? - s : s ;
const valColor = effS === null ? "text-slate-200"
: effS > 0 ? "text-emerald-400"
: effS < 0 ? "text-red-400"
: "text-slate-500" ;
const effSurpr = invertSurprise && surpriseVsCons !== null ? - ( surpriseVsCons ?? 0 ) : surpriseVsCons ;
const surpriseCls = effSurpr == null ? "" : effSurpr > 0 ? "text-emerald-500" : effSurpr < 0 ? "text-red-500" : "text-slate-500" ;
const surpriseArr = effSurpr == null ? "" : effSurpr > 0 ? "▲" : effSurpr < 0 ? "▼" : "▬" ;
2026-06-01 23:24:32 +02:00
2026-05-27 13:18:45 +02:00
return (
2026-06-02 20:51:20 +02:00
< div className = "space-y-0.5" >
< div className = "flex items-center justify-between text-[12px]" >
2026-05-27 13:18:45 +02:00
< div className = "flex items-center gap-1.5 min-w-0" >
2026-06-02 20:51:20 +02:00
< span className = "text-slate-600 shrink-0" > → </ span >
< span className = "text-slate-400 truncate" >{ label }</ span >
2026-06-02 17:52:36 +02:00
{ info && (
< span className = "relative group/info inline-flex shrink-0 cursor-help" >
2026-06-02 20:51:20 +02:00
< span className = "inline-flex items-center justify-center w-3 h-3 rounded-full border border-slate-700 text-slate-500 text-[7px] font-bold leading-none" > i </ span >
< span className = "pointer-events-none absolute bottom-full left-0 mb-1.5 w-52 px-2 py-1.5 rounded-md bg-slate-950 text-slate-300 text-[10px] leading-snug opacity-0 group-hover/info:opacity-100 transition-opacity duration-150 z-50 shadow-lg whitespace-normal border border-slate-700" >
2026-06-02 17:52:36 +02:00
{ info }
</ span >
</ span >
)}
2026-05-27 13:18:45 +02:00
</ div >
2026-06-02 17:37:34 +02:00
{ tooltip ? (
< span className = "relative group/val cursor-default shrink-0" >
2026-06-02 20:51:20 +02:00
< span className = { `font-semibold tabular-nums ${ valColor } ` }>{ fmt ( value )}</ span >
< span className = "pointer-events-none absolute bottom-full right-0 mb-1.5 w-52 px-2 py-1.5 rounded-md bg-slate-950 text-slate-300 text-[10px] leading-snug opacity-0 group-hover/val:opacity-100 transition-opacity duration-150 z-50 shadow-lg whitespace-normal border border-slate-700" >
2026-06-02 17:37:34 +02:00
{ tooltip }
</ span >
</ span >
) : (
2026-06-02 20:51:20 +02:00
< span className = { `font-semibold tabular-nums shrink-0 ${ valColor } ` }>{ fmt ( value )}</ span >
2026-06-02 17:37:34 +02:00
)}
2026-05-27 13:18:45 +02:00
</ div >
2026-06-02 20:51:20 +02:00
< div className = "flex items-center justify-between pl-4 text-[10px] text-slate-600" >
< span > Préc . < span className = "text-slate-500" >{ fmt ( prev )}</ span ></ span >
{ surpriseVsCons !== null && surpriseVsCons !== undefined ? (
< span className = { `font-medium ${ surpriseCls } ` }>
Surpr . { surpriseArr }{( effSurpr ?? 0 ) > 0 ? "+" : "" }{( effSurpr ?? 0 ). toFixed ( 2 )}{ unit }
2026-06-01 23:24:32 +02:00
</ span >
2026-06-02 20:51:20 +02:00
) : consensus !== null && consensus !== undefined ? (
< span > Cons . < span className = "text-blue-400 font-medium" >{ fmt ( consensus )}</ span ></ span >
) : null }
2026-05-27 13:18:45 +02:00
</ div >
</ div >
);
}
2026-06-02 20:51:20 +02:00
function SignalBar ({ strength , direction } : { strength : number ; direction : SignalDir }) {
return (
< div className = "relative w-full h-1.5 bg-slate-700/50 rounded-full overflow-hidden" >
< motion.div
className = { `absolute left-0 top-0 h-full rounded-full ${ sigBar ( direction ) } ` }
initial = {{ width : 0 }}
animate = {{ width : ` ${ strength } %` }}
transition = {{ duration : 0.5 , ease : "easeOut" }}
/>
</ div >
);
}
// ─── Composant principal ──────────────────────────────────────────────────────
export default function CurrencyCard ({
currency , expectations , yields , sentiment , cot , ratePath , onDivergenceUpdate
} : Props ) {
const meta = CURRENCY_META [ currency ];
// ── State ────────────────────────────────────────────────────────────────────
const [ data , setData ] = useState < MacroData | null >( null );
const [ phase , setPhase ] = useState < BiasPhase >( "hawkish_pause" );
const [ loading , setLoading ] = useState ( true );
const [ rateExp , setRateExp ] = useState < RateExpectation | null >( null );
const [ fromCache , setFromCache ] = useState ( false );
const [ cacheAge , setCacheAge ] = useState < string | null >( null );
const [ activeTab , setActiveTab ] = useState < Tab >( "overview" );
2026-06-02 15:35:58 +02:00
const [ inflFilter , setInflFilter ] = useState < "all" | "mom" | "yoy" > ( "mom" );
2026-06-02 20:51:20 +02:00
const [ expandedSig , setExpandedSig ] = useState < string | null >( null );
2026-05-27 13:18:45 +02:00
2026-06-02 20:51:20 +02:00
// ── Data fetch ───────────────────────────────────────────────────────────────
2026-05-27 13:18:45 +02:00
const load = useCallback ( async () => {
setLoading ( true );
2026-05-28 22:58:27 +02:00
const cacheKey = `macro_ ${ currency } ` ;
2026-05-27 13:18:45 +02:00
try {
const res = await fetch ( `/api/macro?currency= ${ currency } ` );
2026-05-28 22:58:27 +02:00
if ( ! res . ok ) throw new Error ( `HTTP ${ res . status } ` );
2026-05-27 13:18:45 +02:00
const json : MacroData = await res . json ();
2026-05-28 22:58:27 +02:00
if ( "error" in json ) throw new Error ( String (( json as Record < string , unknown >). error ));
2026-05-28 23:13:40 +02:00
const prevCache = loadCache < MacroData >( cacheKey );
const merged : MacroData = {
... json ,
indicators : {
... json . indicators ,
pmiMfg : json.indicators.pmiMfg ?? prevCache ? . data . indicators . pmiMfg ?? null ,
pmiServices : json.indicators.pmiServices ?? prevCache ? . data . indicators . pmiServices ?? null ,
},
};
setData ( merged );
2026-05-28 22:58:27 +02:00
setFromCache ( false );
setCacheAge ( null );
2026-05-28 23:13:40 +02:00
saveCache ( cacheKey , merged );
2026-05-27 13:18:45 +02:00
2026-05-28 23:13:40 +02:00
const rateInd = merged . indicators . policyRate ;
2026-05-28 22:58:27 +02:00
if ( rateInd ? . trend === "up" ) setPhase ( "tightening" );
2026-05-27 13:18:45 +02:00
else if ( rateInd ? . trend === "down" ) setPhase ( "easing" );
2026-05-28 22:58:27 +02:00
else setPhase ( "hawkish_pause" );
} catch {
const cached = loadCache < MacroData >( cacheKey );
if ( cached ) {
setData ( cached . data );
setFromCache ( true );
setCacheAge ( formatCacheDate ( cached . savedAt ));
const rateInd = cached . data . indicators . policyRate ;
if ( rateInd ? . trend === "up" ) setPhase ( "tightening" );
else if ( rateInd ? . trend === "down" ) setPhase ( "easing" );
else setPhase ( "hawkish_pause" );
}
2026-05-27 13:18:45 +02:00
} finally {
setLoading ( false );
}
}, [ currency ]);
useEffect (() => { load (); }, [ load ]);
useEffect (() => {
if ( ! expectations ) return ;
const all = [
...(( expectations . rate_hikes ?? []) as RateExpectation []),
...(( expectations . rate_cuts ?? []) as RateExpectation []),
];
const cbShort = meta . cbShort . toLowerCase ();
2026-06-02 20:51:20 +02:00
setRateExp ( all . find (( e ) =>
e . cb . toLowerCase (). includes ( cbShort ) || e . cb . toLowerCase (). includes ( currency . toLowerCase ())
) ?? null );
2026-05-27 13:18:45 +02:00
}, [ expectations , currency , meta . cbShort ]);
2026-06-02 20:51:20 +02:00
// ── Computed values ──────────────────────────────────────────────────────────
const inds = data ? . indicators ;
const fc = data ? . forecasts ?? null ;
2026-05-27 13:18:45 +02:00
const forScoring = {
2026-06-02 20:51:20 +02:00
policyRate : { value : inds?.policyRate?.value ?? null , prev : inds?.policyRate?.prev ?? null , consensus : null , surprise : inds?.policyRate?.surprise ?? null , trend : inds?.policyRate?.trend ?? null , lastUpdated : "" },
cpiCore : { value : inds?.cpiCore?.value ?? null , prev : inds?.cpiCore?.prev ?? null , consensus : null , surprise : inds?.cpiCore?.surprise ?? null , trend : inds?.cpiCore?.trend ?? null , lastUpdated : "" },
pmiMfg : { value : inds?.pmiMfg?.value ?? null , prev : inds?.pmiMfg?.prev ?? null , consensus : null , surprise : inds?.pmiMfg?.surprise ?? null , trend : inds?.pmiMfg?.trend ?? null , lastUpdated : "" },
pmiServices : { value : inds?.pmiServices?.value ?? null , prev : inds?.pmiServices?.prev ?? null , consensus : null , surprise : inds?.pmiServices?.surprise ?? null , trend : inds?.pmiServices?.trend ?? null , lastUpdated : "" },
gdp : { value : inds?.gdp?.value ?? null , prev : inds?.gdp?.prev ?? null , consensus : null , surprise : inds?.gdp?.surprise ?? null , trend : inds?.gdp?.trend ?? null , lastUpdated : "" },
retailSales : { value : inds?.retailSales?.value ?? null , prev : inds?.retailSales?.prev ?? null , consensus : null , surprise : inds?.retailSales?.surprise ?? null , trend : inds?.retailSales?.trend ?? null , lastUpdated : "" },
2026-05-27 13:18:45 +02:00
unemployment : { value : inds?.unemployment?.value ?? null , prev : inds?.unemployment?.prev ?? null , consensus : null , surprise : inds?.unemployment?.surprise ?? null , trend : inds?.unemployment?.trend ?? null , lastUpdated : "" },
2026-06-02 20:51:20 +02:00
employment : { value : inds?.employment?.value ?? null , prev : inds?.employment?.prev ?? null , consensus : null , surprise : inds?.employment?.surprise ?? null , trend : inds?.employment?.trend ?? null , lastUpdated : "" },
2026-05-27 13:18:45 +02:00
};
const macroScore = calcMacroScore ( forScoring , phase );
const biasText = biasLabel ( macroScore );
2026-06-02 20:51:20 +02:00
const dir = scoreDir ( macroScore );
const yield10Y = yields ? . yields [ currency ] ?? null ;
2026-05-27 13:18:45 +02:00
const spread10Y = yields ? . spreads [ currency ] ?? null ;
2026-06-02 20:51:20 +02:00
useEffect (() => { onDivergenceUpdate ( currency , macroScore ); }, [ macroScore , currency , onDivergenceUpdate ]);
2026-06-01 23:24:32 +02:00
const rateConsensus = (() => {
const rate = inds ? . policyRate ? . value ?? null ;
if ( rate === null ) return null ;
if ( ratePath && ratePath . meetings . length > 0 ) {
const next = ratePath . meetings [ 0 ];
2026-06-02 20:51:20 +02:00
if ( next . probMovePct > 50 )
2026-06-01 23:24:32 +02:00
return next . probIsCut
? parseFloat (( rate - 0.25 ). toFixed ( 2 ))
: parseFloat (( rate + 0.25 ). toFixed ( 2 ));
return parseFloat ( rate . toFixed ( 2 ));
}
if ( ! rateExp ) return null ;
const desc = rateExp . prob_desc . toLowerCase ();
if ( desc . includes ( "no change" )) return parseFloat ( rate . toFixed ( 2 ));
if ( rateExp . direction === "cut" && rateExp . prob_pct > 50 ) return parseFloat (( rate - 0.25 ). toFixed ( 2 ));
if ( rateExp . direction === "hike" && rateExp . prob_pct > 50 ) return parseFloat (( rate + 0.25 ). toFixed ( 2 ));
return parseFloat ( rate . toFixed ( 2 ));
})();
2026-06-02 20:51:20 +02:00
// ── Mispricing signals (computed from live data) ──────────────────────────────
const mispricingSignals : {
id : string ; label : string ; value : string ; detail : string ;
direction : SignalDir ; strength : number ; icon : React.ReactNode ;
}[] = [];
// COT signal — champs disponibles : net, longPct, shortPct, totalLev
if ( cot ) {
const cotDir : SignalDir = cot . longPct > 60 ? "bearish" // majorité long = contrarian bearish
: cot . shortPct > 60 ? "bullish" // majorité short = contrarian bullish
: cot . net > 0 ? "bullish" : cot . net < 0 ? "bearish" : "neutral" ;
const imbalance = Math . abs ( cot . longPct - cot . shortPct );
mispricingSignals . push ({
id : "cot" , label : "Sentiment Retail (COT)" , direction : cotDir ,
value : ` ${ cot . longPct . toFixed ( 0 ) } % L / ${ cot . shortPct . toFixed ( 0 ) } % S` ,
detail : `Retail ${ cot . longPct . toFixed ( 0 ) } % long vs ${ cot . shortPct . toFixed ( 0 ) } % short (imbalance ${ imbalance . toFixed ( 0 ) } %). ${ cotDir === "bullish" ? "Majorité short → signal contrarian haussier." : cotDir === "bearish" ? "Majorité long → signal contrarian baissier." : "Sentiment équilibré." } ` ,
strength : Math.min ( 100 , imbalance * 2 ),
icon : < BarChart2 size = { 13 } />,
});
}
// OIS / rate probability
if ( ratePath && ratePath . meetings . length > 0 ) {
const peak = ratePath . peakMeeting ;
if ( peak ) {
const oisDir : SignalDir = peak . probIsCut ? "bearish" : "bullish" ;
mispricingSignals . push ({
id : "ois" , label : "OIS Probabilité de Move" , direction : oisDir ,
value : ` ${ peak . probMovePct . toFixed ( 0 ) } % ${ peak . probIsCut ? "Cut" : "Hike" } ` ,
detail : `Pic de probabilité OIS : ${ peak . probMovePct . toFixed ( 0 ) } % ${ peak . probIsCut ? "de baisse" : "de hausse" } lors de la réunion ${ peak . label } . Taux fin d'année implicite : ${ ratePath . yearEndImplied ? . toFixed ( 2 ) ?? "—" } %.` ,
strength : peak.probMovePct ,
icon : < Target size = { 13 } />,
});
}
}
// Yield curve / spread
if ( spread10Y !== null && currency !== "USD" ) {
const curveDir : SignalDir = spread10Y > 0 ? "bullish" : spread10Y < - 150 ? "bearish" : "neutral" ;
mispricingSignals . push ({
id : "yield" , label : "Spread 10Y vs USD" , direction : curveDir ,
value : ` ${ spread10Y > 0 ? "+" : "" }${ spread10Y } bps` ,
detail : `Différentiel de taux 10 ans vs US : ${ spread10Y > 0 ? "+" : "" }${ spread10Y } bps. ${ Math . abs ( spread10Y ) > 100 ? "Écart important — potentiel de compression/expansion non pricé." : "Différentiel modéré." } ` ,
strength : Math.min ( 100 , Math . abs ( spread10Y ) / 2 ),
icon : < Activity size = { 13 } />,
});
}
// Inflation pressure
const cpiYoY = inds ? . cpiYoY ? . value ?? inds ? . cpiCore ? . value ?? null ;
const policyRate = inds ? . policyRate ? . value ?? null ;
if ( cpiYoY !== null && policyRate !== null ) {
const realRate = policyRate - cpiYoY ;
const inflDir : SignalDir = realRate < 0 ? "bearish" : realRate > 1.5 ? "bullish" : "neutral" ;
mispricingSignals . push ({
id : "inflation" , label : "Taux Réel (Taux − Inflation)" , direction : inflDir ,
value : ` ${ realRate > 0 ? "+" : "" }${ realRate . toFixed ( 2 ) } %` ,
detail : `Taux directeur ${ policyRate . toFixed ( 2 ) } % − Inflation YoY ${ cpiYoY . toFixed ( 2 ) } % = Taux réel ${ realRate . toFixed ( 2 ) } %. ${ realRate < 0 ? "Taux réel négatif → politique encore accommodante → bearish devise." : "Taux réel positif → politique restrictive → bullish devise." } ` ,
strength : Math.min ( 100 , Math . abs ( realRate ) * 25 ),
icon : < Zap size = { 13 } />,
});
}
// Sentiment
if ( sentiment ) {
const sentDir : SignalDir = sentiment . signal === "contrarian_bullish" ? "bullish"
: sentiment . signal === "contrarian_bearish" ? "bearish" : "neutral" ;
mispricingSignals . push ({
id : "sentiment" , label : "Sentiment Retail (Contrarian)" , direction : sentDir ,
value : ` ${ sentiment . longPct . toFixed ( 0 ) } % Long` ,
detail : `Retail ${ sentiment . longPct . toFixed ( 0 ) } % long / ${ sentiment . shortPct . toFixed ( 0 ) } % short. Signal contrarian : ${ sentDir === "bullish" ? "majorité short → opportunité haussière" : sentDir === "bearish" ? "majorité long → opportunité baissière" : "sentiment neutre" } .` ,
strength : Math.abs ( sentiment . longPct - 50 ) * 2 ,
icon : < Eye size = { 13 } />,
});
}
const bullCount = mispricingSignals . filter ( s => s . direction === "bullish" ). length ;
const bearCount = mispricingSignals . filter ( s => s . direction === "bearish" ). length ;
const avgStr = mispricingSignals . length > 0
? Math . round ( mispricingSignals . reduce (( a , s ) => a + s . strength , 0 ) / mispricingSignals . length ) : 0 ;
const mispricDir : SignalDir = bullCount > bearCount ? "bullish" : bearCount > bullCount ? "bearish" : "neutral" ;
// ── Tabs config ──────────────────────────────────────────────────────────────
const TABS : { id : Tab ; label : string ; icon : React.ReactNode }[] = [
{ id : "overview" , label : "Aperçu" , icon : < Layers size = { 10 } /> },
{ id : "mispricing" , label : "Signaux" , icon : < Eye size = { 10 } /> },
{ id : "focus" , label : "Focus" , icon : < Target size = { 10 } /> },
];
// ── Render ────────────────────────────────────────────────────────────────────
2026-05-27 13:18:45 +02:00
return (
2026-06-02 20:51:20 +02:00
< div className = "bg-slate-900 border border-slate-800 rounded-2xl flex flex-col" >
{ /* ── Header ──────────────────────────────────────────────────────────── */ }
< div className = "px-4 pt-4 pb-3 border-b border-slate-800" >
< div className = "flex items-start justify-between mb-2" >
< div className = "flex items-center gap-2.5" >
2026-05-27 13:18:45 +02:00
< span className = "text-2xl leading-none" >{ meta . flag }</ span >
< div >
2026-06-02 20:51:20 +02:00
< div className = "flex items-center gap-2" >
< span className = "text-xl font-black text-white tracking-tight" >{ currency }</ span >
{ ! loading && (
< span className = { `text-sm font-bold ${ sigColor ( dir ) } ` }>{ biasText }</ span >
)}
</ div >
< div className = "text-[11px] text-slate-500" >{ meta . cbShort } · { meta . name }</ div >
2026-05-27 13:18:45 +02:00
</ div >
</ div >
< div className = "text-right" >
{ loading
2026-06-02 20:51:20 +02:00
? < Loader2 size = { 16 } className = "animate-spin text-slate-600 mt-1" />
2026-05-27 13:18:45 +02:00
: <>
2026-06-02 20:51:20 +02:00
< div className = { `text-2xl font-black tabular-nums ${ sigColor ( dir ) } ` }>
{ macroScore > 0 ? "+" : "" }{ macroScore }
</ div >
< div className = "text-[10px] text-slate-600" > score </ div >
2026-05-27 13:18:45 +02:00
</>
}
</ div >
</ div >
2026-06-02 20:51:20 +02:00
{ /* Phase pill + mispricing + cache */ }
< div className = "flex items-center gap-2 flex-wrap" >
< span className = { `text-[10px] font-semibold px-2 py-0.5 rounded-full border ${ phaseStyle ( phase ) } ` }>
{ phaseLabel ( phase )}
</ span >
{ mispricingSignals . length > 0 && (
< span className = { `text-[10px] font-semibold px-2 py-0.5 rounded-full border flex items-center gap-1 ${ sigBg ( mispricDir ) } ` }>
< span className = { sigColor ( mispricDir )}>
{ mispricDir === "bullish" ? "▲" : mispricDir === "bearish" ? "▼" : "—" } { avgStr }
</ span >
< span className = "text-slate-600" > signaux </ span >
</ span >
)}
2026-05-28 22:58:27 +02:00
{ fromCache && cacheAge && (
2026-06-02 20:51:20 +02:00
< span className = "flex items-center gap-0.5 text-[9px] text-amber-500" title = "Données depuis le cache local" >
< Database size = { 9 } /> cache { cacheAge }
</ span >
2026-05-28 22:58:27 +02:00
)}
</ div >
2026-05-27 13:18:45 +02:00
</ div >
2026-06-02 20:51:20 +02:00
{ /* ── Tab bar ─────────────────────────────────────────────────────────── */ }
< div className = "flex border-b border-slate-800 px-1 pt-1" >
{ TABS . map (( t ) => (
< button
key = { t . id }
onClick = {() => setActiveTab ( t . id )}
className = { `flex items-center gap-1.5 px-3 py-2 text-[11px] font-medium rounded-t-lg transition-all ${
activeTab === t . id
? "text-white bg-slate-800"
: "text-slate-500 hover:text-slate-300"
} ` }
>
{ t . icon }
{ t . label }
{ t . id === "mispricing" && mispricingSignals . length > 0 && (
< span className = { `text-[9px] font-bold px-1 rounded ${
mispricDir === "bullish" ? "bg-emerald-500/20 text-emerald-400"
: mispricDir === "bearish" ? "bg-red-500/20 text-red-400"
: "bg-slate-700 text-slate-400"
} ` }>
{ bullCount > bearCount ? ` ${ bullCount } ↑` : bearCount > bullCount ? ` ${ bearCount } ↓` : "—" }
2026-06-01 23:24:32 +02:00
</ span >
)}
2026-06-02 20:51:20 +02:00
</ button >
))}
< div className = "flex-1" />
< NarrativeButton currency = { currency } phase = { phase } macroScore = { macroScore } />
</ div >
2026-06-01 23:24:32 +02:00
2026-06-02 20:51:20 +02:00
{ /* ── Tab content ─────────────────────────────────────────────────────── */ }
< div className = "flex-1 overflow-y-auto p-3" >
< AnimatePresence mode = "wait" >
< motion.div
key = { activeTab }
initial = {{ opacity : 0 , y : 5 }}
animate = {{ opacity : 1 , y : 0 }}
exit = {{ opacity : 0 , y : - 5 }}
transition = {{ duration : 0.12 }}
className = "space-y-2"
>
{ /* ════ APERÇU ════════════════════════════════════════════════════ */ }
{ activeTab === "overview" && (
<>
{ /* OIS mini-chart */ }
{ ratePath && ratePath . meetings . length > 0 && (
< div className = "bg-slate-800/40 rounded-xl border border-slate-700/30 p-3" >
< div className = "flex items-center justify-between mb-2" >
< span className = "text-[10px] text-slate-500 uppercase tracking-wider font-semibold" > OIS · Probabilités de move </ span >
< span className = "text-[10px] text-slate-600" > au { ratePath . asOf }</ span >
</ div >
< ResponsiveContainer width = "100%" height = { 48 }>
< LineChart
data = { ratePath . meetings . map ( m => ({
label : m.label ,
prob : m.probMovePct ,
}))}
margin = {{ top : 2 , right : 0 , left : 0 , bottom : 0 }}
>
< Line type = "monotone" dataKey = "prob" stroke = "#f59e0b" strokeWidth = { 1.5 } dot = {{ r : 2 , fill : "#f59e0b" }} />
< XAxis dataKey = "label" tick = {{ fontSize : 8 , fill : "#64748b" }} axisLine = { false } tickLine = { false } />
< Tooltip
contentStyle = {{ background : "#0f172a" , border : "1px solid #334155" , borderRadius : 6 , fontSize : 10 }}
labelStyle = {{ color : "#94a3b8" }}
itemStyle = {{ color : "#f59e0b" }}
formatter = {( v : number ) => [ ` ${ v . toFixed ( 0 ) } %` , "Probabilité" ]}
/>
</ LineChart >
</ ResponsiveContainer >
{ ratePath . peakMeeting && (
< div className = "flex items-center justify-between mt-1 text-[10px]" >
< span className = "text-slate-600" > Pic : < span className = "text-slate-400" >{ ratePath . peakMeeting . label }</ span ></ span >
< span className = { ratePath . peakMeeting . probIsCut ? "text-sky-400 font-bold" : "text-red-400 font-bold" }>
{ ratePath . peakMeeting . probMovePct . toFixed ( 0 )} % { ratePath . peakMeeting . probIsCut ? "Cut" : "Hike" }
</ span >
{ ratePath . yearEndImplied !== null && (
< span className = "text-slate-600" > fin an : { ratePath . yearEndImplied . toFixed ( 2 )} % </ span >
)}
</ div >
)}
</ div >
)}
2026-06-01 23:24:32 +02:00
2026-06-02 20:51:20 +02:00
{ /* Politique Monétaire */ }
< MacroBlock title = "Politique Monétaire" >
< IRow
label = "Taux directeur"
ind = { inds ? . policyRate ?? null }
unit = "%" consensus = { rateConsensus }
/>
< div className = "flex items-center justify-between text-[12px]" >
< div className = "flex items-center gap-1.5" >
< span className = "text-slate-600" > → </ span >
< span className = "text-slate-400" > 10 Y Yield </ span >
</ div >
< div className = "flex items-center gap-2" >
< span className = "font-semibold text-slate-200 tabular-nums" >
{ yield10Y !== null ? ` ${ yield10Y . toFixed ( 2 ) } %` : "—" }
</ span >
{ spread10Y !== null && (
< span className = { `text-[10px] ${ spread10Y > 0 ? "text-emerald-500" : "text-red-500" } ` }>
({ spread10Y > 0 ? "+" : "" }{ spread10Y } bps vs US )
</ span >
)}
</ div >
2026-06-01 23:24:32 +02:00
</ div >
2026-06-02 20:51:20 +02:00
</ MacroBlock >
{ /* Inflation avec filtre MoM/YoY */ }
< div className = "bg-slate-800/40 rounded-xl border border-slate-700/30 p-3 space-y-1.5" >
< div className = "flex items-center justify-between mb-2" >
< span className = "text-[10px] text-slate-500 uppercase tracking-wider font-semibold" > Inflation </ span >
< div className = "flex gap-0.5" >
{([ "all" , "mom" , "yoy" ] as const ). map (( v ) => (
< button
key = { v }
onClick = {() => setInflFilter ( v )}
className = { `text-[8px] px-1.5 py-0.5 rounded font-semibold uppercase tracking-wide transition-colors ${
inflFilter === v
? "bg-slate-600 text-white"
: "text-slate-500 hover:text-slate-300"
} ` }
>
{ v === "all" ? "Tout" : v . toUpperCase ()}
</ button >
2026-06-01 23:24:32 +02:00
))}
</ div >
2026-06-02 20:51:20 +02:00
</ div >
{( inflFilter === "all" || inflFilter === "mom" ) && (
<>
< IRow label = "PPI MoM" ind = { inds ? . ppiMoM ?? null } unit = "%" consensus = { fc ? . ppiMoM ?? null } />
< IRow label = "CPI MoM" ind = { inds ? . cpiMoM ?? null } unit = "%" consensus = { fc ? . cpiMoM ?? null } info = "Source : Inflation Rate MoM (TradingEconomics)" />
< IRow
label = {(() => {
const isQoQ = ( inds ? . cpiCoreMoM as ( Ind & { isQoQ? : boolean }) | null ) ? . isQoQ ;
return isQoQ ? "Core CPI QoQ" : "Core CPI MoM" ;
})()}
ind = { inds ? . cpiCoreMoM ?? null }
unit = "%" consensus = { fc ? . cpiCoreMoM ?? null }
info = "=Core Inflation Rate MoM"
tooltip = {(() => {
const raw = ( inds ? . cpiCoreMoM as ( Ind & { _raw ?: { last : number ; prev : number ; refMonth : string } }) | null ) ? . _raw ;
return raw ? `Index: last= ${ raw . last } prev= ${ raw . prev } ref= ${ raw . refMonth } ` : null ;
})()}
/>
</>
)}
{( inflFilter === "all" || inflFilter === "yoy" ) && (
<>
< IRow
label = "Core CPI YoY"
ind = { inds ? . cpiCore ?? null }
unit = "%" consensus = { fc ? . cpiCore ?? fc ? . cpi ?? null }
surpriseVsCons = { fc ? . cpiSurprise ?? null }
info = {(() => {
const fd = ( inds ? . cpiCore as ( Ind & { _finalForecast? : number ; _finalDelta? : number }) | null );
const base = "=Core Inflation Rate YoY" ;
if ( fd ? . _finalForecast === undefined ) return base ;
const d = fd . _finalDelta ?? 0 ;
return ` ${ base } • Final prévu : ${ fd . _finalForecast ? . toFixed ( 1 ) } % ( ${ d > 0 ? "↑" : d < 0 ? "↓" : "=" } )` ;
})()}
/>
< IRow
label = "Inflation Rate YoY"
ind = { inds ? . cpiYoY ?? null }
unit = "%" consensus = { fc ? . cpi ?? null }
info = {(() => {
const fd = ( inds ? . cpiYoY as ( Ind & { _finalForecast? : number ; _finalDelta? : number }) | null );
const base = "=Inflation Rate YoY" ;
if ( fd ? . _finalForecast === undefined ) return base ;
const d = fd . _finalDelta ?? 0 ;
return ` ${ base } • Final prévu : ${ fd . _finalForecast ? . toFixed ( 1 ) } % ( ${ d > 0 ? "↑" : d < 0 ? "↓" : "=" } )` ;
})()}
/>
{ inds ? . commodityPricesYoY && (
< IRow label = "Commodity Prices YoY" ind = { inds . commodityPricesYoY } unit = "%" info = "AUD — RBA Commodity Price Index" />
)}
</>
2026-06-01 23:24:32 +02:00
)}
</ div >
2026-06-02 20:51:20 +02:00
{ /* Croissance */ }
< MacroBlock title = "Croissance" >
< IRow label = "PIB (QoQ%)" ind = { inds ? . gdp ?? null } unit = "%" consensus = { fc ? . gdp ?? null } surpriseVsCons = { fc ? . gdpSurprise ?? null } />
< IRow label = "PMI Composite" ind = { inds ? . pmiComposite ?? null } consensus = { fc ? . pmiComposite ?? null } surpriseVsCons = { fc ? . pmiCompositeSurprise ?? null } />
< IRow label = "Retail Sales" ind = { inds ? . retailSales ?? null } unit = "%" consensus = { fc ? . retailSales ?? null } surpriseVsCons = { fc ? . retailSalesSurprise ?? null } />
</ MacroBlock >
{ /* Emploi */ }
< MacroBlock title = "Emploi" >
< IRow label = "Variation emploi" ind = { inds ? . employment ?? null } unit = "k" consensus = { fc ? . employment ?? null } surpriseVsCons = { fc ? . employmentSurprise ?? null } />
< IRow label = "Taux de chômage" ind = { inds ? . unemployment ?? null } unit = "%" invertSurprise consensus = { fc ? . unemployment ?? null } surpriseVsCons = { fc ? . unemploymentSurprise ?? null } />
</ MacroBlock >
{ /* PMI détail */ }
< MacroBlock title = "PMI Détail" >
< IRow label = "PMI Manufacturing" ind = { inds ? . pmiMfg ?? null } consensus = { fc ? . pmiMfg ?? null } surpriseVsCons = { fc ? . pmiMfgSurprise ?? null } />
< IRow label = "PMI Services" ind = { inds ? . pmiServices ?? null } consensus = { fc ? . pmiSvc ?? null } surpriseVsCons = { fc ? . pmiSvcSurprise ?? null } />
</ MacroBlock >
</>
2026-06-01 23:24:32 +02:00
)}
2026-06-02 20:51:20 +02:00
{ /* ════ SIGNAUX / MISPRICING ════════════════════════════════════ */ }
{ activeTab === "mispricing" && (
<>
{ /* Biais global */ }
< div className = { `rounded-xl border p-3 flex items-center justify-between ${ sigBg ( mispricDir ) } ` }>
< div >
< div className = "text-[10px] text-slate-500 uppercase tracking-widest mb-0.5" > Biais Signaux </ div >
< div className = { `text-sm font-bold ${ sigColor ( mispricDir ) } ` }>
{ mispricDir === "bullish" ? ` ${ currency } Haussier` : mispricDir === "bearish" ? ` ${ currency } Baissier` : "Signal Mixte" }
</ div >
</ div >
< div className = "text-right" >
< div className = "text-[10px] text-slate-500 mb-0.5" > Force moy .</ div >
< div className = { `text-2xl font-black tabular-nums ${ sigColor ( mispricDir ) } ` }>{ avgStr }</ div >
</ div >
2026-06-01 23:24:32 +02:00
</ div >
2026-06-02 20:51:20 +02:00
{ /* COT mini chart */ }
{ cot && cot . history . length > 1 && (
< div className = "bg-slate-800/40 rounded-xl border border-slate-700/30 p-3" >
< div className = "flex items-center justify-between mb-2" >
< div className = "flex items-center gap-1.5" >
< BarChart2 size = { 11 } className = "text-slate-400" />
< span className = "text-[10px] text-slate-400 font-semibold uppercase tracking-wider" > Positions COT Nettes ( historique )</ span >
</ div >
< span className = { `text-xs font-bold ${ cot . deltaWoW > 0 ? "text-emerald-400" : "text-red-400" } ` }>
{ cot . deltaWoW > 0 ? "+" : "" }{ cot . deltaWoW . toFixed ( 0 )} k WoW
</ span >
</ div >
< ResponsiveContainer width = "100%" height = { 52 }>
< AreaChart
data = { cot . history . slice ( - 8 ). map ( h => ({ date : h.weekEnding.slice ( 5 ), net : h.net }))}
margin = {{ top : 2 , right : 0 , left : 0 , bottom : 0 }}
>
< defs >
< linearGradient id = { `cot- ${ currency } ` } x1 = "0" y1 = "0" x2 = "0" y2 = "1" >
< stop offset = "5%" stopColor = { cot . deltaWoW > 0 ? "#10b981" : "#ef4444" } stopOpacity = { 0.3 } />
< stop offset = "95%" stopColor = { cot . deltaWoW > 0 ? "#10b981" : "#ef4444" } stopOpacity = { 0 } />
</ linearGradient >
</ defs >
< Area type = "monotone" dataKey = "net"
stroke = { cot . deltaWoW > 0 ? "#10b981" : "#ef4444" }
fill = { `url(#cot- ${ currency } )` } strokeWidth = { 1.5 } dot = { false }
/>
< XAxis dataKey = "date" tick = {{ fontSize : 8 , fill : "#64748b" }} axisLine = { false } tickLine = { false } />
< Tooltip
contentStyle = {{ background : "#0f172a" , border : "1px solid #334155" , borderRadius : 6 , fontSize : 10 }}
labelStyle = {{ color : "#94a3b8" }}
itemStyle = {{ color : cot.deltaWoW > 0 ? "#10b981" : "#ef4444" }}
formatter = {( v : number ) => [ ` ${ v . toFixed ( 0 ) } k` , "Nette" ]}
/>
</ AreaChart >
</ ResponsiveContainer >
</ div >
)}
{ /* Signal list */ }
< div className = "space-y-2" >
{ mispricingSignals . length === 0 && (
< div className = "text-center text-slate-600 text-[11px] py-4" > Données insuffisantes pour calculer les signaux </ div >
)}
{ mispricingSignals . map (( sig ) => (
< div key = { sig . id }
className = "rounded-xl border overflow-hidden bg-slate-800/30"
style = {{
borderColor : sig.direction === "bullish" ? "rgba(16,185,129,0.15)"
: sig . direction === "bearish" ? "rgba(239,68,68,0.15)"
: "rgba(100,116,139,0.2)"
}}
>
< button
className = "w-full px-3 py-2.5 flex items-center gap-3 text-left"
onClick = {() => setExpandedSig ( expandedSig === sig . id ? null : sig . id )}
>
< span className = { `p-1.5 rounded-lg shrink-0 ${
sig . direction === "bullish" ? "bg-emerald-500/15 text-emerald-400"
: sig . direction === "bearish" ? "bg-red-500/15 text-red-400"
: "bg-amber-500/15 text-amber-400"
} ` }>
{ sig . icon }
</ span >
< div className = "flex-1 min-w-0" >
< div className = "flex items-center justify-between mb-1" >
< span className = "text-[11px] text-slate-300 font-medium" >{ sig . label }</ span >
< span className = { `text-[11px] font-bold tabular-nums ${ sigColor ( sig . direction ) } ` }>{ sig . value }</ span >
</ div >
< SignalBar strength = { sig . strength } direction = { sig . direction } />
</ div >
< ChevronRight size = { 12 } className = { `text-slate-600 shrink-0 transition-transform ${ expandedSig === sig . id ? "rotate-90" : "" } ` } />
</ button >
< AnimatePresence >
{ expandedSig === sig . id && (
< motion.div
initial = {{ height : 0 , opacity : 0 }}
animate = {{ height : "auto" , opacity : 1 }}
exit = {{ height : 0 , opacity : 0 }}
transition = {{ duration : 0.2 }}
className = "overflow-hidden"
>
< div className = "px-3 pb-3 pt-1 border-t border-slate-700/40" >
< p className = "text-[11px] text-slate-400 leading-relaxed" >{ sig . detail }</ p >
</ div >
</ motion.div >
)}
</ AnimatePresence >
</ div >
))}
2026-06-01 23:24:32 +02:00
</ div >
2026-06-02 20:51:20 +02:00
</>
)}
{ /* ════ FOCUS DONNÉES ══════════════════════════════════════════════ */ }
{ activeTab === "focus" && (
<>
{ /* Context phase */ }
< div className = { `rounded-xl border p-3 text-[11px] ${ sigBg ( trendDir ( phase === "tightening" ? "up" : phase === "easing" ? "down" : null )) } ` }>
< div className = { `font-semibold mb-1 ${ sigColor ( trendDir ( phase === "tightening" ? "up" : phase === "easing" ? "down" : null )) } ` }>
Phase { phaseLabel ( phase )}
</ div >
< p className = "text-slate-400 leading-relaxed" >
{ phase === "tightening" && "Surveiller les données soutenant une poursuite du resserrement : inflation, emploi solide, PMI expansionniste." }
{ phase === "easing" && "Surveiller les données justifiant des baisses : désinflation, ralentissement du marché de l'emploi, PMI en contraction." }
{ phase === "hawkish_pause" && "Surveiller les données qui pourraient forcer la main : regain d'inflation ou au contraire fort ralentissement." }
{ phase === "dovish_pause" && "Surveiller les signes de reprise permettant une normalisation de la politique monétaire." }
{ phase === "transition" && "Phase de transition — tous les indicateurs sont importants pour déterminer la direction future." }
2026-06-01 23:24:32 +02:00
</ p >
2026-06-02 20:51:20 +02:00
</ div >
{ /* Données clés selon phase */ }
< MacroBlock title = { phase === "easing" || phase === "dovish_pause" ? "Données Assouplissement" : "Données Resserrement" }>
{( phase === "easing" || phase === "dovish_pause" )
? <>
< IRow label = "Taux de chômage" ind = { inds ? . unemployment ?? null } unit = "%" invertSurprise consensus = { fc ? . unemployment ?? null } />
< IRow label = "Variation emploi" ind = { inds ? . employment ?? null } unit = "k" consensus = { fc ? . employment ?? null } />
< IRow label = "PIB (QoQ%)" ind = { inds ? . gdp ?? null } unit = "%" consensus = { fc ? . gdp ?? null } />
< IRow label = "PMI Composite" ind = { inds ? . pmiComposite ?? null } consensus = { fc ? . pmiComposite ?? null } />
< IRow label = "Retail Sales" ind = { inds ? . retailSales ?? null } unit = "%" consensus = { fc ? . retailSales ?? null } />
</>
: <>
< IRow label = "CPI MoM" ind = { inds ? . cpiMoM ?? null } unit = "%" consensus = { fc ? . cpiMoM ?? null } />
< IRow label = "Core CPI YoY" ind = { inds ? . cpiCore ?? null } unit = "%" consensus = { fc ? . cpiCore ?? null } />
< IRow label = "Inflation Rate YoY" ind = { inds ? . cpiYoY ?? null } unit = "%" consensus = { fc ? . cpi ?? null } />
< IRow label = "PMI Services" ind = { inds ? . pmiServices ?? null } consensus = { fc ? . pmiSvc ?? null } />
< IRow label = "Variation emploi" ind = { inds ? . employment ?? null } unit = "k" consensus = { fc ? . employment ?? null } />
</>
}
</ MacroBlock >
{ /* Autres indicateurs (référence) */ }
< MacroBlock title = "Référence (autres données)" >
{( phase === "easing" || phase === "dovish_pause" )
? <>
< IRow label = "CPI MoM" ind = { inds ? . cpiMoM ?? null } unit = "%" />
< IRow label = "Core CPI YoY" ind = { inds ? . cpiCore ?? null } unit = "%" />
< IRow label = "PPI MoM" ind = { inds ? . ppiMoM ?? null } unit = "%" />
</>
: <>
< IRow label = "Taux de chômage" ind = { inds ? . unemployment ?? null } unit = "%" invertSurprise />
< IRow label = "PIB (QoQ%)" ind = { inds ? . gdp ?? null } unit = "%" />
< IRow label = "Retail Sales" ind = { inds ? . retailSales ?? null } unit = "%" />
</>
}
</ MacroBlock >
{ /* Taux directeur */ }
< MacroBlock title = "Politique Monétaire" >
< IRow label = "Taux directeur" ind = { inds ? . policyRate ?? null } unit = "%" consensus = { rateConsensus } />
{ yield10Y !== null && (
< div className = "flex items-center justify-between text-[12px]" >
< div className = "flex items-center gap-1.5" >
< span className = "text-slate-600" > → </ span >
< span className = "text-slate-400" > 10 Y Yield </ span >
</ div >
< span className = "font-semibold text-slate-200 tabular-nums" >{ yield10Y . toFixed ( 2 )} % </ span >
</ div >
)}
</ MacroBlock >
</>
2026-06-01 23:24:32 +02:00
)}
2026-05-27 13:18:45 +02:00
2026-06-02 20:51:20 +02:00
</ motion.div >
</ AnimatePresence >
2026-05-27 13:18:45 +02:00
</ div >
</ div >
);
}