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 10Y–3M: {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": ""}}""" 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", "")}