#!/usr/bin/env python3
"""Verhält sich ECHTES CME-Volumen anders als die CFD-Tick-Zählung?

ANLASS (2026-08-08, User-Idee „Volume-Spike-Alert auf CL=F"). Das Projekt hat
„2× Volumen = echt" schon gemessen (`backtest_candles.py`): Überschuss
**−0,117 / −0,204** — *monoton in die FALSCHE Richtung*. Mehr Volumen bedeutet
eine STÄRKERE Gegenbewegung; der Spike ist die Klimax-Kerze, und der Kurs läuft
danach GEGEN sie. Ein naiver „viel Volumen → mitgehen"-Alert stupst also
messbar falsch.

⚠ ABER: jene Messung lief auf `tick_volume` des **CFD** — das ist eine Zählung
von Preisänderungen, KEIN gehandeltes Volumen. Der Broker ist Market Maker; was
dort als „Volumen" erscheint, hat mit den Kontrakten an der NYMEX nur mittelbar
zu tun. Genau das ist die offene Frage: trägt ECHTES Börsenvolumen (CL=F, CME)
eine andere Information?

⚠⚠ DESHALB LAUFEN BEIDE QUELLEN AUF DEMSELBEN FENSTER UND MIT DERSELBEN METHODE.
Der dokumentierte Wert −0,117/−0,204 stammt aus einem anderen Zeitraum und von
anderen Bars — ihn direkt danebenzustellen wäre derselbe Fehler wie am 07.08.,
als eine 6-Tage-Live-Stichprobe gegen ein 208-Tage-Mittel gehalten wurde.
Verglichen wird nur CME gegen CFD, Zeitpunkt für Zeitpunkt.

METHODE (wie `backtest_candles.py`):
  · Volumen-Verhältnis = V[i] / Ø(V[i−20 … i−1])
  · Forward-Return über 12 Bars (60 min), in ×ATR, VORZEICHEN = Kerzenrichtung
  · gelesen als ÜBERSCHUSS über die Drift des Zeitraums — sonst liest man in
    einem Trendfenster die Drift als Edge (die Lehre von `analyze_tageszeit.py`)
  · zwei Hälften des Fensters

⚠⚠ GRENZE, DIE VORAB ZU NENNEN IST: yfinance liefert für 5-min-Bars maximal
~60 Tage. Beide Hälften liegen damit im SELBEN Regime — genau die Konstellation,
an der ORB und die Dead-Hour-Stunde 13 gescheitert sind. **Ein ✅ heißt hier
„nicht ausgeschlossen", NICHT „bestätigt".** Ein ❌ ist dagegen belastbar.

Aufruf:  python backtest_cme_volumen.py
"""
from __future__ import annotations

import datetime as dt

import MetaTrader5 as mt5

_FWD = 12          # 12 M5-Bars = 60 min
_LOOK = 20         # Fenster fuer das Durchschnittsvolumen
_BUCKETS = ((0, 1.5), (1.5, 2.0), (2.0, 2.5), (2.5, 99))
_BROKER_OFF = 10800    # Brokerzeit = UTC+3


def atr_series(H, L, C, p=14):
    tr = [0.0]
    for i in range(1, len(C)):
        tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
    out = [None]
    for i in range(1, len(C)):
        seg = tr[max(1, i - p + 1):i + 1]
        out.append(sum(seg) / len(seg))
    return out


def hole_cme():
    """CL=F 5-min von yfinance. ⚠ MultiIndex-Spalten sind hier NORMAL — das
    `.iloc[0]`-Muster im User-Skript ist deshalb korrekt, kein Fehler."""
    import yfinance as yf
    df = yf.download("CL=F", interval="5m", period="60d",
                     progress=False, auto_adjust=False)
    if df is None or df.empty:
        return None
    reihen = []
    for ts, r in df.iterrows():
        def f(sp):
            v = r[sp]
            return float(v.iloc[0] if hasattr(v, "iloc") else v)
        try:
            reihen.append((int(ts.timestamp()), f("High"), f("Low"),
                           f("Close"), f("Volume")))
        except Exception:
            continue
    return reihen


def hole_cfd(von_ts, bis_ts):
    """SpotCrude M5 aus MT5, auf dasselbe Zeitfenster beschnitten.
    ⚠ `time` ist Broker-Wallclock (UTC+3) — fuer den Abgleich in echte
    UTC-Epoch umrechnen (die im Projekt mehrfach dokumentierte Falle)."""
    mt5.initialize()
    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_M5, 0, 30000)
    mt5.shutdown()
    if bars is None:
        return None, sym
    out = []
    for b in bars:
        t = int(b["time"]) - _BROKER_OFF
        if von_ts - 300 <= t <= bis_ts + 300:
            out.append((t, float(b["high"]), float(b["low"]),
                        float(b["close"]), float(b["tick_volume"])))
    return out, sym


def auswerten(reihen, name):
    """Ueberschuss ueber die Drift je Volumen-Bucket, in zwei Haelften."""
    T = [r[0] for r in reihen]; H = [r[1] for r in reihen]
    L = [r[2] for r in reihen]; C = [r[3] for r in reihen]
    V = [r[4] for r in reihen]
    AT = atr_series(H, L, C)
    mid = len(C) // 2
    print(f"\n  ── {name} ── {len(C)} Bars, "
          f"{dt.datetime.utcfromtimestamp(T[0]):%d.%m.} bis "
          f"{dt.datetime.utcfromtimestamp(T[-1]):%d.%m.}")

    for hn, lo, hi in (("H1", _LOOK + 15, mid), ("H2", mid, len(C) - _FWD - 1)):
        # Drift des Fensters: mittlerer Forward-Return OHNE Richtungsvorzeichen
        drift = []
        for i in range(lo, hi):
            a = AT[i]
            if a and a > 0:
                drift.append((C[i + _FWD] - C[i]) / a)
        d_mean = sum(drift) / len(drift) if drift else 0.0
        print(f"     {hn}  Drift des Fensters: {d_mean:+.3f}×ATR "
              f"(darueber wird gelesen)")
        print(f"        {'Volumen':<14}{'n':>7}{'Ueberschuss':>14}{'Treffer':>10}")
        for b_lo, b_hi in _BUCKETS:
            werte = []
            for i in range(lo, hi):
                a = AT[i]
                if not a or a <= 0:
                    continue
                vs = V[max(0, i - _LOOK):i]
                if len(vs) < _LOOK:
                    continue
                m = sum(vs) / len(vs)
                if m <= 0:
                    continue
                q = V[i] / m
                if not (b_lo <= q < b_hi):
                    continue
                d = 1 if C[i] > (H[i] + L[i]) / 2 else -1     # Kerzenrichtung
                r = (C[i + _FWD] - C[i]) * d / a
                werte.append(r - d * d_mean)                  # Ueberschuss
            if len(werte) < 25:
                print(f"        {f'{b_lo}–{b_hi}x':<14}{len(werte):>7}{'zu wenig':>14}")
                continue
            m = sum(werte) / len(werte)
            tr = 100 * sum(1 for x in werte if x > 0) / len(werte)
            print(f"        {f'{b_lo}–{b_hi}x':<14}{len(werte):>7}{m:>+14.3f}{tr:>9.0f}%")


def main():
    print("=" * 84)
    print("  ECHTES CME-VOLUMEN gegen CFD-TICK-ZAEHLUNG — gleiches Fenster, gleiche Methode")
    print("=" * 84)
    cme = hole_cme()
    if not cme or len(cme) < 500:
        print(f"  CL=F lieferte zu wenig ({0 if not cme else len(cme)} Bars)"); return
    cfd, sym = hole_cfd(cme[0][0], cme[-1][0])
    if not cfd or len(cfd) < 500:
        print(f"  {sym} lieferte zu wenig ({0 if not cfd else len(cfd)} Bars)"); return

    print(f"\n  Fenster: {dt.datetime.utcfromtimestamp(cme[0][0]):%d.%m.%Y} bis "
          f"{dt.datetime.utcfromtimestamp(cme[-1][0]):%d.%m.%Y} (UTC)")
    print(f"  CL=F (CME, echte Kontrakte): {len(cme)} Bars")
    print(f"  {sym} (CFD, tick_volume):    {len(cfd)} Bars")
    print("\n  ⚠ ~60 Tage = EIN Regime. Ein ✅ heisst 'nicht ausgeschlossen',")
    print("    NICHT 'bestaetigt'. Ein ❌ ist dagegen belastbar.")
    print("\n  Erwartung aus der Vormessung (CFD, anderer Zeitraum): der Ueberschuss")
    print("  wird mit steigendem Volumen NEGATIVER (Klimax, Kurs laeuft dagegen).")

    auswerten(cme, "CL=F — echtes CME-Kontraktvolumen")
    auswerten(cfd, f"{sym} — CFD-tick_volume (Kontrolle)")


if __name__ == "__main__":
    main()
