diff --git a/backend/services/impact_service.py b/backend/services/impact_service.py index 3ff13a2..c1e664e 100644 --- a/backend/services/impact_service.py +++ b/backend/services/impact_service.py @@ -41,6 +41,9 @@ def _get_api_key() -> str: return key +_KNOWN_TYPES = {"geopolitical", "fundamental", "event_calendar", "report", "sentiment", "technical"} + + def _build_prompt( event: Dict[str, Any], all_cats: List[Dict], @@ -48,23 +51,25 @@ def _build_prompt( ) -> str: # ── Category selection block ────────────────────────────────────────────── if all_cats: + # Number each category so the AI can refer to it unambiguously cat_lines = "\n".join( - f" {i+1}. [{c['type']}] {c['name']}" - + (f" — {c.get('sub_type','')}" if c.get('sub_type') else "") - + (f" : {c.get('description','')[:80]}" if c.get('description') else "") + f" CAT_{i+1}: \"{c['name']}\"" + + (f" ({c.get('description','')[:70]})" if c.get('description') else "") for i, c in enumerate(all_cats) ) + # Build the valid names list for the JSON instruction + valid_names = " | ".join(f'"{c["name"]}"' for c in all_cats[:6]) + " | ..." cat_selection_block = f""" -ÉTAPE 1 — CHOIX DE CATÉGORIE : -Voici toutes les catégories disponibles. Choisis celle qui correspond le mieux à cet événement (ou "none" si aucune ne convient) : +LISTE DES CATÉGORIES DISPONIBLES (utilise uniquement ces noms) : {cat_lines} -Retourne le nom EXACT de la catégorie choisie dans le champ "matched_category" du JSON. +→ Dans le JSON, "matched_category" DOIT être l'un des noms entre guillemets ci-dessus (ex: {valid_names}), ou null si vraiment aucune ne convient. + NE retourne PAS un type générique comme "geopolitical" ou "fundamental" — ces valeurs sont invalides ici. """ else: cat_selection_block = "" - # ── Default impacts from pre-selected category (fallback heuristic) ─────── + # ── Default impacts from pre-selected category ──────────────────────────── defaults_block = "" if preselected_defaults: top = sorted(preselected_defaults, key=lambda x: x.get("sensitivity", 0), reverse=True)[:8] @@ -73,44 +78,37 @@ Retourne le nom EXACT de la catégorie choisie dans le champ "matched_category" f"direction usuelle = {d.get('typical_direction','?')}" for d in top ] - defaults_block = "\nImpacts par défaut (ajuste si l'événement est atypique) :\n" + "\n".join(lines) + defaults_block = "\nImpacts par défaut de la catégorie (ajuste si besoin) :\n" + "\n".join(lines) return f"""Tu es un analyste macro senior spécialisé en options et trading multi-actifs. ÉVÉNEMENT À ANALYSER : - Nom : {event.get('name', '')} - Date : {event.get('start_date', '')} -- Type : {event.get('category', '')} / {event.get('sub_type', '')} +- Famille : {event.get('category', '')} - Description : {event.get('description', '')} -- Impact attendu (résumé) : {event.get('market_impact', '')} +- Impact attendu : {event.get('market_impact', '')} {cat_selection_block}{defaults_block} -ÉTAPE 2 — IMPACTS PAR INSTRUMENT : -Évalue l'impact potentiel de cet événement sur chacun des 20 instruments. -Si tu as choisi une catégorie à l'étape 1, utilise ses impacts par défaut comme point de départ et ajuste selon le contexte spécifique de l'événement. -Instruments disponibles : {_INST_LIST_STR} +MISSION — IMPACTS PAR INSTRUMENT : +Choisis d'abord la catégorie la plus proche dans la liste ci-dessus, puis évalue l'impact sur chacun des instruments. +Si une catégorie est choisie, utilise ses impacts par défaut comme point de départ. +Instruments : {_INST_LIST_STR} -Pour chaque instrument IMPACTÉ (score >= 0.15), retourne : -- instrument_id : identifiant exact (ex: "SPY", "EURUSD=X", "BTC-USD") -- impact_score : 0.0 (aucun) → 1.0 (impact majeur, mouvement > 2%) +Pour chaque instrument IMPACTÉ (score >= 0.15) : +- instrument_id : identifiant exact (ex: "SPY", "EURUSD=X") +- impact_score : 0.0 → 1.0 - direction : "bullish" | "bearish" | "neutral" | "depends_on_outcome" -- rationale : 1 phrase MAX expliquant le mécanisme -- confidence : 0.0-1.0 (ta certitude sur l'estimation) - -Critères de scoring : -- 0.0-0.2 : négligeable (bruit) -- 0.2-0.4 : faible (< 0.5% mouvement) -- 0.4-0.6 : modéré (0.5-1% mouvement) -- 0.6-0.8 : fort (1-2% mouvement) -- 0.8-1.0 : majeur (> 2%, event driver primaire) +- rationale : 1 phrase MAX +- confidence : 0.0-1.0 FORMAT JSON STRICT : {{ - "matched_category": "Nom exact de la catégorie choisie ou null", + "matched_category": "Nom exact d'une CAT ci-dessus, ou null", "impacts": [ {{"instrument_id": "SPY", "impact_score": 0.85, "direction": "bearish", "rationale": "...", "confidence": 0.90}} ], - "summary": "Contexte macro de cet événement en 1 phrase", + "summary": "Contexte macro en 1 phrase", "key_driver": "Instrument le plus impacté et pourquoi" }}""" @@ -210,7 +208,7 @@ def evaluate_event_impacts( ai_cat_name = (raw.get("matched_category") or "").strip() logger.info(f"[impact_service] AI returned matched_category='{ai_cat_name}' for event {event_id}") - if ai_cat_name and ai_cat_name.lower() not in ("none", "null", ""): + if ai_cat_name and ai_cat_name.lower() not in ("none", "null", "") and ai_cat_name.lower() not in _KNOWN_TYPES: # Exact match first, then normalized fuzzy match matched = ( next((c for c in all_cats if c["name"].lower() == ai_cat_name.lower()), None)