#!/usr/bin/env python3
"""Squeeze-Einstieg auf M5, EXIT-Abstände aus dem M15-ATR — trägt das?

ANLASS (2026-08-08). Der M15-EINSTIEG ist gemessen durchgefallen
(`backtest_squeeze_m15.py`: n=33 in H1, Nachbarn kippen). Offen bleibt die
andere Hälfte der Idee: den validierten M5-Einstieg behalten und nur den EXIT
gröber führen. Dafür spricht die Kostenseite (Median Spread/ATR: M15 **0,104**
gegen M5 0,193) und der dokumentierte Befund, dass der Verlust-Hebel im Exit und
im Sizing liegt, nicht im Signal.

⚠ IM CODE STEHT DIESE FRAGE SEIT DEM 31.07. OFFEN — `core/trailing.py`:
„Falls die Staffelung je zurück soll: erst `backtest_trailing.py` auf
M15-/M30-BASIS-Signalen laufen lassen — **bisher misst er nur M5**."
Live nimmt das Trailing den ATR der WELLEN-Zeitebene als Abstandsmaß; die
Heuristik landet häufig auf M15/M30 (31.07.: 19 von 33 Phasen). Welcher ATR das
sein SOLLTE, ist nie gemessen worden.

⚠⚠ DIE FALLE, DIE DIESE MESSUNG SONST WERTLOS MACHT: `simulate()` normiert das
Ergebnis mit DEMSELBEN ATR, den es für SL/Trail/TP benutzt (R = Gewinn/ATR). Mit
dem ~1,8× größeren M15-ATR würde dieselbe Dollar-Bewegung zu einem kleineren R —
die Varianten wären **nicht vergleichbar**, und die M15-Variante sähe
mechanisch schlechter aus. Beide werden deshalb in **M5-Risikoeinheiten**
zurückgerechnet: R_m5 = R_ergebnis × ATR_verwendet / ATR_M5.

⚠ NICHT dasselbe wie `backtest_sl_basis.py` (05.08.): dort wurde nur der
INITIAL-SL variiert (Ergebnis: Wash beim Squeeze, Tail-Risiko ×3, daraufhin
`_calc_sl_tp(atr_tf=M5)` für Squeeze-Trades gebaut). Hier hängt der GESAMTE Exit
am ATR — SL, Trail-Abstand, Breakeven, Lock-Schwellen und TP.

AUFBAU: feste, geteilte Entry-Liste (Squeeze auf M5, dokumentierte Parameter
Box 10 / k 0,1 / Floor 0,12), Touch-Fill, Echtkosten, kanonischer Exit-Kern,
2 Halbjahre. M15-ATR kausal aus den M5-Bars aggregiert.

⚠⚠ ENTSCHEIDUNGSREGEL, VORAB: die M15-Variante muss in BEIDEN Hälften mehr ΣR
(in M5-Einheiten) liefern UND darf den Worst-Case nicht verschlechtern. Ein
breiterer Exit kauft Ertrag sonst mit Tail-Risiko — genau die Abwägung, die bei
`backtest_sl_basis.py` gegen die Umstellung sprach.

Aufruf:  python backtest_exit_atr_tf.py [n_M5_bars]      (Default 80000)
"""
from __future__ import annotations

import sys

import MetaTrader5 as mt5

from core.exit_model import LIVE, simulate
from core.squeeze_scan import atr_series, scan

_FLOOR = 0.12       # dokumentierter M5-Floor (backtest_squeeze_touchfill.py)
_M15 = 3            # 3 M5-Bars = 1 M15-Bar


def atr_m15_auf_m5(H, L, C):
    """ATR aus M15-Bars, auf den M5-Index abgebildet.

    ⚠ LOOK-AHEAD-frei: an M5-Index i zählt nur der letzte ABGESCHLOSSENE
    M15-Bar. Dieselbe Konstruktion wie `_htf_signs` in `backtest_auto_signal`."""
    hh, hl, hc = [], [], []
    for s in range(0, len(C) - _M15 + 1, _M15):
        hh.append(max(H[s:s + _M15])); hl.append(min(L[s:s + _M15]))
        hc.append(C[s + _M15 - 1])
    a15 = atr_series(hh, hl, hc)
    out = [None] * len(C)
    for i in range(len(C)):
        j = i // _M15 - 1
        out[i] = a15[j] if 0 <= j < len(a15) else None
    return out


def kz(Rs, label):
    if not Rs:
        return f"    {label:<26} n=0"
    n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs)
    g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0)
    return (f"    {label:<26} n={n:>4} Treffer={100*w/n:>3.0f}% ØR={s/n:+.3f} "
            f"PF={(g/v if v > 0 else 9.99):>4.2f} ΣR={s:>+6.0f} Worst={min(Rs):+.2f}")


def main():
    n = 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)
    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]
    A5 = atr_series(H, L, C)
    A15 = atr_m15_auf_m5(H, L, C)
    mid = len(C) // 2

    gueltig = [(a15 / a5) for a5, a15 in zip(A5, A15) if a5 and a15 and a5 > 0]
    gueltig.sort()
    print("=" * 98)
    print(f"  EXIT-ABSTAND aus M5-ATR gegen M15-ATR — {sym}, {len(C)} M5-Bars")
    print(f"  Verhältnis ATR_M15/ATR_M5: Median {gueltig[len(gueltig)//2]:.2f}×")
    print("  ⚠ Beide Varianten in M5-RISIKOEINHEITEN gerechnet, sonst unvergleichbar")
    print("=" * 98)

    for nm, lo, hi in (("H1", 40, mid), ("H2", mid, len(C) - LIVE.max_hold - 2)):
        # ── EINE feste Entry-Liste (der Einstieg ist in beiden Varianten gleich)
        eintraege = []

        # ⚠ NUR komprimierte Ausbrueche = die Squeeze-Population. `exit_fn`
        # allein taugt dafuer NICHT: es wird bei JEDEM Box-Ausbruch gerufen und
        # lieferte still die Kontrollgruppe (519 statt 132 Einstiege).
        def sammeln(j, d, lvl, atr, komprimiert, _l=eintraege):
            if komprimiert:
                _l.append((lvl, d, j + 1))

        scan(H, L, C, A5, SP, 1.0, lo, hi, 2.5, box_n=10, k=0.1,
             atr_min=_FLOOR, max_hold=LIVE.max_hold, on_entry=sammeln)

        print(f"\n  ── {nm} ── {len(eintraege)} feste Squeeze-Einstiege, "
              f"beide Varianten identisch")
        for label, quelle in (("M5-ATR (heute)", A5), ("M15-ATR", A15)):
            Rs = []
            for entry, d, j0 in eintraege:
                i = j0 - 1
                a5 = A5[i]
                a = quelle[i]
                if not a or not a5 or a5 <= 0:
                    continue
                a = max(a, _FLOOR)
                R = simulate(entry, d, a, H, L, C, j0, LIVE)
                # ⚠ zurueck in M5-Einheiten: R × verwendeter ATR ÷ ATR_M5
                r_m5 = R * a / max(a5, _FLOOR)
                kosten = (SP[i] if SP[i] > 0 else 0.0225) / max(a5, _FLOOR)
                Rs.append(r_m5 - kosten)
            print(kz(Rs, label))


if __name__ == "__main__":
    main()
