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>
This commit is contained in:
Axel Hocks
2026-07-24 08:29:23 +02:00
co-authored by Claude Opus 4.8
commit 75d28827e8
104 changed files with 21059 additions and 0 deletions
+201
View File
@@ -0,0 +1,201 @@
"""
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}