2026-06-03 00:59:04 +02:00
// lib/newsfeed.ts
// Agrège les news forex/macro depuis plusieurs sources publiques :
// 1. investinglive.com/forex/ — articles forex de Giuseppe Dellamotta
// 2. Reuters RSS — flux marchés publics
// 3. Bloomberg meta tags — best-effort (titres publics malgré paywall)
// Applique un moteur de pertinence devise pour chaque article.
import type { Currency } from "./types" ;
// ── Types ─────────────────────────────────────────────────────────────────────
export interface NewsImpact {
ccy : Currency ;
direction : "bullish" | "bearish" | "neutral" ;
reason : string ;
}
export interface NewsItem {
id : string ;
title : string ;
url : string ;
source : string ;
publishedAt : string ; // ISO date string
summary? : string ;
impacts : NewsImpact [];
categories : string []; // "Énergie", "Banque Centrale", "Géopolitique", etc.
}
// ── Moteur de pertinence devise ───────────────────────────────────────────────
interface ImpactRule {
pattern : RegExp ;
impacts : { ccy : Currency ; dir : "bullish" | "bearish" | "neutral" }[];
categories : string [];
reason : string ;
}
2026-06-03 01:04:07 +02:00
// ── Noms des gouverneurs de banques centrales ─────────────────────────────────
// Mis à jour juin 2026
const CB_GOVERNORS : Record < Currency , string [] > = {
USD : [ "Powell" , "Jerome Powell" , "FOMC chair" ],
EUR : [ "Lagarde" , "Christine Lagarde" , "ECB president" , "ECB chief" ],
GBP : [ "Bailey" , "Andrew Bailey" , "BoE governor" ],
JPY : [ "Ueda" , "Kazuo Ueda" , "BoJ governor" , "Bank of Japan governor" ],
CHF : [ "Schlegel" , "Martin Schlegel" , "SNB chairman" , "SNB chair" ],
CAD : [ "Macklem" , "Tiff Macklem" , "BoC governor" ],
AUD : [ "Bullock" , "Michele Bullock" , "RBA governor" ],
NZD : [ "Orr" , "Adrian Orr" , "RBNZ governor" ],
};
// ── Noms des chefs d'État pertinents (sauf EUR : uniquement Lagarde) ──────────
const HEADS_OF_STATE : Record < string , { ccy : Currency ; names : string [] }> = {
USD : { ccy : "USD" , names : [ "Trump" , "Biden" , "US president" , "White House" , "US treasury" , "Bessent" , "Yellen" , "Scott Bessent" ] },
GBP : { ccy : "GBP" , names : [ "Starmer" , "Keir Starmer" , "UK prime minister" , "UK chancellor" , "Rachel Reeves" ] },
JPY : { ccy : "JPY" , names : [ "Ishiba" , "Shigeru Ishiba" , "Japan prime minister" , "Japan PM" , "Japanese PM" , "Akazawa" ] },
CAD : { ccy : "CAD" , names : [ "Carney" , "Mark Carney" , "Canada prime minister" , "Canadian PM" ] },
AUD : { ccy : "AUD" , names : [ "Albanese" , "Anthony Albanese" , "Australia prime minister" , "Australian PM" ] },
NZD : { ccy : "NZD" , names : [ "Luxon" , "Christopher Luxon" , "New Zealand PM" , "NZ prime minister" ] },
CHF : { ccy : "CHF" , names : [ "Swiss Federal Council" , "Swiss government" , "Swiss president" ] },
};
// Génère les règles pour les gouverneurs et chefs d'État
function buildPersonRules () : ImpactRule [] {
const rules : ImpactRule [] = [];
// Gouverneurs CB
for ( const [ ccyStr , names ] of Object . entries ( CB_GOVERNORS )) {
const ccy = ccyStr as Currency ;
const pattern = new RegExp ( `\\b( ${ names . map ( n => n . replace ( /[.*+?^${}()|[\]\\]/g , "\\$&" )). join ( "|" ) } )\\b` , "i" );
rules . push ({
pattern ,
impacts : [{ ccy , dir : "neutral" }],
categories : [ "Discours BC" ],
reason : `Déclaration gouverneur ${ ccy } ` ,
});
}
// Chefs d'État
for ( const { ccy , names } of Object . values ( HEADS_OF_STATE )) {
const pattern = new RegExp ( `\\b( ${ names . map ( n => n . replace ( /[.*+?^${}()|[\]\\]/g , "\\$&" )). join ( "|" ) } )\\b` , "i" );
rules . push ({
pattern ,
impacts : [{ ccy , dir : "neutral" }],
categories : [ "Chef d'État" ],
reason : `Déclaration politique officielle ${ ccy } ` ,
});
}
return rules ;
}
2026-06-03 00:59:04 +02:00
const IMPACT_RULES : ImpactRule [] = [
2026-06-03 01:04:07 +02:00
// ── Décisions de taux (priorité maximale) ─────────────────────────────────
{
pattern : /\b(rate (decision|cut|hike|hold|change|increase|decrease)|interest rate (cut|hike|hold|decision|change)|rate (unchanged|on hold)|basis point(s)?|bps (cut|hike))\b/i ,
impacts : [],
categories : [ "Décision Taux" ],
reason : "Décision de taux directeur" ,
},
// ── Presse/Conférences CB ─────────────────────────────────────────────────
{
pattern : /\b(press conference|monetary policy (statement|meeting|decision|minutes)|forward guidance|dot plot|summary of projections|soe|statement on (monetary|rates))\b/i ,
impacts : [],
categories : [ "Discours BC" ],
reason : "Communication banque centrale officielle" ,
},
// ── Données clés emploi ────────────────────────────────────────────────────
{
pattern : /\b(unemployment (rate|data|report|figures)|non.?farm payrolls|nfp|jobs (report|data|figures)|jobless (rate|claims)|employment (change|data|report|figures)|labor market|labour market|payroll(s)?)\b/i ,
impacts : [],
categories : [ "Données Clés" , "Emploi" ],
reason : "Publication données emploi" ,
},
// ── Données clés inflation ─────────────────────────────────────────────────
{
pattern : /\b(cpi (data|release|report|print|reading|figure)|inflation (data|report|release|print|reading|figure)|consumer price index (release|data|report)|pce deflator)\b/i ,
impacts : [],
categories : [ "Données Clés" , "Inflation" ],
reason : "Publication données inflation" ,
},
// ── Crises financières ─────────────────────────────────────────────────────
{
pattern : /\b(banking (crisis|collapse|failure|run|contagion)|financial (crisis|contagion|meltdown|turmoil)|bank (collapse|bail.?out|default|insolvency)|systemic (risk|crisis)|credit (crisis|crunch|event))\b/i ,
impacts : [
{ ccy : "JPY" , dir : "bullish" },
{ ccy : "CHF" , dir : "bullish" },
{ ccy : "USD" , dir : "bullish" },
{ ccy : "AUD" , dir : "bearish" },
{ ccy : "NZD" , dir : "bearish" },
],
categories : [ "Crise" , "Risk-Off" ],
reason : "Crise financière → refuges (JPY/CHF/USD) haussiers" ,
},
// ── Guerre Ukraine ─────────────────────────────────────────────────────────
{
pattern : /\b(ukraine|zelenskyy|zelensky|kyiv|russia (attack|invade|shelling|drone|missile)|nato (ukraine|summit|support)|war in europe|eastern europe (conflict|war|tension))\b/i ,
impacts : [
{ ccy : "EUR" , dir : "bearish" },
{ ccy : "JPY" , dir : "bullish" },
{ ccy : "CHF" , dir : "bullish" },
{ ccy : "USD" , dir : "bullish" },
{ ccy : "AUD" , dir : "bullish" }, // commodity/wheat exporter
],
categories : [ "Géopolitique" , "Guerre" ],
reason : "Guerre Ukraine → EUR baissier (proximité), refuges (JPY/CHF/USD) haussiers, AUD haussier (blé/gaz)" ,
},
// ── Détroit d'Hormuz / Moyen-Orient ───────────────────────────────────────
{
pattern : /\b(strait of hormuz|hormuz|houthi(s)?|red sea (attack|blockade|shipping)|iran (attack|sanction|nuclear|strait)|middle east (conflict|escalat|war|tension)|israel.?(hamas|hezbollah|iran)|persian gulf (tension|conflict))\b/i ,
impacts : [
{ ccy : "JPY" , dir : "bearish" }, // Japon importe ~90% énergie via Hormuz
{ ccy : "CAD" , dir : "bullish" }, // Pétrole monte
{ ccy : "CHF" , dir : "bullish" },
{ ccy : "USD" , dir : "bullish" },
],
categories : [ "Géopolitique" , "Énergie" , "Guerre" ],
reason : "Tensions Hormuz → pétrole hausse (CAD bullish), JPY baissier (90% énergie importée via Hormuz)" ,
},
// ── Tensions Taiwan / Asie du Pacifique ───────────────────────────────────
{
pattern : /\b(taiwan (strait|tension|conflict|independence|invasion|crisis|blockade)|china (taiwan|military|invasion)|south china sea (tension|dispute|conflict))\b/i ,
impacts : [
{ ccy : "AUD" , dir : "bearish" },
{ ccy : "NZD" , dir : "bearish" },
{ ccy : "JPY" , dir : "bullish" },
{ ccy : "USD" , dir : "bullish" },
],
categories : [ "Géopolitique" , "Guerre" ],
reason : "Tensions Taiwan → AUD/NZD baissiers (exposition Chine), JPY/USD haussiers (refuges/proxémité)" ,
},
// ── Russie / Sanctions ─────────────────────────────────────────────────────
{
pattern : /\b(russia (sanction|oil cap|energy|export ban|gas supply|lng)|(new )?(sanction|embargo) (on|against) russia|russian (oil|gas|energy|export))\b/i ,
impacts : [
{ ccy : "EUR" , dir : "bearish" },
{ ccy : "CAD" , dir : "bullish" },
{ ccy : "AUD" , dir : "bullish" },
],
categories : [ "Géopolitique" , "Énergie" ],
reason : "Sanctions Russie → EUR baissier (dépendance gaz), CAD/AUD haussiers (exportateurs énergie alternatifs)" ,
},
// ── Press release probabilité de taux ─────────────────────────────────────
{
pattern : /\b(rate (cut|hike) (probability|odds|chance(s)?|expectation(s)?|pricing)|market (prices? in|expects?|anticipates?) (cut|hike|pause|hold)|(ois|swap|futures?) (price(s)?|imply|signal) (cut|hike|easing|tightening)|probability of (rate|cut|hike) (change|move|rise|fall))\b/i ,
impacts : [],
categories : [ "Probabilités Taux" ],
reason : "Repricing des probabilités de taux OIS/futures" ,
},
2026-06-03 00:59:04 +02:00
// ── Fed / USD ──────────────────────────────────────────────────────────────
{
pattern : /\b(hawkish fed|fed hike|fed rate (up|rise)|us inflation (surge|higher|above)|fomc (hike|tighten))\b/i ,
impacts : [{ ccy : "USD" , dir : "bullish" }],
categories : [ "Banque Centrale" , "USD" ],
reason : "Fed hawkish → USD haussier" ,
},
{
pattern : /\b(fed (cut|ease|pivot|pause)|dovish fed|us inflation (cool|drop|lower|ease)|fomc (cut|ease|pause))\b/i ,
impacts : [{ ccy : "USD" , dir : "bearish" }],
categories : [ "Banque Centrale" , "USD" ],
reason : "Fed dovish → USD baissier" ,
},
{
pattern : /\b(fed|fomc|federal reserve|powell|us monetary policy|us interest rate)\b/i ,
impacts : [{ ccy : "USD" , dir : "neutral" }],
categories : [ "Banque Centrale" , "USD" ],
reason : "Politique monétaire Fed" ,
},
// ── ECB / EUR ──────────────────────────────────────────────────────────────
{
pattern : /\b(ecb (cut|ease|lower rate|dovish)|lagarde (cut|dovish|ease)|eurozone (recession|weak|slow|deflat))\b/i ,
impacts : [{ ccy : "EUR" , dir : "bearish" }],
categories : [ "Banque Centrale" , "EUR" ],
reason : "BCE dovish → EUR baissier" ,
},
{
pattern : /\b(ecb (hike|hawkish|hold|tighten)|lagarde (hawkish|hike)|eurozone inflation (higher|above|surge))\b/i ,
impacts : [{ ccy : "EUR" , dir : "bullish" }],
categories : [ "Banque Centrale" , "EUR" ],
reason : "BCE hawkish → EUR haussier" ,
},
{
pattern : /\b(ecb|european central bank|lagarde|eurozone (gdp|cpi|inflation|rate|pmi))\b/i ,
impacts : [{ ccy : "EUR" , dir : "neutral" }],
categories : [ "Banque Centrale" , "EUR" ],
reason : "Politique monétaire BCE" ,
},
// ── BoE / GBP ──────────────────────────────────────────────────────────────
{
pattern : /\b(boe (cut|ease|dovish)|bank of england (cut|ease|pause)|uk inflation (drop|lower|cool))\b/i ,
impacts : [{ ccy : "GBP" , dir : "bearish" }],
categories : [ "Banque Centrale" , "GBP" ],
reason : "BoE dovish → GBP baissier" ,
},
{
pattern : /\b(boe (hike|hawkish)|bank of england (hike|hawkish)|uk inflation (surge|higher|above|sticky))\b/i ,
impacts : [{ ccy : "GBP" , dir : "bullish" }],
categories : [ "Banque Centrale" , "GBP" ],
reason : "BoE hawkish → GBP haussier" ,
},
{
pattern : /\b(bank of england|boe|sterling|uk (gdp|cpi|inflation|jobs|pmi|rate))\b/i ,
impacts : [{ ccy : "GBP" , dir : "neutral" }],
categories : [ "Banque Centrale" , "GBP" ],
reason : "Politique monétaire BoE" ,
},
// ── BoJ / JPY ──────────────────────────────────────────────────────────────
{
pattern : /\b(boj (hike|tighten|hawkish)|bank of japan (hike|raise|hawkish)|yen (strengthens?|stronger|surge)|japan (rate hike|wage))\b/i ,
impacts : [{ ccy : "JPY" , dir : "bullish" }],
categories : [ "Banque Centrale" , "JPY" ],
reason : "BoJ hawkish → JPY haussier" ,
},
{
pattern : /\b(boj (cut|ease|hold|pause|dovish)|yen (weak|falls?|slide)|japan (rate hold|deflat))\b/i ,
impacts : [{ ccy : "JPY" , dir : "bearish" }],
categories : [ "Banque Centrale" , "JPY" ],
reason : "BoJ dovish → JPY baissier" ,
},
{
pattern : /\b(yen intervention|mof japan|ministry of finance japan|japan (intervene|verbal intervention))\b/i ,
impacts : [{ ccy : "JPY" , dir : "bullish" }],
categories : [ "Géopolitique" , "JPY" ],
reason : "Intervention MoF → JPY haussier (défense du plancher)" ,
},
{
pattern : /\b(bank of japan|boj|yen|japan (gdp|cpi|inflation|monetary|pmi))\b/i ,
impacts : [{ ccy : "JPY" , dir : "neutral" }],
categories : [ "Banque Centrale" , "JPY" ],
reason : "Politique monétaire BoJ" ,
},
// ── SNB / CHF ──────────────────────────────────────────────────────────────
{
pattern : /\b(snb|swiss national bank|swiss franc|switzerland (rate|inflation|gdp))\b/i ,
impacts : [{ ccy : "CHF" , dir : "neutral" }],
categories : [ "Banque Centrale" , "CHF" ],
reason : "Politique monétaire SNB" ,
},
// ── BoC / CAD ──────────────────────────────────────────────────────────────
{
pattern : /\b(bank of canada|boc|canada (rate|gdp|inflation|jobs)|loonie)\b/i ,
impacts : [{ ccy : "CAD" , dir : "neutral" }],
categories : [ "Banque Centrale" , "CAD" ],
reason : "Politique monétaire BoC" ,
},
// ── RBA / AUD ──────────────────────────────────────────────────────────────
{
pattern : /\b(rba|reserve bank of australia|australia (rate|gdp|inflation|jobs)|aussie)\b/i ,
impacts : [{ ccy : "AUD" , dir : "neutral" }],
categories : [ "Banque Centrale" , "AUD" ],
reason : "Politique monétaire RBA" ,
},
// ── RBNZ / NZD ────────────────────────────────────────────────────────────
{
pattern : /\b(rbnz|reserve bank of new zealand|new zealand (rate|gdp|inflation)|kiwi)\b/i ,
impacts : [{ ccy : "NZD" , dir : "neutral" }],
categories : [ "Banque Centrale" , "NZD" ],
reason : "Politique monétaire RBNZ" ,
},
// ── Pétrole → CAD haussier / JPY NZD baissiers ────────────────────────────
{
pattern : /\b(oil (price(s)?|surge|rise|higher|rally|soar|spike|record)|crude (oil )?(up|higher|rally|surge)|opec (cut|supply cut|restrict)|energy (price(s)? )?(surge|spike|higher))\b/i ,
impacts : [
{ ccy : "CAD" , dir : "bullish" },
{ ccy : "JPY" , dir : "bearish" },
{ ccy : "NZD" , dir : "bearish" },
],
categories : [ "Énergie" , "Commodités" ],
reason : "Pétrole en hausse → CAD haussier (exportateur), JPY/NZD baissiers (importateurs ~90% énergie)" ,
},
{
pattern : /\b(oil (price(s)?|fall|drop|plunge|lower|crash|collapse|weak)|crude (oil )?(down|lower|fall|plunge)|opec (output|increase|supply up)|oil demand (weak|drop|slow))\b/i ,
impacts : [
{ ccy : "CAD" , dir : "bearish" },
{ ccy : "JPY" , dir : "bullish" },
{ ccy : "NZD" , dir : "bullish" },
],
categories : [ "Énergie" , "Commodités" ],
reason : "Pétrole en baisse → CAD baissier (exportateur), JPY/NZD haussiers (importateurs bénéficient)" ,
},
// ── Or → AUD haussier / CHF ────────────────────────────────────────────────
{
pattern : /\b(gold (price(s)?|rise|rally|surge|record|higher|all.time)|gold (up|soar)|precious metal(s)? (up|rally|higher))\b/i ,
impacts : [
{ ccy : "AUD" , dir : "bullish" },
{ ccy : "CHF" , dir : "bullish" },
],
categories : [ "Commodités" , "Or" ],
reason : "Or en hausse → AUD haussier (3ème producteur mondial), CHF haussier (valeur refuge liée à l'or)" ,
},
{
pattern : /\b(gold (fall|drop|plunge|lower|crash)|gold (down|weak)|precious metal(s)? (down|fall|lower))\b/i ,
impacts : [
{ ccy : "AUD" , dir : "bearish" },
],
categories : [ "Commodités" , "Or" ],
reason : "Or en baisse → AUD baissier" ,
},
// ── Minerai de fer / Métaux → AUD ─────────────────────────────────────────
{
pattern : /\b(iron ore (price|rise|higher|surge|rally)|steel demand (up|higher|strong)|base metal(s)? (up|rally|higher)|copper (rise|surge|rally|higher))\b/i ,
impacts : [{ ccy : "AUD" , dir : "bullish" }],
categories : [ "Commodités" , "Métaux" ],
reason : "Minerai de fer / métaux en hausse → AUD haussier (1er exportateur mondial minerai de fer)" ,
},
{
pattern : /\b(iron ore (fall|drop|lower|plunge|weak)|steel demand (weak|drop|slow|fall)|base metal(s)? (down|fall|lower))\b/i ,
impacts : [{ ccy : "AUD" , dir : "bearish" }],
categories : [ "Commodités" , "Métaux" ],
reason : "Minerai de fer en baisse → AUD baissier" ,
},
// ── Chine / Demande asiatique → AUD / NZD ─────────────────────────────────
{
pattern : /\b(china (growth|gdp|stimulus|pmi|recovery|demand|boom|strong)|chinese (economy|manufacturing|demand) (up|strong|better|recover|boom|grow)|china stimulus|china (fiscal|monetary) (stimulus|ease|boost))\b/i ,
impacts : [
{ ccy : "AUD" , dir : "bullish" },
{ ccy : "NZD" , dir : "bullish" },
],
categories : [ "Chine" , "Géopolitique" ],
reason : "Croissance chinoise → AUD/NZD haussiers (principaux exportateurs vers la Chine : fer, LNG, produits laitiers)" ,
},
{
pattern : /\b(china (slowdown|weak|contraction|deflat|property crisis|debt|evergrande|recession)|chinese (economy|demand|pmi) (weak|slow|contract|drop|fall)|china (trade war|tariff))\b/i ,
impacts : [
{ ccy : "AUD" , dir : "bearish" },
{ ccy : "NZD" , dir : "bearish" },
],
categories : [ "Chine" , "Géopolitique" ],
reason : "Ralentissement Chine → AUD/NZD baissiers (demande commodités en baisse)" ,
},
// ── Blé / Agriculture → USD / AUD / CAD ───────────────────────────────────
{
pattern : /\b(wheat (price|supply|shortage|disruption|export block|higher)|grain (crisis|supply|shortage)|ukraine (wheat|grain|export)|food (inflation|crisis|price surge))\b/i ,
impacts : [
{ ccy : "USD" , dir : "bullish" },
{ ccy : "AUD" , dir : "bullish" },
{ ccy : "CAD" , dir : "bullish" },
],
categories : [ "Agriculture" , "Géopolitique" ],
reason : "Disruption blé/céréales → USD/AUD/CAD haussiers (grands exportateurs mondiaux)" ,
},
// ── Produits laitiers → NZD ────────────────────────────────────────────────
{
pattern : /\b(dairy (price(s)?|index|gdt|auction|higher|surge)|fonterra|milk (price|higher)|new zealand export(s)? (up|higher|strong))\b/i ,
impacts : [{ ccy : "NZD" , dir : "bullish" }],
categories : [ "Agriculture" , "NZD" ],
reason : "Produits laitiers en hausse → NZD haussier (principal exportateur mondial lait/beurre)" ,
},
{
pattern : /\b(dairy (price(s)?|index|gdt|auction) (lower|fall|drop|weak)|fonterra (cut|lower|reduce))\b/i ,
impacts : [{ ccy : "NZD" , dir : "bearish" }],
categories : [ "Agriculture" , "NZD" ],
reason : "Produits laitiers en baisse → NZD baissier" ,
},
// ── Risk-Off → JPY / CHF haussiers, AUD / NZD baissiers ───────────────────
{
pattern : /\b(risk[- ]off|safe[- ]haven (demand|flow|bid|surge)|geopolit(ical)? (tension|risk|crisis|escalat)|war|military (conflict|strike|attack|tension)|sanction(s)?|nuclear (threat|risk)|market (crash|sell.off|panic|rout|turmoil)|recession (fear|risk|warning)|stock(s)? (crash|plunge|sell.off))\b/i ,
impacts : [
{ ccy : "JPY" , dir : "bullish" },
{ ccy : "CHF" , dir : "bullish" },
{ ccy : "USD" , dir : "bullish" },
{ ccy : "AUD" , dir : "bearish" },
{ ccy : "NZD" , dir : "bearish" },
],
categories : [ "Risk-Off" , "Géopolitique" ],
reason : "Risk-off → JPY/CHF/USD haussiers (valeurs refuges), AUD/NZD baissiers (devises risquées)" ,
},
{
pattern : /\b(risk[- ]on|risk appetite (return|improve|recover)|market (rally|surge|boom|exuberance)|stock(s)? (rally|surge|soar|record)|optimism (return|grow)|peace (deal|agreement|ceasefire))\b/i ,
impacts : [
{ ccy : "AUD" , dir : "bullish" },
{ ccy : "NZD" , dir : "bullish" },
{ ccy : "JPY" , dir : "bearish" },
{ ccy : "CHF" , dir : "bearish" },
],
categories : [ "Risk-On" ],
reason : "Risk-on → AUD/NZD haussiers, JPY/CHF baissiers (refuges vendus)" ,
},
// ── Tarifs douaniers / Guerre commerciale ──────────────────────────────────
{
pattern : /\b(tariff(s)?|trade war|trade barrier|import duty|import tax|protectionism|trade (sanction|restriction)|export ban)\b/i ,
impacts : [
{ ccy : "CAD" , dir : "bearish" },
{ ccy : "EUR" , dir : "bearish" },
{ ccy : "CNY" , dir : "neutral" } as never ,
]. filter ( i => [ "CAD" , "EUR" ]. includes ( i . ccy )) as { ccy : Currency ; dir : "bullish" | "bearish" | "neutral" }[],
categories : [ "Géopolitique" , "Commerce" ],
reason : "Tarifs douaniers → EUR/CAD baissiers (forte dépendance export vers US)" ,
},
// ── Inflation générale ─────────────────────────────────────────────────────
{
pattern : /\b(global inflation (surge|spike|higher|accelerat|above)|inflation (surprise|beat|above expect))\b/i ,
impacts : [
{ ccy : "USD" , dir : "bullish" },
{ ccy : "GBP" , dir : "bullish" },
],
categories : [ "Données Macro" , "Inflation" ],
reason : "Inflation surprend → pression sur les banques centrales pour maintenir des taux élevés" ,
},
// ── Croissance mondiale ────────────────────────────────────────────────────
{
pattern : /\b(global (recession|slowdown|contraction|downturn)|world (growth|gdp) (slow|drop|contract|negative)|imf (cut|lower|downgrade) (forecast|outlook|gdp))\b/i ,
impacts : [
{ ccy : "AUD" , dir : "bearish" },
{ ccy : "NZD" , dir : "bearish" },
{ ccy : "CAD" , dir : "bearish" },
{ ccy : "JPY" , dir : "bullish" },
{ ccy : "CHF" , dir : "bullish" },
],
categories : [ "Données Macro" , "Géopolitique" ],
reason : "Récession mondiale → refuges (JPY/CHF) haussiers, commodités (AUD/NZD/CAD) baissiers" ,
},
];
// ── Helpers ───────────────────────────────────────────────────────────────────
2026-06-03 01:04:07 +02:00
// Règles dynamiques gouverneurs/chefs d'État (construites une seule fois)
const PERSON_RULES : ImpactRule [] = buildPersonRules ();
const ALL_RULES : ImpactRule [] = [... IMPACT_RULES , ... PERSON_RULES ];
2026-06-03 00:59:04 +02:00
function applyRules ( text : string ) : { impacts : NewsImpact []; categories : string [] } {
2026-06-08 14:33:23 +02:00
return applyRulesPublic ( text );
}
// Export pour les modules externes (financialjuice.ts, etc.)
export function applyRulesPublic ( text : string ) : { impacts : NewsImpact []; categories : string [] } {
2026-06-03 00:59:04 +02:00
const impacts : NewsImpact [] = [];
const categories = new Set < string >();
const seenCcy = new Set < Currency >();
2026-06-03 01:04:07 +02:00
for ( const rule of ALL_RULES ) {
2026-06-03 00:59:04 +02:00
if ( ! rule . pattern . test ( text )) continue ;
for ( const cat of rule . categories ) categories . add ( cat );
for ( const { ccy , dir } of rule . impacts ) {
if ( seenCcy . has ( ccy )) continue ; // première règle matchée par devise gagne
seenCcy . add ( ccy );
impacts . push ({ ccy , direction : dir , reason : rule.reason });
}
if ( seenCcy . size >= 8 ) break ; // toutes les devises couvertes
}
2026-06-03 01:04:07 +02:00
return { impacts , categories : Array.from ( categories ) };
2026-06-03 00:59:04 +02:00
}
function parseDate ( raw : string ) : string {
try {
return new Date ( raw ). toISOString ();
} catch {
return new Date (). toISOString ();
}
}
function extractMeta ( html : string , property : string ) : string {
const m = html . match ( new RegExp ( `<meta[^>]*(?:name|property)=["'] ${ property } ["'][^>]*content=["']([^"']+)["']` , "i" ))
?? html . match ( new RegExp ( `<meta[^>]*content=["']([^"']+)["'][^>]*(?:name|property)=["'] ${ property } ["']` , "i" ));
return m ? .[ 1 ] ?? "" ;
}
const TE_HEADERS = {
"User-Agent" : "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/124.0.0.0 Safari/537.36" ,
"Accept" : "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8" ,
"Accept-Language" : "en-US,en;q=0.9" ,
};
2026-06-08 14:33:23 +02:00
// ── Parseur RSS générique ─────────────────────────────────────────────────────
2026-06-03 00:59:04 +02:00
function parseRssItems ( xml : string , source : string ) : NewsItem [] {
const items : NewsItem [] = [];
2026-06-08 14:33:23 +02:00
// Support RSS <item> et Atom <entry>
const blockPattern = /<(?:item|entry)>([\s\S]*?)<\/(?:item|entry)>/gi ;
let m : RegExpExecArray | null ;
while (( m = blockPattern . exec ( xml )) !== null && items . length < 25 ) {
const block = m [ 1 ];
// Titre : CDATA ou texte brut
const titleM = block . match ( /<title>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/title>/i );
// Lien : <link href="…"/> (Atom) ou <link>…</link> (RSS) ou <guid>
const linkM = block . match ( /<link[^>]*href=["']([^"']+)["']/i )
?? block . match ( /<link[^>]*>([\s\S]*?)<\/link>/i )
?? block . match ( /<guid[^>]*isPermaLink=["']true["'][^>]*>([\s\S]*?)<\/guid>/i )
?? block . match ( /<guid[^>]*>(https?:\/\/[^\s<]+)<\/guid>/i );
const dateM = block . match ( /<(?:pubDate|published|updated|dc:date)>([\s\S]*?)<\/(?:pubDate|published|updated|dc:date)>/i );
const descM = block . match ( /<(?:description|summary|content)>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/(?:description|summary|content)>/i );
2026-06-03 00:59:04 +02:00
if ( ! titleM || ! linkM ) continue ;
2026-06-08 14:33:23 +02:00
const title = titleM [ 1 ]. replace ( /<[^>]+>/g , "" ). replace ( /&/g , "&" ). replace ( /</g , "<" ). replace ( />/g , ">" ). replace ( /"/g , '"' ). trim ();
const url = linkM [ 1 ]. trim ();
const dateStr = dateM ? .[ 1 ] ? . trim () ?? "" ;
const summary = descM ? .[ 1 ]. replace ( /<[^>]+>/g , "" ). trim (). slice ( 0 , 300 );
if ( ! title || ! url . startsWith ( "http" )) continue ;
2026-06-03 00:59:04 +02:00
2026-06-29 12:17:11 +02:00
const combined = ` ${ title } ${ summary ?? "" } ` ;
const bodyResult = applyRules ( combined );
const titleResult = applyRules ( title );
// Mêmes filtres que ZeroHedge :
// - person cats uniquement si nom dans le titre
// - "Crise" uniquement si mot-clé de crise dans le titre
// - si uniquement person cats → titre doit avoir mot-clé macro
const PERSON_CATS_RSS = new Set ([ "Discours BC" , "Chef d'État" ]);
const CRISIS_TITLE_KW_RSS = /\b(bank|banking|financial|crisis|crash|collapse|systemic|meltdown|turmoil|default|contagion|insolvency|bail.?out|credit crunch|debt crisis)\b/i ;
const MACRO_TITLE_RSS = /\b(rate|rates|inflation|gdp|economy|monetary|bond|yield|forex|currency|dollar|euro|pound|yen|franc|tariff|recession|deficit|trade war|rate cut|rate hike|interest)\b/i ;
const categories = bodyResult . categories . filter ( c => {
if ( PERSON_CATS_RSS . has ( c )) return titleResult . categories . includes ( c );
if ( c === "Crise" ) return CRISIS_TITLE_KW_RSS . test ( title );
return true ;
});
const impacts = bodyResult . impacts ;
if ( impacts . length === 0 && categories . length === 0 ) continue ;
const onlyPersonCats = categories . length > 0 && categories . every ( c => PERSON_CATS_RSS . has ( c ));
if ( onlyPersonCats && ! MACRO_TITLE_RSS . test ( title )) continue ;
// Si aucun impact devise spécifique : le titre doit mentionner l'une de nos 8 devises/BC
// Évite Corée du Sud, Philippines, Turquie etc. qui déclenchent "Décision Taux" sans rapport
const OUR_CB_KW = /\b(fed|fomc|ecb|boe|boj|rba|rbnz|snb|boc|powell|lagarde|bailey|ueda|bullock|orr|jordan|macklem|dollar|euro|pound|sterling|yen|franc|loonie|aussie|kiwi|usd|eur|gbp|jpy|chf|cad|aud|nzd|g7|g10|forex)\b/i ;
if ( impacts . length === 0 && ! OUR_CB_KW . test ( title )) continue ;
2026-06-03 00:59:04 +02:00
items . push ({
id : `rss- ${ Buffer . from ( url ). toString ( "base64" ). slice ( 0 , 12 ) } ` ,
title ,
url ,
source ,
publishedAt : parseDate ( dateStr ),
summary ,
impacts ,
categories ,
});
}
return items ;
}
2026-06-08 14:33:23 +02:00
async function fetchRssFeed ( url : string , source : string ) : Promise < NewsItem [] > {
try {
const res = await fetch ( url , {
next : { revalidate : 1800 },
headers : {
... TE_HEADERS ,
"Accept" : "application/rss+xml, application/atom+xml, application/xml, text/xml, */*" ,
},
});
if ( ! res . ok ) return [];
const xml = await res . text ();
if ( ! xml . includes ( "<item>" ) && ! xml . includes ( "<entry>" )) return [];
return parseRssItems ( xml , source );
} catch { return []; }
}
2026-07-08 16:46:05 +02:00
// ── Source 1 : InvestingLive via leur API interne (Nuxt/api.investinglive.com) ─
// L'ancien endpoint WordPress (wp-json) renvoie 404 depuis la migration du site
// vers Nuxt.js (rebrand ForexLive → investingLive) — remplacé par l'API JSON
// qui alimente leur propre page d'accueil (aucune clé requise, publique).
2026-06-08 14:33:23 +02:00
async function fetchInvestingLiveNews () : Promise < NewsItem [] > {
try {
const res = await fetch (
2026-07-08 16:46:05 +02:00
"https://api.investinglive.com/api/homepage/articles" ,
2026-06-08 14:33:23 +02:00
{
2026-07-08 16:46:05 +02:00
next : { revalidate : 900 },
2026-06-08 14:33:23 +02:00
headers : { ... TE_HEADERS , "Accept" : "application/json" },
}
);
if ( ! res . ok ) return [];
2026-07-08 16:46:05 +02:00
const data = await res . json () as {
LatestArticles? : Array < {
Id : string ;
Title : string ;
Slug : string ;
Tldr? : string [] | string | null ; // schéma incohérent selon le type d'article
PublishedOn : string ;
Category ?: { Name : string ; Slug : string };
} > ;
};
const posts = data . LatestArticles ;
2026-06-08 14:33:23 +02:00
if ( ! Array . isArray ( posts )) return [];
return posts . slice ( 0 , 20 ). map ( post => {
2026-07-08 16:46:05 +02:00
const title = post . Title ? . trim () ?? "" ;
const summary = ( Array . isArray ( post . Tldr ) ? post . Tldr . join ( " " ) : post . Tldr ?? "" ). slice ( 0 , 250 );
const catSlug = post . Category ? . Slug ?? "news" ;
const url = `https://investinglive.com/ ${ catSlug } / ${ post . Slug } /` ;
2026-06-08 14:33:23 +02:00
const combined = ` ${ title } ${ summary ?? "" } ` ;
const { impacts , categories } = applyRules ( combined );
return {
2026-07-08 16:46:05 +02:00
id : `il- ${ post . Id } ` ,
2026-06-08 14:33:23 +02:00
title ,
2026-07-08 16:46:05 +02:00
url ,
2026-06-08 14:33:23 +02:00
source : "InvestingLive" ,
2026-07-08 16:46:05 +02:00
publishedAt : parseDate ( post . PublishedOn ),
2026-06-08 14:33:23 +02:00
summary ,
impacts ,
categories ,
} as NewsItem ;
}). filter ( i => i . title . length > 5 );
} catch { return []; }
}
// ── Source 2 : Flux RSS forex/marchés ────────────────────────────────────────
// FXStreet : très forex-focused, RSS public
// ForexLive : commentaires macro en temps réel
// MarketWatch : marchés généraux
const RSS_FEEDS : { url : string ; source : string }[] = [
{ url : "https://www.fxstreet.com/rss/news" , source : "FXStreet" },
{ url : "https://www.forexlive.com/feed/news" , source : "ForexLive" },
{ url : "https://feeds.content.dowjones.io/public/rss/mw_realtimeheadlines" , source : "MarketWatch" },
{ url : "https://finance.yahoo.com/rss/topfinstories" , source : "Yahoo Finance" },
{ url : "https://feeds.reuters.com/reuters/businessNews" , source : "Reuters" },
{ url : "https://www.investing.com/rss/news.rss" , source : "Investing.com" },
];
async function fetchRssFeeds () : Promise < NewsItem [] > {
2026-06-03 00:59:04 +02:00
const results = await Promise . allSettled (
2026-06-08 14:33:23 +02:00
RSS_FEEDS . map (({ url , source }) => fetchRssFeed ( url , source ))
2026-06-03 00:59:04 +02:00
);
const all : NewsItem [] = [];
const seenUrls = new Set < string >();
for ( const r of results ) {
if ( r . status !== "fulfilled" ) continue ;
for ( const item of r . value ) {
if ( ! seenUrls . has ( item . url )) {
seenUrls . add ( item . url );
all . push ( item );
}
}
}
return all ;
}
// ── Source 3 : Bloomberg (meta tags publics) ─────────────────────────────────
const BLOOMBERG_PAGES = [
{ url : "https://www.bloomberg.com/economics" , source : "Bloomberg Economics" },
{ url : "https://www.bloomberg.com/economics/central-banks" , source : "Bloomberg CB" },
{ url : "https://www.bloomberg.com/fx-center" , source : "Bloomberg FX" },
];
async function fetchBloombergMeta () : Promise < NewsItem [] > {
const items : NewsItem [] = [];
for ( const { url , source } of BLOOMBERG_PAGES ) {
try {
const res = await fetch ( url , {
next : { revalidate : 3600 },
headers : TE_HEADERS ,
});
if ( ! res . ok ) continue ;
const html = await res . text ();
// Extraire les articles depuis les meta og:title / article JSON-LD
const ldBlocks = html . match ( /<script[^>]*type=["']application\/ld\+json["'][^>]*>([\s\S]*?)<\/script>/gi ) ?? [];
for ( const block of ldBlocks . slice ( 0 , 5 )) {
try {
const json = JSON . parse ( block . replace ( /<script[^>]*>|<\/script>/gi , "" ). trim ());
const articles = Array . isArray ( json ) ? json : [ json ];
for ( const art of articles ) {
if ( ! art . headline || ! art . url ) continue ;
const title = String ( art . headline );
const artUrl = String ( art . url );
const dateStr = String ( art . datePublished ?? art . dateModified ?? "" );
const { impacts , categories } = applyRules ( title );
items . push ({
id : `bb- ${ Buffer . from ( artUrl ). toString ( "base64" ). slice ( 0 , 12 ) } ` ,
title ,
url : artUrl ,
source ,
publishedAt : parseDate ( dateStr ),
impacts ,
categories ,
});
}
} catch { /* skip malformed JSON-LD */ }
}
// Fallback: og:title de la page elle-même
const ogTitle = extractMeta ( html , "og:title" );
const ogDesc = extractMeta ( html , "og:description" );
if ( ogTitle && ogTitle . length > 10 ) {
const { impacts , categories } = applyRules ( ` ${ ogTitle } ${ ogDesc } ` );
if ( impacts . length > 0 ) {
items . push ({
id : `bb-page- ${ Buffer . from ( url ). toString ( "base64" ). slice ( 0 , 12 ) } ` ,
title : ogTitle ,
url ,
source ,
publishedAt : new Date (). toISOString (),
summary : ogDesc || undefined ,
impacts ,
categories ,
});
}
}
} catch { /* Bloomberg peut bloquer, on continue */ }
}
return items ;
}
2026-06-08 14:33:23 +02:00
// ── Source 5 : ZeroHedge (FeedBurner RSS) ────────────────────────────────────
// ZeroHedge couvre des dizaines de sujets (crypto, politique, marchés, macro).
// On filtre : seuls les articles où applyRules trouve au moins une devise ou
// catégorie macro sont conservés. Ça élimine le bruit (IPO, crypto pures, etc.)
async function fetchZeroHedgeNews () : Promise < NewsItem [] > {
const FEED_URL = "https://feeds.feedburner.com/zerohedge/feed" ;
try {
const res = await fetch ( FEED_URL , {
next : { revalidate : 600 }, // 10 min — ZH publie très fréquemment
headers : {
... TE_HEADERS ,
"Accept" : "application/rss+xml, application/xml, text/xml, */*" ,
},
});
if ( ! res . ok ) return [];
const xml = await res . text ();
if ( ! xml . includes ( "<item>" )) return [];
const items : NewsItem [] = [];
const blockPattern = /<item>([\s\S]*?)<\/item>/gi ;
let m : RegExpExecArray | null ;
while (( m = blockPattern . exec ( xml )) !== null && items . length < 40 ) {
const block = m [ 1 ];
const titleM = block . match ( /<title>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/title>/i );
const linkM = block . match ( /<link>([\s\S]*?)<\/link>/i )
?? block . match ( /<guid[^>]*>(https?:\/\/[^\s<]+)<\/guid>/i );
const dateM = block . match ( /<pubDate>([\s\S]*?)<\/pubDate>/i );
const descM = block . match ( /<description>(?:<!\[CDATA\[)?([\s\S]*?)(?:\]\]>)?<\/description>/i );
if ( ! titleM || ! linkM ) continue ;
const title = titleM [ 1 ]. replace ( /<[^>]+>/g , "" ). replace ( /&/g , "&" ). replace ( /</g , "<" ). replace ( />/g , ">" ). trim ();
const url = linkM [ 1 ]. trim ();
if ( ! title || ! url . startsWith ( "http" )) continue ;
// Décoder les entités HTML puis supprimer les balises (ZH encode son HTML dans CDATA)
const rawDesc = ( descM ? .[ 1 ] ?? "" )
. replace ( /</g , "<" ). replace ( />/g , ">" ). replace ( /&/g , "&" ). replace ( /"/g , '"' );
const plainText = rawDesc . replace ( /<[^>]+>/g , "" ). replace ( /\s+/g , " " ). trim ();
const summary = plainText . slice ( 0 , 300 );
const analysisText = plainText . slice ( 0 , 800 );
2026-06-29 12:17:11 +02:00
// Filtre de pertinence forex ZeroHedge (3 niveaux) :
// 1. "Discours BC" / "Chef d'État" uniquement si nom dans le TITRE
// 2. "Crise" uniquement si titre contient mot-clé de crise financière explicite
// (évite les articles d'opinion avec doom-language dans le body)
// 3. Si seule rétention = catégorie personne, titre doit avoir mot-clé macro
2026-06-28 15:16:05 +02:00
const bodyResult = applyRules ( ` ${ title } ${ analysisText } ` );
const titleResult = applyRules ( title );
2026-06-29 12:17:11 +02:00
const PERSON_CATS = new Set ([ "Discours BC" , "Chef d'État" ]);
const CRISIS_TITLE_KW = /\b(bank|banking|financial|crisis|crash|collapse|systemic|meltdown|turmoil|default|contagion|insolvency|bail.?out|credit crunch|debt crisis)\b/i ;
const categories = bodyResult . categories . filter ( c => {
if ( PERSON_CATS . has ( c )) return titleResult . categories . includes ( c );
if ( c === "Crise" ) return CRISIS_TITLE_KW . test ( title );
return true ;
});
2026-06-28 15:16:05 +02:00
const impacts = bodyResult . impacts ;
2026-06-08 14:33:23 +02:00
if ( impacts . length === 0 && categories . length === 0 ) continue ;
2026-06-29 12:17:11 +02:00
// Si toutes les catégories viennent uniquement de noms propres, exiger un
// mot-clé macro dans le titre (rate, inflation, gdp, etc.)
const MACRO_TITLE = /\b(rate|rates|inflation|gdp|economy|monetary|bond|yield|forex|currency|dollar|euro|pound|yen|franc|tariff|recession|deficit|trade war|rate cut|rate hike|interest)\b/i ;
const onlyPersonCats = categories . length > 0 && categories . every ( c => PERSON_CATS . has ( c ));
if ( onlyPersonCats && ! MACRO_TITLE . test ( title )) continue ;
2026-06-08 14:33:23 +02:00
const dateStr = dateM ? .[ 1 ] ? . trim () ?? "" ;
items . push ({
id : `zh- ${ Buffer . from ( url ). toString ( "base64" ). slice ( 0 , 12 ) } ` ,
title ,
url ,
source : "ZeroHedge" ,
publishedAt : parseDate ( dateStr ),
summary ,
impacts ,
categories ,
});
}
return items ;
} catch { return []; }
}
2026-06-03 00:59:04 +02:00
// ── Main export ───────────────────────────────────────────────────────────────
export async function fetchAllNews () : Promise < NewsItem [] > {
2026-06-08 14:33:23 +02:00
// Import dynamique pour éviter les dépendances circulaires
const { fetchFinancialJuiceNews } = await import ( "./financialjuice" );
const [ ilNews , rssNews , bbNews , fjNews , zhNews ] = await Promise . allSettled ([
2026-06-03 00:59:04 +02:00
fetchInvestingLiveNews (),
2026-06-08 14:33:23 +02:00
fetchRssFeeds (),
2026-06-03 00:59:04 +02:00
fetchBloombergMeta (),
2026-06-08 14:33:23 +02:00
fetchFinancialJuiceNews (),
fetchZeroHedgeNews (),
2026-06-03 00:59:04 +02:00
]);
const all : NewsItem [] = [
2026-06-08 14:33:23 +02:00
...( fjNews . status === "fulfilled" ? fjNews . value : []), // FJ en priorité
...( zhNews . status === "fulfilled" ? zhNews . value : []), // ZH en 2ème (macro heavy)
...( ilNews . status === "fulfilled" ? ilNews . value : []),
...( rssNews . status === "fulfilled" ? rssNews . value : []),
...( bbNews . status === "fulfilled" ? bbNews . value : []),
2026-06-03 00:59:04 +02:00
];
2026-06-08 14:33:23 +02:00
const EIGHT_DAYS_MS = 8 * 24 * 3600 _000 ;
const cutoff = Date . now () - EIGHT_DAYS_MS ;
// Dédupliquer, filtrer > 8 jours, trier par date décroissante
2026-06-03 00:59:04 +02:00
const seenUrls = new Set < string >();
const deduped = all . filter ( item => {
if ( seenUrls . has ( item . url )) return false ;
seenUrls . add ( item . url );
2026-06-08 14:33:23 +02:00
// Exclure les articles trop anciens (> 8 jours)
const t = new Date ( item . publishedAt ). getTime ();
if ( ! isNaN ( t ) && t < cutoff ) return false ;
2026-06-03 00:59:04 +02:00
return true ;
});
2026-06-27 14:12:05 +02:00
const PRIORITY_SET = new Set ([ "Discours BC" , "Décision Taux" , "Crise" , "Guerre" , "Chef d'État" , "Probabilités Taux" ]);
const isPrio = ( item : NewsItem ) => item . categories . some ( c => PRIORITY_SET . has ( c ));
2026-06-27 16:20:09 +02:00
const PRIO_BONUS_MS = 3 * 60 * 60 * 1000 ; // +3h boost pour les prioritaires
2026-06-27 14:12:05 +02:00
deduped . sort (( a , b ) => {
2026-06-27 16:20:09 +02:00
const ta = new Date ( a . publishedAt ). getTime () + ( isPrio ( a ) ? PRIO_BONUS_MS : 0 );
const tb = new Date ( b . publishedAt ). getTime () + ( isPrio ( b ) ? PRIO_BONUS_MS : 0 );
return tb - ta ;
2026-06-27 14:12:05 +02:00
});
2026-06-03 00:59:04 +02:00
return deduped ;
}