diff --git a/.github/scripts/fetch-rate-data.mjs b/.github/scripts/fetch-rate-data.mjs
index f200cef..4935514 100644
--- a/.github/scripts/fetch-rate-data.mjs
+++ b/.github/scripts/fetch-rate-data.mjs
@@ -1,6 +1,6 @@
// Runs in GitHub Actions (not Vercel) — fetches from rateprobability.com
// GitHub Actions IPs are not blocked by the site.
-import { writeFileSync, mkdirSync } from "fs";
+import { writeFileSync, mkdirSync, readFileSync } from "fs";
const CB_KEYS = [
["USD", "fed"],
@@ -45,10 +45,37 @@ for (const [ccy, slug] of CB_KEYS) {
await new Promise(r => setTimeout(r, 300)); // 300ms entre requêtes
}
+// Rotation : si les données existantes datent de 5–9 jours → les sauvegarder comme previousWeek
+let previousWeek = null;
+let previousWeekFetchedAt = null;
+try {
+ const existing = JSON.parse(readFileSync("data/rate-probabilities.json", "utf8"));
+ const ageMs = Date.now() - new Date(existing.fetchedAt).getTime();
+ const day = 86400000;
+ if (ageMs >= 5 * day && ageMs <= 9 * day) {
+ // Les données courantes datent d'une semaine → les promouvoir en previousWeek
+ previousWeek = existing.data;
+ previousWeekFetchedAt = existing.fetchedAt;
+ console.log(`\nRotated existing data (${(ageMs / day).toFixed(1)}d old) to previousWeek`);
+ } else if (existing.previousWeek && existing.previousWeekFetchedAt) {
+ // Conserver le previousWeek existant s'il est encore dans la fenêtre utile (< 11 jours)
+ const prevAge = Date.now() - new Date(existing.previousWeekFetchedAt).getTime();
+ if (prevAge < 11 * day) {
+ previousWeek = existing.previousWeek;
+ previousWeekFetchedAt = existing.previousWeekFetchedAt;
+ }
+ }
+} catch { /* pas de fichier existant */ }
+
mkdirSync("data", { recursive: true });
writeFileSync(
"data/rate-probabilities.json",
- JSON.stringify({ data: results, fetchedAt: new Date().toISOString() }, null, 2)
+ JSON.stringify({
+ data: results,
+ fetchedAt: new Date().toISOString(),
+ ...(previousWeek ? { previousWeek, previousWeekFetchedAt } : {}),
+ }, null, 2)
);
console.log(`\nSaved ${Object.keys(results).length} CBs: ${Object.keys(results).join(", ")}`);
+if (previousWeek) console.log(`previousWeek preserved (${Object.keys(previousWeek).join(", ")})`);
diff --git a/app/page.tsx b/app/page.tsx
index b7cbbb5..c847c18 100644
--- a/app/page.tsx
+++ b/app/page.tsx
@@ -634,15 +634,8 @@ export default function Dashboard() {
Cliquer sur chaque signal pour voir l'analyse détaillée
- {/* Footer */}
-
+ {/* Padding bas pour éviter que le contenu s'arrête brutalement au scroll */}
+
);
}
diff --git a/components/CurrencyCard.tsx b/components/CurrencyCard.tsx
index 470db08..6b68ad0 100644
--- a/components/CurrencyCard.tsx
+++ b/components/CurrencyCard.tsx
@@ -341,7 +341,7 @@ function SignalBar({ strength, direction }: { strength: number; direction: Signa
function OISEnhancedBlock({ ratePath }: { ratePath: CBRatePath }) {
const [chartTab, setChartTab] = useState<"curve" | "implied" | "scenarios">("curve");
- const { currentRate, meetings, yearEndImplied, ilDelta, ilCurrent } = ratePath;
+ const { currentRate, meetings, yearEndImplied, ilDelta, ilCurrent, prevMeetings, prevWeekDate } = ratePath;
if (!meetings.length) return null;
const m0 = meetings[0];
@@ -368,11 +368,16 @@ function OISEnhancedBlock({ ratePath }: { ratePath: CBRatePath }) {
// Chart data (limit to 10 meetings)
const chartMeetings = meetings.slice(0, 10);
- const rateCurveData = chartMeetings.map(m => ({
- label: m.label,
- current: +m.impliedRate.toFixed(3),
- ...(ilDelta ? { weekAgo: +(m.impliedRate + ilDelta.bpsDelta / 100).toFixed(3) } : {}),
- }));
+ // prevMeetings (snapshot RP) = per-meeting exact; ilDelta (IL article) = décalage uniforme approximatif
+ const rateCurveData = chartMeetings.map(m => {
+ const prevM = prevMeetings?.find(p => p.dateIso === m.dateIso);
+ const weekAgo = prevM
+ ? +prevM.impliedRate.toFixed(3)
+ : ilDelta ? +(m.impliedRate + ilDelta.bpsDelta / 100).toFixed(3) : undefined;
+ return { label: m.label, current: +m.impliedRate.toFixed(3), ...(weekAgo !== undefined ? { weekAgo } : {}) };
+ });
+ const hasPrevCurve = !!(prevMeetings?.length || ilDelta);
+ const prevCurveLabel = prevWeekDate ?? ilDelta?.prevDate ?? null;
const impliedPtsData = chartMeetings.map(m => ({
label: m.label,
@@ -507,9 +512,9 @@ function OISEnhancedBlock({ ratePath }: { ratePath: CBRatePath }) {
Actuel
-
-
- {ilDelta ? `Sem. préc. (${ilDelta.prevDate})` : "Sem. préc. (indispo)"}
+
+
+ {hasPrevCurve && prevCurveLabel ? `Sem. préc. (${prevCurveLabel})` : hasPrevCurve ? "Sem. préc." : "Sem. préc. (indispo)"}
@@ -526,7 +531,7 @@ function OISEnhancedBlock({ ratePath }: { ratePath: CBRatePath }) {
formatter={(v: number, name: string) => [`${v.toFixed(3)}%`, name === "current" ? "Actuel" : "Sem. préc."]}
/>
- {ilDelta && (
+ {hasPrevCurve && (
)}
diff --git a/lib/investinglive.ts b/lib/investinglive.ts
index f7b7591..0102e60 100644
--- a/lib/investinglive.ts
+++ b/lib/investinglive.ts
@@ -52,12 +52,15 @@ async function tryUrl(daysAgo: number): Promise {
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 {
+ // GET au lieu de HEAD — certains serveurs WordPress refusent HEAD (405/404 même si la page existe)
const res = await fetch(url, {
- method: "HEAD",
+ 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",
});
- return res.ok ? { url, dateStr, daysAgo } : null;
+ if (!res.ok) return null;
+ await res.body?.cancel(); // libère la connexion sans télécharger le body
+ return { url, dateStr, daysAgo };
} catch { return null; }
}
@@ -73,8 +76,8 @@ async function findArticleRefs(): Promise<{ current: ArticleRef | null; previous
if (!current) return { current: null, previous: null };
let previous: ArticleRef | null = null;
- // Start the day after the current article and look up to 21 days further back
- for (let d = current.daysAgo + 1; d <= current.daysAgo + 21; d++) {
+ // Start the day after the current article and look up to 28 days further back
+ for (let d = current.daysAgo + 1; d <= current.daysAgo + 28; d++) {
const found = await tryUrl(d);
if (found) { previous = found; break; }
}
diff --git a/lib/rateprobability.ts b/lib/rateprobability.ts
index 267d6b9..1c41ebc 100644
--- a/lib/rateprobability.ts
+++ b/lib/rateprobability.ts
@@ -44,6 +44,8 @@ export interface CBRatePath {
yearEndImplied: number | null; // taux impliqué à la dernière réunion connue (SOFR)
ilCurrent?: ILCurrent; // valeurs absolues de l'article IL courant
ilDelta?: ILWeeklyDelta; // delta vs article IL semaine précédente
+ prevMeetings?: RateProbMeeting[]; // réunions semaine précédente (snapshot RP)
+ prevWeekDate?: string; // date du snapshot semaine précédente
}
export type RateProbData = Partial>;
@@ -244,6 +246,21 @@ function loadCachedRPBody(ccy: string, slug: string): Record |
} catch { return null; }
}
+function loadPrevWeekCachedBody(ccy: string): { body: Record; date: string } | null {
+ try {
+ const filePath = join(process.cwd(), "data", "rate-probabilities.json");
+ const raw = readFileSync(filePath, "utf8");
+ const parsed = JSON.parse(raw) as { previousWeek?: Record; previousWeekFetchedAt?: string };
+ if (!parsed.previousWeek || !parsed.previousWeekFetchedAt) return null;
+ const entry = parsed.previousWeek[ccy] as Record | undefined;
+ if (!entry) return null;
+ const ageMs = Date.now() - new Date(parsed.previousWeekFetchedAt).getTime();
+ // Le snapshot semaine précédente doit dater de 4 à 10 jours
+ if (ageMs < 3 * 86400000 || ageMs > 11 * 86400000) return null;
+ return { body: entry, date: parsed.previousWeekFetchedAt.slice(0, 10) };
+ } catch { return null; }
+}
+
// Reparse un body brut de rateprobability.com (même format que fetchCBPath)
function parseCBBody(ccy: Currency, body: Record): CBRatePath | null {
const today = body["today"] as Record | undefined;
@@ -301,6 +318,18 @@ export async function fetchAllCBPaths(): Promise {
}
}
+ // Enrichissement prevMeetings depuis snapshot semaine précédente (GitHub Actions)
+ for (const [ccy] of CB_KEYS) {
+ const path = data[ccy as Currency];
+ if (!path) continue;
+ const prev = loadPrevWeekCachedBody(ccy);
+ if (!prev) continue;
+ const prevPath = parseCBBody(ccy as Currency, prev.body);
+ if (prevPath?.meetings.length) {
+ data[ccy as Currency] = { ...path, prevMeetings: prevPath.meetings, prevWeekDate: prev.date };
+ }
+ }
+
// CHF/SNB : rateprobability ne couvre pas la SNB → InvestingLive est la seule source
if (!data["CHF"] && ilData["CHF"]) {
const snbPath = buildSNBPath(ilData, 0.00); // taux actuel SNB = 0%