mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-22 17:08:05 +00:00
feat: banques centrales (vote+dot plot+previsions), calendrier elargi, fix InvestingLive, M3 auto
Central bank governance (nouvel onglet Banques centrales) : - lib/centralBankGovernance.ts + app/api/central-bank-sources : scraping live du vote de la derniere reunion, dot plot Fed (SEP), et desormais les previsions macro (PIB + inflation) publiees par chaque BC elle-meme (Fed SEP, Eurosystem staff projections, BoJ Outlook Report PDF, SNB conditional forecast, BoC MPR, RBA SMP). GBP/NZD laisses honnetement vides quand aucune source chiffree fiable n'est accessible (RBNZ bloque par Cloudflare). - components/CentralBankSourcesTab.tsx : nouvel onglet avec cards par banque. Taux directeurs + Money Supply M3 : - data/rate_decisions.json corrige (JPY, EUR, NZD etc. etaient perimes d'1-2 decisions) et desormais auto-maintenu : .github/workflows/update-rate-decisions.yml (horaire) detecte les changements de taux via Trading Economics et fait glisser current -> prev sans perte de donnee. - data/money-supply-m3.json (nouveau) + .github/workflows/fetch-money-supply.yml (hebdo) : M3 par devise (proxy M2 pour l'USD, la Fed ne publiant plus M3 depuis 2006), affiche dans CurrencyCard. Calendrier economique elargi : - lib/calendar-countries.ts, lib/calendar-taxonomy.ts, lib/fxstreetCalendar.ts : couverture pays elargie + classification des evenements + source FXStreet. Fix InvestingLive : - lib/investinglive.ts : l'ancienne API WordPress (wp-json) renvoyait 404 depuis leur migration Nuxt.js -> reecrit vers api.investinglive.com/api/homepage/articles, + fix crash silencieux (Tldr pas toujours un tableau). Divers : - .vercel/ ajoute au .gitignore (ne doit jamais etre commite, cf. son propre README). - scripts/ (lancement PWA, push env Vercel), captures d'ecran, cache InvestingLive. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
82a732fbdf
commit
0657b50c07
@@ -39,10 +39,15 @@ interface MacroForecasts {
|
||||
gdp: number | null; gdpSurprise: number | null;
|
||||
employment: number | null; employmentSurprise: number | null;
|
||||
}
|
||||
interface MoneySupplyM3 {
|
||||
value: number; unit: string; period: string; isProxy: boolean;
|
||||
proxyLabel?: string; source: string;
|
||||
}
|
||||
interface MacroData {
|
||||
currency: string;
|
||||
indicators: Record<string, Ind | null>;
|
||||
forecasts?: MacroForecasts | null;
|
||||
moneySupplyM3?: MoneySupplyM3 | null;
|
||||
fetchedAt: string;
|
||||
}
|
||||
|
||||
@@ -216,6 +221,16 @@ const ENERGY_PROFILE: Record<string, {
|
||||
NZD: { type: "import", desc: "Import pétrole. Renouvelables ~85% élec (hydro). Indépendant localement.", products: ["Lait / Produits laitiers", "Viande bovine", "Bois", "Laine"] },
|
||||
};
|
||||
|
||||
// ─── Money Supply M3 — formatage compact (niveau, devise locale) ─────────────
|
||||
|
||||
function formatM3(m3: MoneySupplyM3): string {
|
||||
const mult = m3.unit.includes("Bn") ? 1e9 : m3.unit.includes("Mn") ? 1e6 : 1;
|
||||
const raw = m3.value * mult;
|
||||
const ccy = m3.unit.split(" ")[0];
|
||||
const compact = new Intl.NumberFormat("en-US", { notation: "compact", maximumFractionDigits: 2 }).format(raw);
|
||||
return `${compact} ${ccy}`;
|
||||
}
|
||||
|
||||
function trendDir(t: "up"|"down"|"flat"|null): SignalDir {
|
||||
if (t === "up") return "bullish";
|
||||
if (t === "down") return "bearish";
|
||||
@@ -387,6 +402,7 @@ function SourcesPopup({ currency, onClose }: { currency: string; onClose: () =>
|
||||
icon: "🏦",
|
||||
sources: [
|
||||
{ label: `Trading Economics — ${country} interest rate`, url: `https://tradingeconomics.com/${country}/interest-rate`, note: `Taux directeur ${cbName} actuel` },
|
||||
{ label: `Trading Economics — ${country} money supply M3`, url: `https://tradingeconomics.com/${country}/money-supply-m3`, note: currency === "USD" ? "M2 utilisé en proxy (M3 non publié par la Fed depuis 2006)" : "Masse monétaire M3" },
|
||||
{ label: "FRED — Federal Reserve Economic Data", url: "https://fred.stlouisfed.org", note: "Spreads crédit HY/IG (BAMLH0A0HYM2, BAMLC0A0CM)" },
|
||||
],
|
||||
},
|
||||
@@ -523,6 +539,11 @@ function OISEnhancedBlock({ ratePath, syncChartTab, onChartTabChange }: {
|
||||
const altRate = mlIsMove ? currentRate : m0.impliedRate;
|
||||
const altProb = mlIsMove ? 100 - m0.probMovePct : m0.probMovePct;
|
||||
const altIsMove = !mlIsMove;
|
||||
// moveLabel vaut toujours "Hold" quand le scénario principal est un statu quo (isMoveExpected
|
||||
// false) — le réutiliser pour l'alternative affichait donc "X% Hold" au lieu de "X% Hike/Cut"
|
||||
// quand l'alternative EST le mouvement. La direction de l'alternative-mouvement est toujours
|
||||
// celle de m0.probIsCut, indépendamment du scénario principal.
|
||||
const altLabel = altIsMove ? (m0.probIsCut ? "Cut" : "Hike") : "Hold";
|
||||
|
||||
// Chart data (limit to 10 meetings)
|
||||
const chartMeetings = meetings.slice(0, 10);
|
||||
@@ -588,7 +609,7 @@ function OISEnhancedBlock({ ratePath, syncChartTab, onChartTabChange }: {
|
||||
{altProb >= 15 && altIsMove !== mlIsMove && (
|
||||
<>
|
||||
<span className="text-[8px] text-slate-700 shrink-0">·</span>
|
||||
<span className="text-[9px] text-slate-500 shrink-0">{Math.round(altProb)}% {altIsMove ? moveLabel : "Hold"}</span>
|
||||
<span className="text-[9px] text-slate-500 shrink-0">{Math.round(altProb)}% {altLabel}</span>
|
||||
</>
|
||||
)}
|
||||
<span className="ml-auto text-[7px] text-slate-700 shrink-0">au {ratePath.asOf}</span>
|
||||
@@ -939,6 +960,7 @@ export default function CurrencyCard({
|
||||
// ── Computed values ──────────────────────────────────────────────────────────
|
||||
const inds = data?.indicators;
|
||||
const fc = data?.forecasts ?? null;
|
||||
const m3 = data?.moneySupplyM3 ?? null;
|
||||
|
||||
const forScoring = {
|
||||
policyRate: { value: inds?.policyRate?.value ?? null, prev: inds?.policyRate?.prev ?? null, consensus: null, surprise: inds?.policyRate?.surprise ?? null, trend: inds?.policyRate?.trend ?? null, lastUpdated: "" },
|
||||
@@ -1638,8 +1660,24 @@ export default function CurrencyCard({
|
||||
className="absolute inset-0 overflow-hidden"
|
||||
>
|
||||
|
||||
{/* Skeleton chargement — affiché quand pas encore de données */}
|
||||
{loading && !data && (
|
||||
<div className="bg-slate-800/40 rounded-xl border border-slate-700/30 p-3 space-y-2.5 animate-pulse h-full">
|
||||
<div className="h-2 bg-slate-700/60 rounded w-28 mb-3" />
|
||||
{[68, 82, 55, 76, 60].map((w, i) => (
|
||||
<div key={i} className="space-y-1">
|
||||
<div className="flex justify-between items-center">
|
||||
<div className="h-2.5 bg-slate-700/50 rounded" style={{ width: `${w}%` }} />
|
||||
<div className="h-2.5 bg-slate-700/40 rounded w-10" />
|
||||
</div>
|
||||
<div className="h-1.5 bg-slate-700/25 rounded w-1/3 ml-4" />
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* MON. — Politique Monétaire */}
|
||||
{macroSlide === "mon" && (
|
||||
{!loading && macroSlide === "mon" && (
|
||||
<MacroBlock title="Politique Monétaire">
|
||||
<IRow label="Taux directeur" ind={inds?.policyRate ?? null} unit="%" consensus={rateConsensus} isNew={indIsNew("policyRate")} />
|
||||
{(() => {
|
||||
@@ -1666,11 +1704,28 @@ export default function CurrencyCard({
|
||||
<span className="font-semibold text-slate-200 tabular-nums">{yield10Y.toFixed(2)}%</span>
|
||||
</div>
|
||||
)}
|
||||
{m3 && (
|
||||
<div className="flex items-center justify-between text-[12px]">
|
||||
<div className="flex items-center gap-1.5 min-w-0">
|
||||
<span className="text-slate-600 shrink-0">→</span>
|
||||
<span className="text-slate-400 truncate">Money Supply M3{m3.isProxy ? " (M2)" : ""}</span>
|
||||
{m3.isProxy && (
|
||||
<span className="relative group/info inline-flex shrink-0 cursor-help">
|
||||
<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">
|
||||
{m3.proxyLabel}
|
||||
</span>
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="font-semibold text-slate-200 tabular-nums shrink-0" title={`${m3.period} · ${m3.source}`}>{formatM3(m3)}</span>
|
||||
</div>
|
||||
)}
|
||||
</MacroBlock>
|
||||
)}
|
||||
|
||||
{/* INFL. — Inflation */}
|
||||
{macroSlide === "infl" && (
|
||||
{!loading && macroSlide === "infl" && (
|
||||
<MacroBlock title="Inflation">
|
||||
<IRow label="CPI MoM" ind={inds?.cpiMoM ?? null} unit="%" consensus={fc?.cpiMoM ?? null} isNew={indIsNew("cpiMoM")} />
|
||||
<IRow label="PPI MoM" ind={inds?.ppiMoM ?? null} unit="%" consensus={fc?.ppiMoM ?? null} isNew={indIsNew("ppiMoM")} />
|
||||
@@ -1680,7 +1735,7 @@ export default function CurrencyCard({
|
||||
)}
|
||||
|
||||
{/* CRO. — Croissance */}
|
||||
{macroSlide === "cro" && (
|
||||
{!loading && macroSlide === "cro" && (
|
||||
<MacroBlock title="Croissance">
|
||||
<IRow label="PIB (QoQ%)" ind={inds?.gdp ?? null} unit="%" consensus={fc?.gdp ?? null} surpriseVsCons={fc?.gdpSurprise ?? null} isNew={indIsNew("gdp")} />
|
||||
<IRow label="PMI Composite" ind={inds?.pmiComposite ?? null} consensus={fc?.pmiComposite ?? null} surpriseVsCons={fc?.pmiCompositeSurprise ?? null} isNew={indIsNew("pmiComposite")} />
|
||||
@@ -1691,7 +1746,7 @@ export default function CurrencyCard({
|
||||
)}
|
||||
|
||||
{/* EMPL. — Emploi */}
|
||||
{macroSlide === "empl" && (
|
||||
{!loading && macroSlide === "empl" && (
|
||||
<MacroBlock title="Emploi">
|
||||
<IRow label="Variation emploi" ind={inds?.employment ?? null} unit="k" consensus={fc?.employment ?? null} surpriseVsCons={fc?.employmentSurprise ?? null} isNew={indIsNew("employment")} />
|
||||
<IRow label="Taux de chômage" ind={inds?.unemployment ?? null} unit="%" invertSurprise consensus={fc?.unemployment ?? null} surpriseVsCons={fc?.unemploymentSurprise ?? null} isNew={indIsNew("unemployment")} />
|
||||
|
||||
Reference in New Issue
Block a user