Pfeil-Bugfix (User: "liquiditaetspfeile werden immer noch nicht angezeigt"):
(1) ANCHOR_LEFT ist fuer OBJ_ARROW UNGUELTIG (nur TOP/BOTTOM)
(2) Pfeile sitzen rechts neben der letzten Kerze -> ohne CHART_SHIFT kein
sichtbarer Bereich dort
Blink-Fix (User: "blinken nur manchmal auf"): hl_walls setzte den Cache bei jedem
Fehlschlag auf None -> LQ-Zeilen fehlten -> Indikator loeschte die Pfeile. Jetzt
letzter guter Stand 120s Grace.
Neu (alles reine Anzeige):
- TR;<O|C>;zeit -> Kerze der Eroeffnung HELLGRUEN, des Close DUNKELGRUEN umrandet
- EQ;<H|L>;lo;hi;zeit -> Equal Highs/Lows aus M30-Pivots als goldenes Band +
gestrichelte Kante (wie im User-Referenzbild). Als Signal gemessen verworfen.
- PX;bid -> aktueller Kurs fett oben rechts
- Beschriftung der Liquiditaets-Trendlinie per Default aus (v1.16)
Alles kompiliert und live in sr_levels.csv verifiziert.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
107 lines
5.5 KiB
Python
107 lines
5.5 KiB
Python
"""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
|