#!/usr/bin/env python3
"""Sagt die Hyperliquid-POSITIONIERUNG (Funding / Premium) den CFD voraus?

ANLASS (2026-08-06, User: „koennen wir mehr HL-Daten nutzen?"). Die Order-Flow-
Klasse ist zweimal unabhaengig gemessen und traegt nicht (`bookflow_report.txt`:
0,2–1,0 bp gegen eine ~3-bp-Kostenschwelle, KIPPT; `analyze_verdict_modules.py`:
Modul „Orderbuch" ueber 2.688 Episoden beidhaelftig negativ). Ungetestet ist eine
ANDERE Klasse: **Positionierung statt Preis.**

⚠ WARUM DAS NICHT AM LEAD-LAG SCHEITERT. Gemessen fuehrt **Pepperstone** (Peak bei
k=−1 ≈ 6 s), HL-Richtung trifft nach 6 s zu 51 %. Damit ist alles PREIS-Abgeleitete
von HL erledigt. Funding und Premium sind aber keine Preisprognose, sondern der
Preis dafuer, eine Position zu HALTEN — eine Bestandsgroesse. Der Lead-Lag-Einwand
greift dort nicht.

✅ UND ES IST SOFORT MESSBAR: `fundingHistory` liefert fuer `xyz:CL` **5.095
Stundenwerte ueber 7 Monate** (inkl. `premium`) — kein wochenlanges Sammeln noetig,
anders als beim Bookflow-Recorder.
⚠ **Open Interest ist NICHT dabei**: die HL-Info-API hat keinen OI-History-
Endpunkt (`metaAndAssetCtxs` liefert nur den Momentanwert). OI waere also
sammelpflichtig und bleibt hier offen.

GEMESSEN WIRD gegen den **Pepperstone-CFD** (das ist, was gehandelt wird), nicht
gegen den HL-Preis:
  · Forward-Return ueber 4 / 12 / 24 h, ATR-normiert
  · **gegen die DRIFT gelesen** — sonst faerbt das Regime alle Buckets ein
  · Buckets nach Funding bzw. Premium (Quintile)
  · **2 Haelften** (Jan–Apr / Apr–Aug)

VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
  Tragfaehig nur, wenn ein Bucket in BEIDEN Haelften denselben Vorzeichen-
  Ueberschuss zeigt UND der Betrag > 0,1×ATR liegt (B3-Huerde). Ein Effekt, der
  zwischen den Haelften kippt, ist Rauschen — daran sind 23 Signal-Ideen
  gescheitert.

Aufruf:  python analyze_hl_funding.py
"""
import bisect
import sys
import time
from datetime import datetime, timedelta, timezone

import MetaTrader5 as mt5

sys.path.insert(0, r"c:/Users/ah/Downloads/HyperLiquid-WTI Trader")
from core.hl_info import post, url_for          # noqa: E402

_COIN = "xyz:CL"
_BROKER_OFF = 3 * 3600        # MT5-Bar-Zeit ist Broker-Wallclock (UTC+3)
_HORIZONTE = (4, 12, 24)      # Stunden
_HUERDE = 0.10                # B3


def hole_funding() -> list[dict]:
    """Blaettert `fundingHistory` zurueck (500 je Antwort), dedupliziert."""
    u = url_for("mainnet")
    jetzt = int(time.time() * 1000)
    start = jetzt - 400 * 86400000
    aus: dict[int, dict] = {}
    for _ in range(15):
        r = post(u, {"type": "fundingHistory", "coin": _COIN,
                     "startTime": start}, timeout=20)
        if not r:
            break
        for x in r:
            aus[int(x["time"])] = x
        if len(r) < 500:
            break
        start = int(r[-1]["time"]) + 1
    return [aus[k] for k in sorted(aus)]


def _atr(H, L, C, n=14):
    tr = [0.0] + [max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))
                  for i in range(1, len(C))]
    out, s = [0.0] * len(C), 0.0
    for i in range(1, len(C)):
        s += tr[i]
        if i > n:
            s -= tr[i - n]
        out[i] = s / min(i, n)
    return out


def quintile(werte: list[float]) -> list[float]:
    s = sorted(werte)
    return [s[int(q * len(s))] for q in (0.2, 0.4, 0.6, 0.8)]


def main():
    fh = hole_funding()
    if len(fh) < 500:
        print(f"zu wenig Funding-Historie ({len(fh)})"); return 1

    if not mt5.initialize():
        print("MT5 nicht erreichbar"); return 1
    sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
                if mt5.symbol_info(c)), None)
    bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_H1, 0, 20000)
    mt5.shutdown()
    H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars]
    C = [float(b["close"]) for b in bars]
    # Bar-Zeit → ECHTE UTC-Epoch (die Rohwerte sind Broker-Wallclock)
    EP = [int(b["time"]) - _BROKER_OFF for b in bars]
    A = _atr(H, L, C)

    # ── Beobachtungen bauen ──────────────────────────────────────────────
    beob = []
    for x in fh:
        ts = int(x["time"]) // 1000
        j = bisect.bisect_right(EP, ts) - 1
        if j < 20 or j + max(_HORIZONTE) >= len(C) or A[j] <= 0:
            continue
        if abs(EP[j] - ts) > 2 * 3600:        # keine Bar in der Naehe (Wochenende)
            continue
        beob.append({"ts": ts, "j": j,
                     "fund": float(x["fundingRate"]),
                     "prem": float(x["premium"]),
                     "atr": A[j]})
    if len(beob) < 400:
        print(f"zu wenige verwertbare Beobachtungen ({len(beob)})"); return 1

    f = lambda s: datetime.fromtimestamp(s, timezone.utc).strftime("%d.%m.%Y")
    print("=" * 100)
    print(f"  HL-POSITIONIERUNG gegen den Pepperstone-CFD ({sym})")
    print("=" * 100)
    print(f"  {len(fh)} Funding-Stundenwerte · {len(beob)} mit passender H1-Bar")
    print(f"  {f(beob[0]['ts'])} bis {f(beob[-1]['ts'])} · 2 Hälften · "
          f"gegen die Drift gelesen · B3-Hürde {_HUERDE:.2f}×ATR")
    mitte = len(beob) // 2

    for feld, lbl in (("fund", "FUNDING-RATE"), ("prem", "PREMIUM (mark − oracle)")):
        print()
        print("=" * 100)
        print(f"  {lbl}")
        print("=" * 100)
        namen = ["Q1 (am negativsten)", "Q2", "Q3 (neutral)", "Q4",
                 "Q5 (am positivsten)"]
        for h in _HORIZONTE:
            # ⚠⚠ ENTÜBERLAPPEN. Die Beobachtungen kommen STÜNDLICH, das Fenster ist
            # h Stunden lang — aufeinanderfolgende Zeilen teilen sich h−1 Stunden
            # ihres Forward-Returns. Ungefiltert ist das kein n=506, sondern ~n/h
            # unabhängige Fälle, und jede „Robustheit" daraus ist Autokorrelation.
            # Deshalb nur jede h-te Beobachtung.
            print(f"  ── Horizont {h} h  (entüberlappt: jede {h}. Beobachtung) "
                  + "─" * 22)
            print(f"     {'Bucket':<22}{'H1 Übersch.':>14}{'H2 Übersch.':>14}"
                  f"{'n H1/H2':>12}  Urteil")
            for q in range(5):
                zeile = []
                for lo, hi in ((0, mitte), (mitte, len(beob))):
                    # ⚠ Quintile JE HÄLFTE. Global gebildet wandern die Grenzen mit
                    # dem Funding-Regime, und die Bucket-Zugehörigkeit wäre mit der
                    # Zeit konfundiert (real: Q5 hatte 506 Fälle in H1, aber nur 219
                    # in H2 — das misst den Regimewechsel, nicht das Merkmal).
                    alle = beob[lo:hi:h]
                    if len(alle) < 50:
                        zeile.append((None, 0)); continue
                    gr = quintile([b[feld] for b in alle])
                    bucket = lambda v: sum(1 for g in gr if v > g)
                    grp = [b for b in alle if bucket(b[feld]) == q]
                    if len(grp) < 15:
                        zeile.append((None, len(grp))); continue
                    r = lambda b: (C[b["j"] + h] - C[b["j"]]) / b["atr"]
                    m = sum(r(b) for b in grp) / len(grp)
                    drift = sum(r(b) for b in alle) / len(alle)
                    zeile.append((m - drift, len(grp)))
                (u1, n1), (u2, n2) = zeile
                if u1 is None or u2 is None:
                    print(f"     {namen[q]:<22}{'—':>14}{'—':>14}{f'{n1}/{n2}':>12}")
                    continue
                robust = (u1 > 0) == (u2 > 0) and min(abs(u1), abs(u2)) > _HUERDE
                print(f"     {namen[q]:<22}{u1:>+14.3f}{u2:>+14.3f}"
                      f"{f'{n1}/{n2}':>12}  {'✅ robust' if robust else ''}")
    print()
    print("=" * 100)
    print("  LESART: Überschuss = Kursbewegung MINUS der allgemeinen Drift der")
    print("  jeweiligen Hälfte. ✅ nur, wenn das Vorzeichen in BEIDEN Hälften gleich")
    print("  ist UND der Betrag über der Kostenhürde liegt.")
    print("  ⚠ Open Interest fehlt: die HL-Info-API hat dafür keine Historie.")
    print("  ⚠ `xyz:CL` ist mit ~3 Mio OI ein winziger Markt gegen NYMEX-CL — dass")
    print("    seine Positionierung einen globalen Benchmark vorhersagt, ist a priori")
    print("    unwahrscheinlich. Genau deshalb wird gemessen statt vermutet.")
    return 0


if __name__ == "__main__":
    sys.exit(main())
