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

202 lines
7.9 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/tu_rating.py — Traders-Union-Analyse (tradersunion.com)
=============================================================
Holt die technische Analyse für WTI von der öffentlichen Traders-Union-API
(quotes.tradersunion.com) — dieselben Daten, die der Tacho auf
https://tradersunion.com/currencies/forecast/wti-crude-oil/signals/ anzeigt.
API: GET /api/v3/informer/technical-analysis/detailed/?symbol=WTI/USD
Liefert pro Zeitebene (m5, m15, m30, h1, h4, d1, w1):
- forecast → Gesamt-Verdikt ("Strong Sell" … "Strong Buy")
- ta → Oszillator-Zähler (buy/sell/neutral, 13 Indikatoren)
- ma → Moving-Average-Zähler (MA5MA200, SMA+EMA)
- indicators → 14 Einzelindikatoren mit Werten
Auto-Trader-Signal (trade_signal):
Verdikt je TF → Score (-2 Strong Sell … +2 Strong Buy)
LONG wenn m15 ≥ +1 UND m30 ≥ +1 UND h1 nicht dagegen (≥ 0)
SHORT spiegelbildlich. Konfidenz steigt mit Strong-Verdikten und
H1-/M5-Bestätigung. Daten älter als 5 min → WARTEN (kein Stale-Trading).
Kein API-Key nötig. fetch() blockiert (HTTP) — im Hintergrund-Thread rufen;
snapshot()/trade_signal() liefern den letzten Stand sofort.
"""
from __future__ import annotations
import threading
import time
from core.logger import get_logger
log = get_logger("turating")
_API_URL = ("https://quotes.tradersunion.com/api/v3/"
"informer/technical-analysis/detailed/")
_UA = ("Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 "
"(KHTML, like Gecko) Chrome/125.0 Safari/537.36")
# Zeitebenen wie auf der Website: Anzeige-Key → API-Key
INTERVALS: dict[str, str] = {
"5m": "m5",
"15m": "m15",
"30m": "m30",
"1h": "h1",
"4h": "h4",
"1d": "d1",
"1w": "w1",
}
# Verdikt → numerischer Score (für Nadel + Trading-Logik)
_VERDICT_SCORE = {
"strong sell": -2, "sell": -1, "neutral": 0, "buy": 1, "strong buy": 2,
}
_STALE_S = 300 # Daten älter als 5 min → kein Trading-Signal
def _vscore(verdict: str | None) -> int:
return _VERDICT_SCORE.get((verdict or "").strip().lower(), 0)
def _counts_rating(c: dict) -> float | None:
"""Nadel-Position -1..+1 aus Buy/Sell/Neutral-Zählern."""
total = (c.get("buy") or 0) + (c.get("sell") or 0) + (c.get("neutral") or 0)
if not total:
return None
return ((c.get("buy") or 0) - (c.get("sell") or 0)) / total
class TradersUnionProvider:
"""Holt + cached die Traders-Union-Analyse für ein Symbol."""
def __init__(self, symbol: str = "WTI/USD"):
self.symbol = symbol
self._lock = threading.Lock()
self._busy = False
self._data: dict = {} # {interval_key: {...}}
self._error: str | None = None
self._last_update: float | None = None
# ── HTTP-Fetch (blockierend — im Hintergrund-Thread aufrufen) ───────────
def fetch(self) -> bool:
with self._lock:
if self._busy:
return False
self._busy = True
try:
import requests
resp = requests.get(
_API_URL, params={"symbol": self.symbol},
headers={"User-Agent": _UA, "Accept": "application/json"},
timeout=15)
resp.raise_for_status()
payload = resp.json().get("data") or {}
data = {}
for key, api_key in INTERVALS.items():
tf = payload.get(api_key)
if not isinstance(tf, dict):
continue
osc = tf.get("ta") or {}
ma = tf.get("ma") or {}
c_osc = {"buy": osc.get("buy", 0),
"sell": osc.get("sell", 0),
"neutral": osc.get("neutral", 0)}
c_ma = {"buy": ma.get("buy", 0),
"sell": ma.get("sell", 0),
"neutral": ma.get("neutral", 0)}
total = {k: c_osc[k] + c_ma[k] for k in c_osc}
data[key] = {
"verdict": tf.get("forecast") or "—",
"verdict_osc": osc.get("forecast") or "—",
"verdict_ma": ma.get("forecast") or "—",
"rating": _counts_rating(total),
"rating_osc": _counts_rating(c_osc),
"rating_ma": _counts_rating(c_ma),
"counts": total,
"counts_osc": c_osc,
"counts_ma": c_ma,
"score": _vscore(tf.get("forecast")),
}
if not data:
raise ValueError(f"keine TF-Daten für {self.symbol}")
with self._lock:
self._data = data
self._error = None
self._last_update = time.time()
log.info(f"{self.symbol}: " + " ".join(
f"{k}={d['verdict']}" for k, d in data.items()))
return True
except Exception as e:
with self._lock:
self._error = str(e)[:120]
log.warning(f"TU-Analyse Fetch fehlgeschlagen: {e}")
return False
finally:
with self._lock:
self._busy = False
# ── Snapshot für UI (Tachos) ─────────────────────────────────────────────
def snapshot(self) -> dict:
with self._lock:
return {
"symbol": self.symbol,
"intervals": dict(self._data),
"error": self._error,
"last_update": self._last_update,
"busy": self._busy,
}
# ── Trading-Signal für den Auto-Trader ───────────────────────────────────
def trade_signal(self) -> dict:
"""
Konsens-Signal aus der Traders-Union-Analyse:
LONG: m15 ≥ +1 und m30 ≥ +1 und h1 ≥ 0
SHORT: m15 ≤ 1 und m30 ≤ 1 und h1 ≤ 0
Konfidenz: 60 % Basis, +10 je Strong-Verdikt (m15/m30),
+10 bei H1-Bestätigung, +5 bei M5-Bestätigung (max 90).
"""
with self._lock:
data = dict(self._data)
ts = self._last_update
base = {"signal": "WARTEN", "conf_pct": 0, "score": 0.0,
"setup": "TU_KONSENS", "regime": None, "rsi": None,
"reasons": []}
if not data:
base["reasons"] = ["keine TU-Daten"]
return base
if not ts or time.time() - ts > _STALE_S:
base["reasons"] = ["TU-Daten veraltet"]
return base
sc = {k: data.get(k, {}).get("score", 0) for k in
("5m", "15m", "30m", "1h")}
reasons = [f"{k}: {data[k]['verdict']}" for k in
("5m", "15m", "30m", "1h", "4h", "1d") if k in data]
signal = "WARTEN"
if sc["15m"] >= 1 and sc["30m"] >= 1 and sc["1h"] >= 0:
signal = "LONG"
elif sc["15m"] <= -1 and sc["30m"] <= -1 and sc["1h"] <= 0:
signal = "SHORT"
conf = 0
if signal != "WARTEN":
d = 1 if signal == "LONG" else -1
# Basis 60: mit H1-Bestätigung (+10) erreicht ein sauberer
# Konsens die Auto-Schwelle (70) auch ohne Strong-Verdikt
conf = 60
conf += 10 * sum(1 for k in ("15m", "30m") if sc[k] * d >= 2)
if sc["1h"] * d >= 1:
conf += 10
if sc["5m"] * d >= 1:
conf += 5
conf = min(conf, 90)
# Score -1..+1 (Mittel der Trading-TFs, normiert auf ±2)
score = (sc["15m"] + sc["30m"] + sc["1h"]) / 6.0
return {"signal": signal, "conf_pct": conf, "score": score,
"setup": "TU_KONSENS", "regime": None, "rsi": None,
"reasons": reasons}