"""core/hl_walls.py — ECHTE Liquiditäts-Wände von Hyperliquid für den MT5-Chart. Holt vom HL-Dashboard (Port 8001, `GET /api/walls`) das größte L2-Orderbuch-Level je Seite — **basis-korrigiert auf das Pepperstone-Preisniveau** — und stellt es dem Level-Export (`engine._write_levels_file` → `sr_levels.csv` → `SR_Levels.mq5`) bereit. WARUM (User-Wunsch 2026-07-30): Alle S/R-Linien im Projekt sind aus **Pivots geschätzt** („wo war mal ein Docht"). Das hier ist die einzige Quelle mit **tatsächlich ruhenden Orders** — Pepperstone ist Market Maker und liefert kein Orderbuch, Hyperliquid schon (20 Levels je Seite). ⚠ Zwei Dinge im Blick behalten: 1. **Basis-Differenz**: HL `xyz:CL` notiert ~0,45 $ unter dem CFD (anderer Kontrakt/Contango). Die Umrechnung passiert SERVERSEITIG im HL-Projekt (Median über die letzten 60 Snapshots) — hier kommt schon `bid_mt5`/`ask_mt5` an. Fehlt die Basis (zu wenige Snapshots), wird NICHT gezeichnet. 2. **Anzeige, KEIN Signal.** Ob HL-Orderbuch-Signale auf den CFD übertragbar sind, ist eine offene Frage und wird gerade gemessen (`analyze_bookflow.py` im HL-Projekt). Bis dahin fließt hier nichts in Empfehlung/Trade ein. Fail-safe: kurzer Timeout, Ergebnis gecacht; ist der HL-Server aus, liefert `walls()` einfach None und der Chart bleibt unverändert. """ from __future__ import annotations import json import threading import time import urllib.request from core.logger import get_logger log = get_logger("hlwalls") _TIMEOUT_S = 2.0 # der MT5-Loop darf nicht am HTTP hängen _TTL_S = 10.0 # Cache — die Wände wandern langsamer als der Export-Takt (~5 s) _MAX_AGE_S = 60.0 # älter = HL-Feed steht → nicht mehr zeichnen _STALE_GRACE_S = 120.0 # nach einem Fehlschlag den letzten guten Stand so lange # weiterreichen (Blink-Fix — s. Kommentar in `walls()`) class HLWalls: def __init__(self, url: str = "http://127.0.0.1:8001/api/walls", enabled: bool = False): self.url = url self.enabled = bool(enabled) self._lock = threading.Lock() self._cache: dict | None = None self._cache_ts = 0.0 self._fail_logged = False def walls(self) -> dict | None: """{'bid','bid_sz','ask','ask_sz','basis','imb5'} auf MT5-Preisniveau — oder None (aus / HL nicht erreichbar / Basis unbekannt / Daten zu alt).""" if not self.enabled: return None now = time.time() with self._lock: if self._cache is not None and now - self._cache_ts < _TTL_S: return self._cache data = None try: with urllib.request.urlopen(self.url, timeout=_TIMEOUT_S) as r: d = json.loads(r.read().decode()) if d.get("ok") and d.get("bid_mt5") and d.get("ask_mt5"): # Feed-Alter prüfen: ein stehengebliebenes Buch darf nicht als # aktuelle Liquidität gezeichnet werden. age = now - float(d.get("ts") or 0) if age <= _MAX_AGE_S: data = {"bid": float(d["bid_mt5"]), "ask": float(d["ask_mt5"]), "bid_sz": float(d.get("bid_sz") or 0), "ask_sz": float(d.get("ask_sz") or 0), "basis": d.get("basis"), "imb5": d.get("imb5"), # Wirkrichtung je Wand (Pfeil im Chart) + größere Wand. # ⚠ Lesart „die Wand hält" — INTERPRETATION, nicht gemessen. "arrow_bid": d.get("arrow_bid") or "up", "arrow_ask": d.get("arrow_ask") or "down", "dominant": d.get("dominant"), # Liquiditäts-Trendlinie (Regression über den volumen- # gewichteten Wand-Schwerpunkt): {t1,p1,t2,p2,dir,slope_per_h} # — Zeiten sind LOKALE Epoch, der Aufrufer rechnet sie in # Broker-Zeit um (MT5-Charts brauchen Broker-Zeit). "trend": d.get("trend")} self._fail_logged = False except Exception as e: if not self._fail_logged: # nur 1× melden, nicht jeden Tick log.info(f"HL-Wände nicht abrufbar ({e}) — Chart bleibt ohne " f"Liquiditätslinien (HL-Dashboard auf 8001 läuft nicht?)") self._fail_logged = True with self._lock: if data is not None: self._last_good = data self._last_good_ts = now elif (getattr(self, "_last_good", None) is not None and now - getattr(self, "_last_good_ts", 0.0) < _STALE_GRACE_S): # ⚠ BLINK-FIX (2026-07-30, User „die Liquidationspfeile blinken nur # manchmal auf"): ein einzelner Fehlschlag (HTTP-Timeout, HL-Server # kurz busy, Snapshot minimal zu alt) darf die Pfeile NICHT löschen. # Ohne den letzten guten Wert fehlen die LQ-Zeilen in der CSV → der # Indikator löscht beim Redraw alle Objekte und zeichnet sie nicht neu # → Pfeil weg, beim nächsten Erfolg wieder da = sichtbares Blinken. # Innerhalb der Grace wird der letzte gute Stand weitergereicht; # erst danach verschwinden die Pfeile wirklich (echter Ausfall). data = self._last_good self._cache = data self._cache_ts = now return data