Files
forex-dashboard/app/api/narrative/route.ts
T
Capucine Gest a3d847ce59 feat: calendar prev week, fullscreen mode, AI weekly highlights
Calendar — "Semaine dernière" tab:
- API now fetches from last Monday (instead of today) and tags events as "prev"
- New tab in CalendarTab with last week's published events (actual values visible)
- fromDate filter bypassed for prev tab; week banner styled in slate

Dashboard — fullscreen:
- Maximize2/Minimize2 button in header using Fullscreen API
- Listens to fullscreenchange to sync icon state

Report — AI "Faits marquants":
- New report_highlights mode in /api/narrative: reads published events from
  prev+current week + drivers, generates 3 structured highlights via Groq
- AiHighlightsButton next to "Faits marquants de la semaine" label
- Parses AI response (-- delimited) into title+body Theme objects

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-08 20:39:52 +02:00

158 lines
6.3 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import OpenAI from "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.
Tes analyses sont :
- Directes et factuelles — pas de conditionnel excessif
- Structurées en 3-4 points maximum
- Focalisées sur les divergences et signaux de trading
- En français
- Sans disclaimers légaux
Format de réponse : texte court, 80-120 mots maximum par devise.`;
export async function POST(req: NextRequest) {
const apiKey = process.env.GROQ_API_KEY;
if (!apiKey) {
return NextResponse.json({ error: "GROQ_API_KEY manquant — ajouter dans Vercel → Settings → Environment Variables" }, { status: 503 });
}
let body: {
mode: "cb_analysis" | "expert_opinion" | "summary" | "divergence" | "report_ccy" | "report_highlights";
currency?: string;
data?: unknown;
userInput?: string;
};
try {
body = await req.json();
} catch {
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
}
const { mode, currency, data, userInput } = body;
let userMessage = "";
switch (mode) {
case "cb_analysis":
userMessage = `Analyse le communiqué de la banque centrale pour ${currency}.
Données contextuelles : ${JSON.stringify(data, null, 2)}
Fournis une analyse en 4 points :
1. Changement de ton (hawkish/dovish/neutre)
2. Évolution des projections de taux
3. Phrases clés ajoutées ou supprimées vs précédent
4. Impact suggéré sur ${currency} (+1 haussier / 0 neutre / -1 baissier)`;
break;
case "expert_opinion":
userMessage = `Point de Vérité — Confrontation IA :
Avis expert injecté : "${userInput}"
Données actuelles du dashboard pour ${currency} : ${JSON.stringify(data, null, 2)}
Analyse :
- Convergences entre l'avis expert et les données quantitatives
- Divergences et points de tension
- Score avant/après ajustement suggéré
- Validation ou rejet de l'avis par devise`;
break;
case "divergence":
userMessage = `Analyse les divergences de positionnement détectées pour ${currency} :
${JSON.stringify(data, null, 2)}
Explique en 3 phrases : pourquoi cette configuration est significative et quelle action de trading elle suggère.`;
break;
case "report_ccy": {
const d = data as {
weeklyPct?: string; weekFrom?: string; weekTo?: string;
cotNetTff?: number; cotDeltaTff?: number; cotLongPctTff?: number;
cotNetLegacy?: number; cotDeltaLegacy?: number;
yieldCurrent?: number; yieldDelta?: number;
hint?: string;
};
const weekRange = d.weekFrom && d.weekTo ? `${d.weekFrom} au ${d.weekTo}` : "de la semaine";
userMessage = `Rédige le paragraphe d'analyse hebdomadaire pour ${currency} (semaine du ${weekRange}).
Données disponibles :
- Performance hebdomadaire vs USD : ${d.weeklyPct ?? "N/D"}
- COT Hedge Funds (TFF) : net ${d.cotNetTff ?? "N/D"} contrats, variation semaine : ${d.cotDeltaTff != null ? (d.cotDeltaTff > 0 ? "+" : "") + d.cotDeltaTff : "N/D"}, % long : ${d.cotLongPctTff ?? "N/D"}%
- COT Non-Commercial (Legacy) : net ${d.cotNetLegacy ?? "N/D"} contrats, variation : ${d.cotDeltaLegacy != null ? (d.cotDeltaLegacy > 0 ? "+" : "") + d.cotDeltaLegacy : "N/D"}
- Rendement obligataire : ${d.yieldCurrent != null ? d.yieldCurrent + "%" : "N/D"}${d.yieldDelta != null ? (d.yieldDelta > 0 ? "+" : "") + d.yieldDelta + "%" : "N/D"})
${d.hint ? `- Contexte additionnel : ${d.hint}` : ""}
Rédige un paragraphe fluide de 80 à 120 mots, style analyste macro professionnel. Structure : (1) performance et catalyseurs de la semaine, (2) lecture institutionnelle (COT), (3) niveau ou seuil clé à surveiller. Pas de bullet points — texte continu.`;
break;
}
case "report_highlights": {
const d = data as {
events?: Array<{ title: string; currency: string; actual?: string | null; forecast?: string | null; previous?: string | null; impact: string }>;
drivers?: { vix?: number | null; brent?: number | null; us10y?: number | null };
weekFrom?: string; weekTo?: string;
};
const published = (d.events ?? [])
.filter(e => e.actual && e.impact !== "low")
.slice(0, 12)
.map(e => `${e.currency} · ${e.title} : actuel=${e.actual ?? "—"} / prévu=${e.forecast ?? "—"} / précédent=${e.previous ?? "—"}`)
.join("\n");
const weekRange = d.weekFrom && d.weekTo ? `${d.weekFrom} au ${d.weekTo}` : "de la semaine";
userMessage = `Génère 3 faits marquants de la semaine du ${weekRange} pour un trader Forex G10.
Données publiées cette semaine :
${published || "Aucune publication disponible"}
Contexte marché : VIX=${d.drivers?.vix?.toFixed(1) ?? "N/D"}, Brent=$${d.drivers?.brent?.toFixed(1) ?? "N/D"}, US 10Y=${d.drivers?.us10y?.toFixed(2) ?? "N/D"}%
Pour chaque fait marquant, fournis :
- TITRE (6 mots max, en majuscules)
- DESCRIPTION (30-40 mots) : chiffre clé, surprise vs consensus, impact sur les devises concernées
Formate ta réponse EXACTEMENT ainsi (3 blocs, séparés par --) :
TITRE1
Description1
--
TITRE2
Description2
--
TITRE3
Description3`;
break;
}
case "summary":
default:
userMessage = `Génère une synthèse macro hebdomadaire pour ${currency} basée sur ces données :
${JSON.stringify(data, null, 2)}
Résumé en 3 points : situation actuelle, signal directionnel, risque principal.`;
}
try {
const completion = await groq.chat.completions.create({
model: "llama-3.1-8b-instant",
messages: [
{ role: "system", content: SYSTEM_PROMPT },
{ role: "user", content: userMessage },
],
max_tokens: mode === "report_highlights" ? 500 : 300,
temperature: 0.3,
});
const text = completion.choices[0]?.message?.content ?? "";
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: `Groq error: ${message}` }, { status: 502 });
}
}