#!/usr/bin/env python3
"""backtest_volume_profile.py — Volume Profile / echte Liquiditätszonen (B2,
User-Frage 2026-07-30 „System zur besseren Vorhersage — Liquiditätslinien?").

WARUM diese Klasse überhaupt getestet wird (die anderen sind durch): Order Blocks /
SMC-„Liquiditätslinien" sind strukturell Pivot-/Swing-Zonen — dieselbe Klasse, die
hier schon 5× durchfiel (`backtest_structure.py`, `backtest_doubletop.py`,
`backtest_patterns.py`, P(break)-Entry, `backtest_gaps.py`). Das Volume Profile ist
eine ANDERE Datenquelle: nicht „wo war ein Docht", sondern **wo wurde tatsächlich
gehandelt** (Akzeptanz). Damit ist es legitim testbar statt Runde 6.

Begriffe:
  · POC (Point of Control) = Preis-Bin mit dem MEISTEN Volumen im Fenster
  · HVN (High Volume Node) = viel Volumen  → „Akzeptanz", Kurs verweilt/prallt ab
  · LVN (Low Volume Node)  = wenig Volumen → „Ablehnung", Kurs läuft schnell durch
  · VA (Value Area)        = engster Preisbereich, der ~70 % des Volumens enthält

Aufbau: feste globale Preis-Bins (`_BIN`) → rollierendes Fenster (`_WIN` Bars),
Bar-Volumen gleichmäßig über [low, high] verteilt (Standard-Approximation).
Volumen = `tick_volume` (CFD liefert kein real_volume — bei MT5-CFDs Standard-Proxy,
korreliert gut mit echtem Volumen).

TEIL 1 — INFORMATION (hat die Volumen-Position überhaupt Vorhersagewert?):
  Forward-Return (2 h, in ATR) gebucketet nach Kursposition im Profil. Kein Exit,
  keine Kosten → isoliert die reine Information. Trägt nur, wenn ein Bucket in
  BEIDEN Hälften konsistent abweicht.

TEIL 2 — HANDELBAR (sequenzielle 1-Positions-Sim, Live-Exit, Echtkosten):
  (A) Mean-Reversion am POC/HVN: Kurs erreicht die Zone → Gegenrichtung
      (= die „Liquiditätslinie hält"-Idee)
  (B) Momentum durch LVN: Kurs betritt eine Leerzone → in Bewegungsrichtung
      (= „Liquidity Void, läuft schnell durch")
  Maßstab: der validierte Squeeze (ØR +0,14…+0,23, PF>1). Beide Hälften positiv,
  sonst raus.

Aufruf:  python backtest_volume_profile.py [n_bars]
"""
from __future__ import annotations
import sys
from collections import defaultdict

import MetaTrader5 as mt5

_BIN      = 0.05     # Preis-Bin-Größe ($) — bei WTI ~85 = 0,06 %, ~0,25×ATR
_WIN      = 288      # Profil-Fenster (M5-Bars) = 24 h
_FWD      = 24       # Forward-Return-Horizont (Bars) = 2 h
_MAXH     = 288      # Sim-Horizont
_ATRMIN   = 0.06
_SL_ATR, _TRAIL, _TRAIL_ON, _BE_ON, _TIMESTOP = 2.0, 1.5, 0.3, 1.3, 24
_COOL     = 12       # Cooldown zwischen Trades (Bars)
_VA_FRAC  = 0.70     # Value Area = 70 % des Volumens


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] * len(C); run = 0.0
    for i in range(1, len(C)):
        run += tr[i]
        if i > p: run -= tr[i - p]
        out[i] = run / min(i, p)
    return out


def _bins_of(lo, hi):
    """Bin-Indizes, die eine Bar-Range überdeckt (mind. 1)."""
    a, b = int(lo / _BIN), int(hi / _BIN)
    return range(a, b + 1)


class Profile:
    """Rollierendes Volume Profile über feste Preis-Bins (effizient: add/remove)."""
    def __init__(self):
        self.vol = defaultdict(float)

    def add(self, lo, hi, v):
        bs = _bins_of(lo, hi); share = v / max(1, len(bs))
        for b in bs: self.vol[b] += share

    def remove(self, lo, hi, v):
        bs = _bins_of(lo, hi); share = v / max(1, len(bs))
        for b in bs:
            self.vol[b] -= share
            if self.vol[b] <= 1e-9: del self.vol[b]

    def stats(self):
        """(poc_bin, total, sorted_bins_desc) — None wenn leer."""
        if not self.vol: return None
        items = sorted(self.vol.items(), key=lambda kv: -kv[1])
        return items[0][0], sum(self.vol.values()), items

    def classify(self, price):
        """Wo liegt `price` im Profil? → (label, rel_vol, in_va)
        rel_vol = Volumen des Preis-Bins / POC-Volumen (1,0 = am POC)."""
        s = self.stats()
        if not s: return None, 0.0, False
        poc, total, items = s
        pv = items[0][1]
        b = int(price / _BIN)
        v = self.vol.get(b, 0.0)
        rel = v / pv if pv > 0 else 0.0
        # Value Area: Bins absteigend nach Volumen aufsummieren bis _VA_FRAC
        acc = 0.0; va = set()
        for bb, vv in items:
            va.add(bb); acc += vv
            if acc >= _VA_FRAC * total: break
        in_va = b in va
        if b == poc:      lab = "POC"
        elif rel >= 0.60: lab = "HVN"
        elif rel <= 0.15: lab = "LVN"
        else:             lab = "mid"
        return lab, rel, in_va


def _sim(entry, d, atr, H, L, C, j0):
    eff = entry - d * _SL_ATR * atr; hw = entry; started = False
    end = min(j0 + _MAXH, len(C) - 1); exit_px = C[end]
    for j in range(j0, end + 1):
        if (L[j] <= eff) if d > 0 else (H[j] >= eff): exit_px = eff; break
        hw = max(hw, H[j]) if d > 0 else min(hw, L[j])
        prof = (C[j] - entry) * d
        if prof >= _TRAIL_ON * atr:
            started = True
            cand = hw - d * _TRAIL * atr
            if prof >= _BE_ON * atr:
                cand = max(cand, entry) if d > 0 else min(cand, entry)
            eff = max(eff, cand) if d > 0 else min(eff, cand)
        if not started and (j - j0) >= _TIMESTOP: exit_px = C[j]; break
    return (exit_px - entry) * d / atr


def _rep(name, Rs, kind="R"):
    if not Rs or len(Rs) < 5:
        print(f"  {name:<30} n={len(Rs):>4}  (zu wenige)"); return None
    n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
    up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0)
    pf = up / dn if dn > 0 else 9.99
    print(f"  {name:<30} n={n:>5} pos={100*w/n:>3.0f}% Ø{kind}={s/n:+.3f} "
          f"PF={pf:.2f} Σ={s:+.0f}")
    return s / n


def main():
    n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
    mt5.initialize()
    sym = None
    for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"):
        if mt5.symbol_info(c): sym = c; break
    bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
    si = mt5.symbol_info(sym); point = si.point
    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]; V = [float(b["tick_volume"]) for b in bars]
    SP = [float(b["spread"]) * point for b in bars]
    A = _atr_series(H, L, C)
    N = len(C); mid = N // 2

    print("=" * 92)
    print(f"  VOLUME PROFILE / Liquiditätszonen — {sym} M5 ({N} Bars)")
    print(f"  Bin={_BIN}$ · Fenster={_WIN} Bars (24h) · Volumen=tick_volume · VA={_VA_FRAC:.0%}")
    print("=" * 92)

    # ── Profil einmal durchlaufen und je Bar die Klassifikation cachen ──────────
    prof = Profile()
    lab_at = [None] * N; rel_at = [0.0] * N; poc_at = [None] * N; inva_at = [False] * N
    for i in range(N):
        prof.add(L[i], H[i], V[i])
        if i >= _WIN: prof.remove(L[i - _WIN], H[i - _WIN], V[i - _WIN])
        if i >= _WIN:
            lab, rel, in_va = prof.classify(C[i])
            lab_at[i] = lab; rel_at[i] = rel; inva_at[i] = in_va
            s = prof.stats()
            poc_at[i] = (s[0] + 0.5) * _BIN if s else None

    # ── TEIL 1: INFORMATION (Forward-Return je Bucket, ohne Exit/Kosten) ────────
    print("\nTEIL 1 — INFORMATION: Forward-Return 2 h (in ATR) je Kursposition im Profil")
    print("  (positiv/negativ = Richtungs-Drift; 'pos%' = Anteil Aufwärts)")
    for label, lo, hi in (("H1 (alt)", _WIN, mid), ("H2 (neu)", mid, N - _FWD - 1)):
        print(f"\n{label}:")
        buckets = defaultdict(list)
        for i in range(lo, hi):
            atr = A[i]
            if not atr or atr < _ATRMIN or lab_at[i] is None: continue
            fwd = (C[i + _FWD] - C[i]) / atr
            buckets[lab_at[i]].append(fwd)
            buckets["in VA" if inva_at[i] else "außerhalb VA"].append(fwd)
        for k in ("POC", "HVN", "mid", "LVN", "in VA", "außerhalb VA"):
            if k in buckets: _rep(k, buckets[k], kind="fwd")

    # ── TEIL 2: HANDELBAR ──────────────────────────────────────────────────────
    print("\n" + "=" * 92)
    print("TEIL 2 — HANDELBAR (seq. Sim · Live-Exit · Echtkosten) · Maßstab Squeeze +0,14…+0,23")
    def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225) / atr

    verdict = defaultdict(list)
    for label, lo, hi in (("H1 (alt)", _WIN, mid), ("H2 (neu)", mid, N)):
        print(f"\n{label}:")
        # (A) Mean-Reversion am POC/HVN — Kurs erreicht die Zone von außen
        for zone in ("POC", "HVN"):
            Rs = []; i = max(lo, _WIN + 2)
            while i < min(hi, N - _MAXH - 2):
                atr = A[i]
                if not atr or atr < _ATRMIN or lab_at[i] is None: i += 1; continue
                # frischer Eintritt in die Zone (vorher NICHT drin)
                if lab_at[i] == zone and lab_at[i - 1] != zone:
                    # Gegenrichtung zur Anlaufbewegung (prallt ab)
                    d = -1 if C[i] > C[i - 3] else 1
                    Rs.append(_sim(C[i], d, atr, H, L, C, i + 1) - cost(i, atr))
                    i += _COOL; continue
                i += 1
            verdict[f"A {zone}-Reversion"].append(_rep(f"A) Reversion am {zone}", Rs))
        # (B) Momentum durch LVN — Kurs betritt Leerzone, läuft weiter
        Rs = []; i = max(lo, _WIN + 2)
        while i < min(hi, N - _MAXH - 2):
            atr = A[i]
            if not atr or atr < _ATRMIN or lab_at[i] is None: i += 1; continue
            if lab_at[i] == "LVN" and lab_at[i - 1] != "LVN":
                d = 1 if C[i] > C[i - 3] else -1        # in Bewegungsrichtung
                Rs.append(_sim(C[i], d, atr, H, L, C, i + 1) - cost(i, atr))
                i += _COOL; continue
            i += 1
        verdict["B LVN-Momentum"].append(_rep("B) Momentum durch LVN", Rs))

    print("\n" + "=" * 92)
    print("  URTEIL (trägt nur, wenn ØR in BEIDEN Hälften > 0 UND >= +0,14 = Squeeze-Band):")
    any_ok = False
    for k, vals in verdict.items():
        if len(vals) != 2 or any(v is None for v in vals):
            print(f"  {k:<28} unvollständig"); continue
        ok = all(v > 0 for v in vals); band = all(v >= 0.14 for v in vals)
        mark = "✅ TRÄGT" if (ok and band) else ("⚠ positiv, unter Band" if ok else "❌ fällt durch")
        if ok and band: any_ok = True
        print(f"  {k:<28} H1 {vals[0]:+.3f} · H2 {vals[1]:+.3f}   {mark}")
    print("\n  " + ("→ Kandidat gefunden: Parameter-Robustheit prüfen, dann klein live (B4)."
                    if any_ok else
                    "→ KEINE Variante trägt beidhälftig im Squeeze-Band → Volume Profile "
                    "bleibt Kontext, kein Signal."))


if __name__ == "__main__":
    main()
