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:
@@ -0,0 +1,159 @@
|
||||
"""
|
||||
core/daily_levels.py — Tägliche WTI-Intraday-Level per Web-Recherche
|
||||
=====================================================================
|
||||
Holt einmal morgens (Engine-Scheduler) über **z.ai/GLM mit Web-Suche**
|
||||
(`web_search`-Tool, Thinking aus) die heutigen Schlüssel-Level (Support /
|
||||
Widerstandsband / Bias) für WTI und liefert sie als **Kontext**: fließen in
|
||||
S/R-Anzeige, Close-Alarme und Verdict — NICHT in die Handelsrichtung
|
||||
(Dritt-Prognose, kein gemessener Edge).
|
||||
|
||||
Strenge Plausibilitätsprüfung gegen den aktuellen Kurs verhindert, dass
|
||||
halluzinierte Zahlen als Zonen landen (fail-safe: bei Zweifel kein Update).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
import threading
|
||||
import time
|
||||
|
||||
from core.logger import get_logger
|
||||
|
||||
log = get_logger("daily")
|
||||
|
||||
_MAX_DEV = 0.12 # Level muss innerhalb ±12 % des Kurses liegen (sonst verworfen)
|
||||
|
||||
_PROMPT = (
|
||||
"Du bist Rohstoff-Marktanalyst. Suche im Web die heutige INTRADAY-Lage für "
|
||||
"WTI-Rohöl (US Crude, aktueller Kurs ~{price:.2f} USD). Nenne die für HEUTE "
|
||||
"wichtigsten technischen Level rund um den aktuellen Kurs.\n\n"
|
||||
"Antworte AUSSCHLIESSLICH in genau diesem Format (nur Zahlen, Punkt als "
|
||||
"Dezimaltrenner, deutsche Sprache):\n"
|
||||
"SUPPORT: <preis unter dem Kurs>\n"
|
||||
"RESISTANCE_LOW: <preis über dem Kurs>\n"
|
||||
"RESISTANCE_HIGH: <preis über dem Kurs, >= RESISTANCE_LOW>\n"
|
||||
"BIAS: bullish|bearish|neutral\n"
|
||||
"SUMMARY: <1-2 Sätze, nur preisbewegende Faktoren>\n\n"
|
||||
"Die Level müssen NAHE am aktuellen Kurs liegen (Intraday, keine Jahresziele). "
|
||||
"Keine Disclaimer, keine Einleitung."
|
||||
)
|
||||
|
||||
|
||||
class DailyLevels:
|
||||
def __init__(self, api_key: str, base_url: str = "https://api.z.ai/api/paas/v4",
|
||||
model: str = "glm-4.5-flash"):
|
||||
self.api_key = (api_key or "").strip()
|
||||
self.base_url = (base_url or "https://api.z.ai/api/paas/v4").rstrip("/")
|
||||
self.model = (model or "glm-4.5-flash").strip()
|
||||
self.support: float | None = None
|
||||
self.res_lo: float | None = None
|
||||
self.res_hi: float | None = None
|
||||
self.bias: str = "neutral"
|
||||
self.summary: str = ""
|
||||
self.ts: float = 0.0
|
||||
self.error: str | None = None
|
||||
self._lock = threading.Lock()
|
||||
|
||||
def is_configured(self) -> bool:
|
||||
return bool(self.api_key)
|
||||
|
||||
@staticmethod
|
||||
def _num(line: str):
|
||||
s = line.split(":", 1)[1] if ":" in line else line
|
||||
s = s.replace(",", ".")
|
||||
buf = ""
|
||||
for c in s:
|
||||
if c.isdigit() or c == ".":
|
||||
buf += c
|
||||
elif buf:
|
||||
break
|
||||
try:
|
||||
return float(buf)
|
||||
except ValueError:
|
||||
return None
|
||||
|
||||
def _call_llm(self, prompt: str) -> str:
|
||||
"""z.ai/GLM (OpenAI-kompatibel) mit Web-Suche + Thinking AUS (sonst geht
|
||||
das Token-Budget komplett ins „Denken")."""
|
||||
import requests
|
||||
resp = requests.post(
|
||||
self.base_url + "/chat/completions",
|
||||
headers={"Authorization": "Bearer " + self.api_key},
|
||||
json={"model": self.model,
|
||||
"messages": [{"role": "user", "content": prompt}],
|
||||
"tools": [{"type": "web_search",
|
||||
"web_search": {"enable": True, "search_result": True}}],
|
||||
"thinking": {"type": "disabled"},
|
||||
"max_tokens": 500},
|
||||
timeout=90)
|
||||
resp.raise_for_status()
|
||||
return (resp.json()["choices"][0]["message"].get("content") or "").strip()
|
||||
|
||||
def run(self, price: float) -> bool:
|
||||
"""Holt + validiert die Tages-Level. price = aktueller Kurs (Anker +
|
||||
Plausibilitätsbasis). True bei erfolgreichem, plausiblem Update."""
|
||||
if not self.is_configured():
|
||||
with self._lock:
|
||||
self.error = "Kein z.ai-Key"
|
||||
return False
|
||||
if not price or price <= 0:
|
||||
with self._lock:
|
||||
self.error = "Kein Kurs als Anker"
|
||||
return False
|
||||
try:
|
||||
text = self._call_llm(_PROMPT.format(price=price))
|
||||
|
||||
sup = rlo = rhi = None
|
||||
bias, summary = "neutral", ""
|
||||
for raw in text.split("\n"):
|
||||
low = raw.strip().lower()
|
||||
if low.startswith("support:"): sup = self._num(raw)
|
||||
elif low.startswith("resistance_low:"): rlo = self._num(raw)
|
||||
elif low.startswith("resistance_high:"): rhi = self._num(raw)
|
||||
elif low.startswith("bias:"):
|
||||
v = low.split(":", 1)[1]
|
||||
bias = ("bullish" if "bull" in v else
|
||||
"bearish" if "bear" in v else "neutral")
|
||||
elif low.startswith("summary:"):
|
||||
summary = raw.split(":", 1)[1].strip()
|
||||
|
||||
ok, why = self._validate(price, sup, rlo, rhi)
|
||||
if not ok:
|
||||
with self._lock:
|
||||
self.error = f"unplausibel: {why}"
|
||||
log.warning(f"Tages-Level verworfen ({why}) — "
|
||||
f"sup={sup} rlo={rlo} rhi={rhi} bei Kurs {price:.2f}")
|
||||
return False
|
||||
|
||||
with self._lock:
|
||||
self.support, self.res_lo, self.res_hi = sup, rlo, rhi
|
||||
self.bias, self.summary = bias, summary
|
||||
self.ts, self.error = time.time(), None
|
||||
log.info(f"Tages-Level: Sup {sup:.2f} · Res {rlo:.2f}-{rhi:.2f} · "
|
||||
f"Bias {bias} · {summary[:70]}")
|
||||
return True
|
||||
except Exception as e:
|
||||
with self._lock:
|
||||
self.error = str(e)[:120]
|
||||
log.warning(f"DailyLevels.run: {e}")
|
||||
return False
|
||||
|
||||
def _validate(self, price, sup, rlo, rhi):
|
||||
if sup is None or rlo is None or rhi is None:
|
||||
return False, "Level fehlen"
|
||||
if not (sup < price < rhi):
|
||||
return False, "Reihenfolge support<Kurs<resistance verletzt"
|
||||
if rlo > rhi:
|
||||
return False, "res_low>res_high"
|
||||
for lv in (sup, rlo, rhi):
|
||||
if abs(lv - price) / price > _MAX_DEV:
|
||||
return False, f"{lv} >{_MAX_DEV*100:.0f}% vom Kurs entfernt"
|
||||
return True, ""
|
||||
|
||||
def zone_lines(self) -> list[float]:
|
||||
with self._lock:
|
||||
return [x for x in (self.support, self.res_lo, self.res_hi)
|
||||
if x is not None]
|
||||
|
||||
def snapshot(self) -> dict:
|
||||
with self._lock:
|
||||
return {"support": self.support, "res_lo": self.res_lo,
|
||||
"res_hi": self.res_hi, "bias": self.bias,
|
||||
"summary": self.summary, "ts": self.ts, "error": self.error}
|
||||
Reference in New Issue
Block a user