From 929283045f616bd9086e37301b8adb601702e828 Mon Sep 17 00:00:00 2001 From: OpenSquared Date: Tue, 16 Jun 2026 20:43:48 +0200 Subject: [PATCH] fix: score differentiation + auto Super Contexte synthesis MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ai_analyzer: add explicit calibration rules to SYSTEM_SCORER and batch prompt so GPT-4o produces a spread of scores rather than defaulting to 50 for all patterns (0-news patterns capped at 35, contra patterns at 40, high-signal patterns can reach 70-85) - auto_cycle: add _auto_synthesize_knowledge() called after each auto portfolio snapshot; skips if last synthesis < 6h old to avoid redundant GPT-4o calls — Super Contexte now updates automatically every cycle without manual intervention Co-Authored-By: Claude Sonnet 4.6 --- backend/services/ai_analyzer.py | 18 +++++- backend/services/auto_cycle.py | 100 ++++++++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/backend/services/ai_analyzer.py b/backend/services/ai_analyzer.py index 8f368c1..b75d9cb 100644 --- a/backend/services/ai_analyzer.py +++ b/backend/services/ai_analyzer.py @@ -305,7 +305,15 @@ Règles: score total = 1a+1b+1c+2a+2b+3a+3b+3c+3d+3e+4a+4b; ne pas dépasser les SYSTEM_SCORER = """Tu es un gestionnaire de portefeuille senior spécialisé en options géopolitiques. Tu analyses des patterns géopolitiques avec leur contexte marché enrichi (news, prix, IV) pour identifier les meilleures opportunités de trading options (~1000€, horizon 3 mois). -Tu es rigoureux, quantitatif et pragmatique. Réponds UNIQUEMENT en JSON valide.""" +Tu es rigoureux, quantitatif et pragmatique. Réponds UNIQUEMENT en JSON valide. + +RÈGLE DE DISTRIBUTION DES SCORES (IMPÉRATIVE): +- Les scores doivent refléter une vraie distribution : certains patterns sont forts (70+), d'autres faibles (30-) +- Il est INTERDIT de noter tous les patterns au même score ou proche de 50 +- Base tes scores sur : relevant_news_count (catalyseurs actifs), macro_regime.asset_class_bias, has_strong_contra, horizon vs catalyseurs imminents +- Un pattern sans aucune news récente pertinente (relevant_news_count=0) ne peut pas dépasser 35/100 +- Un pattern avec has_strong_contra=true ne dépasse pas 40/100 +- Un pattern avec 3+ news pertinentes ET macro favorable peut atteindre 70-85/100""" def score_patterns_with_context( @@ -618,6 +626,14 @@ TEMPLATE DE NOTATION: + f"\n\n⚠️ OBLIGATOIRE: Tu dois retourner EXACTEMENT {len(batch)} objets dans scored_patterns — un pour CHAQUE pattern de la liste, SANS EXCEPTION. Même si un pattern a score=0 (non pertinent actuellement), il doit figurer dans la liste.\n\n" + f"Pour chacun des {len(batch)} patterns, score chaque sous-pilier + commente en français.\n" + "Le champ \"score\" = somme exacte de tous les sous-piliers.\n\n" + + "🎯 CALIBRATION OBLIGATOIRE — Les scores DOIVENT être différenciés, jamais tous identiques:\n" + + " score 75-100 = catalyseur actif fort, signal clair, timing excellent\n" + + " score 55-74 = signal modéré, contexte favorable mais incertain\n" + + " score 35-54 = neutre, peu de signal, attente\n" + + " score 15-34 = contra-signal, régime défavorable, risque élevé\n" + + " score 0-14 = aucune pertinence dans le contexte actuel\n" + + "⛔ Ne pas mettre tous les patterns à 50 — c'est un échec de calibration.\n" + + " Utilise les relevant_news_count, macro_regime.asset_class_bias, et contra_signals pour vraiment différencier.\n\n" + "⚠️ TRADE RANKINGS (OBLIGATOIRE): Un pattern peut avoir plusieurs suggested_trades (ex: Long Call WTI + Bull Spread XLE).\n" + "Ces trades ne méritent PAS tous le même score. Pour chaque pattern, remplis \"trade_rankings\" en:\n" + "- classant les trades du meilleur (rank 1) au moins bon\n" diff --git a/backend/services/auto_cycle.py b/backend/services/auto_cycle.py index 4691ccf..5ff8e50 100644 --- a/backend/services/auto_cycle.py +++ b/backend/services/auto_cycle.py @@ -578,10 +578,110 @@ Génère un rapport JSON : f"[AutoSnapshot] Portfolio report #{report_id} saved automatically " f"({len(meaningful)} meaningful trades, avg P&L {avg_str})" ) + + # ── Auto-synthesize Super Contexte if stale (>6h or never generated) ── + _auto_synthesize_knowledge(ai_key) + except Exception as e: logger.error(f"[AutoSnapshot] Failed: {e}", exc_info=True) +def _auto_synthesize_knowledge(ai_key: str) -> None: + """ + Synthesize the Super Contexte knowledge base after a portfolio report is generated. + Skipped if the last synthesis is < 6 hours old, to avoid redundant GPT-4o calls. + """ + try: + import os, json as _json + from datetime import datetime as _dt, timedelta as _td + import openai + + os.environ["OPENAI_API_KEY"] = ai_key + + from services.database import ( + get_latest_reasoning_state, save_reasoning_state, + get_all_kb_entries, save_kb_entry, + list_ai_reports, get_mtm_trades_with_traces, + ) + + # Skip if last synthesis < 6 hours ago + last_state = get_latest_reasoning_state() + if last_state: + try: + last_at = _dt.fromisoformat(last_state["created_at"]) + age_h = (_dt.utcnow() - last_at).total_seconds() / 3600 + if age_h < 6: + logger.info( + f"[AutoSynth] Super Contexte is {age_h:.1f}h old — skipping re-synthesis (threshold: 6h)" + ) + return + except Exception: + pass + + reports = list_ai_reports(limit=10) + mtm_data = get_mtm_trades_with_traces(days=90) + trades = mtm_data.get("all_trades", []) if isinstance(mtm_data, dict) else [] + kb_entries = get_all_kb_entries() + + logger.info( + f"[AutoSynth] Starting Super Contexte synthesis " + f"({len(reports)} rapports, {len(trades)} trades, {len(kb_entries)} KB entries)" + ) + + # Build synthesis prompt (reuse router logic inline to avoid import cycle) + from routers.knowledge import _build_synthesis_prompt + system_msg, user_msg = _build_synthesis_prompt(reports, trades, kb_entries) + + client = openai.OpenAI(api_key=ai_key) + resp = client.chat.completions.create( + model="gpt-4o", + messages=[ + {"role": "system", "content": system_msg}, + {"role": "user", "content": user_msg}, + ], + temperature=0.3, + max_tokens=2500, + response_format={"type": "json_object"}, + ) + raw = resp.choices[0].message.content or "{}" + synthesis = _json.loads(raw) + narrative = synthesis.pop("narrative", "Synthèse non disponible.") + + state_id = save_reasoning_state( + narrative=narrative, + synthesis=synthesis, + sources_count=len(reports) + len(trades), + reports_used=len(reports), + trades_analyzed=len(trades), + ) + + # Auto-persist new KB entries from synthesis + added = 0 + for regime in synthesis.get("regime_insights", []): + if regime.get("observation"): + save_kb_entry("régimes", f"Régime: {regime.get('regime','?')}", regime["observation"], + regime.get("confidence", 50), "auto-synth") + added += 1 + for pattern in synthesis.get("pattern_insights", []): + if pattern.get("observation"): + save_kb_entry("patterns", f"Pattern: {pattern.get('pattern','?')}", pattern["observation"], + pattern.get("confidence", 50), "auto-synth") + added += 1 + for mistake in synthesis.get("recurring_mistakes", []): + if mistake.get("mistake"): + save_kb_entry("erreurs", mistake["mistake"][:80], + f"{mistake.get('mistake','')} → {mistake.get('mitigation','')}", + 70, "auto-synth") + added += 1 + + logger.info( + f"[AutoSynth] Super Contexte v{state_id} saved automatically " + f"({added} KB entries added)" + ) + except Exception as e: + logger.error(f"[AutoSynth] Failed: {e}", exc_info=True) + + # ── Scheduler ───────────────────────────────────────────────────────────────── def _scheduler_loop(stop_event: threading.Event):