User-Fund: app.hyperliquid.xyz zeigt WTIOIL-USDC bei 81,392, das Dashboard 86,424. Dahinter steckten zwei Fehler. (1) Falsches Label (vom 01.08.): angezeigt wurde mid_mt5 = HL + Basis, also der auf Pepperstone-Niveau umgerechnete Wert - unter der Beschriftung "HYPERLIQUID KURS". Jetzt steht dort der ROHE HL-Kurs (exakt die Zahl von der HL-Seite); das CFD-Aequivalent ist in den Tooltip gewandert. (2) Die Basis-Korrektur war bei geschlossenem Markt ZIRKULAER. Gegenstueck im HL-Projekt (eigener Commit): die Basis wird jetzt auf dem letzten Stand mit lebendem Broker eingefroren und als basis_stale durchgereicht. Belegt aus leadlag.db: Basis bei lebendem Broker konstant +0,42...+0,58, nach dem Freeze +5,03. Der Wochenend-Move war echt (So 03:00->05:00 von 85,51 auf 80,84, danach 13 h stabil um 81 bei ~600 Messungen/h). Materielle Folge: die offene SHORT-Position (0,57 ab 86,163, TP 84,123) wurde mit -15,02 EUR angezeigt; mit eingefrorener Basis sind es +211 EUR, und der TP liegt 2,3 $ ueber dem echten Niveau. Vorbehalt bleibt (Oracle-Perp) -> weiter "≈". Ausserdem dokumentiert: restart_server.bat hat zweimal still NICHT neu gestartet. Eine korrekt ausgelieferte app.js?v=N belegt NICHTS ueber den geladenen Python-Code (statische Dateien werden je Request von der Platte gelesen). Zuverlaessig nur: Prozess killen, direkt starten, danach an einem NEUEN Snapshot-Feld pruefen. v=132. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
132 lines
7.4 KiB
Python
132 lines
7.4 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"),
|
||
# ── Ergänzt 2026-08-01 (User: „blende den Hyperliquid-
|
||
# Kurs neben den Pepperstone-Kurs ein"): der HL-MITTELKURS
|
||
# auf MT5-Niveau. HL `xyz:CL` notiert wegen anderem
|
||
# Kontrakt/Contango ~0,45 $ unter dem CFD — `basis` ist die
|
||
# serverseitig gemessene Differenz (Median über 60
|
||
# Snapshots), `mid_mt5` also direkt mit dem Broker-Kurs
|
||
# vergleichbar. Nützlich vor allem am Wochenende: HL
|
||
# handelt 24/7, Pepperstone nicht.
|
||
"hl_mid": (float(d["hl_mid"])
|
||
if d.get("hl_mid") is not None else None),
|
||
"mid_mt5": (round(float(d["hl_mid"])
|
||
+ float(d.get("basis") or 0.0), 3)
|
||
if d.get("hl_mid") is not None else None),
|
||
# ⚠ `basis_stale` (2026-08-02): der Broker tickt nicht mehr,
|
||
# die Basis ist auf dem letzten Stand mit LEBENDEM Markt
|
||
# eingefroren. Vorher rechnete die Gegenseite die Basis
|
||
# blind aus den letzten 60 Snapshots — bei stehendem Broker
|
||
# sog sie den kompletten HL-Wochenend-Move auf und
|
||
# `hl_mid + basis` ergab wieder den EINGEFRORENEN Broker-
|
||
# Kurs (real 86,41 statt 81,89, Differenz zum Broker-Bid
|
||
# nur 0,081 = zirkulär). Alles, was daraus abgeleitet wird,
|
||
# ist bei `basis_stale` eine SCHÄTZUNG und wird so markiert.
|
||
"basis_stale": bool(d.get("basis_stale")),
|
||
"mt5_still_s": d.get("mt5_still_s"),
|
||
"age_s": round(age, 1)}
|
||
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
|