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>
This commit is contained in:
OpenSquared
2026-06-16 20:29:59 +02:00
commit d256b65d30
69 changed files with 18301 additions and 0 deletions

297
backend/routers/ai.py Normal file
View File

@@ -0,0 +1,297 @@
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", "")}