fix: AI returns type instead of category name — clearer prompt + guard
The AI was returning 'geopolitical' (the event type/family) instead of a category name like 'Guerre — Moyen-Orient'. Two fixes: 1. Prompt: rename 'Type' → 'Famille', list categories as CAT_N: "name", explicit warning not to return a generic type, show valid name examples 2. Code: reject any response that matches a known type string before matching Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user