"""Wandert die Squeeze-Marke dem Kurs entgegen — und was kostet das? (2026-08-20)

ANLASS (User, live beobachtet): "warum hat der breakout squeeze ausgeloest, ich
kann keinen breakout erkennen". Die Logspur vom 20.08. gibt ihm recht:

    07:25:08  BUY_STOP @ 85.457
    07:30:01  BUY_STOP @ 85.432
    07:35:04  BUY_STOP @ 85.417
    07:40:03  BUY_STOP @ 85.342      <- 11,5 Cent tiefer als 15 min zuvor
    07:40:12  FILL      @ 85.344     <- 9 Sekunden spaeter

Der Kurs stieg in dem Fenster von 85,176 auf 85,355 - er hat die Marke NICHT
durchbrochen, die MARKE kam ihm entgegen.

URSACHE. `box_hi` ist das Hoch der letzten `_SQ_N` abgeschlossenen M5-Bars, also
ein ROLLIERENDES Fenster. Faellt der Kurs, rollen die alten hohen Bars hinten
heraus und das Box-Dach sinkt mit; die ruhende Order wird nachgezogen.

⚠⚠ DAS VERLETZT DAS PRINZIP, AUF DEM DER PENDING-UMBAU VOM 05.08. BERUHT und das
im Projekt woertlich steht: "Ein Level ist nur dann ein Ausfuehrungs-Vorteil,
wenn es STILLSTEHT." Genau daran ist der SIG-Pending-Pfad gescheitert
(OeR -0,392/-0,338, abgeschaltet) - `_pend` wandert, und die Order faengt
systematisch die Fehlausbrueche ein. Der Squeeze galt als der Fall, wo das Level
STEHT: `backtest_squeeze_touchfill.py` misst den Ausbruch aus einer Box, die im
Moment des Ausbruchs feststeht. Live gleitet sie.

WAS HIER GEMESSEN WIRD - zwei Varianten auf DENSELBEN Bars:

  A) NACHGEZOGEN (= live): die Marke wird jede Bar neu aus der aktuellen Box
     berechnet; gefuellt wird, sobald der Kurs die JEWEILS AKTUELLE Marke
     beruehrt. Bildet das beobachtete Verhalten nach.
  B) EINGEFROREN: die Marke wird EINMAL gesetzt, wenn die Kompression beginnt,
     und bleibt stehen, bis die Kompression endet. Gefuellt wird nur bei
     Beruehrung DIESER Marke. Das ist, was der Backtest unterstellt.

Zusaetzlich wird in A ausgezaehlt, wie viele Fills reine NACHLAEUFER sind: die
Marke lag beim Fill TIEFER (LONG) bzw. HOEHER (SHORT) als bei der Armierung, und
der Kurs hat die urspruengliche Marke nie erreicht. Genau der Fall des Users.

⚠ KEIN NACHBAU: die Kompression kommt aus der echten `wave_rec._squeeze_one`,
der Exit aus `exit_model.simulate` (kanonisch), Kosten aus dem Bar-Spread.

REGEL VORAB FIXIERT: "Einfrieren" ist nur dann eine Verbesserung, wenn B in
BEIDEN Halbjahren einen hoeheren OeR liefert UND die Nachlaeufer-Quote in A
substanziell ist (>10 %). Ist die Quote klein, ist der Live-Fall ein Einzelfall
und nichts zu aendern.

Aufruf:  python backtest_squeeze_frozen.py [bars]
"""
import sys

import MetaTrader5 as mt5

from backtest_cost_gate import kennzahlen
from core.exit_model import LIVE, simulate
from core.squeeze_scan import atr_series
from core.wave_rec import WaveRecommender

ATRMIN = 0.06          # LIVE-Wert (nicht der Modul-Default 0,12)
MAX_WARTE = 24         # Bars, die eine armierte Marke hoechstens gilt


def zustand(H, L, C, A, i):
    """Echte `_squeeze_one`-Auswertung fuer Bar i (Fenster wie live)."""
    lo = max(0, i - 40)
    return WaveRecommender._squeeze_one(H[lo:i + 1], L[lo:i + 1],
                                        C[lo:i + 1], A[i])


def lauf(H, L, C, A, SP, von, bis, ende, modus):
    """modus 'nach' = Marke wandert (live) · 'fix' = beim Armieren eingefroren."""
    rs, nachlaeufer, gesamt = [], 0, 0
    i = von
    marke = None          # (preis, richtung, bar_armiert, preis_bei_armierung)
    while i < bis:
        atr = A[i]
        if not atr or atr < ATRMIN:
            i += 1; marke = None; continue
        z = zustand(H, L, C, A, i)
        if z.get("state") is None:
            marke = None; i += 1; continue
        lvl, d = z.get("level"), (1 if z.get("dir") == "LONG" else -1)
        if lvl is None:
            i += 1; continue
        if marke is None or marke[1] != d or i - marke[2] > MAX_WARTE:
            marke = (lvl, d, i, lvl)            # neu armieren
        elif modus == "nach":
            marke = (lvl, d, marke[2], marke[3])  # Marke nachziehen
        # Fill? Beruehrung der AKTUELL gueltigen Marke im naechsten Bar
        preis = marke[0]
        j = i + 1
        if j >= ende:
            break
        if ((H[j] >= preis) if d > 0 else (L[j] <= preis)) and j + 1 < ende:
            gesamt += 1
            # Nachlaeufer: Marke ist dem Kurs entgegengekommen UND die
            # urspruengliche Marke wurde nie erreicht.
            urspr = marke[3]
            entgegen = (preis < urspr - 1e-9) if d > 0 else (preis > urspr + 1e-9)
            nie_erreicht = (max(H[marke[2]:j + 1]) < urspr) if d > 0 \
                else (min(L[marke[2]:j + 1]) > urspr)
            if entgegen and nie_erreicht:
                nachlaeufer += 1
            r = simulate(preis, d, atr, H, L, C, j + 1, LIVE,
                         timestop=True, use_tp=True)
            rs.append(r - ((SP[j] if SP[j] > 0 else 0.0225) / atr))
            marke = None
            i = j + 3            # kleiner Cooldown wie live
            continue
        i += 1
    return rs, nachlaeufer, gesamt


def zeig(lbl, k, nl, ges):
    if not k:
        print("    {:<24} --".format(lbl)); return
    ci = k.get("ci"); m = ""
    if ci:
        m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1])
             + ("" if ci[0] * ci[1] > 0 else " ~0"))
    quote = (100.0 * nl / ges) if ges else 0.0
    print("    {:<24} n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f}{}   Nachlaeufer {:.0f} %"
          .format(lbl, k["n"], k["wr"], k["oer"], k["pf"], m, quote))


def main():
    n = int(sys.argv[1]) if len(sys.argv) > 1 else 40000
    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]
    A = atr_series(H, L, C)
    N = len(C); mid = N // 2; ende = N - LIVE.max_hold - 1

    print("=" * 100)
    print("  WANDERNDE gegen EINGEFRORENE Squeeze-Marke — {} M5 ({} Bars)".format(sym, N))
    print("  echte `_squeeze_one` · kanonischer Exit · Echtkosten · 95-%-KI")
    print("=" * 100)
    for modus, titel in (("nach", "A) NACHGEZOGEN (= live)"),
                         ("fix", "B) EINGEFROREN beim Armieren")):
        print("\n  " + titel)
        print("  " + "-" * 96)
        for lbl, (lo, hi) in (("H1", (0, mid)), ("H2", (mid, ende))):
            rs, nl, ges = lauf(H, L, C, A, SP, lo, hi, ende, modus)
            zeig(lbl, kennzahlen(rs), nl, ges)
    print("\n  " + "=" * 96)
    print("    LESART: 'Nachlaeufer' = die Marke kam dem Kurs ENTGEGEN und die")
    print("    urspruengliche Marke wurde nie erreicht — also kein Ausbruch.")
    print("    Ist die Quote klein, war der Live-Fall ein Einzelfall.")


if __name__ == "__main__":
    main()
