Forex Dashboard v8.0

This commit is contained in:
Capucine Gest
2026-05-27 13:45:06 +02:00
parent 236afa3087
commit 0a779fb4eb
2 changed files with 114 additions and 98 deletions
+103 -96
View File
@@ -1,43 +1,83 @@
import { NextResponse } from "next/server"; import { NextResponse } from "next/server";
// ── Yahoo Finance v7/quote ──────────────────────────────────────────────────── // ── Cache mémoire serveur ──────────────────────────────────────────────────────
// Un seul appel pour tous les prix marchés : VIX, S&P, commodités. // Évite de retaper AV à chaque requête page. Ne cache les succès que (jamais null).
// Pas de clé API. Gratuit. regularMarketChange = delta vs clôture j-1. const _cache = new Map<string, { v: unknown; ts: number }>();
const TTL_24H = 86_400_000;
const TTL_1H = 3_600_000;
type YQuote = { // ── Alpha Vantage GLOBAL_QUOTE ────────────────────────────────────────────────
symbol: string; // Clé existante. 25 req/jour gratuit.
regularMarketPrice: number; // Symboles utilisés : ^VIX, ^GSPC, GC=F, SI=F, BZ=F, CL=F → 6 req/jour.
regularMarketChange: number; // Séquentiels pour respecter 5 req/min.
regularMarketChangePercent: number;
regularMarketPreviousClose: number;
};
async function yahooQuotes(symbols: string[]): Promise<Record<string, YQuote>> { type AVQ = { value: number | null; delta: number | null; deltaPct: number | null };
async function avQuote(symbol: string, avKey: string): Promise<AVQ> {
const cacheKey = `av_${symbol}`;
const hit = _cache.get(cacheKey);
if (hit && Date.now() - hit.ts < TTL_24H) return hit.v as AVQ;
const empty: AVQ = { value: null, delta: null, deltaPct: null };
try { try {
const url = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=${symbols.join(",")}&fields=regularMarketPrice,regularMarketChange,regularMarketChangePercent,regularMarketPreviousClose`; const url = `https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=${encodeURIComponent(symbol)}&apikey=${avKey}`;
const res = await fetch(url, { const res = await fetch(url, { next: { revalidate: 86400 } });
next: { revalidate: 3600 }, // 1h — prix de marché if (!res.ok) return empty;
headers: { const json = await res.json();
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", const q = json?.["Global Quote"];
"Accept": "application/json", // AV rate-limit renvoie {"Note":"..."} avec un "Global Quote" vide
}, if (!q || !q["05. price"]) return empty;
});
if (!res.ok) return {}; const value = parseFloat(q["05. price"]);
const results: YQuote[] = (await res.json())?.quoteResponse?.result ?? []; const delta = parseFloat(q["09. change"]);
return Object.fromEntries(results.map((q) => [q.symbol, q])); const deltaPct = parseFloat((q["10. change percent"] ?? "0%").replace("%", ""));
} catch { return {}; } const result: AVQ = {
value: isNaN(value) ? null : parseFloat(value.toFixed(2)),
delta: isNaN(delta) ? null : parseFloat(delta.toFixed(2)),
deltaPct: isNaN(deltaPct) ? null : parseFloat(deltaPct.toFixed(2)),
};
_cache.set(cacheKey, { v: result, ts: Date.now() }); // cache uniquement si succès
return result;
} catch { return empty; }
} }
function yVal(q: YQuote | undefined): number | null { // ── Binance (Bitcoin — gratuit, sans clé, temps réel) ────────────────────────
return q?.regularMarketPrice ?? null;
async function binanceBTC(): Promise<{ value: number | null; change24h: number | null }> {
const k = "binance_btc";
const hit = _cache.get(k);
if (hit && Date.now() - hit.ts < TTL_1H) return hit.v as { value: number | null; change24h: number | null };
try {
const res = await fetch("https://api.binance.com/api/v3/ticker/24hr?symbol=BTCUSDT", { cache: "no-store" });
if (!res.ok) return { value: null, change24h: null };
const d = await res.json();
const price = parseFloat(d.lastPrice);
const pctChg = parseFloat(d.priceChangePercent);
const result = {
value: isNaN(price) ? null : Math.round(price),
change24h: isNaN(pctChg) ? null : parseFloat(pctChg.toFixed(2)),
};
_cache.set(k, { v: result, ts: Date.now() });
return result;
} catch { return { value: null, change24h: null }; }
} }
function yDelta(q: YQuote | undefined): number | null {
const d = q?.regularMarketChange; // ── CoinGecko (Bitcoin — fallback si Binance échoue) ─────────────────────────
return d != null ? parseFloat(d.toFixed(2)) : null;
} async function coingeckoBTC(): Promise<{ value: number | null; change24h: number | null }> {
function yDeltaPct(q: YQuote | undefined): number | null { try {
const p = q?.regularMarketChangePercent; const res = await fetch(
return p != null ? parseFloat(p.toFixed(2)) : null; "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true",
{ cache: "no-store" }
);
if (!res.ok) return { value: null, change24h: null };
const d = await res.json();
return {
value: d?.bitcoin?.usd ?? null,
change24h: d?.bitcoin?.usd_24h_change ?? null,
};
} catch { return { value: null, change24h: null }; }
} }
// ── FRED (spreads crédit + taux — 24h cache) ────────────────────────────────── // ── FRED (spreads crédit + taux — 24h cache) ──────────────────────────────────
@@ -54,61 +94,28 @@ async function fredObs(series: string, apiKey: string): Promise<number | null> {
} catch { return null; } } catch { return null; }
} }
// ── CoinGecko (Bitcoin — gratuit, sans clé, cache 1h) ────────────────────────
async function coingeckoBTC() {
try {
const url = "https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=usd&include_24hr_change=true";
const res = await fetch(url, { next: { revalidate: 3600 } });
if (!res.ok) return { value: null, change24h: null };
const d = await res.json();
return { value: d?.bitcoin?.usd ?? null, change24h: d?.bitcoin?.usd_24h_change ?? null };
} catch { return { value: null, change24h: null }; }
}
// ── Alpha Vantage (S&P 500 via SPY — fallback si Yahoo échoue) ───────────────
async function avGlobalQuote(symbol: string, avKey: string) {
try {
const url = `https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=${symbol}&apikey=${avKey}`;
const res = await fetch(url, { next: { revalidate: 86400 } });
if (!res.ok) return { value: null, changePct: null };
const q = (await res.json())?.["Global Quote"];
if (!q) return { value: null, changePct: null };
const value = parseFloat(q["05. price"]);
const changePct = parseFloat((q["10. change percent"] ?? "0%").replace("%", ""));
return {
value: isNaN(value) ? null : value,
changePct: isNaN(changePct) ? null : changePct,
};
} catch { return { value: null, changePct: null }; }
}
// ── GET ─────────────────────────────────────────────────────────────────────── // ── GET ───────────────────────────────────────────────────────────────────────
export async function GET() { export async function GET() {
const fredKey = process.env.FRED_API_KEY; const fredKey = process.env.FRED_API_KEY;
const avKey = process.env.ALPHA_VANTAGE_KEY; const avKey = process.env.ALPHA_VANTAGE_KEY;
if (!fredKey) return NextResponse.json({ error: "FRED_API_KEY missing" }, { status: 500 }); if (!fredKey) return NextResponse.json({ error: "FRED_API_KEY missing" }, { status: 500 });
if (!avKey) return NextResponse.json({ error: "ALPHA_VANTAGE_KEY missing" }, { status: 500 });
// 1. Yahoo Finance — tout en un seul appel (VIX + S&P + commodités + BTC) // 1. Indices + commodités — Alpha Vantage GLOBAL_QUOTE (cache 24h mémoire)
const [quotes, btcCgRes] = await Promise.all([ // Appels séquentiels → respect limite 5 req/min AV
yahooQuotes(["^VIX", "^GSPC", "GC=F", "SI=F", "BZ=F", "CL=F", "BTC-USD"]), const vixQ = await avQuote("^VIX", avKey);
coingeckoBTC(), // fallback si Yahoo échoue pour BTC const sp500Q = await avQuote("^GSPC", avKey);
]); const goldQ = await avQuote("GC=F", avKey);
const silverQ = await avQuote("SI=F", avKey);
const brentQ = await avQuote("BZ=F", avKey);
const wtiQ = await avQuote("CL=F", avKey);
const vix = quotes["^VIX"]; // 2. Bitcoin — Binance (temps réel, sans clé), fallback CoinGecko
const sp500 = quotes["^GSPC"]; const btcBin = await binanceBTC();
const gold = quotes["GC=F"]; const btcCg = btcBin.value === null ? await coingeckoBTC() : { value: null, change24h: null };
const silver = quotes["SI=F"];
const brent = quotes["BZ=F"];
const wti = quotes["CL=F"];
const btcYQ = quotes["BTC-USD"]; // Yahoo primary pour BTC
// Fallback S&P via AV si Yahoo a échoué (rare) // 3. FRED — spreads crédit + taux directeurs (cache 24h)
const sp500Fallback = !sp500 && avKey ? await avGlobalQuote("SPY", avKey) : null;
// 2. FRED — spreads crédit + taux (24h cache, données macro)
const [hyRaw, igRaw, us10y, us2y] = await Promise.all([ const [hyRaw, igRaw, us10y, us2y] = await Promise.all([
fredObs("BAMLH0A0HYM2", fredKey), fredObs("BAMLH0A0HYM2", fredKey),
fredObs("BAMLC0A0CM", fredKey), fredObs("BAMLC0A0CM", fredKey),
@@ -118,29 +125,29 @@ export async function GET() {
return NextResponse.json({ return NextResponse.json({
// Sentiment / Risk-On // Sentiment / Risk-On
vix: yVal(vix), vix: vixQ.value,
vixDelta: yDelta(vix), vixDelta: vixQ.delta,
sp500: yVal(sp500) ?? sp500Fallback?.value, sp500: sp500Q.value,
sp500Change: yDelta(sp500), sp500Change: sp500Q.delta,
sp500ChangePct: yDeltaPct(sp500) ?? sp500Fallback?.changePct, sp500ChangePct: sp500Q.deltaPct,
btc: yVal(btcYQ) ?? btcCgRes.value, btc: btcBin.value ?? btcCg.value,
btcChange24h: yDeltaPct(btcYQ) ?? btcCgRes.change24h, btcChange24h: btcBin.change24h ?? btcCg.change24h,
// Crédit (FRED, fraction × 100 = bps) // Crédit (FRED, % × 100 = bps)
hySpread: hyRaw != null ? Math.round(hyRaw * 100) : null, hySpread: hyRaw != null ? Math.round(hyRaw * 100) : null,
igSpread: igRaw != null ? Math.round(igRaw * 100) : null, igSpread: igRaw != null ? Math.round(igRaw * 100) : null,
// Taux (dxy injecté par page.tsx depuis /api/fx) // Taux (DXY injecté par page.tsx depuis /api/fx)
us10y, us10y,
us2y, us2y,
curveSlope: us10y !== null && us2y !== null ? Math.round((us10y - us2y) * 100) : null, curveSlope: us10y !== null && us2y !== null ? Math.round((us10y - us2y) * 100) : null,
// Commodités — Yahoo Finance (quasi temps-réel, delta vs clôture j-1) // Commodités — delta = variation vs clôture veille
gold: yVal(gold), gold: goldQ.value,
goldDelta: yDelta(gold), goldDelta: goldQ.delta,
silver: yVal(silver), silver: silverQ.value,
silverDelta: yDelta(silver), silverDelta: silverQ.delta,
brent: yVal(brent), brent: brentQ.value,
brentDelta: yDelta(brent), brentDelta: brentQ.delta,
wti: yVal(wti), wti: wtiQ.value,
wtiDelta: yDelta(wti), wtiDelta: wtiQ.delta,
// Compat // Compat
copper: null, copper: null,
timestamp: Date.now(), timestamp: Date.now(),
+11 -2
View File
@@ -42,8 +42,17 @@ async function fetchFrankfurter() {
try { try {
const res = await fetch("https://api.frankfurter.app/latest?from=USD", { next: { revalidate: 86400 } }); const res = await fetch("https://api.frankfurter.app/latest?from=USD", { next: { revalidate: 86400 } });
if (!res.ok) throw new Error(`Frankfurter ${res.status}`); if (!res.ok) throw new Error(`Frankfurter ${res.status}`);
const data = await res.json(); const data = await res.json();
return NextResponse.json({ rates: data.rates, dxy: null, base: "USD", source: "frankfurter", date: data.date }); const rates = data.rates as Record<string, number>;
// DXY approximé depuis les taux ECB (même formule que la branche AV)
// rates.X = "1 USD = X unités" — même convention que AV
const e = rates.EUR, g = rates.GBP, j = rates.JPY, c = rates.CAD, ch = rates.CHF;
const dxy = (e && g && j && c && ch)
? parseFloat((50.14348112 * Math.pow(e,0.576) * Math.pow(1/j,0.136) * Math.pow(g,0.119) * Math.pow(1/c,0.091) * Math.pow(1/ch,0.036)).toFixed(2))
: null;
return NextResponse.json({ rates, dxy, base: "USD", source: "frankfurter", date: data.date });
} catch (err) { } catch (err) {
return NextResponse.json({ error: String(err) }, { status: 502 }); return NextResponse.json({ error: String(err) }, { status: 502 });
} }