""" core/ai.py — KI-Marktanalyse via OpenAI ChatGPT ================================================= Nutzt `gpt-4o-mini-search-preview` (oder das größere `gpt-4o-search-preview`) für eine Crude-Oil-Marktanalyse (WTI + Brent) mit Web-Suche. Parsed eine strukturierte Antwort: SENTIMENT: bullish|bearish|neutral CONFIDENCE: 0-100 SUMMARY: <2-3 Sätze> DRIVERS: | | Loggt jede Analyse in die History-DB (falls injiziert). """ from __future__ import annotations import threading import time from core.logger import get_logger log_ai = get_logger("ai") log_hist = get_logger("hist") class OpenAIAnalyzer: """ Ruft die OpenAI Chat-Completions API auf. Web-Search ist bei *-search-preview-Modellen automatisch aktiv, konfigurierbar über `search_context` (low|medium|high). """ PROMPT = ( "Du bist ein professioneller Rohstoff-Marktanalyst. Suche im Web nach den " "wichtigsten Crude-Oil-News der letzten 24 Stunden — sowohl für " "WTI (US Crude) als auch Brent (OPEC, US-Lagerbestände/EIA, " "Geopolitik Naher Osten, Förderdaten, Nachfrage-Indikatoren, " "Pipelines, Raffinerien).\n\n" "Antworte AUSSCHLIESSLICH in genau diesem Format (deutsche Sprache):\n" "SENTIMENT: bullish|bearish|neutral\n" "CONFIDENCE: <0-100>\n" "SUMMARY: <2-3 Sätze, prägnant, nur preisbewegende Faktoren>\n" "DRIVERS: | | \n\n" "Sei knapp und konkret. Keine Disclaimer, keine Einleitung." ) # Preis pro Mio Token (USD) – grobe Schätzung für Cost-Anzeige PRICES = { "gpt-4o-mini-search-preview": (0.15, 0.60, 25.0), "gpt-4o-search-preview": (2.50, 10.00, 30.0), "_default": (0.15, 0.60, 25.0), } def __init__(self, api_key: str, model: str, search_context: str = "low"): self.api_key = api_key.strip() self.model = model.strip() or "gpt-4o-mini-search-preview" self.search_context = search_context.strip().lower() or "low" self.sentiment = None self.confidence = None self.summary = "" self.drivers = [] self.last_update = None self.last_cost = None self.error = None self.busy = False self._lock = threading.Lock() # History-Logger wird vom main() injiziert self.history = None def is_configured(self) -> bool: return bool(self.api_key) and self.api_key.startswith(("sk-", "sk-proj-")) def _estimate_cost(self, in_tok: int, out_tok: int) -> float: in_p, out_p, search_p = self.PRICES.get(self.model, self.PRICES["_default"]) token_cost = (in_tok * in_p + out_tok * out_p) / 1_000_000 search_cost = search_p / 1000.0 return token_cost + search_cost def analyze(self): if not self.is_configured(): with self._lock: self.error = "Kein API-Key (config.ini)" return try: from openai import OpenAI except ImportError: with self._lock: self.error = "pip install openai" return with self._lock: if self.busy: return self.busy = True self.error = None try: client = OpenAI(api_key=self.api_key) kwargs = { "model": self.model, "messages": [{"role": "user", "content": self.PROMPT}], } if "search-preview" in self.model: kwargs["web_search_options"] = { "search_context_size": self.search_context, } else: kwargs["max_tokens"] = 700 res = client.chat.completions.create(**kwargs) text = (res.choices[0].message.content or "").strip() # Strukturierte Antwort parsen sentiment = "neutral" confidence = 50 summary = text[:300] drivers = [] for raw in text.split("\n"): line = raw.strip() low = line.lower() if low.startswith("sentiment:"): v = low.split(":", 1)[1].strip() sentiment = ("bullish" if "bull" in v else "bearish" if "bear" in v else "neutral") elif low.startswith("confidence:"): digits = "".join(c for c in line.split(":", 1)[1] if c.isdigit())[:3] if digits: confidence = max(0, min(100, int(digits))) elif low.startswith("summary:"): summary = line.split(":", 1)[1].strip() elif low.startswith("drivers:"): parts = line.split(":", 1)[1] drivers = [d.strip() for d in parts.split("|") if d.strip()][:4] # Token-Verbrauch & Kosten usage = getattr(res, "usage", None) cost_est = None if usage: in_tok = getattr(usage, "prompt_tokens", 0) or 0 out_tok = getattr(usage, "completion_tokens", 0) or 0 cost_est = self._estimate_cost(in_tok, out_tok) with self._lock: self.sentiment = sentiment self.confidence = confidence self.summary = summary self.drivers = drivers self.last_update = time.time() self.last_cost = cost_est self.error = None log_ai.info(f"{sentiment.upper()} ({confidence}%) — {summary[:80]}") if cost_est: log_ai.info(f"Geschätzte Kosten: ${cost_est:.4f}") if self.history: try: self.history.log_ai( sentiment = sentiment, confidence = confidence, summary = summary, drivers = drivers, cost_estimate= cost_est, model = self.model, ) except Exception as e: log_hist.error(f"log_ai: {e}") except Exception as e: err = str(e) with self._lock: self.error = err[:120] log_ai.error(f"Fehler: {err}") finally: with self._lock: self.busy = False def snapshot(self): with self._lock: return { "sentiment": self.sentiment, "confidence": self.confidence, "summary": self.summary, "drivers": list(self.drivers), "last_update": self.last_update, "last_cost": self.last_cost, "error": self.error, "busy": self.busy, "configured": self.is_configured(), "model": self.model, } class GeminiAnalyzer: """ Ruft die Google Gemini API auf (REST, via requests). Nutzt Google Search Grounding für aktuelle Web-Daten. """ PROMPT = ( "Du bist ein professioneller Rohstoff-Marktanalyst. Suche im Web nach den " "wichtigsten Crude-Oil-News der letzten 24 Stunden — sowohl für " "WTI (US Crude) als auch Brent (OPEC, US-Lagerbestände/EIA, " "Geopolitik Naher Osten, Förderdaten, Nachfrage-Indikatoren, " "Pipelines, Raffinerien).\n\n" "Antworte AUSSCHLIESSLICH in genau diesem Format (deutsche Sprache):\n" "SENTIMENT: bullish|bearish|neutral\n" "CONFIDENCE: <0-100>\n" "SUMMARY: <2-3 Sätze, prägnant, nur preisbewegende Faktoren>\n" "DRIVERS: | | \n\n" "Sei knapp und konkret. Keine Disclaimer, keine Einleitung." ) _API_BASE = "https://generativelanguage.googleapis.com/v1beta/models" def __init__(self, api_key: str, model: str = "gemini-2.0-flash"): self.api_key = api_key.strip() self.model = model.strip() or "gemini-2.0-flash" self.sentiment = None self.confidence = None self.summary = "" self.drivers = [] self.last_update = None self.error = None self.busy = False self._lock = threading.Lock() self.history = None def is_configured(self) -> bool: return bool(self.api_key) and self.api_key.startswith("AIza") def analyze(self): if not self.is_configured(): with self._lock: self.error = "Kein Gemini API-Key (config.ini)" return try: import requests as _req except ImportError: with self._lock: self.error = "pip install requests" return with self._lock: if self.busy: return self.busy = True self.error = None try: url = f"{self._API_BASE}/{self.model}:generateContent" payload = { "contents": [{"role": "user", "parts": [{"text": self.PROMPT}]}], "tools": [{"google_search": {}}], "generationConfig": { "temperature": 0.3, "maxOutputTokens": 600, }, } # Key im Header statt als URL-Parameter — sonst landet er bei # jedem HTTP-Fehler im Klartext in der Log-Fehlermeldung (URL) resp = _req.post( url, headers={"x-goog-api-key": self.api_key}, json=payload, timeout=60, ) resp.raise_for_status() result = resp.json() text = "" try: text = result["candidates"][0]["content"]["parts"][0]["text"].strip() except (KeyError, IndexError): text = str(result)[:300] sentiment = "neutral" confidence = 50 summary = text[:300] drivers = [] for raw in text.split("\n"): line = raw.strip() low = line.lower() if low.startswith("sentiment:"): v = low.split(":", 1)[1].strip() sentiment = ("bullish" if "bull" in v else "bearish" if "bear" in v else "neutral") elif low.startswith("confidence:"): digits = "".join(c for c in line.split(":", 1)[1] if c.isdigit())[:3] if digits: confidence = max(0, min(100, int(digits))) elif low.startswith("summary:"): summary = line.split(":", 1)[1].strip() elif low.startswith("drivers:"): parts = line.split(":", 1)[1] drivers = [d.strip() for d in parts.split("|") if d.strip()][:4] with self._lock: self.sentiment = sentiment self.confidence = confidence self.summary = summary self.drivers = drivers self.last_update = time.time() self.error = None log_ai.info(f"[Gemini] {sentiment.upper()} ({confidence}%) — {summary[:80]}") if self.history: try: self.history.log_ai( sentiment = sentiment, confidence = confidence, summary = summary, drivers = drivers, cost_estimate= None, model = self.model, ) except Exception as e: log_hist.error(f"[Gemini] log_ai: {e}") except Exception as e: err = str(e) with self._lock: self.error = err[:120] log_ai.error(f"[Gemini] Fehler: {err}") finally: with self._lock: self.busy = False def snapshot(self): with self._lock: return { "sentiment": self.sentiment, "confidence": self.confidence, "summary": self.summary, "drivers": list(self.drivers), "last_update": self.last_update, "last_cost": None, "error": self.error, "busy": self.busy, "configured": self.is_configured(), "model": self.model, }