User: "wenn Pepperstone closed ist trotzdem Kurs und Liquiditaet von
Hyperliquid einbinden" -> nachgeschaerft zu "blende den HL-Kurs NEBEN den
Pepperstone-Kurs ein" (ehrlicher als ein stiller Tausch) -> plus "bei
geschlossenem Markt auch den G/V mit HL berechnen".
Snapshot: hl_live {mid_mt5, hl_mid, basis, age_s, bid/ask, bid_sz/ask_sz,
imb5, trend, slope_per_h, vs_mt5} - IMMER befuellt. mid_mt5 = HL-Mid +
gemessene Basis, also direkt mit dem Broker-Kurs vergleichbar. Dazu
market_closed, tick_age_s, pnl_hl.
core/hl_walls.py liefert jetzt zusaetzlich hl_mid/mid_mt5/age_s.
Dashboard v=129: eigene Header-Box "HL" neben KURS mit Tooltip (Rohkurs,
Basis, Alter, Abweichung). Bei geschlossenem Markt: Label "KURS · ZU",
Session-Zeile "MARKT ZU", und das G/V wechselt auf die HL-Schaetzung, mit
"≈" markiert.
MT5 v1.34: HL;<mt5_aequiv>;<alter>;<bid>;<ask>;<U|D|-> -> bernsteinfarbenes
Label unter Kurs und G/V (InpShowHL/InpHLColor).
DREI SACKGASSEN dokumentiert, damit sie niemand wiederholt:
1) tick_local_ts ist die ABHOLZEIT, nicht das Tick-Alter.
2) tick_server_ts minus _broker_offset_s() ist ZIRKULAER - die
Offset-Funktion leitet den Versatz selbst aus dem letzten Tick ab und
misst bei geschlossenem Markt die Veraltung (-7,41 h statt +3 h); das
Alter hebt sich auf (-347 s).
3) pnl / Kursbewegung als EUR-je-Punkt ergab 155 statt ~86, weil
trader.pnl den Wochenend-Swap enthaelt.
Verwendet wird stattdessen: Markt zu = der Kurs hat sich seit 180 s nicht
BEWEGT (_last_bid/_last_bid_move_ts). Und der G/V nutzt die vorhandene,
korrekte trader.live_pnl(bid, ask) mit dem HL-Kurs +- halbem Spread.
Live verifiziert: market_closed=True nach 198 s, Broker 86.333 eingefroren,
HL 86.349 (7 s alt), Broker-P&L -15,02 -> HL-Schaetzung -14,37 EUR.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
121 lines
6.5 KiB
Python
121 lines
6.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"),
|
||
# ── 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),
|
||
"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
|