Files
OpenFin/backend/routers/ai.py
OpenSquared d256b65d30 Initial commit — GeoOptions Intelligence Cockpit v2.0
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>
2026-06-16 20:29:59 +02:00

298 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from fastapi import APIRouter, HTTPException
from pydantic import BaseModel
from typing import Optional, List, Dict, Any
import json
from services.ai_analyzer import (
analyze_speech, evaluate_pattern, suggest_pattern_from_context,
rank_trade_ideas, analyze_news_item, get_client, score_patterns_with_context,
suggest_patterns_from_market_context, ai_score_news_batch,
DEFAULT_ANALYSIS_TEMPLATE, _chat,
)
from services.data_fetcher import fetch_geo_news, get_all_quotes
from services.geo_analyzer import compute_geo_risk_score, match_patterns
from services.database import get_config, get_custom_patterns, get_analysis_config, save_pattern_scores, get_pattern_scores, get_score_deltas, compute_pattern_similarity, log_geo_alert, log_trade_entries
router = APIRouter(prefix="/api/ai", tags=["ai"])
def require_ai():
key = get_config("openai_api_key") or ""
if not key:
raise HTTPException(400, "Clé OpenAI non configurée — aller dans Config")
import os
os.environ["OPENAI_API_KEY"] = key
class SpeechRequest(BaseModel):
text: str
speaker: Optional[str] = ""
class PatternEvalRequest(BaseModel):
pattern: Dict[str, Any]
class PatternSuggestRequest(BaseModel):
context: str
class NewsAnalyzeRequest(BaseModel):
title: str
summary: str
class ScorePatternsRequest(BaseModel):
top_n: Optional[int] = None # override config default
category_filter: Optional[str] = None # "all"|"energy"|"metals"|"agriculture"|"forex"|"indices"|"equities"
template: Optional[str] = None # override config template
@router.get("/status")
def ai_status():
key = get_config("openai_api_key") or ""
return {
"enabled": bool(key),
"key_configured": bool(key),
"model": "gpt-4o",
"fast_model": "gpt-4o-mini",
}
@router.post("/analyze-speech")
def analyze_speech_endpoint(req: SpeechRequest):
require_ai()
return analyze_speech(req.text, req.speaker)
@router.post("/analyze-news")
def analyze_news_endpoint(req: NewsAnalyzeRequest):
require_ai()
return analyze_news_item(req.title, req.summary)
@router.post("/evaluate-pattern")
def evaluate_pattern_endpoint(req: PatternEvalRequest):
require_ai()
return evaluate_pattern(req.pattern)
@router.post("/suggest-pattern")
def suggest_pattern_endpoint(req: PatternSuggestRequest):
require_ai()
return suggest_pattern_from_context(req.context)
@router.get("/top-ideas")
def top_ideas():
require_ai()
from routers.geopolitical import _news_cache
news = _news_cache.get("data") or fetch_geo_news()
matches = match_patterns(news)
geo_score = compute_geo_risk_score(news)
ideas = rank_trade_ideas(matches, geo_score, news, {})
return {"ideas": ideas, "count": len(ideas)}
@router.post("/score-patterns")
def score_patterns(req: ScorePatternsRequest):
"""Score all patterns with rich context (news + prices + IV) via GPT-4o."""
require_ai()
# Load config defaults
cfg = get_analysis_config()
top_n = req.top_n or cfg.get("top_n", 10)
category_filter = req.category_filter or cfg.get("category_filter", "all")
template = req.template or cfg.get("template") or DEFAULT_ANALYSIS_TEMPLATE
# Gather context
from routers.geopolitical import _news_cache
news = _news_cache.get("data") or fetch_geo_news()
# AI-rescore news: get accurate impact magnitudes + directional signals per asset class
# This enables contra-signal detection in pattern scoring (e.g. peace deal → oil bearish)
news = ai_score_news_batch(news)
_news_cache["data"] = news # propagate AI-enriched scores back to news feed
geo_score = compute_geo_risk_score(news)
quotes = get_all_quotes()
# Macro regime context (use cached value from /api/market/macro-regime if available)
from routers.market_data import _macro_cache
macro_regime = _macro_cache.get("data")
if not macro_regime:
from services.data_fetcher import get_macro_gauges, score_macro_scenarios
gauges = get_macro_gauges()
macro_regime = {"gauges": gauges, "scenarios": score_macro_scenarios(gauges)}
# All patterns from DB (builtin-seeded + custom)
all_patterns = get_custom_patterns()
# Score all active patterns
scored = score_patterns_with_context(
patterns=all_patterns,
recent_news=news,
quotes_by_class=quotes,
geo_score=geo_score,
template=template,
top_n=len(all_patterns),
category_filter=None,
macro_regime=macro_regime,
)
# Persist scores for later retrieval
run_id = save_pattern_scores(scored, meta={"geo_score": geo_score.get("score"), "total": len(scored)})
# Journal de Bord — log geo alert + trade entry prices at this moment
top_patterns_for_log = sorted(
[{"pattern_id": sp.get("pattern_id"), "name": sp.get("geo_trigger"), "score": sp.get("score", 0)}
for sp in scored if sp.get("score", 0) > 0],
key=lambda x: -x["score"]
)[:10]
log_geo_alert(
geo_score=int(geo_score.get("score") or 0),
top_patterns=top_patterns_for_log,
news_count=len(news),
run_id=run_id,
)
log_trade_entries(run_id=run_id, scored_patterns=scored, quotes=quotes)
return {
"scored_patterns": scored,
"count": len(scored),
"top_n": top_n,
"category_filter": category_filter,
"geo_score": geo_score.get("score"),
}
@router.get("/last-scores")
def last_scores():
"""Return last persisted AI pattern scores with inter-run score deltas."""
data = get_pattern_scores()
deltas = get_score_deltas()
scored = data.get("scores", [])
for sp in scored:
pid = sp.get("pattern_id", "")
sp["score_trend"] = deltas.get(pid) # None = first run, int = change vs previous run
return {
"scored_patterns": scored,
"scored_at": data.get("scored_at"),
"meta": data.get("meta", {}),
"count": len(scored),
}
@router.get("/pattern-similarity")
def pattern_similarity():
"""Return pairs of patterns with high keyword overlap (Jaccard >= 0.25)."""
patterns = get_custom_patterns()
pairs = compute_pattern_similarity(patterns, threshold=0.25)
return {"pairs": pairs, "count": len(pairs)}
@router.post("/suggest-new-patterns")
def suggest_new_patterns():
"""Ask GPT-4o to propose new patterns from current geo/market context (no text input needed)."""
require_ai()
from routers.geopolitical import _news_cache
news = _news_cache.get("data") or fetch_geo_news()
quotes = get_all_quotes()
from services.data_fetcher import get_economic_calendar
calendar = get_economic_calendar()
# Macro regime context
from routers.market_data import _macro_cache
macro_regime = _macro_cache.get("data")
if not macro_regime:
from services.data_fetcher import get_macro_gauges, score_macro_scenarios
gauges = get_macro_gauges()
macro_regime = {"gauges": gauges, "scenarios": score_macro_scenarios(gauges)}
# Geo risk score for additional context
geo_score = compute_geo_risk_score(news)
patterns = suggest_patterns_from_market_context(news, quotes, calendar, macro_regime=macro_regime, geo_score=geo_score)
return {"suggested_patterns": patterns, "count": len(patterns)}
@router.get("/analysis-template")
def get_template():
cfg = get_analysis_config()
return {
"template": cfg.get("template") or DEFAULT_ANALYSIS_TEMPLATE,
"top_n": cfg.get("top_n", 10),
"category_filter": cfg.get("category_filter", "all"),
}
class MacroNarrationRequest(BaseModel):
macro_regime: Dict[str, Any]
@router.post("/macro-narration")
def macro_narration(req: MacroNarrationRequest):
"""Ask GPT-4o to narrate the current macro regime for the MacroRegime page."""
require_ai()
sc = req.macro_regime.get("scenarios", {})
gauges = req.macro_regime.get("gauges", {})
dom = sc.get("dominant", "incertain")
scores = sc.get("scores", {})
reasons = sc.get("reasons", {})
def gv(key: str) -> str:
v = gauges.get(key, {}).get("value")
return str(round(v, 3)) if v is not None else "N/A"
def gc(key: str) -> str:
v = gauges.get(key, {}).get("change_pct")
return f"{v:+.2f}%" if v is not None else "N/A"
user = f"""Tu es un stratège macro senior utilisant un framework à 3 axes : Inflation / Croissance / Liquidité.
Scénario dominant: {dom.upper()}
Scores des 8 scénarios: {json.dumps(scores, ensure_ascii=False)}
Raisons du scénario dominant: {json.dumps(reasons.get(dom, []), ensure_ascii=False)}
AXE INFLATION (énergie + taux réels):
- Brent (var J-1): {gc('brent')}
- Gaz naturel (var J-1): {gc('ng')}
- TIPS ETF (var J-1): {gc('tips')}
AXE CROISSANCE (cycle réel):
- Cuivre (var J-1): {gc('copper')}"Dr Copper"
- S&P vs 200j MA: {gv('spx_vs_200d')}%
- Russell vs S&P (breadth): {gv('iwm_spx_ratio')} pts%
- Industriels XLI (proxy ISM, var J-1): {gc('xli')}
AXE LIQUIDITÉ / STRESS (conditions financières):
- VIX: {gv('vix')}
- Pente 10Y3M: {gv('slope_10y3m')}% (négatif = récession probable)
- HYG spreads HY (var J-1): {gc('hyg')}
- LQD spreads IG (var J-1): {gc('lqd')}
- Dollar DXY (var J-1): {gc('dxy')}
SIGNAUX DÉRIVÉS:
- Ratio Or/Cuivre: {gv('gold_copper_ratio')} (>700 = peur, <500 = expansion)
- Or (var J-1): {gc('gold')}
- IEF Trésor (var J-1): {gc('ief')}
Donne une analyse narrative COURTE (5-7 phrases) en français pour un trader options:
1. Confirme le régime dominant et ses 2-3 signaux les plus forts
2. Identifie 1-2 compteurs en contradiction ou tension (signal ambigu)
3. Évalue si la LIQUIDITÉ confirme ou contredit le régime inflation/croissance
4. Cite les 2-3 classes d'actifs les plus favorisées/défavorisées
5. Donne 1 biais tactique concret options pour les prochains jours
Réponds UNIQUEMENT en JSON: {{"narration": "<ton texte 5-7 phrases>"}}"""
result = _chat(
"Tu es un stratège macro senior. Analyse concise et actionnable pour traders options. JSON uniquement.",
user,
model="gpt-4o",
json_mode=True,
max_tokens=600,
)
if not result:
return {"narration": "IA non disponible — vérifier la clé OpenAI."}
return {"narration": result.get("narration", "")}