Files
Axel HocksandClaude Opus 4.8 75d28827e8 Initial commit: Oil Trading Bot (MT5, WTI)
Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/),
Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind
via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 08:29:23 +02:00

344 lines
13 KiB
Python
Raw Permalink 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.
"""
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: <Treiber1> | <Treiber2> | <Treiber3>
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: <Treiber1> | <Treiber2> | <Treiber3>\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: <Treiber1> | <Treiber2> | <Treiber3>\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,
}