Files
AH-Oil-Trader/core/hl_walls.py
T
Axel HocksandClaude Opus 5 89e4af4561 HL-Kurs: Rohkurs anzeigen + Basis bei stehendem Broker einfrieren
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>
2026-08-02 17:42:28 +02:00

132 lines
7.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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