fix: OIS indisponible sur toutes les cards + transparence sur l'instrument utilisé

Le fetch /api/rate-probabilities n'avait aucun fallback cache côté client
(contrairement aux autres widgets) et pouvait planter en bloc si InvestingLive
timeoutait (fetch sans AbortController) - une seule panne coupait les 8 devises
d'un coup au lieu de dégrader devise par devise.

- route.ts : try/catch pour toujours renvoyer un JSON valide
- page.tsx : fallback localStorage si le fetch échoue
- investinglive.ts : timeout 8s sur les fetches vers investinglive.com
- fetch-rate-data.mjs / rateprobability.ts : le pipeline écrit maintenant la
  source réelle utilisée par devise (Rate Monitor USD, futures Euribor/SONIA
  pour EUR/GBP, InvestingLive pour les 5 autres) et la carte l'affiche
- CurrencyCard.tsx : SourcesPopup corrigé (CME FedWatch retiré, plus de fausse
  mention "Rate Monitor" pour les devises qui n'en ont pas)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
caty21
2026-07-14 23:09:50 +02:00
co-authored by Claude Sonnet 5
parent 55eca6e2f0
commit 3a384eb820
6 changed files with 81 additions and 21 deletions
+10 -1
View File
@@ -59,15 +59,19 @@ async function tryUrl(daysAgo: number): Promise<ArticleRef | null> {
`https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-event-${yyyymmdd}/`,
];
const hits = await Promise.all(candidates.map(async (url) => {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch(url, {
method: "GET",
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36" },
cache: "no-store",
signal: controller.signal,
});
res.body?.cancel().catch(() => {});
return res.ok ? url : null;
} catch { return null; }
finally { clearTimeout(timeout); }
}));
const found = hits.find((u): u is string => u !== null);
return found ? { url: found, dateStr, daysAgo } : null;
@@ -106,6 +110,8 @@ async function findArticleRefs(): Promise<{ current: ArticleRef | null; previous
// ── Article fetch + parse ─────────────────────────────────────────────────────
async function fetchAndParse(ref: ArticleRef): Promise<ILExpectationsMap> {
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 8000);
try {
const res = await fetch(ref.url, {
headers: {
@@ -113,7 +119,8 @@ async function fetchAndParse(ref: ArticleRef): Promise<ILExpectationsMap> {
"Accept": "text/html,application/xhtml+xml",
"Accept-Language": "en-US,en;q=0.9",
},
next: { revalidate: 21600 },
next: { revalidate: 21600 },
signal: controller.signal,
});
if (!res.ok) return {};
@@ -136,6 +143,8 @@ async function fetchAndParse(ref: ArticleRef): Promise<ILExpectationsMap> {
} catch (err) {
console.error("[IL] fetch error:", err);
return {};
} finally {
clearTimeout(timeout);
}
}
+4 -1
View File
@@ -72,6 +72,7 @@ export interface CBRatePath {
prevMeetings?: RateProbMeeting[]; // réunions semaine précédente (snapshot RP)
prevWeekDate?: string; // date du snapshot semaine précédente
history?: Array<{ date: string; meetings: RateProbMeeting[] }>; // snapshots hebdo accumulés
instrumentSource?: string; // instrument réellement utilisé pour produire ces données (ground truth, écrit par le pipeline de fetch)
}
export type RateProbData = Partial<Record<Currency, CBRatePath>>;
@@ -217,6 +218,7 @@ function buildOfficialCalendarPath(
meetings,
peakMeeting: peakMeeting.probMovePct > 0 ? peakMeeting : null,
yearEndImplied: meetings.at(-1)?.impliedRate ?? null,
instrumentSource: "InvestingLive — estimation hebdomadaire analyste (pas de futures/OIS coté public pour cette devise) + calendrier officiel de réunions",
};
}
@@ -288,7 +290,8 @@ function parseCBBody(ccy: Currency, body: Record<string, unknown>): CBRatePath |
const currentYear = new Date().getFullYear();
const meetsThisYear = meetings.filter(m => m.dateIso <= `${currentYear}-12-31`);
const yearEndImplied = meetsThisYear.length > 0 ? meetsThisYear.at(-1)!.impliedRate : meetings[0].impliedRate;
return { currency: ccy, asOf, currentRate, meetings, peakMeeting, yearEndImplied };
const instrumentSource = typeof today["source"] === "string" ? today["source"] as string : undefined;
return { currency: ccy, asOf, currentRate, meetings, peakMeeting, yearEndImplied, instrumentSource };
}
// ── Fetch toutes les CB — depuis le cache GitHub Actions + enrichissement IL ───