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:
@@ -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"
|
||||||
|
|||||||
@@ -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):
|
||||||
|
|||||||
Reference in New Issue
Block a user