mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-15 13:38:06 +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
+105
-17
@@ -5,6 +5,7 @@ import type { Currency } from "@/lib/types";
|
||||
export const dynamic = "force-dynamic";
|
||||
import cpiOverridesRaw from "@/data/cpi_overrides.json";
|
||||
import rateDecisionsRaw from "@/data/rate_decisions.json";
|
||||
import moneySupplyM3Raw from "@/data/money-supply-m3.json";
|
||||
import { fetchFFThisWeek, fetchFFEvents } from "@/lib/forexfactory";
|
||||
import type { FFEvent } from "@/lib/forexfactory";
|
||||
import { fetchTECoreInflation, fetchTEMoMInflation, fetchTEInflationYoY, fetchTECoreCPIMoM, fetchTECoreConsumerPricesIndex, fetchTEPPIMoM, fetchTECoreInflationPages, fetchTEInflationYoYPages, fetchTEAUDCommodityYoY, fetchTEGDPGrowthRate, fetchTEUnemploymentRate, fetchTESTIRRate, fetchTEEmploymentChange } from "@/lib/tecpi";
|
||||
@@ -129,6 +130,78 @@ async function boeRate(): Promise<Obs[]> {
|
||||
} catch { return []; }
|
||||
}
|
||||
|
||||
// ── SNB officiel (CHF policy rate) ─────────────────────────────────────────────
|
||||
// snb.ch publie ses décisions sous /press-releases-restricted/pre_YYYYMMDD.
|
||||
// On découvre la plus récente depuis la page listing (1er lien du pattern),
|
||||
// puis on lit le taux dans le titre : "leaves ... unchanged at X%" / "lowers/raises ... to X%".
|
||||
async function scrapeSnbRate(): Promise<number | null> {
|
||||
const headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,*/*;q=0.8",
|
||||
};
|
||||
try {
|
||||
const listRes = await fetch("https://www.snb.ch/en/the-snb/mandates-goals/monetary-policy/decisions", {
|
||||
next: { revalidate: REVALIDATE }, headers,
|
||||
});
|
||||
if (!listRes.ok) return null;
|
||||
const listHtml = await listRes.text();
|
||||
const linkMatch = listHtml.match(/href="(\/en\/publications\/communication\/press-releases-restricted\/pre_\d{8}[^"]*)"/);
|
||||
if (!linkMatch) return null;
|
||||
|
||||
const prRes = await fetch(`https://www.snb.ch${linkMatch[1]}`, {
|
||||
next: { revalidate: REVALIDATE }, headers,
|
||||
});
|
||||
if (!prRes.ok) return null;
|
||||
const prHtml = await prRes.text();
|
||||
const rateMatch = prHtml.match(/SNB policy rate\s+(?:unchanged at|to)\s+(-?[\d.]+)%/i);
|
||||
return rateMatch ? parseFloat(rateMatch[1]) : null;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
// ── Fed officiel (USD policy rate) ──────────────────────────────────────────────
|
||||
// fomccalendars.htm liste, pour chaque réunion, un lien de statement
|
||||
// /newsevents/pressreleases/monetary(YYYYMMDD)a.htm. On prend la réunion passée
|
||||
// la plus récente puis on lit la fourchette officielle : "target range for the
|
||||
// federal funds rate at/to X to Y percent" (X/Y en fractions type "3-1/2").
|
||||
// On retourne le haut de fourchette (convention "upper bound" déjà utilisée ici).
|
||||
function parseFedFraction(s: string): number {
|
||||
const m = s.match(/^(\d+)(?:-(\d+)\/(\d+))?$/);
|
||||
if (!m) return NaN;
|
||||
const whole = parseFloat(m[1]);
|
||||
return m[2] ? whole + parseFloat(m[2]) / parseFloat(m[3]) : whole;
|
||||
}
|
||||
|
||||
async function scrapeFedRate(): Promise<number | null> {
|
||||
const headers = {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml,*/*;q=0.8",
|
||||
};
|
||||
try {
|
||||
const calRes = await fetch("https://www.federalreserve.gov/monetarypolicy/fomccalendars.htm", {
|
||||
next: { revalidate: REVALIDATE }, headers,
|
||||
});
|
||||
if (!calRes.ok) return null;
|
||||
const calHtml = await calRes.text();
|
||||
|
||||
const todayCompact = new Date().toISOString().slice(0,10).replace(/-/g,"");
|
||||
const dates = new Set<string>();
|
||||
for (const m of Array.from(calHtml.matchAll(/\/newsevents\/pressreleases\/monetary(\d{8})a\.htm/g))) dates.add(m[1]);
|
||||
const latestPast = Array.from(dates).filter(d => d <= todayCompact).sort().reverse()[0];
|
||||
if (!latestPast) return null;
|
||||
|
||||
const stRes = await fetch(`https://www.federalreserve.gov/newsevents/pressreleases/monetary${latestPast}a.htm`, {
|
||||
next: { revalidate: REVALIDATE }, headers,
|
||||
});
|
||||
if (!stRes.ok) return null;
|
||||
const stHtml = await stRes.text();
|
||||
const rateMatch = stHtml.match(/target range for the federal funds rate[\s\S]{0,60}?\d+(?:-\d\/\d)?\s*to\s*(\d+(?:-\d\/\d)?)\s*percent/i);
|
||||
if (!rateMatch) return null;
|
||||
|
||||
const upper = parseFedFraction(rateMatch[1]);
|
||||
return isNaN(upper) ? null : upper;
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
// ── DBnomics API (agrégateur IMF/IFS, BIS, OECD…) ────────────────────────────
|
||||
// Format : https://api.db.nomics.world/v22/series/{provider}/{dataset}/{code}?observations=1
|
||||
// Utilisé pour les séries absentes de FRED : JPY CPI, AUD/NZD CPI fallback
|
||||
@@ -530,6 +603,19 @@ function getRateDecision(ccy: string): RateDecision | null {
|
||||
return entry?.decisions?.[ccy] ?? null;
|
||||
}
|
||||
|
||||
// ── money-supply-m3.json — masse monétaire M3 (niveau, statique) ─────────────
|
||||
|
||||
type MoneySupplyM3 = {
|
||||
value: number; unit: string; period: string; isProxy: boolean;
|
||||
proxyLabel?: string; source: string;
|
||||
};
|
||||
|
||||
function getMoneySupplyM3(ccy: string): MoneySupplyM3 | null {
|
||||
type MsRaw = [{ series: Record<string, MoneySupplyM3> }];
|
||||
const entry = (moneySupplyM3Raw as unknown as MsRaw)[0];
|
||||
return entry?.series?.[ccy] ?? null;
|
||||
}
|
||||
|
||||
/** Construit un IndicatorResult à partir de la valeur TE + prev de l'override */
|
||||
function buildRateIndicator(current: number, prev: number, today: string): IndicatorResult {
|
||||
const surprise = parseFloat((current - prev).toFixed(4));
|
||||
@@ -846,11 +932,13 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
// USD — midpoint de la fourchette cible (DFEDTARU + DFEDTARL) / 2
|
||||
// Rateprobability.com / marchés quotent le midpoint (3.625%) pas l'upper bound (3.75%)
|
||||
// USD — TE scraping (taux Fed upper bound officiel = 3.75%)
|
||||
// USD — Fed officiel en primaire (source directe federalreserve.gov, cf. scrapeFedRate)
|
||||
// Repli TE scraping puis FRED upper bound si federalreserve.gov indisponible.
|
||||
// Ancien calcul midpoint FRED (DFEDTARU+DFEDTARL)/2 = 3.625% → pas le taux annoncé
|
||||
if (currency === "USD") {
|
||||
const te = await scrapeTeRate(TE_COUNTRY.USD);
|
||||
const fed = await scrapeFedRate();
|
||||
const ovr = getRateDecision("USD");
|
||||
const te = fed ?? await scrapeTeRate(TE_COUNTRY.USD);
|
||||
if (te !== null && ovr) {
|
||||
indicators.policyRate = buildRateIndicator(te, ovr.prev, today);
|
||||
} else if (te !== null) {
|
||||
@@ -930,15 +1018,17 @@ export async function GET(req: NextRequest) {
|
||||
}
|
||||
}
|
||||
|
||||
// CHF — TE scraping (taux SNB officiel exact = 0.00%)
|
||||
// CHF — SNB officiel en primaire (source directe snb.ch, cf. scrapeSnbRate)
|
||||
// Repli TE scraping si snb.ch indisponible.
|
||||
// IR3TIB01CHM156N = SARON 3M (~-0.04%) ≠ taux SNB officiel
|
||||
if (currency === "CHF") {
|
||||
const te = await scrapeTeRate(TE_COUNTRY.CHF);
|
||||
const snb = await scrapeSnbRate();
|
||||
const ovr = getRateDecision("CHF");
|
||||
if (te !== null && ovr) {
|
||||
indicators.policyRate = buildRateIndicator(te, ovr.prev, today);
|
||||
} else if (te !== null) {
|
||||
indicators.policyRate = { value: te, prev: null, surprise: null, trend: null, lastUpdated: today };
|
||||
const rate = snb ?? await scrapeTeRate(TE_COUNTRY.CHF);
|
||||
if (rate !== null && ovr) {
|
||||
indicators.policyRate = buildRateIndicator(rate, ovr.prev, today);
|
||||
} else if (rate !== null) {
|
||||
indicators.policyRate = { value: rate, prev: null, surprise: null, trend: null, lastUpdated: today };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1047,18 +1137,16 @@ export async function GET(req: NextRequest) {
|
||||
toDateObj.setDate(toDateObj.getDate() + 21);
|
||||
const toDate = toDateObj.toISOString().slice(0, 10);
|
||||
|
||||
const [ffPMI, pmiMfgRaw, pmiSvcRaw, pmiCompositeRaw, ffForecasts, teForecastMap, invForecastMap] = await Promise.all([
|
||||
const [ffPMI, pmiMfgRaw, pmiSvcRaw, pmiCompositeRaw, ffForecasts, teForecastMap] = await Promise.all([
|
||||
fetchFFPMI(currency),
|
||||
scrapePMI(currency, "manufacturing-pmi"),
|
||||
scrapePMI(currency, "services-pmi"),
|
||||
scrapePMI(currency, "composite-pmi"),
|
||||
fetchFFForecasts(currency),
|
||||
fetchTEInflationForecasts(today, toDate),
|
||||
(await import("@/lib/investing")).fetchInvestingInflationForecasts(today, toDate),
|
||||
]);
|
||||
|
||||
const teCpiForecast = teForecastMap[currency];
|
||||
const invForecast = invForecastMap[currency];
|
||||
// FF en priorité (forecast + actual) ; TE en fallback
|
||||
indicators.pmiMfg = ffPMI.mfg ? toPmiIndicator(ffPMI.mfg) : toPmiIndicator(pmiMfgRaw);
|
||||
indicators.pmiServices = ffPMI.svc ? toPmiIndicator(ffPMI.svc) : toPmiIndicator(pmiSvcRaw);
|
||||
@@ -1337,14 +1425,14 @@ export async function GET(req: NextRequest) {
|
||||
|
||||
const data = {
|
||||
currency, indicators,
|
||||
moneySupplyM3: getMoneySupplyM3(currency),
|
||||
forecasts: {
|
||||
// CPI — TE calendar forecast (priorité) puis ForexFactory
|
||||
// Les forecasts TE sont des strings "2.8%" → parseFloat les convertit en number
|
||||
cpi: parseTeF(teCpiForecast?.cpiYoY) ?? parseTeF(invForecast?.cpiYoY) ?? ffForecasts.cpi ?? null,
|
||||
cpiCore: parseTeF(teCpiForecast?.cpiCore) ?? parseTeF(invForecast?.cpiCore) ?? null,
|
||||
cpiMoM: parseTeF(teCpiForecast?.cpiMoM) ?? parseTeF(invForecast?.cpiMoM) ?? null,
|
||||
cpiCoreMoM: parseTeF(teCpiForecast?.cpiCoreMoM) ?? parseTeF(invForecast?.cpiCoreMoM) ?? null,
|
||||
ppiMoM: parseTeF(teCpiForecast?.ppiMoM) ?? parseTeF(invForecast?.ppiMoM) ?? null,
|
||||
cpi: parseTeF(teCpiForecast?.cpiYoY) ?? ffForecasts.cpi ?? null,
|
||||
cpiCore: parseTeF(teCpiForecast?.cpiCore) ?? null,
|
||||
cpiMoM: parseTeF(teCpiForecast?.cpiMoM) ?? null,
|
||||
cpiCoreMoM: parseTeF(teCpiForecast?.cpiCoreMoM) ?? null,
|
||||
ppiMoM: parseTeF(teCpiForecast?.ppiMoM) ?? null,
|
||||
cpiSurprise: ffForecasts.cpiSurprise,
|
||||
unemployment: ffForecasts.unemployment,
|
||||
unemploymentSurprise: ffForecasts.unemploymentSurprise,
|
||||
|
||||
Reference in New Issue
Block a user