#!/usr/bin/env python3
"""SL-Breite: ATR_M15 (live) gegen ATR_M5 (kanonisch) — 2026-08-04.

Anlass: `analyze_manual_close.py` zeigte, dass die 69 per Broker-SL geschlossenen
Trades mit dem kanonischen Exit **1.229 € weniger** verloren hätten. Der dortige
Confounder war explizit benannt: `simulate()` setzt den Initial-SL auf
**2,0×ATR_M5**, live kommt er aus **ATR_M15** (`_calc_sl_tp`, SL_TF fix M15) und
ist damit oft deutlich weiter — unter Margin-Sizing schlägt das voll durch.
Diese Messung trennt beides.

⚠ `backtest_sl_tf_mismatch.py` hat die M5-Umstellung 2026-07-23 schon geprüft und
wegen H2 verworfen — aber NUR auf Squeeze-Entries und mit einem eigenen
Exit-Nachbau. Hier: die **gegatete Live-Signal-Population** und der **kanonische**
Exit. Damit ist es keine Wiederholung, sondern die Messung unter den Bedingungen,
unter denen der Bot tatsächlich läuft.

Variiert wird AUSSCHLIESSLICH die Breite des Initial-SL; Trailing, Breakeven,
Time-Stop und TP bleiben identisch — sonst wäre nicht zuzuordnen, was wirkt.
Alle Ergebnisse in **ATR_M5-Einheiten**, damit die Varianten vergleichbar sind.

Aufruf:  python backtest_sl_width.py [bars]
"""
import dataclasses
import statistics as stx
import sys
from bisect import bisect_left, bisect_right

import MetaTrader5 as mt5

from core.analysis import calc_trend_angle
from core.exit_model import LIVE, simulate
from core.wave_rec import (WaveRecommender, _ANGLE_LR, _EMA_FAST, _EMA_SLOW,
                           _HTF_DEADBAND, _ema_series)

_ATRMIN = 0.06
_COOL = 3
_PIVK = 3
_PIV_LOOK = 300


class _TU:
    def snapshot(self):
        return {}


def _atr_series(H, L, C, p=14):
    t = [0.0]
    for i in range(1, len(C)):
        t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
    return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1])))
            if i else None for i in range(len(C))]


def _atr_m15(H, L, C, N):
    """ATR_M15 je M5-Bar (kausal): aus 3er-Aggregaten, auf M5 zurückgemappt."""
    HH = [max(H[i:i + 3]) for i in range(0, N - 2, 3)]
    LL = [min(L[i:i + 3]) for i in range(0, N - 2, 3)]
    CC = [C[min(i + 2, N - 1)] for i in range(0, N - 2, 3)]
    a15 = _atr_series(HH, LL, CC)
    out = [None] * N
    for j, v in enumerate(a15):
        for k in range(j * 3, min(j * 3 + 3, N)):
            out[k] = v
    return out


def _htf_sign(C, i, n, atr):
    lo = max(0, i - n * (_EMA_SLOW + 5))
    sub = C[lo:i + 1:n]
    if len(sub) < _EMA_SLOW + 2 or not atr:
        return 0
    d = (_ema_series(sub, _EMA_FAST)[-1] - _ema_series(sub, _EMA_SLOW)[-1]) / atr
    return 0 if abs(d) < _HTF_DEADBAND else (1 if d > 0 else -1)


def st(Rs):
    if not Rs:
        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)
    return dict(n=n, wr=100 * w / n, oR=s / n, pf=(up / dn if dn > 0 else 9.99),
                sum=s, worst=min(Rs))


def main():
    n_req = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
    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, n_req)
    point = mt5.symbol_info(sym).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]; SP = [float(b["spread"]) * point for b in bars]
    N = len(C); mid = N // 2
    A = _atr_series(H, L, C)
    A15 = _atr_m15(H, L, C, N)
    EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)

    verh = [A15[i] / A[i] for i in range(200, N)
            if A[i] and A15[i] and A[i] >= _ATRMIN]
    print(f"ATR_M15 / ATR_M5 — Median {stx.median(verh):.2f}   "
          f"10./90. Perzentil {sorted(verh)[len(verh)//10]:.2f} / "
          f"{sorted(verh)[9*len(verh)//10]:.2f}")
    print(f"→ Der Live-SL (2,0×ATR_M15) ist im Median rund "
          f"{2.0*stx.median(verh):.1f}×ATR_M5 breit, der kanonische 2,0.\n")

    print("Vorberechnung (Winkel, HTF, Pivots) ...", flush=True)
    AD = [90.0] * N
    for i in range(_ANGLE_LR + 2, N):
        AD[i] = calc_trend_angle(C[i - (_ANGLE_LR + 1):i + 1], _ANGLE_LR)
    HTF = [0] * N; H1 = [0] * N
    for i in range(N):
        if A[i]:
            HTF[i] = _htf_sign(C, i, 6, A[i]); H1[i] = _htf_sign(C, i, 12, A[i])
    PH = [j for j in range(_PIVK, N - _PIVK) if H[j] == max(H[j - _PIVK:j + _PIVK + 1])]
    PL = [j for j in range(_PIVK, N - _PIVK) if L[j] == min(L[j - _PIVK:j + _PIVK + 1])]

    w = WaveRecommender(_TU(), mt5.TIMEFRAME_M5)
    w.set_reversal_enabled(False); w.set_entry_room(0.6)
    w.set_dead_hours([]); w.set_breakout_k(0.3)

    # Entries EINMAL bestimmen — alle Varianten sehen dieselbe Liste (sonst wäre
    # nicht der SL verglichen, sondern eine andere Trade-Folge).
    print("Entries bestimmen ...", flush=True)
    entries = []
    pend = None
    i = _PIV_LOOK + _EMA_SLOW + 5
    while i < N - LIVE.max_hold - 2:
        atr = A[i]
        if not atr or atr < _ATRMIN or not A15[i]:
            i += 1; continue
        w._pb_feats = {"atr": max(atr, 0.12)}
        # ⚠ bisect statt linearer Filterung (2026-08-07). Die alte Fassung
        # scannte je Bar die KOMPLETTE Pivot-Liste = O(Bars x Pivots) und war
        # der Engpass des Projekts (80k-Lauf: ~9 min -> 8,9 s). PH/PL sind
        # sortiert, die Binaersuche liefert DIESELBE Menge — bewiesen in
        # tests/test_pivotfenster.py, nicht behauptet.
        _a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIVK)
        _a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIVK)
        w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]],
                        "pl": [C[j] for j in PL[_a2:_b2]]}
        rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
                          htf_trend=HTF[i], h1_trend=H1[i], angle=AD[i])
        sig = rec.get("signal")
        if sig in ("LONG", "SHORT"):
            d = 1 if sig == "LONG" else -1
            if pend is None or pend["dir"] != d:
                pend = {"dir": d, "level": C[i] + d * 0.3 * atr,
                        "invalid": C[i] - d * 0.3 * atr, "confirmed": False, "bar": i}
            if not pend["confirmed"]:
                broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"])
                if broke:
                    pend["confirmed"] = True
                else:
                    if ((C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])) \
                            or (i - pend["bar"]) > 12:
                        pend = {"dir": d, "level": C[i] + d * 0.3 * atr,
                                "invalid": C[i] - d * 0.3 * atr,
                                "confirmed": False, "bar": i}
                    i += 1; continue
            rec = w._room_gate(rec, C[i])
            if rec.get("signal") in ("LONG", "SHORT"):
                entries.append((i, d, C[i], atr, A15[i]))
                pend = None
                i += LIVE.timestop_bars       # fester Vorlauf, für alle Varianten gleich
                continue
        elif sig == "WARTEN":
            pend = None
        i += 1
    print(f"{len(entries)} Entries (gegatet, Reversal aus, k=0,3)\n")

    varianten = [
        ("A  SL 2,0×ATR_M5  (kanonisch)", lambda a5, a15: 2.0),
        ("B  SL 2,0×ATR_M15 (LIVE)", lambda a5, a15: 2.0 * a15 / a5),
        ("C  SL 1,5×ATR_M5", lambda a5, a15: 1.5),
        ("D  SL 2,5×ATR_M5", lambda a5, a15: 2.5),
        ("E  SL 3,0×ATR_M5", lambda a5, a15: 3.0),
    ]
    print(f"{'Variante':<32}{'Hälfte':<10}{'n':>6}{'WR':>6}{'ØR':>9}{'PF':>7}"
          f"{'ΣR':>9}{'Worst':>8}")
    print("-" * 88)
    for lbl, f in varianten:
        for hl, lo, hi in (("H1 (alt)", 0, mid), ("H2 (neu)", mid, N)):
            Rs = []
            for (i, d, px, a5, a15) in entries:
                if not (lo <= i < hi):
                    continue
                p = dataclasses.replace(LIVE, sl_atr=max(0.5, f(a5, a15)))
                r, _ = simulate(px, d, a5, H, L, C, i + 1, p,
                                timestop=True, use_tp=True, ret_bar=True)
                Rs.append(r - (SP[i] if SP[i] > 0 else 0.0225) / a5)
            s = st(Rs)
            if s:
                print(f"{lbl:<32}{hl:<10}{s['n']:>6}{s['wr']:>5.0f}%{s['oR']:>+9.3f}"
                      f"{s['pf']:>7.2f}{s['sum']:>+9.0f}{s['worst']:>+8.2f}")
        print()
    print("  Regel (vorab): eine engere SL-Basis wird nur empfohlen, wenn sie in BEIDEN")
    print("  Hälften mehr ΣR liefert UND der Worst-Case nicht schlechter wird.")
    print()


if __name__ == "__main__":
    main()
