From ecb815619baf5a165cb89bdb256624c09cd94594 Mon Sep 17 00:00:00 2001 From: Capucine Gest Date: Thu, 28 May 2026 22:52:49 +0200 Subject: [PATCH] feat: WTI/Brent via Stooq, PMI scraping (TE), Groq replace Bytez MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - drivers: switch Brent (cb.f) and WTI (cl.f) from stale FRED to Stooq near-real-time futures — same stooqMetal() already used for gold/silver - macro: add scrapePMI() to scrape Trading Economics meta description for manufacturing-pmi and services-pmi for all 8 currencies (no API key needed) - narrative: replace dead Bytez API with Groq (OpenAI-compatible, free tier) env var renamed BYTEZ_API_KEY → GROQ_API_KEY, model llama-3.1-8b-instant - NarrativeButton: rebrand "Erreur Bytez" / "Bytez AI" → "Erreur IA" / "Groq AI" Co-Authored-By: Claude Sonnet 4.6 --- app/api/drivers/route.ts | 16 ++++--- app/api/macro/route.ts | 79 +++++++++++++++++++++++++++++++++- app/api/narrative/route.ts | 18 ++++---- components/NarrativeButton.tsx | 6 +-- 4 files changed, 99 insertions(+), 20 deletions(-) diff --git a/app/api/drivers/route.ts b/app/api/drivers/route.ts index 00dfe59..fe7eab3 100644 --- a/app/api/drivers/route.ts +++ b/app/api/drivers/route.ts @@ -105,12 +105,16 @@ export async function GET() { // 1. Marchés indices — FRED (données fin de journée, cache 24h) // VIXCLS = VIX clôture CBOE | SP500 = S&P 500 - // DCOILBRENTEU = Brent | DCOILWTICO = WTI - const [vixQ, sp500Q, brentQ, wtiQ] = await Promise.all([ - fredObsDelta("VIXCLS", fredKey), - fredObsDelta("SP500", fredKey), - fredObsDelta("DCOILBRENTEU", fredKey), - fredObsDelta("DCOILWTICO", fredKey), + const [vixQ, sp500Q] = await Promise.all([ + fredObsDelta("VIXCLS", fredKey), + fredObsDelta("SP500", fredKey), + ]); + + // Pétrole — Stooq futures (quasi temps réel, cache 5 min, sans clé API) + // cl.f = WTI NYMEX | cb.f = Brent ICE → delta = variation intraday vs ouverture + const [brentQ, wtiQ] = await Promise.all([ + stooqMetal("cb.f"), + stooqMetal("cl.f"), ]); // 2. Métaux précieux — Stooq (quasi temps réel, cache 5 min, sans clé API) diff --git a/app/api/macro/route.ts b/app/api/macro/route.ts index 0a61b43..79fb00a 100644 --- a/app/api/macro/route.ts +++ b/app/api/macro/route.ts @@ -79,6 +79,63 @@ async function boeRate() { } catch { return []; } } +// ── Trading Economics PMI scraping ─────────────────────────────────────────── +// URL pattern: https://tradingeconomics.com/{country}/{indicator} +// Source : balise dans le HTML +// Ex: "Manufacturing PMI in the United States increased to 55.30 points in May +// from 54.50 points in April of 2026" + +const TE_COUNTRY: Record = { + USD: "united-states", + EUR: "euro-area", + GBP: "united-kingdom", + JPY: "japan", + CHF: "switzerland", + CAD: "canada", + AUD: "australia", + NZD: "new-zealand", +}; + +async function scrapePMI( + currency: string, + indicator: "manufacturing-pmi" | "services-pmi" +): Promise<{ value: number | null; prev: number | null }> { + const country = TE_COUNTRY[currency]; + if (!country) return { value: null, prev: null }; + try { + const url = `https://tradingeconomics.com/${country}/${indicator}`; + const res = await fetch(url, { + next: { revalidate: 3600 }, // cache 1h — données PMI mensuelles + headers: { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36", + "Accept-Language": "en-US,en;q=0.9", + }, + }); + if (!res.ok) return { value: null, prev: null }; + const html = await res.text(); + // Cherche la balise meta description + const metaMatch = html.match(/= 1) { + return { + value: parseFloat(nums[0]), + prev: nums[1] ? parseFloat(nums[1]) : null, + }; + } + return { value: null, prev: null }; + } + return { value: parseFloat(m[1]), prev: parseFloat(m[2]) }; + } catch { return { value: null, prev: null }; } +} + // ── Shared helpers ──────────────────────────────────────────────────────────── type Obs = { date: string; value: number }; @@ -187,8 +244,26 @@ export async function GET(req: NextRequest) { for (const field of Object.keys(fieldMap)) { if (!(field in indicators)) indicators[field] = null; } - indicators.pmiMfg = null; - indicators.pmiServices = null; + // ── PMI scraping (Trading Economics) ────────────────────────────────────── + const [pmiMfgRaw, pmiSvcRaw] = await Promise.all([ + scrapePMI(currency, "manufacturing-pmi"), + scrapePMI(currency, "services-pmi"), + ]); + + // Convertit le résultat { value, prev } en format Indicator (surprise = diff) + const toPmiIndicator = (raw: { value: number | null; prev: number | null }) => { + if (raw.value === null) return null; + const surprise = raw.prev !== null ? parseFloat((raw.value - raw.prev).toFixed(2)) : null; + return { + value: raw.value, + prev: raw.prev, + surprise, + trend: surprise !== null ? (surprise > 0 ? "up" : surprise < 0 ? "down" : "flat") as "up"|"down"|"flat" : null, + lastUpdated: null, + }; + }; + indicators.pmiMfg = toPmiIndicator(pmiMfgRaw); + indicators.pmiServices = toPmiIndicator(pmiSvcRaw); const data = { currency, indicators, fetchedAt: new Date().toISOString() }; _cache.set(currency, { data, ts: Date.now() }); diff --git a/app/api/narrative/route.ts b/app/api/narrative/route.ts index 1004235..f703d9f 100644 --- a/app/api/narrative/route.ts +++ b/app/api/narrative/route.ts @@ -1,10 +1,10 @@ import { NextRequest, NextResponse } from "next/server"; import OpenAI from "openai"; -// Bytez uses an OpenAI-compatible API -const bytez = new OpenAI({ - apiKey: process.env.BYTEZ_API_KEY ?? "", - baseURL: "https://api.bytez.com/models/openai", +// Groq — API OpenAI-compatible, niveau gratuit, modèles Llama +const groq = new OpenAI({ + apiKey: process.env.GROQ_API_KEY ?? "", + baseURL: "https://api.groq.com/openai/v1", }); const SYSTEM_PROMPT = `Tu es un analyste macro Forex senior. Tu analyses les données macroéconomiques de 8 devises majeures (USD, EUR, GBP, JPY, CHF, CAD, AUD, NZD) et tu fournis des synthèses concises et actionnables pour un trader particulier. @@ -19,9 +19,9 @@ Tes analyses sont : Format de réponse : texte court, 80-120 mots maximum par devise.`; export async function POST(req: NextRequest) { - const apiKey = process.env.BYTEZ_API_KEY; + const apiKey = process.env.GROQ_API_KEY; if (!apiKey) { - return NextResponse.json({ error: "BYTEZ_API_KEY not configured" }, { status: 503 }); + return NextResponse.json({ error: "GROQ_API_KEY not configured" }, { status: 503 }); } let body: { @@ -81,8 +81,8 @@ Résumé en 3 points : situation actuelle, signal directionnel, risque principal } try { - const completion = await bytez.chat.completions.create({ - model: "meta-llama/Llama-3.1-8B-Instruct", + const completion = await groq.chat.completions.create({ + model: "llama-3.1-8b-instant", messages: [ { role: "system", content: SYSTEM_PROMPT }, { role: "user", content: userMessage }, @@ -95,6 +95,6 @@ Résumé en 3 points : situation actuelle, signal directionnel, risque principal return NextResponse.json({ analysis: text, model: completion.model, mode }); } catch (err: unknown) { const message = err instanceof Error ? err.message : String(err); - return NextResponse.json({ error: `Bytez error: ${message}` }, { status: 502 }); + return NextResponse.json({ error: `Groq error: ${message}` }, { status: 502 }); } } diff --git a/components/NarrativeButton.tsx b/components/NarrativeButton.tsx index b1c2418..111da30 100644 --- a/components/NarrativeButton.tsx +++ b/components/NarrativeButton.tsx @@ -53,7 +53,7 @@ export default function NarrativeButton({ currency, indicators, macroScore }: Pr {error && ( - Erreur Bytez + Erreur IA )} @@ -66,7 +66,7 @@ export default function NarrativeButton({ currency, indicators, macroScore }: Pr
- Analyse {currency} — Bytez AI + Analyse {currency} — Groq AI

{analysis}

- Powered by Bytez · Llama 3.1 8B + Powered by Groq · Llama 3.1 8B