fix: score differentiation + auto Super Contexte synthesis

- 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 <noreply@anthropic.com>
This commit is contained in:
OpenSquared
2026-06-16 20:43:48 +02:00
parent d256b65d30
commit 929283045f
2 changed files with 117 additions and 1 deletions

View File

@@ -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. 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 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). 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( 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"\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" + 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" + "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" + "⚠️ 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" + "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" + "- classant les trades du meilleur (rank 1) au moins bon\n"

View File

@@ -578,10 +578,110 @@ Génère un rapport JSON :
f"[AutoSnapshot] Portfolio report #{report_id} saved automatically " f"[AutoSnapshot] Portfolio report #{report_id} saved automatically "
f"({len(meaningful)} meaningful trades, avg P&L {avg_str})" 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: except Exception as e:
logger.error(f"[AutoSnapshot] Failed: {e}", exc_info=True) 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 ───────────────────────────────────────────────────────────────── # ── Scheduler ─────────────────────────────────────────────────────────────────
def _scheduler_loop(stop_event: threading.Event): def _scheduler_loop(stop_event: threading.Event):