"""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()