// Sources (in priority order): // 1. CME FedWatch API — USD seul, JSON officieux, fiable // 2. Investing.com monitors — toutes CBs, HTML scraped (Cloudflare possible) // 3. InvestingLive fallback — articles Giuseppe Dellamotta, hebdo, toutes CBs import { writeFileSync, mkdirSync, readFileSync } from "fs"; // ── Helpers ─────────────────────────────────────────────────────────────────── const MONTHS = { Jan:"01",Feb:"02",Mar:"03",Apr:"04",May:"05",Jun:"06", Jul:"07",Aug:"08",Sep:"09",Oct:"10",Nov:"11",Dec:"12" }; function normalizeDate(raw) { if (!raw) return null; if (/^\d{8}$/.test(raw)) return `${raw.slice(0,4)}-${raw.slice(4,6)}-${raw.slice(6,8)}`; if (/^\d{4}-\d{2}-\d{2}$/.test(raw)) return raw; const m = raw.match(/([A-Z][a-z]{2})\s+(\d{1,2}),?\s+(\d{4})/); if (m) return `${m[3]}-${MONTHS[m[1]]??'01'}-${m[2].padStart(2,'0')}`; return null; } const CHROME_HEADERS = { "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36", "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8", "Accept-Language": "en-US,en;q=0.9", "Cache-Control": "no-cache", "Pragma": "no-cache", "Sec-Fetch-Dest": "document", "Sec-Fetch-Mode": "navigate", "Sec-Fetch-Site": "none", "Sec-Fetch-User": "?1", "Upgrade-Insecure-Requests": "1", }; // ── 1. CME FedWatch (USD) ───────────────────────────────────────────────────── async function fetchCMEFedWatch() { const year = new Date().getFullYear(); const candidates = [ // API officieuse FedWatch — essai sur SR3 (SOFR) et ZQ (30-day FF) `https://www.cmegroup.com/CmeWS/mvc/MeetingCalendar/V1/getMeetingCalendarByYear.json?marketCode=SR3&year=${year}`, `https://www.cmegroup.com/CmeWS/mvc/MeetingCalendar/V1/getMeetingCalendarByYear.json?marketCode=FF&year=${year}`, `https://www.cmegroup.com/CmeWS/mvc/MeetingCalendar/V1/getMeetingCalendarByYear.json?marketCode=ZQ&year=${year}`, ]; for (const url of candidates) { try { const res = await fetch(url, { headers: { ...CHROME_HEADERS, "Accept": "application/json, text/plain, */*", "Referer": "https://www.cmegroup.com/markets/interest-rates/cme-fedwatch-tool.html", "Origin": "https://www.cmegroup.com", "Sec-Fetch-Dest": "empty", "Sec-Fetch-Mode": "cors", "Sec-Fetch-Site": "same-origin", } }); console.log(`[CME] ${url.split('?')[1]} → ${res.status}`); if (!res.ok) continue; const body = await res.json(); console.log(`[CME] keys: ${Object.keys(body).join(", ")}`); console.log(`[CME] preview: ${JSON.stringify(body).slice(0, 600)}`); const parsed = parseCMEBody(body); if (parsed) { console.log(`[CME] ✓ ${parsed.today.rows.length} meetings`); return parsed; } } catch (e) { console.error(`[CME] error: ${e.message}`); } } return null; } function parseCMEBody(body) { // Format A : { meetings: [{ date|meetingDate, currentProbs:{minus25,unch,plus25,...}, impliedRate }] } const arr = body.meetings ?? body.data ?? (Array.isArray(body) ? body : null); if (!arr?.length) return null; const nowIso = new Date().toISOString().slice(0,10); let currentRate = null; const rows = []; for (const m of arr) { const dateIso = normalizeDate( m.date ?? m.meetingDate ?? m.meetDate ?? m.meeting_date ?? "" ); if (!dateIso || dateIso < nowIso) continue; const probs = m.currentProbs ?? m.probs ?? m.probabilities ?? m.probability ?? {}; const probCut = parseFloat(probs.minus25 ?? probs.cut25 ?? probs["-25"] ?? probs.minus50 ?? 0) + parseFloat(probs.minus50 ?? probs.cut50 ?? probs["-50"] ?? 0); const probHike = parseFloat(probs.plus25 ?? probs.hike25 ?? probs["+25"] ?? probs.plus50 ?? 0) + parseFloat(probs.plus50 ?? probs.hike50 ?? probs["+50"] ?? 0); const probHold = parseFloat(probs.unch ?? probs.hold ?? probs.unchanged ?? 0); const probIsCut = probCut >= probHike; const probMovePct = probIsCut ? probCut : probHike; const impliedRate = parseFloat( m.impliedRate ?? m.implied_rate ?? m.rate ?? m.impliedFedFunds ?? 0 ); if (currentRate === null) currentRate = parseFloat(m.priorRate ?? m.currentRate ?? m.prior_rate ?? impliedRate ?? 0); const changeBps = (impliedRate - (currentRate ?? 0)) * 100; const dateLabel = new Date(dateIso + "T12:00:00Z") .toLocaleDateString("en-US", { month:"short", day:"numeric" }); rows.push({ meeting: dateLabel, meeting_iso: dateIso, implied_rate_post_meeting: impliedRate, prob_move_pct: probMovePct, prob_is_cut: probIsCut, change_bps: changeBps, num_moves: changeBps / 25, }); } if (!rows.length) return null; return { today: { midpoint: currentRate ?? 0, rows } }; } // ── 2. Investing.com Rate Monitors (all CBs) ────────────────────────────────── const IC_SLUGS = { USD: "fed-rate-monitor", EUR: "ecb-rate-monitor", GBP: "boe-rate-monitor", JPY: "boj-rate-monitor", CAD: "boc-rate-monitor", AUD: "rba-rate-monitor", NZD: "rbnz-rate-monitor", CHF: "snb-rate-monitor", }; // Taux directeurs actuels — fallback si non trouvés dans le HTML // NZD : RBNZ a coupé à 2.25% (juin 2026) ; CHF : SNB à 0.00% const FALLBACK_RATES = { USD: 4.33, EUR: 2.40, GBP: 3.75, JPY: 0.50, CAD: 2.25, AUD: 4.10, NZD: 2.25, CHF: 0.00, }; async function fetchInvestingCom(ccy) { const slug = IC_SLUGS[ccy]; const url = `https://www.investing.com/central-banks/${slug}`; try { const res = await fetch(url, { headers: { ...CHROME_HEADERS, "Referer": "https://www.investing.com/central-banks/" } }); console.log(`[IC/${ccy}] ${res.status}`); if (!res.ok) return null; const html = await res.text(); console.log(`[IC/${ccy}] html ${html.length} chars`); if (/just a moment|cf-browser-verification|enable javascript/i.test(html)) { console.error(`[IC/${ccy}] Cloudflare challenge`); return null; } return parseInvestingComHtml(ccy, html); } catch (e) { console.error(`[IC/${ccy}] error: ${e.message}`); return null; } } function parseInvestingComHtml(ccy, html) { // ── Attempt A: __NEXT_DATA__ (Next.js SSR) ──────────────────────────────── const ndMatch = html.match(/