diff --git a/app/api/drivers/route.ts b/app/api/drivers/route.ts index 8d5c4e7..3c4f180 100644 --- a/app/api/drivers/route.ts +++ b/app/api/drivers/route.ts @@ -1,43 +1,83 @@ import { NextResponse } from "next/server"; -// ── Yahoo Finance v7/quote ──────────────────────────────────────────────────── -// Un seul appel pour tous les prix marchés : VIX, S&P, commodités. -// Pas de clé API. Gratuit. regularMarketChange = delta vs clôture j-1. +// ── Cache mémoire serveur ────────────────────────────────────────────────────── +// Évite de retaper AV à chaque requête page. Ne cache les succès que (jamais null). +const _cache = new Map(); +const TTL_24H = 86_400_000; +const TTL_1H = 3_600_000; -type YQuote = { - symbol: string; - regularMarketPrice: number; - regularMarketChange: number; - regularMarketChangePercent: number; - regularMarketPreviousClose: number; -}; +// ── Alpha Vantage GLOBAL_QUOTE ──────────────────────────────────────────────── +// Clé existante. 25 req/jour gratuit. +// Symboles utilisés : ^VIX, ^GSPC, GC=F, SI=F, BZ=F, CL=F → 6 req/jour. +// Séquentiels pour respecter 5 req/min. -async function yahooQuotes(symbols: string[]): Promise> { +type AVQ = { value: number | null; delta: number | null; deltaPct: number | null }; + +async function avQuote(symbol: string, avKey: string): Promise { + 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 { - const url = `https://query1.finance.yahoo.com/v7/finance/quote?symbols=${symbols.join(",")}&fields=regularMarketPrice,regularMarketChange,regularMarketChangePercent,regularMarketPreviousClose`; - const res = await fetch(url, { - next: { revalidate: 3600 }, // 1h — prix de marché - headers: { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36", - "Accept": "application/json", - }, - }); - if (!res.ok) return {}; - const results: YQuote[] = (await res.json())?.quoteResponse?.result ?? []; - return Object.fromEntries(results.map((q) => [q.symbol, q])); - } catch { return {}; } + const url = `https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol=${encodeURIComponent(symbol)}&apikey=${avKey}`; + const res = await fetch(url, { next: { revalidate: 86400 } }); + if (!res.ok) return empty; + const json = await res.json(); + const q = json?.["Global Quote"]; + // AV rate-limit renvoie {"Note":"..."} avec un "Global Quote" vide + if (!q || !q["05. price"]) return empty; + + const value = parseFloat(q["05. price"]); + const delta = parseFloat(q["09. change"]); + const deltaPct = parseFloat((q["10. change percent"] ?? "0%").replace("%", "")); + 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 { - return q?.regularMarketPrice ?? null; +// ── Binance (Bitcoin — gratuit, sans clé, temps réel) ──────────────────────── + +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; - return d != null ? parseFloat(d.toFixed(2)) : null; -} -function yDeltaPct(q: YQuote | undefined): number | null { - const p = q?.regularMarketChangePercent; - return p != null ? parseFloat(p.toFixed(2)) : null; + +// ── CoinGecko (Bitcoin — fallback si Binance échoue) ───────────────────────── + +async function coingeckoBTC(): Promise<{ value: number | null; change24h: number | null }> { + try { + const res = await fetch( + "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) ────────────────────────────────── @@ -54,61 +94,28 @@ async function fredObs(series: string, apiKey: string): Promise { } 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 ─────────────────────────────────────────────────────────────────────── export async function GET() { const fredKey = process.env.FRED_API_KEY; const avKey = process.env.ALPHA_VANTAGE_KEY; 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) - const [quotes, btcCgRes] = await Promise.all([ - yahooQuotes(["^VIX", "^GSPC", "GC=F", "SI=F", "BZ=F", "CL=F", "BTC-USD"]), - coingeckoBTC(), // fallback si Yahoo échoue pour BTC - ]); + // 1. Indices + commodités — Alpha Vantage GLOBAL_QUOTE (cache 24h mémoire) + // Appels séquentiels → respect limite 5 req/min AV + const vixQ = await avQuote("^VIX", avKey); + 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"]; - const sp500 = quotes["^GSPC"]; - const gold = quotes["GC=F"]; - const silver = quotes["SI=F"]; - const brent = quotes["BZ=F"]; - const wti = quotes["CL=F"]; - const btcYQ = quotes["BTC-USD"]; // Yahoo primary pour BTC + // 2. Bitcoin — Binance (temps réel, sans clé), fallback CoinGecko + const btcBin = await binanceBTC(); + const btcCg = btcBin.value === null ? await coingeckoBTC() : { value: null, change24h: null }; - // Fallback S&P via AV si Yahoo a échoué (rare) - const sp500Fallback = !sp500 && avKey ? await avGlobalQuote("SPY", avKey) : null; - - // 2. FRED — spreads crédit + taux (24h cache, données macro) + // 3. FRED — spreads crédit + taux directeurs (cache 24h) const [hyRaw, igRaw, us10y, us2y] = await Promise.all([ fredObs("BAMLH0A0HYM2", fredKey), fredObs("BAMLC0A0CM", fredKey), @@ -118,29 +125,29 @@ export async function GET() { return NextResponse.json({ // Sentiment / Risk-On - vix: yVal(vix), - vixDelta: yDelta(vix), - sp500: yVal(sp500) ?? sp500Fallback?.value, - sp500Change: yDelta(sp500), - sp500ChangePct: yDeltaPct(sp500) ?? sp500Fallback?.changePct, - btc: yVal(btcYQ) ?? btcCgRes.value, - btcChange24h: yDeltaPct(btcYQ) ?? btcCgRes.change24h, - // Crédit (FRED, fraction × 100 = bps) + vix: vixQ.value, + vixDelta: vixQ.delta, + sp500: sp500Q.value, + sp500Change: sp500Q.delta, + sp500ChangePct: sp500Q.deltaPct, + btc: btcBin.value ?? btcCg.value, + btcChange24h: btcBin.change24h ?? btcCg.change24h, + // Crédit (FRED, % × 100 = bps) hySpread: hyRaw != null ? Math.round(hyRaw * 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, us2y, 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) - gold: yVal(gold), - goldDelta: yDelta(gold), - silver: yVal(silver), - silverDelta: yDelta(silver), - brent: yVal(brent), - brentDelta: yDelta(brent), - wti: yVal(wti), - wtiDelta: yDelta(wti), + // Commodités — delta = variation vs clôture veille + gold: goldQ.value, + goldDelta: goldQ.delta, + silver: silverQ.value, + silverDelta: silverQ.delta, + brent: brentQ.value, + brentDelta: brentQ.delta, + wti: wtiQ.value, + wtiDelta: wtiQ.delta, // Compat copper: null, timestamp: Date.now(), diff --git a/app/api/fx/route.ts b/app/api/fx/route.ts index 20b9148..c44b7d1 100644 --- a/app/api/fx/route.ts +++ b/app/api/fx/route.ts @@ -42,8 +42,17 @@ async function fetchFrankfurter() { try { const res = await fetch("https://api.frankfurter.app/latest?from=USD", { next: { revalidate: 86400 } }); if (!res.ok) throw new Error(`Frankfurter ${res.status}`); - const data = await res.json(); - return NextResponse.json({ rates: data.rates, dxy: null, base: "USD", source: "frankfurter", date: data.date }); + const data = await res.json(); + const rates = data.rates as Record; + + // 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) { return NextResponse.json({ error: String(err) }, { status: 502 }); }