"""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 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")} 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: self._cache = data self._cache_ts = now return data