From 356940a76a2c65af18c2007e4ea2210cc3e05b77 Mon Sep 17 00:00:00 2001 From: caty21 Date: Wed, 8 Jul 2026 20:43:22 +0200 Subject: [PATCH] =?UTF-8?q?fix:=20courbe=20OIS=20USD=20=C3=A9cras=C3=A9e?= =?UTF-8?q?=20par=20un=20fallback=20d=C3=A9grad=C3=A9=20quand=20investing.?= =?UTF-8?q?com=20bloque=20GitHub=20Actions?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause confirmée en direct dans les logs du run GH Actions du 2026-07-08 15:10 : investing.com renvoie 403 spécifiquement aux IPs des runners GitHub Actions (bloc anti-bot par réputation IP — mon IP locale passe sans problème, la même page se parse parfaitement, 12 réunions). C'est intermittent par nature selon la rotation d'IP de GitHub. Le vrai bug n'était pas le scraping mais l'absence total de filet de sécurité : dès qu'un run se prenait ce 403, le code écrasait la vraie courbe FOMC multi-réunions (12 points, table Investing.com) par le fallback InvestingLive — un unique point "year-end" — et cette dégradation restait visible dans data/rate-probabilities.json jusqu'au prochain run qui réussissait à passer. D'où le symptôme : "parfois ça marche et c'est correct, parfois ça bug et affiche peu de données fausses" pour les sous-onglets Courbe/Probas/ Réunions de la card USD. Deux corrections dans fetch-rate-data.mjs : 1. Retry (3 tentatives, backoff 2s/4s/6s) sur le fetch Investing.com — couvre les échecs vraiment transitoires (n'aide pas contre un blocage IP qui persiste sur tout le run, d'où le point 2). 2. Filet de sécurité : si le résultat du jour n'a qu'1 réunion (signature du fallback IL dégradé) alors qu'une vraie courbe multi-réunions existait déjà dans le fichier, on la conserve au lieu de l'écraser. Vérifié : (a) le scrape Investing.com refonctionne depuis ma machine (12 réunions FOMC parsées correctement) ; (b) la logique de préservation testée isolément avec un cas simulé de dégradation restaure bien les 12 réunions. Co-Authored-By: Claude Sonnet 5 --- .github/scripts/fetch-rate-data.mjs | 34 ++++++++++++++++++++++++++++- data/il-enrichment-cache.json | 2 +- 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/scripts/fetch-rate-data.mjs b/.github/scripts/fetch-rate-data.mjs index 7c38786..a77bc89 100644 --- a/.github/scripts/fetch-rate-data.mjs +++ b/.github/scripts/fetch-rate-data.mjs @@ -400,6 +400,18 @@ function buildILFallback(ccy, il) { const CCYS = ["USD","EUR","GBP","JPY","CAD","AUD","NZD","CHF"]; const results = {}; +// Charge les données déjà en place AVANT tout fetch — sert de filet de sécurité +// plus bas : investing.com renvoie parfois 403 spécifiquement aux IPs GitHub +// Actions (bloc IP anti-bot, confirmé en direct — pas un problème de parsing), +// et ce blocage est intermittent. Sans ce filet, un seul run bloqué écrasait +// la vraie courbe multi-réunions USD par le fallback InvestingLive (un unique +// point "year-end"), et cette dégradation restait visible jusqu'au prochain +// run réussi. On préfère désormais garder la dernière bonne donnée connue. +let existingData = {}; +try { + existingData = JSON.parse(readFileSync("data/rate-probabilities.json", "utf8")).data ?? {}; +} catch {} + // 0 — Sources officielles → écrasent les ancres si data/rate_decisions.json a dérivé console.log("\n=== Fed (official) ==="); const fedRate = await fetchFedRate(); @@ -411,10 +423,19 @@ if (snbRate !== null) FALLBACK_RATES.CHF = snbRate; // 1 — Investing.com Fed Rate Monitor → USD uniquement (seule CB avec cet outil ; // les autres slugs *-rate-monitor n'existent pas sur investing.com → 404 attendus) +// Retry (403/erreur réseau ponctuels) — n'aide pas contre un vrai bloc IP +// persistant sur tout le run, mais couvre les échecs vraiment transitoires. const IC_SUPPORTED = ["USD"]; console.log("\n=== Investing.com Rate Monitors ==="); for (const ccy of IC_SUPPORTED) { - const data = await fetchInvestingCom(ccy); + let data = null; + for (let attempt = 1; attempt <= 3 && !data; attempt++) { + if (attempt > 1) { + console.log(`[IC/${ccy}] retry ${attempt}/3…`); + await new Promise(r => setTimeout(r, 2000 * attempt)); + } + data = await fetchInvestingCom(ccy); + } if (data) results[ccy] = data; await new Promise(r => setTimeout(r, 600)); } @@ -432,6 +453,17 @@ if (missing.length) { } } +// 3 — Filet de sécurité : si le résultat du jour n'a qu'1 réunion (fallback IL +// dégradé) alors qu'on avait déjà une vraie courbe multi-réunions, on la garde. +for (const ccy of CCYS) { + const newRows = results[ccy]?.today?.rows?.length ?? 0; + const oldRows = existingData[ccy]?.today?.rows?.length ?? 0; + if (newRows <= 1 && oldRows > 1) { + console.log(`[preserve] ${ccy}: nouveau fetch dégradé (${newRows} réunion) → conserve l'ancienne courbe (${oldRows} réunions)`); + results[ccy] = existingData[ccy]; + } +} + // ── Multi-week snapshot rotation ────────────────────────────────────────────── // snapshots = tableau chronologique (plus récent en [0]) de jusqu'à 12 semaines let snapshots = []; diff --git a/data/il-enrichment-cache.json b/data/il-enrichment-cache.json index 82b3a7e..cf3052f 100644 --- a/data/il-enrichment-cache.json +++ b/data/il-enrichment-cache.json @@ -1 +1 @@ -{"ts":1783528251592,"data":{"current":{"NZD":{"currency":"NZD","nextMeetingProbPct":82,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":62,"publishedDate":"2026-07-03"},"USD":{"currency":"USD","nextMeetingProbPct":17,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":30,"publishedDate":"2026-07-03"},"EUR":{"currency":"EUR","nextMeetingProbPct":28,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":24,"publishedDate":"2026-07-03"},"JPY":{"currency":"JPY","nextMeetingProbPct":1,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":20,"publishedDate":"2026-07-03"},"GBP":{"currency":"GBP","nextMeetingProbPct":7,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":18,"publishedDate":"2026-07-03"},"AUD":{"currency":"AUD","nextMeetingProbPct":15,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":10,"publishedDate":"2026-07-03"},"CAD":{"currency":"CAD","nextMeetingProbPct":9,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":8,"publishedDate":"2026-07-03"},"CHF":{"currency":"CHF","nextMeetingProbPct":4,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":5,"publishedDate":"2026-07-03"}},"prev":{"NZD":{"currency":"NZD","nextMeetingProbPct":62,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":55,"publishedDate":"2026-06-26"},"USD":{"currency":"USD","nextMeetingProbPct":30,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":31,"publishedDate":"2026-06-26"},"EUR":{"currency":"EUR","nextMeetingProbPct":31,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":27,"publishedDate":"2026-06-26"},"GBP":{"currency":"GBP","nextMeetingProbPct":14,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":20,"publishedDate":"2026-06-26"},"JPY":{"currency":"JPY","nextMeetingProbPct":3,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":19,"publishedDate":"2026-06-26"},"CAD":{"currency":"CAD","nextMeetingProbPct":4,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":18,"publishedDate":"2026-06-26"},"AUD":{"currency":"AUD","nextMeetingProbPct":13,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":8,"publishedDate":"2026-06-26"},"CHF":{"currency":"CHF","nextMeetingProbPct":6,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":5,"publishedDate":"2026-06-26"}},"prevDate":"2026-06-26"}} \ No newline at end of file +{"ts":1783535815952,"data":{"current":{"NZD":{"currency":"NZD","nextMeetingProbPct":82,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":62,"publishedDate":"2026-07-03"},"USD":{"currency":"USD","nextMeetingProbPct":17,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":30,"publishedDate":"2026-07-03"},"EUR":{"currency":"EUR","nextMeetingProbPct":28,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":24,"publishedDate":"2026-07-03"},"JPY":{"currency":"JPY","nextMeetingProbPct":1,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":20,"publishedDate":"2026-07-03"},"GBP":{"currency":"GBP","nextMeetingProbPct":7,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":18,"publishedDate":"2026-07-03"},"AUD":{"currency":"AUD","nextMeetingProbPct":15,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":10,"publishedDate":"2026-07-03"},"CAD":{"currency":"CAD","nextMeetingProbPct":9,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":8,"publishedDate":"2026-07-03"},"CHF":{"currency":"CHF","nextMeetingProbPct":4,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":5,"publishedDate":"2026-07-03"}},"prev":{"NZD":{"currency":"NZD","nextMeetingProbPct":62,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":55,"publishedDate":"2026-06-26"},"USD":{"currency":"USD","nextMeetingProbPct":30,"nextMeetingIsHike":true,"nextMeetingIsNoChange":false,"bpsYearEnd":31,"publishedDate":"2026-06-26"},"EUR":{"currency":"EUR","nextMeetingProbPct":31,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":27,"publishedDate":"2026-06-26"},"GBP":{"currency":"GBP","nextMeetingProbPct":14,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":20,"publishedDate":"2026-06-26"},"JPY":{"currency":"JPY","nextMeetingProbPct":3,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":19,"publishedDate":"2026-06-26"},"CAD":{"currency":"CAD","nextMeetingProbPct":4,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":18,"publishedDate":"2026-06-26"},"AUD":{"currency":"AUD","nextMeetingProbPct":13,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":8,"publishedDate":"2026-06-26"},"CHF":{"currency":"CHF","nextMeetingProbPct":6,"nextMeetingIsHike":false,"nextMeetingIsNoChange":true,"bpsYearEnd":5,"publishedDate":"2026-06-26"}},"prevDate":"2026-06-26"}} \ No newline at end of file