mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-16 14:08:06 +00:00
fix: InvestingLive URL (/centralbank/events), procure.ch PMI title, remove signals badge
- investinglive.ts / expectations/route.ts / fetch-rate-data.mjs : try 4 URL variants (/centralbank/ or /news/, events or event) to handle site restructure - calendar/route.ts: displayTitle catches procure.ch prefix to return PMI Manufacturier - page.tsx: remove divergenceCount badge and unused Zap import Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
94602c765f
commit
eca5800372
@@ -137,9 +137,10 @@ const IC_SLUGS = {
|
||||
};
|
||||
|
||||
// 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: 4.25, JPY: 0.50,
|
||||
CAD: 2.75, AUD: 4.10, NZD: 3.25, CHF: 0.00,
|
||||
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) {
|
||||
@@ -339,14 +340,21 @@ async function fetchInvestingLive() {
|
||||
for (let daysAgo = 0; daysAgo <= 14; daysAgo++) {
|
||||
const d = new Date(Date.now() - daysAgo * 86_400_000);
|
||||
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, { headers: { "User-Agent": CHROME_HEADERS["User-Agent"] } });
|
||||
if (!res.ok) continue;
|
||||
const html = await res.text();
|
||||
console.log(`[IL] found article ${yyyymmdd}`);
|
||||
return { data: parseILArticle(html), date: d.toISOString().slice(0,10) };
|
||||
} catch {}
|
||||
const candidates = [
|
||||
`https://investinglive.com/centralbank/how-have-interest-rate-expectations-changed-after-this-weeks-events-${yyyymmdd}/`,
|
||||
`https://investinglive.com/centralbank/how-have-interest-rate-expectations-changed-after-this-weeks-event-${yyyymmdd}/`,
|
||||
`https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-events-${yyyymmdd}/`,
|
||||
`https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-event-${yyyymmdd}/`,
|
||||
];
|
||||
for (const url of candidates) {
|
||||
try {
|
||||
const res = await fetch(url, { headers: { "User-Agent": CHROME_HEADERS["User-Agent"] } });
|
||||
if (!res.ok) continue;
|
||||
const html = await res.text();
|
||||
console.log(`[IL] found article ${yyyymmdd} at ${url}`);
|
||||
return { data: parseILArticle(html), date: d.toISOString().slice(0,10) };
|
||||
} catch {}
|
||||
}
|
||||
}
|
||||
return { data: {}, date: null };
|
||||
}
|
||||
@@ -406,9 +414,11 @@ console.log("\n=== CME FedWatch ===");
|
||||
const cmeData = await fetchCMEFedWatch();
|
||||
if (cmeData) { results["USD"] = cmeData; console.log("[CME] USD ✓"); }
|
||||
|
||||
// 2 — Investing.com → all CBs (USD as backup if CME failed)
|
||||
// 2 — Investing.com → CBs qui ont une page rate-monitor (pas CHF ni NZD → 404)
|
||||
// CHF (snb-rate-monitor) et NZD (rbnz-rate-monitor) n'existent pas sur Investing.com
|
||||
const IC_SUPPORTED = CCYS.filter(c => c !== "CHF" && c !== "NZD");
|
||||
console.log("\n=== Investing.com Rate Monitors ===");
|
||||
for (const ccy of CCYS) {
|
||||
for (const ccy of IC_SUPPORTED) {
|
||||
if (results[ccy]) { console.log(`[IC/${ccy}] skipped (CME)`); continue; }
|
||||
const data = await fetchInvestingCom(ccy);
|
||||
if (data) results[ccy] = data;
|
||||
@@ -428,18 +438,34 @@ if (missing.length) {
|
||||
}
|
||||
}
|
||||
|
||||
// ── Previous week rotation ────────────────────────────────────────────────────
|
||||
let previousWeek = null, previousWeekFetchedAt = null;
|
||||
// ── Multi-week snapshot rotation ──────────────────────────────────────────────
|
||||
// snapshots = tableau chronologique (plus récent en [0]) de jusqu'à 12 semaines
|
||||
let snapshots = [];
|
||||
let previousWeek = null, previousWeekFetchedAt = null; // rétrocompatibilité
|
||||
try {
|
||||
const existing = JSON.parse(readFileSync("data/rate-probabilities.json","utf8"));
|
||||
const ageMs = Date.now() - new Date(existing.fetchedAt).getTime();
|
||||
const day = 86400000;
|
||||
|
||||
// Récupère les snapshots existants et filtre < 12 semaines
|
||||
const existingSnaps = existing.snapshots ?? [];
|
||||
const validSnaps = existingSnaps.filter(s => {
|
||||
const age = Date.now() - new Date(s.fetchedAt).getTime();
|
||||
return age < 84 * day;
|
||||
});
|
||||
|
||||
// Si les données actuelles ont 5-9 jours → les pousser en snapshot[0]
|
||||
if (ageMs >= 5*day && ageMs <= 9*day) {
|
||||
previousWeek = existing.data; previousWeekFetchedAt = existing.fetchedAt;
|
||||
console.log(`\nRotated ${(ageMs/day).toFixed(1)}d-old data → previousWeek`);
|
||||
} else if (existing.previousWeek) {
|
||||
const prevAge = Date.now() - new Date(existing.previousWeekFetchedAt).getTime();
|
||||
if (prevAge < 11*day) { previousWeek = existing.previousWeek; previousWeekFetchedAt = existing.previousWeekFetchedAt; }
|
||||
snapshots = [{ data: existing.data, fetchedAt: existing.fetchedAt }, ...validSnaps].slice(0, 12);
|
||||
console.log(`\nRotated ${(ageMs/day).toFixed(1)}d-old data → snapshots[${snapshots.length}]`);
|
||||
} else {
|
||||
snapshots = validSnaps;
|
||||
}
|
||||
|
||||
// Rétrocompatibilité previousWeek
|
||||
if (snapshots[0]) {
|
||||
previousWeek = snapshots[0].data;
|
||||
previousWeekFetchedAt = snapshots[0].fetchedAt;
|
||||
}
|
||||
} catch {}
|
||||
|
||||
@@ -447,6 +473,7 @@ mkdirSync("data", { recursive: true });
|
||||
writeFileSync("data/rate-probabilities.json", JSON.stringify({
|
||||
data: results,
|
||||
fetchedAt: new Date().toISOString(),
|
||||
snapshots,
|
||||
...(previousWeek ? { previousWeek, previousWeekFetchedAt } : {}),
|
||||
}, null, 2));
|
||||
|
||||
|
||||
@@ -57,7 +57,7 @@ const CURRENCIES = new Set<string>(["USD", "EUR", "GBP", "JPY", "CHF", "CAD", "A
|
||||
function detectCategory(title: string): EventCategory {
|
||||
const t = title.toLowerCase();
|
||||
|
||||
if (/nonfarm|non.farm|employment\s+change|jobs\s+added|employment\s+report|claimant|jobless\s+claims|unemployment\s+rate|jobless\s+rate/.test(t))
|
||||
if (/nonfarm|non.farm|employment\s+change|jobs\s+added|employment\s+report|claimant|jobless\s+claims|unemployment\s+rate|jobless\s+rate|\badp\b|jolts|job\s+openings|job\s+quits|ism\s+\w+\s+employ/.test(t))
|
||||
return "employment";
|
||||
|
||||
if (/\bpmi\b|purchasing\s+managers/.test(t))
|
||||
@@ -69,13 +69,13 @@ function detectCategory(title: string): EventCategory {
|
||||
if (/speaks?|press\s+conf|testimony|speech|statement\b|governor|chair\b|president\b/.test(t))
|
||||
return "cb_speech";
|
||||
|
||||
if (/\bcpi\b|\bhicp\b|core\s+inflation|flash\s+cpi|inflation\s+rate|consumer\s+price/.test(t))
|
||||
if (/\bcpi\b|\bhicp\b|core\s+inflation|flash\s+cpi|inflation\s+rate|consumer\s+price|\bppi\b|producer\s+price/.test(t))
|
||||
return "inflation";
|
||||
|
||||
if (/\bgdp\b|gross\s+domestic/.test(t))
|
||||
return "gdp";
|
||||
|
||||
if (/retail\s+sales|core\s+retail/.test(t))
|
||||
if (/retail\s+sales|core\s+retail|household\s+spending/.test(t))
|
||||
return "retail_sales";
|
||||
|
||||
if (/trade\s+balance|current\s+account/.test(t))
|
||||
@@ -104,16 +104,25 @@ function displayTitle(rawTitle: string, currency: string): string {
|
||||
// USD-specific
|
||||
if (/nonfarm\s+payrolls/i.test(t) && currency === "USD") return "NFP (Non-Farm Payrolls)";
|
||||
if (/adp\s+non.farm/i.test(t)) return "ADP Employment";
|
||||
if (/jobless.*4.?week|4.?week.*jobless|claims.*4.?week/i.test(t)) return "Dem. alloc. (moy. 4 sem.)";
|
||||
if (/unemployment\s+claims/i.test(t)) return "Demandes d'allocations";
|
||||
if (/unemployment\s+rate/i.test(t)) return "Taux de chômage";
|
||||
if (/employment\s+change/i.test(t)) return "Emploi Δ";
|
||||
if (/jolts|job\s+openings/i.test(t)) return "JOLTS (offres d'emploi)";
|
||||
if (/job\s+quits/i.test(t)) return "JOLTS (démissions)";
|
||||
if (/ism\s+\w+\s+employ/i.test(t)) return "ISM Emploi Manufacturier";
|
||||
if (/procure\.ch/i.test(t)) return "PMI Manufacturier";
|
||||
if (/manufacturing\s+pmi|mfg\s+pmi/i.test(t)) return "PMI Manufacturier";
|
||||
if (/services?\s+pmi/i.test(t)) return "PMI Services";
|
||||
if (/composite\s+pmi/i.test(t)) return "PMI Composite";
|
||||
if (/ism\s+non.manufactur/i.test(t)) return "ISM Services PMI";
|
||||
if (/ism\s+manufactur/i.test(t)) return "ISM Manufacturier";
|
||||
if (/flash.*cpi|cpi.*flash/i.test(t)) return "IPC Flash (YoY)";
|
||||
if (/flash.*cpi|cpi.*flash/i.test(t)) return "IPC Flash";
|
||||
if (/final.*cpi|cpi.*final/i.test(t)) return "IPC Final";
|
||||
if (/core.*cpi/i.test(t)) return "IPC Core";
|
||||
if (/ppi.*m.?m|producer.*price.*m.?m/i.test(t)) return "IPP (MoM)";
|
||||
if (/ppi.*y.?y|producer.*price.*y.?y/i.test(t)) return "IPP (YoY)";
|
||||
if (/\bppi\b|producer\s+price/i.test(t)) return "IPP";
|
||||
if (/\bhicp\b/i.test(t)) return "HICP (YoY)";
|
||||
if (/\bcpi\b.*y.*y/i.test(t)) return "IPC (YoY)";
|
||||
if (/\bcpi\b.*m.*m/i.test(t)) return "IPC (MoM)";
|
||||
@@ -122,6 +131,9 @@ function displayTitle(rawTitle: string, currency: string): string {
|
||||
if (/gdp.*m.*m/i.test(t)) return "PIB (MoM)";
|
||||
if (/\bgdp\b/i.test(t)) return "PIB";
|
||||
if (/core\s+retail/i.test(t)) return "Ventes détail Core";
|
||||
if (/household\s+spending.*m.?m/i.test(t)) return "Dép. ménages (MoM)";
|
||||
if (/household\s+spending.*y.?y/i.test(t)) return "Dép. ménages (YoY)";
|
||||
if (/household\s+spending/i.test(t)) return "Dép. ménages";
|
||||
if (/retail\s+sales/i.test(t)) return "Ventes au détail";
|
||||
if (/trade\s+balance/i.test(t)) return "Balance commerciale";
|
||||
if (/interest\s+rate|rate\s+decision/i.test(t)) return "Décision de taux";
|
||||
@@ -145,7 +157,7 @@ interface GroupingResult {
|
||||
const EMPLOYMENT_PARENTS = /nonfarm|non.farm|employment\s+change|claimant|jobless\s+claims/i;
|
||||
const EMPLOYMENT_CHILDREN = /unemployment\s+rate|jobless\s+rate/i;
|
||||
const PMI_PARENTS = /composite\s+pmi|ism\s+(manufactur|non.manufactur)/i;
|
||||
const PMI_CHILDREN = /manufacturing\s+pmi|services?\s+pmi|mfg\s+pmi/i;
|
||||
const PMI_CHILDREN = /manufacturing\s+pmi|services?\s+pmi|mfg\s+pmi|procure\.ch/i;
|
||||
const CPI_PARENTS = /\bcpi\b.*y.*y|flash.*cpi|\bhicp\b/i;
|
||||
const CPI_CHILDREN = /core.*cpi|core.*inflation/i;
|
||||
|
||||
|
||||
@@ -2,6 +2,8 @@ import { NextResponse } from "next/server";
|
||||
import { readFileSync } from "fs";
|
||||
import { join } from "path";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const AUTHOR_URL = "https://investinglive.com/author/giuseppe-dellamotta/";
|
||||
const CATEGORY_URL = "https://investinglive.com/CentralBanks";
|
||||
const RSS_URLS = [
|
||||
@@ -186,16 +188,22 @@ async function candidatesFromHtml(): Promise<string[]> {
|
||||
}
|
||||
|
||||
// ── Étape 0 : scan URL-date direct (le plus fiable, publié chaque semaine) ────
|
||||
// Pattern : /news/how-have-interest-rate-expectations-changed-after-this-weeks-event-YYYYMMDD/
|
||||
// Pattern : /centralbank/...events-YYYYMMDD/ (+ fallbacks /news/, singulier/pluriel)
|
||||
// On teste les 14 derniers jours (une publication par semaine environ)
|
||||
async function candidatesFromUrlScan(): Promise<string[]> {
|
||||
const results: string[] = [];
|
||||
const now = Date.now();
|
||||
// 4 variantes d'URL par jour (la structure du site a changé mi-2026)
|
||||
const checks = Array.from({ length: 14 }, (_, i) => {
|
||||
const d = new Date(now - i * 86400000);
|
||||
const s = d.toISOString().slice(0, 10).replace(/-/g, "");
|
||||
return `https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-event-${s}/`;
|
||||
});
|
||||
return [
|
||||
`https://investinglive.com/centralbank/how-have-interest-rate-expectations-changed-after-this-weeks-events-${s}/`,
|
||||
`https://investinglive.com/centralbank/how-have-interest-rate-expectations-changed-after-this-weeks-event-${s}/`,
|
||||
`https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-events-${s}/`,
|
||||
`https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-event-${s}/`,
|
||||
];
|
||||
}).flat();
|
||||
// HEAD requests en parallèle (rapide, peu de bande passante)
|
||||
const settled = await Promise.allSettled(
|
||||
checks.map(url =>
|
||||
@@ -204,10 +212,14 @@ async function candidatesFromUrlScan(): Promise<string[]> {
|
||||
.catch(() => null)
|
||||
)
|
||||
);
|
||||
const seen = new Set<string>();
|
||||
for (const r of settled) {
|
||||
if (r.status === "fulfilled" && r.value) results.push(r.value);
|
||||
if (r.status === "fulfilled" && r.value && !seen.has(r.value)) {
|
||||
seen.add(r.value);
|
||||
results.push(r.value);
|
||||
}
|
||||
}
|
||||
return results; // déjà triés du plus récent au plus ancien
|
||||
return results; // triés du plus récent au plus ancien (ordre du .flat())
|
||||
}
|
||||
|
||||
async function loadRemoteExpectation() {
|
||||
|
||||
+22
-30
@@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState, useCallback } from "react";
|
||||
import { RefreshCw, Zap, Database, Activity, Maximize2, Minimize2, X, BarChart2 } from "lucide-react";
|
||||
import { RefreshCw, Database, Activity, Maximize2, Minimize2, X, BarChart2 } from "lucide-react";
|
||||
import { CURRENCIES, CURRENCY_META } from "@/lib/constants";
|
||||
import type { Currency, DriverData, SentimentEntry, CotEntry, MacroSection } from "@/lib/types";
|
||||
import type { RateProbData } from "@/lib/rateprobability";
|
||||
@@ -50,6 +50,7 @@ export default function Dashboard() {
|
||||
const [globalMacroSlide, setGlobalMacroSlide] = useState<"mon"|"infl"|"cro"|"empl">("mon");
|
||||
const [globalCardTab, setGlobalCardTab] = useState<"overview"|"mispricing"|"focus">("overview");
|
||||
const [globalSignauxSlide, setGlobalSignauxSlide] = useState<"ois"|"cot"|"sent">("ois");
|
||||
const [globalOisChartTab, setGlobalOisChartTab] = useState<"curve"|"implied"|"scenarios">("curve");
|
||||
const [macroSyncEnabled, setMacroSyncEnabled] = useState(false);
|
||||
|
||||
// ── Sentiment multi-paires Myfxbook → {CCY: {longPct, shortPct, pair}} ──────
|
||||
@@ -139,15 +140,16 @@ export default function Dashboard() {
|
||||
const refresh = useCallback(async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const NO_CACHE = { cache: "no-store" } as const;
|
||||
const [driversRes, expectRes, yieldsRes, fxRes, sentimentRes, cotRes, calRes, rateProbRes] = await Promise.allSettled([
|
||||
fetch("/api/drivers").then((r) => r.json()),
|
||||
fetch("/api/expectations").then((r) => r.json()),
|
||||
fetch("/api/yields").then((r) => r.json()),
|
||||
fetch("/api/fx").then((r) => r.json()),
|
||||
fetch("/api/sentiment").then((r) => r.json()),
|
||||
fetch("/api/cot").then((r) => r.json()),
|
||||
fetch("/api/calendar").then((r) => r.json()),
|
||||
fetch("/api/rate-probabilities").then((r) => r.json()),
|
||||
fetch("/api/drivers", NO_CACHE).then((r) => r.json()),
|
||||
fetch("/api/expectations", NO_CACHE).then((r) => r.json()),
|
||||
fetch("/api/yields", NO_CACHE).then((r) => r.json()),
|
||||
fetch("/api/fx", NO_CACHE).then((r) => r.json()),
|
||||
fetch("/api/sentiment", NO_CACHE).then((r) => r.json()),
|
||||
fetch("/api/cot", NO_CACHE).then((r) => r.json()),
|
||||
fetch("/api/calendar", NO_CACHE).then((r) => r.json()),
|
||||
fetch("/api/rate-probabilities",NO_CACHE).then((r) => r.json()),
|
||||
]);
|
||||
|
||||
// ── Drivers ───────────────────────────────────────────────────────────
|
||||
@@ -299,26 +301,13 @@ export default function Dashboard() {
|
||||
<Activity size={15} className="text-black" />
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-sm font-bold text-white tracking-tight">
|
||||
<span className="text-sm font-bold text-white tracking-tight" suppressHydrationWarning>
|
||||
{new Date().getHours() < 18 ? "Bonjour" : "Bonsoir"} 👋
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3">
|
||||
{divergenceCount > 0 && (
|
||||
<button
|
||||
onClick={() => setActiveTab("dashboard")}
|
||||
title="Devises avec score macro ≥ 2 — cliquer pour voir le dashboard"
|
||||
className="flex items-center gap-1.5 bg-amber-500/10 border border-amber-500/20 rounded-full px-3 py-1.5 hover:bg-amber-500/20 transition-colors"
|
||||
>
|
||||
<Zap size={12} className="text-amber-400" />
|
||||
<span className="text-xs font-medium text-amber-400">
|
||||
{divergenceCount} divergence{divergenceCount > 1 ? "s" : ""} active{divergenceCount > 1 ? "s" : ""}
|
||||
</span>
|
||||
</button>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 text-[11px] text-slate-500">
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-emerald-500 animate-pulse" />
|
||||
{driversFromCache && driversCacheAge && (
|
||||
@@ -327,7 +316,7 @@ export default function Dashboard() {
|
||||
<span>cache {driversCacheAge}</span>
|
||||
</span>
|
||||
)}
|
||||
<span>{lastRefresh.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}</span>
|
||||
<span suppressHydrationWarning>{lastRefresh.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" })}</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -373,8 +362,8 @@ export default function Dashboard() {
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Global drivers bar — visible sur les deux onglets */}
|
||||
{drivers && <DriversBar drivers={drivers} />}
|
||||
{/* Global drivers bar — uniquement sur le dashboard */}
|
||||
{activeTab === "dashboard" && drivers && <DriversBar drivers={drivers} />}
|
||||
|
||||
{activeTab === "dashboard" && (
|
||||
<>
|
||||
@@ -593,6 +582,9 @@ export default function Dashboard() {
|
||||
onCardTabChange={macroSyncEnabled ? (setGlobalCardTab as (id: "overview"|"mispricing"|"focus") => void) : undefined}
|
||||
syncSignauxSlide={macroSyncEnabled ? globalSignauxSlide : undefined}
|
||||
onSignauxSlideChange={macroSyncEnabled ? setGlobalSignauxSlide : undefined}
|
||||
syncOisChartTab={macroSyncEnabled ? globalOisChartTab : undefined}
|
||||
onOisChartTabChange={macroSyncEnabled ? setGlobalOisChartTab : undefined}
|
||||
isLoading={loading}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
@@ -607,10 +599,10 @@ export default function Dashboard() {
|
||||
<div className="h-px flex-1 bg-sky-500/20" />
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<TvAdvancedChart symbol="FOREXCOM:SPXUSD" label="S&P 500 · Daily" interval="D" height={220} />
|
||||
<TvAdvancedChart symbol="CBOE:VIX" label="VIX · Daily" interval="D" height={220} />
|
||||
<TvAdvancedChart symbol="CAPITALCOM:DXY" label="DXY Dollar Index · Weekly" interval="W" height={220} />
|
||||
<TvAdvancedChart symbol="TVC:GOLD" label="Or (XAU/USD) · Weekly" interval="W" height={220} />
|
||||
<TvAdvancedChart symbol="FOREXCOM:SPXUSD" label="S&P 500" interval="D" height={220} />
|
||||
<TvAdvancedChart symbol="PEPPERSTONE:VIX" label="VIX" interval="D" height={220} />
|
||||
<TvAdvancedChart symbol="CAPITALCOM:DXY" label="DXY Dollar Index" interval="W" height={220} />
|
||||
<TvAdvancedChart symbol="TVC:GOLD" label="Or (XAU/USD)" interval="W" height={220} />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
+21
-13
@@ -1,6 +1,6 @@
|
||||
// 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/
|
||||
// URL pattern: /centralbank/how-have-interest-rate-expectations-changed-after-this-weeks-events-YYYYMMDD/ (+ fallbacks)
|
||||
// Published after major market events (typically on Fridays or post-CB-decision)
|
||||
//
|
||||
// Data format in articleBody JSON-LD:
|
||||
@@ -50,18 +50,26 @@ async function tryUrl(daysAgo: number): Promise<ArticleRef | null> {
|
||||
const d = new Date(Date.now() - daysAgo * 86_400_000);
|
||||
const yyyymmdd = d.toISOString().slice(0, 10).replace(/-/g, "");
|
||||
const dateStr = `${yyyymmdd.slice(0,4)}-${yyyymmdd.slice(4,6)}-${yyyymmdd.slice(6,8)}`;
|
||||
const url = `https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-event-${yyyymmdd}/`;
|
||||
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",
|
||||
});
|
||||
if (!res.ok) return null;
|
||||
// Cancel séparé pour ne pas masquer le succès en cas d'erreur de libération
|
||||
res.body?.cancel().catch(() => {});
|
||||
return { url, dateStr, daysAgo };
|
||||
} catch { return null; }
|
||||
// Tester les 4 variantes : /centralbank/ ou /news/, events (pluriel) ou event (singulier)
|
||||
const candidates = [
|
||||
`https://investinglive.com/centralbank/how-have-interest-rate-expectations-changed-after-this-weeks-events-${yyyymmdd}/`,
|
||||
`https://investinglive.com/centralbank/how-have-interest-rate-expectations-changed-after-this-weeks-event-${yyyymmdd}/`,
|
||||
`https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-events-${yyyymmdd}/`,
|
||||
`https://investinglive.com/news/how-have-interest-rate-expectations-changed-after-this-weeks-event-${yyyymmdd}/`,
|
||||
];
|
||||
for (const url of candidates) {
|
||||
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",
|
||||
});
|
||||
if (!res.ok) { res.body?.cancel().catch(() => {}); continue; }
|
||||
res.body?.cancel().catch(() => {});
|
||||
return { url, dateStr, daysAgo };
|
||||
} catch { continue; }
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function findArticleRefs(): Promise<{ current: ArticleRef | null; previous: ArticleRef | null }> {
|
||||
|
||||
Reference in New Issue
Block a user