#!/usr/bin/env python3
"""Kappt der S/R-Auto-Close die Squeeze-Laeufer? — NEU unter dem HEUTIGEN Stand.

ANLASS (2026-08-13). Der Pending-Umbau vom 05.08. hat gewirkt (7 von 8 Bot-Trades
fuellen per ruhender Order), der Squeeze verliert aber weiter. Die Signatur zeigt
auf den Exit, nicht mehr auf den Einstieg:
    Trefferquote 73 % · OeGewinn 13,62 EUR gegen OeVerlust 94,15 EUR (Faktor 7)
    Drei von sieben Pending-Trades per `sr_close` bei +0,26 / +2,88 / +0,28 EUR
    geschlossen, waehrend die Verlierer bis zum vollen Stop liefen.
Der Squeeze lebt gemessen von LAEUFERN (43 % Treffer, Ertrag im Tail).

⚠⚠ WARUM DIE ALTE MESSUNG NICHT MEHR BINDET. `backtest_squeeze_srclose.py`
(24.07.) fand auf Squeeze-Entries einen WASH (+0,190→+0,224 / +0,394→+0,377) —
aber mit dem **alten** P(break)-Modell und Schwelle **0,55/0,60**. Seit dem
31.07. laeuft das **nachtrainierte** Modell mit Schwelle **0,35**, und das
schliesst live **94-97 %** aller Level-Beruehrungen. Gemessen unter X, betrieben
unter Y — dasselbe Muster wie die acht dokumentierten Deployment-Drift-Faelle.

VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
  Der S/R-Close bleibt fuer Squeeze-Trades an, wenn er in BEIDEN Haelften
  mindestens so gut ist wie OHNE ihn (Variante A). Sonst: Squeeze-Trades vom
  S/R-Close ausnehmen — oder, falls eine Mindestgewinn-Schwelle in BEIDEN
  Haelften A schlaegt, diese setzen.

AUFBAU — live-treu, mit den geteilten Kernen:
  · Einstiege ueber `core.squeeze_scan.scan(on_entry=...)` = DERSELBE Code wie
    der Live-Squeeze. ⚠ `on_entry` statt `exit_fn`, sonst bekommt man still die
    Kontrollgruppe ALLER Box-Ausbrueche (real: 519 statt 132).
  · Einstieg als ruhende Stop-Order am Level, **Fill bei Beruehrung** — das ist
    das Live-Verhalten seit dem 05.08.
  · Exit `core.exit_model.simulate` mit dem `stop_when`-Haken; damit wird die
    Phasen-Mechanik NICHT kopiert (die Kopiererei hat vier divergierende
    Exit-Modelle erzeugt).
  · Level ueber `core.sr_levels` (M30-Pivots, geclustert) = derselbe Code wie
    `_draw_levels`. P(break) ueber `core.engine._p_break` mit den Live-Gewichten,
    Merkmale trainingsgleich aus M5.
  · Sequentiell EIN Slot, Echtkosten aus dem Bar-Spread, 2 Halbjahre, Bootstrap-KI.

⚠ ZWEI BEWUSSTE NAEHERUNGEN, ehrlich benannt:
  (1) Der Haken schliesst zum BAR-CLOSE; live feuert der Check im 1-s-Takt.
      Dieselbe Naeherung wie bei der Flip-Close-Messung.
  (2) Live glaettet P(break) ueber ~60 s und hat eine Hysterese von ±5 Pp. Auf
      5-Minuten-Bars ist beides gegenstandslos — jeder Bar ist eine frische
      Auswertung. Die Schwellen-Naehe wird dadurch etwas haeufiger ueberschritten
      als live, also wirkt der S/R-Close hier eher ZU stark. Das faellt zu
      Gunsten der Nullhypothese aus (kein Eingriff).

Aufruf:  python backtest_squeeze_srclose2.py [bars]
"""
from __future__ import annotations

import random
import sys
from bisect import bisect_left, bisect_right

import MetaTrader5 as mt5

from core.engine import _p_break
from core.exit_model import LIVE, simulate
from core.sr_levels import halten, linien, naechste
from core.squeeze_scan import atr_series, scan

_PIV_K = 3
_LOOK = 50
_ATR5_FLOOR = 0.12
_AT_LEVEL = 0.15      # „am Level" wie live (_sr_close_hint)
_WARTE = 8            # Bars, die die ruhende Order liegt
# Live-Parameter des Squeeze (core/wave_rec.py)
_SQ_N, _SQ_K, _SQ_MULT = 10, 0.1, 2.5
# ⚠⚠ ATR-FLOOR: `core.squeeze_scan` hat als Default ATRMIN_STD = 0,12 — LIVE und
#   der dokumentierte Referenzlauf (`backtest_squeeze_touchfill.py`) nutzen aber
#   **0,06** (am 2026-07-xx gemessen gesenkt, `backtest_atrfloor.py`: in BEIDEN
#   Haelften besser). Der erste Lauf hier nahm den Modul-Default und bekam damit
#   eine voellig andere Population — H1 127 gegen H2 483 Einstiege, weil der
#   hohe Floor die Niedrig-Vola-Haelfte wegfiltert. Dieselbe Fehlerklasse wie
#   das fehlende `set_entry_room(0.6)` am 05.08.: **ein aufgerufenes Gate ist
#   nicht dasselbe wie ein aktives Gate.**
_ATRMIN = 0.06


def agg(H, L, C, step):
    hh, hl, hc = [], [], []
    for s in range(0, len(C) - step + 1, step):
        hh.append(max(H[s:s + step])); hl.append(min(L[s:s + step]))
        hc.append(C[s + step - 1])
    return hh, hl, hc


def pivots_m30(H, L, C):
    """ALLE M30-Pivots EINMAL (Index + Preis) — O(log n) statt quadratisch."""
    hh, hl, _ = agg(H, L, C, 6)
    ph_i, ph_v, pl_i, pl_v = [], [], [], []
    for x in range(_PIV_K, len(hh) - _PIV_K):
        if hh[x] == max(hh[x - _PIV_K:x + _PIV_K + 1]):
            ph_i.append(x); ph_v.append(round(hh[x], 3))
        if hl[x] == min(hl[x - _PIV_K:x + _PIV_K + 1]):
            pl_i.append(x); pl_v.append(round(hl[x], 3))
    return (ph_i, ph_v), (pl_i, pl_v)


def level_serie(H, L, C, A5):
    """(res, sup) je M5-Bar — DERSELBE Weg wie `_draw_levels`, inkl. Hysterese."""
    (ph_i, ph_v), (pl_i, pl_v) = pivots_m30(H, L, C)
    out = [(None, None)] * len(C)
    pr = ps = None
    for i in range(len(C)):
        a = A5[i]
        if not a:
            continue
        a5f = max(a, _ATR5_FLOOR)
        j = i // 6 - 1
        if j < _PIV_K + 5:
            continue
        a1, b1 = bisect_left(ph_i, j - _LOOK), bisect_right(ph_i, j - _PIV_K)
        a2, b2 = bisect_left(pl_i, j - _LOOK), bisect_right(pl_i, j - _PIV_K)
        ph, pl = ph_v[a1:b1], pl_v[a2:b2]
        if not ph and not pl:
            continue
        r, s = naechste(linien(ph, pl, a5f), C[i])
        r = halten(r, pr, C[i], a5f, True)
        s = halten(s, ps, C[i], a5f, False)
        pr, ps = r, s
        out[i] = (r, s)
    return out


def merkmale(C, A5):
    """mom6 / mom3 / ema_diff je Bar — trainingsgleich aus M5."""
    def ema(v, p):
        k = 2 / (p + 1); o = [v[0]]
        for x in v[1:]:
            o.append(o[-1] + k * (x - o[-1]))
        return o
    ef, es = ema(C, 12), ema(C, 50)
    m6, m3, ed = [0.0] * len(C), [0.0] * len(C), [0.0] * len(C)
    for i in range(6, len(C)):
        a5f = max(A5[i] or _ATR5_FLOOR, _ATR5_FLOOR)
        m6[i] = (C[i] - C[i - 6]) / a5f
        m3[i] = (C[i] - C[i - 3]) / a5f
        ed[i] = (ef[i] - es[i]) / a5f
    return m6, m3, ed


def entries(H, L, C, A, SP, point, lo, hi):
    """Squeeze-Ausbrueche ueber den GETEILTEN Kern einsammeln."""
    got = []
    scan(H, L, C, A, SP, point, lo, hi, _SQ_MULT, box_n=_SQ_N, k=_SQ_K,
         atr_min=_ATRMIN,
         exit_fn=lambda e, d, a, h, l, c, j: 0.0,
         on_entry=lambda j, d, lvl, a, komp: (
             got.append({"j": j, "d": d, "lvl": lvl, "atr": a}) if komp else None))
    return got


def lauf(ein, H, L, C, SP, A5, LV, M6, M3, ED, schwelle, min_gain=0.0):
    """Sequentiell EIN Slot. `schwelle=None` → ohne S/R-Close."""
    Rs = []
    n_close = 0          # wie oft der S/R-Close wirklich gefeuert hat
    frei_ab = -1
    grenze = len(C) - LIVE.max_hold - 2
    for e in ein:
        i, d, lvl = e["j"], e["d"], e["lvl"]
        if i < frei_ab or i >= grenze:
            continue
        a5f = max(A5[i] or _ATR5_FLOOR, _ATR5_FLOOR)
        fill = None
        for k in range(i + 1, min(i + 1 + _WARTE, grenze)):
            if L[k] <= lvl <= H[k]:
                fill = k; break
        if fill is None:
            continue

        # ── (1) natuerlicher Exit ohne S/R-Close ─────────────────────────
        R, xb = simulate(lvl, d, a5f, H, L, C, fill + 1, LIVE, ret_bar=True)

        # ── (2) S/R-Close davor? ─────────────────────────────────────────
        # ⚠⚠ NICHT ueber `stop_when`. Der Haken schliesst zum BAR-CLOSE, und
        #   die Bedingung „am Level" ist ein Fenster von 0,15xATR ≈ 0,018 $ —
        #   dass ein 5-Minuten-Close genau darin landet, ist selten. Ein
        #   Probelauf zeigte deshalb Δ ≈ 0,000, waehrend der Close live
        #   94-97 % aller Beruehrungen schliesst. Live prueft den LAUFENDEN
        #   Kurs im Sekundentakt — das Analogon ist die Bar-SPANNE.
        # ⚠ Ausstieg zum ERSTEN Moment, in dem die Bedingung feuern kann
        #   (Level minus Fensterbreite), nicht zum Level selbst. Das ist das
        #   konservative Ende und entspricht dem 1-s-Loop.
        # ⚠ Strikt VOR `xb`: faellt beides in denselben Bar, hat der SL
        #   Vorrang — dieselbe pessimistische Konvention wie in `simulate`.
        if schwelle is not None:
            for j in range(fill + 1, xb):
                if profit_bedingung(j, d, lvl, a5f, min_gain, LV, C, M6, M3, ED,
                                    H, L, schwelle):
                    ziel = LV[j][0] if d > 0 else LV[j][1]
                    R = (ziel - d * _AT_LEVEL * a5f - lvl) * d / a5f
                    xb = j
                    n_close += 1
                    break
        Rs.append(R - SP[i] / a5f)
        frei_ab = xb + 1
    return Rs, n_close


def profit_bedingung(j, d, entry, a5f, min_gain, LV, C, M6, M3, ED, H, L, schwelle):
    """Feuert der S/R-Auto-Close in Bar `j`? — die Live-Bedingungen der Reihe nach."""
    ziel = LV[j][0] if d > 0 else LV[j][1]        # Ziel-Seite wie _sr_close_hint
    if ziel is None:
        return False
    # „am Level": der Kurs kam im Bar bis auf 0,15xATR heran (Bar-SPANNE, s. o.)
    nah = (H[j] >= ziel - _AT_LEVEL * a5f) if d > 0 else (L[j] <= ziel + _AT_LEVEL * a5f)
    if not nah:
        return False
    aus = ziel - d * _AT_LEVEL * a5f              # Preis im Moment des Ausloesens
    gewinn = (aus - entry) * d / a5f
    if gewinn <= 0 or gewinn < min_gain:          # live: nur im PLUS
        return False
    gap = abs(ziel - C[j])
    wt = 1.0 if ED[j] * d > 0 else 0.0
    return _p_break(M6[j] * d, M3[j] * d, wt, gap / a5f) < schwelle


def ci(Rs, n=3000, seed=42):
    if len(Rs) < 20:
        return None, None
    r = random.Random(seed); m = len(Rs)
    b = sorted(sum(Rs[r.randrange(m)] for _ in range(m)) / m for _ in range(n))
    return b[int(.025 * n)], b[int(.975 * n)]


def kenn(Rs):
    if not Rs:
        return None
    g = sum(x for x in Rs if x > 0); v = -sum(x for x in Rs if x < 0)
    u, o = ci(Rs)
    return {"n": len(Rs), "oer": sum(Rs) / len(Rs), "sr": sum(Rs),
            "wr": 100 * sum(1 for x in Rs if x > 0) / len(Rs),
            "pf": (g / v if v else float("inf")), "u": u, "o": o}


def zeile(name, k, basis=None):
    if not k:
        return f"  {name:<38}  —"
    kib = "      —      " if k["u"] is None else f"[{k['u']:+.3f} … {k['o']:+.3f}]"
    dlt = "" if basis is None else f"  Δ {k['oer'] - basis['oer']:+.3f}"
    cl = "" if k.get("cl") is None else f"  Close {k['cl']:>3} ({k['clq']:>4.1f}%)"
    return (f"  {name:<38} n={k['n']:>4} WR {k['wr']:>4.1f}% OeR {k['oer']:>+.3f} "
            f"PF {k['pf']:>5.2f} SR {k['sr']:>+7.1f} {kib}{dlt}{cl}")


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()
    if bars is None:
        print("  keine Bars"); return 1
    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)
    LV = level_serie(H, L, C, A5)
    M6, M3, ED = merkmale(C, A5)
    mid = len(C) // 2
    e1 = entries(H, L, C, A5, SP, point, 60, mid)
    e2 = entries(H, L, C, A5, SP, point, mid, len(C))
    print(f"\n  {sym} · {len(C)} M5-Bars · Squeeze-Entries H1 {len(e1)} · H2 {len(e2)}")
    print(f"  Einstieg ruhend am Level (Fill bei Beruehrung) · kanonischer Exit · "
          f"Echtkosten · EIN Slot")
    print(f"  Regel vorab: der S/R-Close bleibt an, wenn er in BEIDEN Haelften "
          f"mindestens so gut ist wie OHNE ihn.\n")

    def rechne(schw, mg=0.0):
        r1, c1 = lauf(e1, H, L, C, SP, A5, LV, M6, M3, ED, schw, mg)
        r2, c2 = lauf(e2, H, L, C, SP, A5, LV, M6, M3, ED, schw, mg)
        k1, k2 = kenn(r1), kenn(r2)
        for k, c in ((k1, c1), (k2, c2)):
            if k:
                k["cl"] = c
                k["clq"] = 100 * c / k["n"] if k["n"] else 0
        return k1, k2

    b1, b2 = rechne(None)
    print("  ── BASIS: OHNE S/R-Close (nur kanonischer Exit) ───────────────")
    print(zeile("A) ohne S/R-Close        H1", b1))
    print(zeile("A) ohne S/R-Close        H2", b2))

    print("\n  ── MIT S/R-Close, Schwellen-Sweep (live = 0,35) ───────────────")
    for s in (0.30, 0.35, 0.45, 0.55):
        k1, k2 = rechne(s)
        mark = "  ← LIVE" if abs(s - 0.35) < 1e-9 else ""
        print(zeile(f"B) p_break < {s:.2f}        H1{mark}", k1, b1))
        print(zeile(f"B) p_break < {s:.2f}        H2{mark}", k2, b2))

    print("\n  ── ABHILFE-VARIANTE: Close erst ab Mindestgewinn (bei 0,35) ───")
    for mg in (0.5, 1.0, 1.5):
        k1, k2 = rechne(0.35, mg)
        print(zeile(f"C) 0,35 + erst ab {mg:.1f}xATR  H1", k1, b1))
        print(zeile(f"C) 0,35 + erst ab {mg:.1f}xATR  H2", k2, b2))

    l1, l2 = rechne(0.35)
    print("\n  ── URTEIL ─────────────────────────────────────────────────────")
    ok = bool(l1 and l2 and b1 and b2
              and l1["oer"] >= b1["oer"] and l2["oer"] >= b2["oer"])
    print(f"    S/R-Close (0,35) mindestens so gut wie ohne, in BEIDEN: "
          f"{'✅ ja — anlassen' if ok else '❌ nein'}")
    if not ok:
        print(f"      H1 {l1['oer']:+.3f} gegen {b1['oer']:+.3f}   "
              f"H2 {l2['oer']:+.3f} gegen {b2['oer']:+.3f}")
        print("      ➜ Squeeze-Trades vom S/R-Close ausnehmen, ODER eine "
              "Mindestgewinn-Schwelle, die BEIDE Haelften schlaegt (Tabelle C).")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
