#!/usr/bin/env python3 """Fuellt die ruhende Squeeze-Order wirklich so, wie der Backtest unterstellt? ANLASS (2026-08-06): Bei `backtest_momentum_v2.py` ist aufgefallen, dass ein "Einstieg am Level" zwei sehr verschiedene Dinge bedeuten kann: (a) einsteigen, WO das Signal ausgeloest hat — also dort, wo der Bar-CLOSE das Level ueberschritten hat, und zwar zum Level-Preis; (b) einsteigen, WANN die liegende Order gefuellt haette — bei der ERSTEN Beruehrung des Levels, unabhaengig davon, wo der Bar schliesst. Live gilt (b). Der Unterschied ist kein Detail: beim Momentum lag er bei **0,45 R** und hat aus +0,40 ein −0,06 gemacht — die Variante (a) konditioniert auf den Bar-Schluss, den eine liegende Order nicht kennen kann. Look-ahead. ⚠ DIESELBE FRAGE MUSS FUER DEN SQUEEZE BEANTWORTET WERDEN, denn `squeeze_pending_ entry` laeuft seit 05.08. mit ECHTEM Geld, und die Erwartung dahinter (+0,456/ +0,611 aus `backtest_squeeze_entry.py`) stammt aus Variante (a). GRUND ZUR HOFFNUNG — aber eben nur Hoffnung, bis gemessen: die Squeeze-Box steht ueber viele Bars still, das Level ist also ein echter, stabiler Ort fuer eine Order. Das Momentum-Level wandert dagegen mit `C[i−N]` jede Bar mit. Wenn der Unterschied klein ist, war die Hoffnung berechtigt; ist er gross, ist die Live-Erwartung zu hoch angesetzt und gehoert korrigiert. Aufruf: python backtest_squeeze_touchfill.py [bars] """ import sys import MetaTrader5 as mt5 import core.wave_rec as wr from backtest_cost_gate import _atr_series, kennzahlen from core.exit_model import LIVE, simulate from core.wave_rec import WaveRecommender _ATRMIN = 0.06 def boxen(H, L, C, A): """Je Bar die AKTUELLE Box-Grenze + Ausbruchs-Level — auch ohne Ausbruch. Das ist der Zustand, in dem die ruhende Order liegt (`armed`): die Box steht, die beiden Stop-Level stehen. Ob der Kurs sie beruehrt, entscheidet der Markt. """ out = [] for i in range(wr._SQ_N + 2, len(C) - 2): atr = A[i - 1] # am Ende der Vorbar bekannt if not atr or atr < _ATRMIN: out.append(None); continue hb = H[i - wr._SQ_N - 1:i - 1]; lb = L[i - wr._SQ_N - 1:i - 1] if not hb or not lb: out.append(None); continue hi, lo = max(hb), min(lb) if (hi - lo) / atr > wr._SQ_MULT: # keine Kompression → keine Order out.append(None); continue out.append((i, hi + wr._SQ_K * atr, lo - wr._SQ_K * atr, atr)) return out def lauf_close(H, L, C, SP, A, lo, hi): """(a) Projekt-Standard: Einstieg dort, wo `_squeeze_one` `active` meldet (Bar-CLOSE hat das Level gebrochen), zum Level-Preis.""" rs, frei_ab, last = [], lo, None for i in range(wr._SQ_N + 2, len(C) - 2): if not (lo <= i < hi) or i < frei_ab: continue atr = A[i] if not atr or atr < _ATRMIN: continue w0 = i - wr._SQ_N - 2 st = WaveRecommender._squeeze_one(H[w0:i + 1], L[w0:i + 1], C[w0:i + 1], atr) if st.get("state") != "active": last = None; continue sig = (st["dir"], st["level"]) if sig == last: continue last = sig d = 1 if st["dir"] == "LONG" else -1 r, xb = simulate(float(st["level"]), d, atr, H, L, C, i + 1, LIVE, timestop=True, use_tp=True, ret_bar=True) rs.append(r - (SP[i] if SP[i] > 0 else 0.0225) / atr) frei_ab = xb + 1 return rs def lauf_touch(H, L, C, SP, A, B, lo, hi): """(b) Realitaet: die Order liegt an beiden Box-Grenzen und fuellt bei der ERSTEN Beruehrung — auch wenn der Bar wieder in die Box zurueckfaellt.""" rs, frei_ab = [], lo for eintrag in B: if eintrag is None: continue i, lvl_hi, lvl_lo, atr = eintrag if not (lo <= i < hi) or i < frei_ab: continue d = 0 if H[i] >= lvl_hi: d, lvl = 1, lvl_hi elif L[i] <= lvl_lo: d, lvl = -1, lvl_lo if not d: continue r, xb = simulate(lvl, d, atr, H, L, C, i + 1, LIVE, timestop=True, use_tp=True, ret_bar=True) rs.append(r - (SP[i] if SP[i] > 0 else 0.0225) / atr) frei_ab = xb + 1 return rs def main(): n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 if not mt5.initialize(): print("MT5 nicht erreichbar"); return 1 sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD") if mt5.symbol_info(c)), None) b = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) point = mt5.symbol_info(sym).point mt5.shutdown() H = [float(x["high"]) for x in b]; L = [float(x["low"]) for x in b] C = [float(x["close"]) for x in b] SP = [float(x["spread"]) * point for x in b] A = _atr_series(H, L, C) B = boxen(H, L, C, A) m = len(C) // 2 print("=" * 92) print(f" SQUEEZE — Fill bei BAR-CLOSE-Ausbruch gegen Fill bei BERUEHRUNG ({sym})") print("=" * 92) print(f" {len(C)} M5-Bars · kanonischer Exit · Echtkosten · sequentiell EIN Slot") print() a1 = kennzahlen(lauf_close(H, L, C, SP, A, 0, m)) a2 = kennzahlen(lauf_close(H, L, C, SP, A, m, len(C))) b1 = kennzahlen(lauf_touch(H, L, C, SP, A, B, 0, m)) b2 = kennzahlen(lauf_touch(H, L, C, SP, A, B, m, len(C))) for lbl, x1, x2 in (("(a) Close bricht Level (Backtest-Standard)", a1, a2), ("(b) Order fuellt bei Beruehrung (LIVE)", b1, b2)): if not x1 or not x2: print(f" {lbl:<44} — keine Trades"); continue print(f" {lbl:<44} H1 ØR {x1['oer']:+.3f} PF {x1['pf']:.2f} n{x1['n']:<5} | " f"H2 ØR {x2['oer']:+.3f} PF {x2['pf']:.2f} n{x2['n']:<5}") if all((a1, a2, b1, b2)): print() print(f" Look-ahead-Aufschlag: H1 {a1['oer']-b1['oer']:+.3f} · " f"H2 {a2['oer']-b2['oer']:+.3f} R je Trade") print(f" Zusaetzliche Fills durch Beruehrung: H1 {b1['n']-a1['n']:+d} · " f"H2 {b2['n']-a2['n']:+d}") gut = b1["oer"] > 0 and b2["oer"] > 0 and b1["pf"] > 1 and b2["pf"] > 1 print() print(f" → Der Squeeze haelt der realistischen Fill-Annahme " f"{'STAND ✅' if gut else 'NICHT stand ❌'}") print(" ⚠ Zum Vergleich Momentum (`backtest_momentum_v2.py`): dort betrug") print(" der Aufschlag ~0,45 R und drehte +0,40 auf −0,06.") return 0 if __name__ == "__main__": sys.exit(main())