Stack: FastAPI + React/TypeScript + SQLite + GPT-4o Features: Radar géopolitique, Marchés, Régime Macro, Journal de Bord MTM, Rapport IA, Super Contexte (base de raisonnement évolutive), Boucle feedback IA. Deploy: Docker + docker-compose + nginx pour openfin.open-squared.tech Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
310 lines
11 KiB
Python
310 lines
11 KiB
Python
from fastapi import APIRouter, HTTPException
|
|
from pydantic import BaseModel
|
|
from typing import Any, Dict, List, Optional
|
|
import json
|
|
import os
|
|
|
|
from services.database import (
|
|
get_kb_entries, get_all_kb_entries, save_kb_entry, update_kb_entry_status,
|
|
get_latest_reasoning_state, get_reasoning_history, get_reasoning_state_by_id,
|
|
save_reasoning_state, list_ai_reports, get_mtm_trades_with_traces,
|
|
)
|
|
|
|
router = APIRouter(prefix="/api/knowledge", tags=["knowledge"])
|
|
|
|
|
|
def _build_synthesis_prompt(reports: List[Dict], trades: List[Dict], kb_entries: List[Dict]):
|
|
"""Build the GPT-4o synthesis prompt from all accumulated data."""
|
|
now_str = __import__("datetime").datetime.utcnow().strftime("%Y-%m-%d %H:%M")
|
|
|
|
# Portfolio reports summary
|
|
reports_block = ""
|
|
for r in reports[:10]:
|
|
rpt = r.get("report") or {}
|
|
stats = r.get("stats") or {}
|
|
date = r.get("created_at", "")[:16]
|
|
headline = rpt.get("headline", "")
|
|
winners = rpt.get("winners_analysis", "")
|
|
losers = rpt.get("losers_analysis", "")
|
|
lessons = rpt.get("key_lessons", [])
|
|
blind = rpt.get("blind_spots", "")
|
|
next_p = rpt.get("next_cycle_priorities", "")
|
|
lessons_str = " | ".join(lessons) if isinstance(lessons, list) else str(lessons)
|
|
reports_block += f"""
|
|
--- Rapport du {date} ---
|
|
Headline: {headline}
|
|
Stats: {stats}
|
|
Gagnants: {winners[:300]}
|
|
Perdants: {losers[:300]}
|
|
Leçons clés: {lessons_str[:400]}
|
|
Angles morts: {blind[:200]}
|
|
Priorités cycle suivant: {next_p[:200]}
|
|
"""
|
|
|
|
# Trade history
|
|
winners = [t for t in trades if (t.get("pnl_pct") or 0) > 0.5]
|
|
losers = [t for t in trades if (t.get("pnl_pct") or 0) < -0.5]
|
|
neutral = [t for t in trades if t not in winners and t not in losers]
|
|
|
|
def trade_line(t):
|
|
return (f"{t.get('underlying','?')} {t.get('strategy','?')} "
|
|
f"P&L={t.get('pnl_pct',0):.2f}% score={t.get('latest_score','?')} "
|
|
f"regime={t.get('macro_regime','?')}")
|
|
|
|
trades_block = f"""
|
|
Gagnants ({len(winners)}): {' | '.join(trade_line(t) for t in winners[:8])}
|
|
Perdants ({len(losers)}): {' | '.join(trade_line(t) for t in losers[:8])}
|
|
Neutres ({len(neutral)}): {len(neutral)} trades sans signal fort
|
|
"""
|
|
|
|
# Existing KB
|
|
kb_block = ""
|
|
if kb_entries:
|
|
by_cat: Dict[str, List] = {}
|
|
for e in kb_entries:
|
|
cat = e.get("category", "général")
|
|
by_cat.setdefault(cat, []).append(e)
|
|
for cat, items in by_cat.items():
|
|
kb_block += f"\n[{cat.upper()}]\n"
|
|
for item in items[:5]:
|
|
kb_block += f" - [{item['confidence']}%] {item['title']}: {item['content'][:150]}\n"
|
|
|
|
system = """Tu es l'intelligence analytique centrale d'un système de trading d'options géopolitiques.
|
|
Tu dois synthétiser TOUT l'historique disponible pour produire un document de raisonnement évolutif.
|
|
Ce document sera utilisé comme contexte enrichi pour tous les prochains cycles d'analyse.
|
|
Réponds UNIQUEMENT en JSON valide selon le schéma spécifié."""
|
|
|
|
user = f"""Date: {now_str}
|
|
|
|
=== HISTORIQUE DES RAPPORTS DE PERFORMANCE ({len(reports)} rapports) ===
|
|
{reports_block}
|
|
|
|
=== HISTORIQUE DES TRADES ({len(trades)} trades) ===
|
|
{trades_block}
|
|
|
|
=== BASE DE CONNAISSANCES EXISTANTE ===
|
|
{kb_block if kb_block else "Aucune entrée existante — première synthèse."}
|
|
|
|
=== MISSION ===
|
|
Produis un JSON avec ces champs:
|
|
|
|
{{
|
|
"narrative": "Un texte narratif riche (500-800 mots) qui décrit l'état actuel du raisonnement du système, les patterns qui fonctionnent, les erreurs récurrentes, les corrélations géopolitiques/macro identifiées, les régimes qui favorisent nos stratégies, et les priorités d'amélioration. C'est le 'cerveau' du système.",
|
|
|
|
"regime_insights": [
|
|
{{"regime": "nom du régime macro", "observation": "ce qu'on sait de ce régime", "confidence": 0-100, "trade_count": N}}
|
|
],
|
|
|
|
"pattern_insights": [
|
|
{{"pattern": "nom du pattern", "observation": "performance et conditions", "confidence": 0-100, "win_rate_pct": 0-100}}
|
|
],
|
|
|
|
"macro_correlations": [
|
|
{{"trigger": "événement géopolitique/macro", "market_reaction": "réaction observée", "reliability": "haute/moyenne/faible"}}
|
|
],
|
|
|
|
"recurring_mistakes": [
|
|
{{"mistake": "description de l'erreur", "frequency": "souvent/parfois", "mitigation": "comment l'éviter"}}
|
|
],
|
|
|
|
"strengths": ["point fort 1", "point fort 2"],
|
|
|
|
"blind_spots": ["angle mort 1", "angle mort 2"],
|
|
|
|
"strategic_priorities": ["priorité 1", "priorité 2", "priorité 3"],
|
|
|
|
"risk_parameters": {{
|
|
"avoid_when": ["condition 1", "condition 2"],
|
|
"prefer_when": ["condition 1", "condition 2"]
|
|
}}
|
|
}}"""
|
|
|
|
return system, user
|
|
|
|
|
|
@router.get("/state")
|
|
def get_state():
|
|
"""Latest synthesized reasoning state."""
|
|
state = get_latest_reasoning_state()
|
|
return {"state": state}
|
|
|
|
|
|
@router.get("/history")
|
|
def get_history(limit: int = 10):
|
|
"""List of reasoning state versions."""
|
|
return {"history": get_reasoning_history(limit)}
|
|
|
|
|
|
@router.get("/history/{state_id}")
|
|
def get_state_version(state_id: int):
|
|
state = get_reasoning_state_by_id(state_id)
|
|
if not state:
|
|
raise HTTPException(404, "Version introuvable")
|
|
return {"state": state}
|
|
|
|
|
|
@router.get("/entries")
|
|
def list_entries(status: str = "all"):
|
|
if status == "all":
|
|
entries = get_all_kb_entries()
|
|
else:
|
|
entries = get_kb_entries(status)
|
|
by_cat: Dict[str, List] = {}
|
|
for e in entries:
|
|
by_cat.setdefault(e.get("category", "général"), []).append(e)
|
|
return {"entries": entries, "by_category": by_cat, "total": len(entries)}
|
|
|
|
|
|
class KbEntryIn(BaseModel):
|
|
category: str
|
|
title: str
|
|
content: str
|
|
confidence: int = 50
|
|
tags: str = ""
|
|
existing_id: Optional[int] = None
|
|
|
|
|
|
@router.post("/entries")
|
|
def add_entry(body: KbEntryIn):
|
|
entry_id = save_kb_entry(
|
|
category=body.category,
|
|
title=body.title,
|
|
content=body.content,
|
|
confidence=body.confidence,
|
|
tags=body.tags,
|
|
existing_id=body.existing_id,
|
|
)
|
|
return {"id": entry_id}
|
|
|
|
|
|
@router.patch("/entries/{entry_id}/status")
|
|
def patch_entry_status(entry_id: int, body: Dict[str, str]):
|
|
status = body.get("status", "active")
|
|
if status not in ("active", "tentative", "invalidated"):
|
|
raise HTTPException(400, "status must be active | tentative | invalidated")
|
|
update_kb_entry_status(entry_id, status)
|
|
return {"id": entry_id, "status": status}
|
|
|
|
|
|
@router.post("/synthesize")
|
|
async def synthesize():
|
|
"""Run GPT-4o synthesis over all historical data and save new reasoning state."""
|
|
ai_key = os.environ.get("OPENAI_API_KEY", "")
|
|
if not ai_key:
|
|
raise HTTPException(400, "OpenAI API key not configured")
|
|
|
|
import openai
|
|
client = openai.OpenAI(api_key=ai_key)
|
|
|
|
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()
|
|
|
|
system_msg, user_msg = _build_synthesis_prompt(reports, trades, kb_entries)
|
|
|
|
try:
|
|
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)
|
|
except Exception as e:
|
|
raise HTTPException(500, f"GPT-4o error: {e}")
|
|
|
|
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),
|
|
)
|
|
|
|
# Persist KB entries from synthesis
|
|
for regime in synthesis.get("regime_insights", []):
|
|
if regime.get("observation"):
|
|
save_kb_entry(
|
|
category="régimes",
|
|
title=f"Régime: {regime.get('regime', '?')}",
|
|
content=regime.get("observation", ""),
|
|
confidence=regime.get("confidence", 50),
|
|
tags="auto-synth",
|
|
)
|
|
|
|
for pattern in synthesis.get("pattern_insights", []):
|
|
if pattern.get("observation"):
|
|
save_kb_entry(
|
|
category="patterns",
|
|
title=f"Pattern: {pattern.get('pattern', '?')}",
|
|
content=pattern.get("observation", ""),
|
|
confidence=pattern.get("confidence", 50),
|
|
tags="auto-synth",
|
|
)
|
|
|
|
for mistake in synthesis.get("recurring_mistakes", []):
|
|
if mistake.get("mistake"):
|
|
save_kb_entry(
|
|
category="erreurs",
|
|
title=mistake.get("mistake", "")[:80],
|
|
content=f"{mistake.get('mistake','')} → {mistake.get('mitigation','')}",
|
|
confidence=70,
|
|
tags="auto-synth",
|
|
)
|
|
|
|
return {
|
|
"state_id": state_id,
|
|
"narrative_preview": narrative[:200],
|
|
"kb_entries_added": (
|
|
len(synthesis.get("regime_insights", [])) +
|
|
len(synthesis.get("pattern_insights", [])) +
|
|
len(synthesis.get("recurring_mistakes", []))
|
|
),
|
|
"sources": {"reports": len(reports), "trades": len(trades)},
|
|
}
|
|
|
|
|
|
@router.get("/context-for-cycle")
|
|
def context_for_cycle():
|
|
"""Compact context to inject into AI cycle prompts."""
|
|
state = get_latest_reasoning_state()
|
|
if not state:
|
|
return {"available": False, "context": ""}
|
|
|
|
synthesis = state.get("synthesis") or {}
|
|
narrative = state.get("narrative", "")
|
|
|
|
priorities = synthesis.get("strategic_priorities", [])
|
|
avoid = synthesis.get("risk_parameters", {}).get("avoid_when", [])
|
|
prefer = synthesis.get("risk_parameters", {}).get("prefer_when", [])
|
|
mistakes = [m.get("mistake", "") for m in synthesis.get("recurring_mistakes", [])[:3]]
|
|
strengths = synthesis.get("strengths", [])
|
|
|
|
context = f"""=== SUPER CONTEXTE — BASE DE RAISONNEMENT ({state.get('created_at','')[:16]}) ===
|
|
{narrative[:600]}
|
|
|
|
PRIORITÉS STRATÉGIQUES: {' | '.join(priorities[:3])}
|
|
ERREURS À ÉVITER: {' | '.join(mistakes)}
|
|
PRÉFÉRER QUAND: {' | '.join(prefer[:2])}
|
|
ÉVITER QUAND: {' | '.join(avoid[:2])}
|
|
FORCES: {' | '.join(strengths[:2])}
|
|
"""
|
|
return {
|
|
"available": True,
|
|
"context": context,
|
|
"version": state.get("version"),
|
|
"created_at": state.get("created_at"),
|
|
"sources": {
|
|
"reports_used": state.get("reports_used"),
|
|
"trades_analyzed": state.get("trades_analyzed"),
|
|
},
|
|
}
|