mirror of
https://github.com/caty21/forex-dashboard.git
synced 2026-08-05 08:47:45 +00:00
fix: Sem. préc. HEAD→GET, prevMeetings cache, remove footer
- investinglive: tryUrl HEAD→GET + res.body?.cancel() (WordPress bloque HEAD) fenêtre recherche article préc. élargie 21→28 jours - rateprobability: CBRatePath + prevMeetings/prevWeekDate, loadPrevWeekCachedBody() fetchAllCBPaths enrichit prevMeetings depuis snapshot GitHub Actions - fetch-rate-data.mjs: rotation current→previousWeek (5–9 jours) pour Rate Curve - CurrencyCard: rateCurveData préfère prevMeetings (exact/meeting) > ilDelta (approx) légende hasPrevCurve pilote affichage ligne bleue et label date - page.tsx: suppression footer Sources/LLM, remplacement par pb-12 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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(", ")})`);
|
||||
|
||||
+2
-9
@@ -634,15 +634,8 @@ export default function Dashboard() {
|
||||
<div className="ml-auto text-slate-700 hidden sm:block">Cliquer sur chaque signal pour voir l'analyse détaillée</div>
|
||||
</div>
|
||||
|
||||
{/* Footer */}
|
||||
<footer className="mt-4 text-center text-xs text-slate-600 space-y-1">
|
||||
<p>
|
||||
Sources: FRED · ECB · BoE · BoC · CFTC · Frankfurter · Myfxbook · ForexFactory
|
||||
</p>
|
||||
<p>
|
||||
LLM: Groq (Llama 3.1) · Données à titre informatif uniquement — pas de conseil financier
|
||||
</p>
|
||||
</footer>
|
||||
{/* Padding bas pour éviter que le contenu s'arrête brutalement au scroll */}
|
||||
<div className="pb-12" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
+15
-10
@@ -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 }) {
|
||||
<span className="inline-block w-5 h-px bg-slate-200 rounded" />
|
||||
Actuel
|
||||
</span>
|
||||
<span className={`flex items-center gap-1 text-[8px] ${ilDelta ? "text-sky-400" : "text-slate-600"}`}>
|
||||
<span className={`inline-block w-5 border-t border-dashed ${ilDelta ? "border-sky-400" : "border-slate-700"}`} />
|
||||
{ilDelta ? `Sem. préc. (${ilDelta.prevDate})` : "Sem. préc. (indispo)"}
|
||||
<span className={`flex items-center gap-1 text-[8px] ${hasPrevCurve ? "text-sky-400" : "text-slate-600"}`}>
|
||||
<span className={`inline-block w-5 border-t border-dashed ${hasPrevCurve ? "border-sky-400" : "border-slate-700"}`} />
|
||||
{hasPrevCurve && prevCurveLabel ? `Sem. préc. (${prevCurveLabel})` : hasPrevCurve ? "Sem. préc." : "Sem. préc. (indispo)"}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -526,7 +531,7 @@ function OISEnhancedBlock({ ratePath }: { ratePath: CBRatePath }) {
|
||||
formatter={(v: number, name: string) => [`${v.toFixed(3)}%`, name === "current" ? "Actuel" : "Sem. préc."]}
|
||||
/>
|
||||
<Line type="monotone" dataKey="current" stroke="#e2e8f0" strokeWidth={1.5} dot={{ r: 2, fill: "#e2e8f0" }} name="current" />
|
||||
{ilDelta && (
|
||||
{hasPrevCurve && (
|
||||
<Line type="monotone" dataKey="weekAgo" stroke="#38bdf8" strokeWidth={1.5} dot={{ r: 2, fill: "#38bdf8" }} strokeDasharray="4 2" name="weekAgo" />
|
||||
)}
|
||||
</LineChart>
|
||||
|
||||
@@ -52,12 +52,15 @@ async function tryUrl(daysAgo: number): Promise<ArticleRef | null> {
|
||||
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; }
|
||||
}
|
||||
|
||||
@@ -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<Record<Currency, CBRatePath>>;
|
||||
@@ -244,6 +246,21 @@ function loadCachedRPBody(ccy: string, slug: string): Record<string, unknown> |
|
||||
} catch { return null; }
|
||||
}
|
||||
|
||||
function loadPrevWeekCachedBody(ccy: string): { body: Record<string, unknown>; 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<string, unknown>; previousWeekFetchedAt?: string };
|
||||
if (!parsed.previousWeek || !parsed.previousWeekFetchedAt) return null;
|
||||
const entry = parsed.previousWeek[ccy] as Record<string, unknown> | 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<string, unknown>): CBRatePath | null {
|
||||
const today = body["today"] as Record<string, unknown> | undefined;
|
||||
@@ -301,6 +318,18 @@ export async function fetchAllCBPaths(): Promise<RateProbData> {
|
||||
}
|
||||
}
|
||||
|
||||
// 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%
|
||||
|
||||
Reference in New Issue
Block a user