mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-19 07:28:06 +00:00
auto: sync 2026-06-02 00:03
This commit is contained in:
+21
-2
@@ -6,6 +6,7 @@ import rateDecisionsRaw from "@/data/rate_decisions.json";
|
|||||||
import { fetchFFThisWeek, fetchFFEvents } from "@/lib/forexfactory";
|
import { fetchFFThisWeek, fetchFFEvents } from "@/lib/forexfactory";
|
||||||
import type { FFEvent } from "@/lib/forexfactory";
|
import type { FFEvent } from "@/lib/forexfactory";
|
||||||
import { fetchTECoreInflation, fetchTEMoMInflation } from "@/lib/tecpi";
|
import { fetchTECoreInflation, fetchTEMoMInflation } from "@/lib/tecpi";
|
||||||
|
import { fetchTEInflationForecasts } from "@/lib/tradingeconomics";
|
||||||
|
|
||||||
const FRED_BASE = "https://api.stlouisfed.org/fred/series/observations";
|
const FRED_BASE = "https://api.stlouisfed.org/fred/series/observations";
|
||||||
const REVALIDATE = 86400; // cache 24h
|
const REVALIDATE = 86400; // cache 24h
|
||||||
@@ -605,6 +606,13 @@ function toPmiIndicator(raw: { value: number | null; prev: number | null }): Ind
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Parser forecast string TE ("2.8%" ou "0.4") → number ─────────────────────
|
||||||
|
function parseTeF(s: string | null | undefined): number | null {
|
||||||
|
if (!s) return null;
|
||||||
|
const n = parseFloat(s.replace("%", "").trim());
|
||||||
|
return isNaN(n) ? null : n;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Server-side cache ─────────────────────────────────────────────────────────
|
// ── Server-side cache ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
const _cache = new Map<string, { data: unknown; ts: number }>();
|
const _cache = new Map<string, { data: unknown; ts: number }>();
|
||||||
@@ -948,13 +956,20 @@ export async function GET(req: NextRequest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── PMI (Mfg + Services + Composite) + consensus FF ──────────────────────────
|
// ── PMI (Mfg + Services + Composite) + consensus FF ──────────────────────────
|
||||||
const [ffPMI, pmiMfgRaw, pmiSvcRaw, pmiCompositeRaw, ffForecasts] = await Promise.all([
|
const toDateObj = new Date();
|
||||||
|
toDateObj.setDate(toDateObj.getDate() + 21);
|
||||||
|
const toDate = toDateObj.toISOString().slice(0, 10);
|
||||||
|
|
||||||
|
const [ffPMI, pmiMfgRaw, pmiSvcRaw, pmiCompositeRaw, ffForecasts, teForecastMap] = await Promise.all([
|
||||||
fetchFFPMI(currency),
|
fetchFFPMI(currency),
|
||||||
scrapePMI(currency, "manufacturing-pmi"),
|
scrapePMI(currency, "manufacturing-pmi"),
|
||||||
scrapePMI(currency, "services-pmi"),
|
scrapePMI(currency, "services-pmi"),
|
||||||
scrapePMI(currency, "composite-pmi"),
|
scrapePMI(currency, "composite-pmi"),
|
||||||
fetchFFForecasts(currency),
|
fetchFFForecasts(currency),
|
||||||
|
fetchTEInflationForecasts(today, toDate),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const teCpiForecast = teForecastMap[currency];
|
||||||
// FF en priorité (forecast + actual) ; TE en fallback
|
// FF en priorité (forecast + actual) ; TE en fallback
|
||||||
indicators.pmiMfg = ffPMI.mfg ? toPmiIndicator(ffPMI.mfg) : toPmiIndicator(pmiMfgRaw);
|
indicators.pmiMfg = ffPMI.mfg ? toPmiIndicator(ffPMI.mfg) : toPmiIndicator(pmiMfgRaw);
|
||||||
indicators.pmiServices = ffPMI.svc ? toPmiIndicator(ffPMI.svc) : toPmiIndicator(pmiSvcRaw);
|
indicators.pmiServices = ffPMI.svc ? toPmiIndicator(ffPMI.svc) : toPmiIndicator(pmiSvcRaw);
|
||||||
@@ -1047,7 +1062,11 @@ export async function GET(req: NextRequest) {
|
|||||||
const data = {
|
const data = {
|
||||||
currency, indicators,
|
currency, indicators,
|
||||||
forecasts: {
|
forecasts: {
|
||||||
cpi: ffForecasts.cpi,
|
// CPI — TE calendar forecast (priorité) puis ForexFactory
|
||||||
|
// Les forecasts TE sont des strings "2.8%" → parseFloat les convertit en number
|
||||||
|
cpi: parseTeF(teCpiForecast?.cpiYoY) ?? ffForecasts.cpi ?? null,
|
||||||
|
cpiCore: parseTeF(teCpiForecast?.cpiCore) ?? null,
|
||||||
|
cpiMoM: parseTeF(teCpiForecast?.cpiMoM) ?? null,
|
||||||
cpiSurprise: ffForecasts.cpiSurprise,
|
cpiSurprise: ffForecasts.cpiSurprise,
|
||||||
unemployment: ffForecasts.unemployment,
|
unemployment: ffForecasts.unemployment,
|
||||||
unemploymentSurprise: ffForecasts.unemploymentSurprise,
|
unemploymentSurprise: ffForecasts.unemploymentSurprise,
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import NarrativeButton from "./NarrativeButton";
|
|||||||
interface Ind { value: number | null; prev: number | null; surprise: number | null; trend: "up"|"down"|"flat"|null; lastUpdated: string | null }
|
interface Ind { value: number | null; prev: number | null; surprise: number | null; trend: "up"|"down"|"flat"|null; lastUpdated: string | null }
|
||||||
interface MacroForecasts {
|
interface MacroForecasts {
|
||||||
cpi: number | null; cpiSurprise: number | null;
|
cpi: number | null; cpiSurprise: number | null;
|
||||||
|
cpiCore: number | null;
|
||||||
|
cpiMoM: number | null;
|
||||||
unemployment: number | null; unemploymentSurprise: number | null;
|
unemployment: number | null; unemploymentSurprise: number | null;
|
||||||
pmiMfg: number | null; pmiMfgSurprise: number | null;
|
pmiMfg: number | null; pmiMfgSurprise: number | null;
|
||||||
pmiSvc: number | null; pmiSvcSurprise: number | null;
|
pmiSvc: number | null; pmiSvcSurprise: number | null;
|
||||||
@@ -356,8 +358,8 @@ export default function CurrencyCard({ currency, expectations, yields, sentiment
|
|||||||
|
|
||||||
{/* ── INFLATION ───────────────────────────────────────────────────── */}
|
{/* ── INFLATION ───────────────────────────────────────────────────── */}
|
||||||
<SectionHeader label="Inflation" />
|
<SectionHeader label="Inflation" />
|
||||||
<Row label="CPI Core YoY" ind={inds?.cpiCore ?? null} unit="%" consensus={fc?.cpi ?? null} surpriseVsCons={fc?.cpiSurprise ?? null} />
|
<Row label="CPI Core YoY" ind={inds?.cpiCore ?? null} unit="%" consensus={fc?.cpiCore ?? fc?.cpi ?? null} surpriseVsCons={fc?.cpiSurprise ?? null} />
|
||||||
<Row label="CPI MoM" ind={inds?.cpiMoM ?? null} unit="%" />
|
<Row label="CPI MoM" ind={inds?.cpiMoM ?? null} unit="%" consensus={fc?.cpiMoM ?? null} />
|
||||||
|
|
||||||
{/* ── CROISSANCE ──────────────────────────────────────────────────── */}
|
{/* ── CROISSANCE ──────────────────────────────────────────────────── */}
|
||||||
<SectionHeader label="Croissance" />
|
<SectionHeader label="Croissance" />
|
||||||
|
|||||||
@@ -215,6 +215,46 @@ function parseCalendarHTML(html: string): TECalendarEvent[] {
|
|||||||
return events;
|
return events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Forecasts CPI depuis le calendrier TE ────────────────────────────────────
|
||||||
|
// Lit les events inflation futurs du calendrier TE et retourne les forecasts
|
||||||
|
// par devise pour cpiCore YoY, cpiMoM et cpiHeadline YoY.
|
||||||
|
// Utilisé par la macro route pour remplir forecasts.cpiCore / forecasts.cpiMoM.
|
||||||
|
|
||||||
|
export interface TEInflationForecasts {
|
||||||
|
cpiYoY: string | null;
|
||||||
|
cpiCore: string | null;
|
||||||
|
cpiMoM: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function fetchTEInflationForecasts(
|
||||||
|
fromDate?: string,
|
||||||
|
toDate?: string,
|
||||||
|
): Promise<Partial<Record<Currency, TEInflationForecasts>>> {
|
||||||
|
const events = await fetchTECalendarHTML(fromDate, toDate);
|
||||||
|
const result: Partial<Record<Currency, TEInflationForecasts>> = {};
|
||||||
|
const now = fromDate ? new Date(fromDate + "T00:00:00Z") : new Date();
|
||||||
|
|
||||||
|
for (const ev of events) {
|
||||||
|
if (ev.category !== "inflation") continue;
|
||||||
|
if (new Date(ev.date) <= now) continue;
|
||||||
|
if (!ev.forecast) continue;
|
||||||
|
|
||||||
|
const ccy = ev.currency;
|
||||||
|
const title = ev.title.toLowerCase();
|
||||||
|
if (!result[ccy]) result[ccy] = { cpiYoY: null, cpiCore: null, cpiMoM: null };
|
||||||
|
|
||||||
|
const r = result[ccy]!;
|
||||||
|
if (!r.cpiCore && /core.*inflation.*yoy|core.*cpi.*yoy|core.*rate.*yoy/i.test(title)) {
|
||||||
|
r.cpiCore = ev.forecast;
|
||||||
|
} else if (!r.cpiMoM && /inflation.*mom|cpi.*mom|rate.*mom/i.test(title)) {
|
||||||
|
r.cpiMoM = ev.forecast;
|
||||||
|
} else if (!r.cpiYoY && /inflation.*yoy|cpi.*yoy|inflation\s+rate.*yoy/i.test(title)) {
|
||||||
|
r.cpiYoY = ev.forecast;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Paid API (when TRADING_ECONOMICS_API_KEY is set) ─────────────────────────
|
// ── Paid API (when TRADING_ECONOMICS_API_KEY is set) ─────────────────────────
|
||||||
|
|
||||||
export async function fetchTECalendar(
|
export async function fetchTECalendar(
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
Reference in New Issue
Block a user