mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-21 16:38:06 +00:00
feat: calendrier économique TE+Investing, OIS SNB, yields 10Y, taux directeurs corrects
Calendrier économique - Nouvelles sources : TE HTML scraping (483 events/2 sem.) + Investing.com en co-primaires - Dédupe déterministe par clé currency+category+date (±1j pour policy_rate) - Filtre catégorie "other", filtre discours CB (fix doublon TE vs Investing) - CHF/Switzerland via page séparée TE (hors G20) - Fenêtre 14 jours avec ?startDate=&endDate= passés à TE - ForexFactory+FRED en secours uniquement si les deux scrapers tombent à vide Probabilités OIS - rateprobability.com (7 CBs : FED/ECB/BOJ/BOE/BOC/RBA/RBNZ) - investinglive.com (Dellamotta) : scan URL-date 14 jours → couvre SNB/CHF manquant - SNB : 3% probabilité hausse au 19 juin 2026 (source : IL article 2026-05-29) - rate_expectations.json MAJ : RBNZ 79% hike (corrigé depuis 70% no-change stale) - Dédup ±1 jour pour policy_rate (fix doublon SNB TE-18/06 vs IL-19/06) Taux directeurs - USD : scrapeTeRate() = 3.75% (fin midpoint FRED 3.625%) - AUD prev : 4.10% (corrigé depuis 4.60%) - NZD/CHF/JPY/CAD : cohérents avec TE officiel Yields 10Y souverains - Nouvelle lib lib/tebonds.ts : scrape TE bonds, données du jour, cache 1h - Remplace FRED IRLTLT01XXM156N mensuel (JPY/CHF/AUD/NZD avaient 1 mois de retard) - 8 devises + spread vs USD calculé automatiquement - /api/yields et /api/drivers utilisent la même lib Drivers globaux - BTC : ticker/price Binance (plus précis que 24hr lastPrice) - HY Spread cache : 86400s → 3600s (données FRED du jour) - DriversBar : suppression affichage direct US 10Y, tooltip Crb 2-10 affiche US10Y+US2Y Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
3a39904ce5
commit
eb416be780
@@ -0,0 +1,142 @@
|
||||
// lib/investinglive.ts
|
||||
// Scrapes Giuseppe Dellamotta's recurring rate-expectations article on investinglive.com
|
||||
// URL pattern: /news/how-have-interest-rate-expectations-changed-after-this-weeks-event-YYYYMMDD/
|
||||
// Published after major market events (typically on Fridays or post-CB-decision)
|
||||
//
|
||||
// Data format in articleBody JSON-LD:
|
||||
// "CB: XX bps (YY% probability of rate hike/no change at the next meeting)"
|
||||
// Covers all 8 CBs including SNB — the only free public source with CHF OIS-equivalent data.
|
||||
|
||||
import type { Currency } from "./types";
|
||||
|
||||
// ── CB name → Currency ────────────────────────────────────────────────────────
|
||||
|
||||
const CB_TO_CCY: Record<string, Currency> = {
|
||||
"fed": "USD",
|
||||
"fomc": "USD",
|
||||
"ecb": "EUR",
|
||||
"boe": "GBP",
|
||||
"boj": "JPY",
|
||||
"boc": "CAD",
|
||||
"rba": "AUD",
|
||||
"rbnz": "NZD",
|
||||
"snb": "CHF",
|
||||
};
|
||||
|
||||
// ── Types ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
export interface ILRateExpectation {
|
||||
currency: Currency;
|
||||
nextMeetingProbPct: number; // probability of change at next meeting (0–100)
|
||||
nextMeetingIsHike: boolean; // true = hike, false = cut (no-change → isHike stays false)
|
||||
nextMeetingIsNoChange: boolean;
|
||||
bpsYearEnd: number; // cumulative bps priced in by year-end
|
||||
publishedDate: string; // YYYY-MM-DD
|
||||
}
|
||||
|
||||
export type ILExpectationsMap = Partial<Record<Currency, ILRateExpectation>>;
|
||||
|
||||
// ── URL discovery ─────────────────────────────────────────────────────────────
|
||||
// Try the last 14 days to find the most recently published article.
|
||||
|
||||
async function findLatestArticleUrl(): Promise<{ url: string; dateStr: string } | null> {
|
||||
for (let daysAgo = 0; daysAgo <= 14; daysAgo++) {
|
||||
const d = new Date(Date.now() - daysAgo * 86400000);
|
||||
const yyyymmdd = d.toISOString().slice(0, 10).replace(/-/g, "");
|
||||
const url = `https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-event-${yyyymmdd}/`;
|
||||
try {
|
||||
const res = await fetch(url, {
|
||||
method: "HEAD",
|
||||
headers: { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36" },
|
||||
next: { revalidate: 21600 }, // re-check every 6h
|
||||
});
|
||||
if (res.ok) return { url, dateStr: `${yyyymmdd.slice(0,4)}-${yyyymmdd.slice(4,6)}-${yyyymmdd.slice(6,8)}` };
|
||||
} catch { /* try next day */ }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Article parser ─────────────────────────────────────────────────────────────
|
||||
// The structured data JSON-LD "articleBody" contains the raw text we need.
|
||||
|
||||
function parseArticleBody(text: string, publishedDate: string): ILExpectationsMap {
|
||||
const result: ILExpectationsMap = {};
|
||||
|
||||
// Matches: "RBNZ: 75 bps (79% probability of rate hike at the next meeting)"
|
||||
// "Fed: 13 bps (99% probability of no change at the next meeting)"
|
||||
const linePattern = /([A-Za-z]+)\s*:\s*(\d+)\s*bps\s*\(\s*(\d+)%\s*probability\s+of\s+(rate\s+(?:hike|cut)|no\s+change)\s+at\s+the\s+next\s+meeting\)/gi;
|
||||
|
||||
let m: RegExpExecArray | null;
|
||||
while ((m = linePattern.exec(text)) !== null) {
|
||||
const [, cbRaw, bpsStr, probStr, directionRaw] = m;
|
||||
const ccy = CB_TO_CCY[cbRaw.toLowerCase()];
|
||||
if (!ccy) continue;
|
||||
|
||||
const bpsYearEnd = parseInt(bpsStr);
|
||||
const probPct = parseInt(probStr);
|
||||
const direction = directionRaw.toLowerCase();
|
||||
const nextMeetingIsNoChange = direction === "no change";
|
||||
const nextMeetingIsHike = !nextMeetingIsNoChange && direction.includes("hike");
|
||||
// Probability of change = 100 - probNoChange OR directProb if it's a hike/cut
|
||||
const nextMeetingProbPct = nextMeetingIsNoChange ? 100 - probPct : probPct;
|
||||
|
||||
result[ccy] = {
|
||||
currency: ccy,
|
||||
nextMeetingProbPct,
|
||||
nextMeetingIsHike,
|
||||
nextMeetingIsNoChange,
|
||||
bpsYearEnd,
|
||||
publishedDate,
|
||||
};
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Main fetch ────────────────────────────────────────────────────────────────
|
||||
|
||||
export async function fetchILExpectations(): Promise<ILExpectationsMap> {
|
||||
try {
|
||||
const found = await findLatestArticleUrl();
|
||||
if (!found) {
|
||||
console.warn("[IL] No recent rate-expectations article found (last 14 days)");
|
||||
return {};
|
||||
}
|
||||
|
||||
const res = await fetch(found.url, {
|
||||
headers: {
|
||||
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36",
|
||||
"Accept": "text/html,application/xhtml+xml",
|
||||
"Accept-Language": "en-US,en;q=0.9",
|
||||
},
|
||||
next: { revalidate: 21600 }, // cache 6h — new article only published after major events
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn("[IL] Fetch failed:", res.status);
|
||||
return {};
|
||||
}
|
||||
|
||||
const html = await res.text();
|
||||
|
||||
// Extract articleBody from JSON-LD structured data
|
||||
const jsonLdMatch = html.match(/"articleBody"\s*:\s*"((?:[^"\\]|\\.)*)"/);
|
||||
if (!jsonLdMatch) {
|
||||
console.warn("[IL] articleBody not found in JSON-LD");
|
||||
return {};
|
||||
}
|
||||
|
||||
const articleBody = jsonLdMatch[1]
|
||||
.replace(/\\n/g, "\n")
|
||||
.replace(/\\"/g, '"')
|
||||
.replace(/\\u003c/g, "<")
|
||||
.replace(/\\u003e/g, ">");
|
||||
|
||||
const parsed = parseArticleBody(articleBody, found.dateStr);
|
||||
const count = Object.keys(parsed).length;
|
||||
console.log(`[IL] Parsed ${count} CBs from article dated ${found.dateStr}`);
|
||||
return parsed;
|
||||
} catch (err) {
|
||||
console.error("[IL] error:", err);
|
||||
return {};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user