"""Pivot-verankerter SL gegen fixen 2,0xATR-SL — mit dem HEUTIGEN Exit (2026-08-20) ANLASS (User): "ueberpruefe genau ob wirklich kein Trade am S/R geschlossen wird ... das hat mich mindestens 3x Geld gekostet." DER FUND, den die Pfad-Pruefung ergeben hat: `trader._calc_sl_tp` legt den Initial-SL an den naechsten M15-PIVOT (LONG: `pivot_low` − Puffer, SHORT: `pivot_high` + Puffer) und klemmt ihn erst danach auf 1,8–2,2×ATR. Der Stop liegt also — wenn der Pivot innerhalb des Bandes liegt — DIREKT an der Unterstuetzung bzw. am Widerstand. Ueber 69 geloggte Orders: SL steht AM Pivot 3 ( 4 %) <- Pivot ist maßgeblich auf 2,2×ATR gekappt 41 (59 %) auf 1,8×ATR geweitet 25 (36 %) In 95 % dominiert also der ATR-Deckel — aber in 4 % ist der Stop exakt das Level, an dem der Markt ohnehin am haeufigsten wickt. WARUM NEU MESSEN. `backtest_sl_method.py` hat die Verankerung schon einmal geprueft und "gedeckelt alle identisch, die Verankerung ist Rauschen" ergeben — aber mit einem ANDEREN Exit-Modell und vor dem kanonischen `exit_model.LIVE` (Trail 1,0 seit 31.07., trail_start 1,3 seit 20.08.). Ein Befund, der eine Live-Schutzlinie rechtfertigt, gehoert auf dem heutigen Exit nachgerechnet. AUFBAU. Squeeze-Einstiege (reproduzierbar, echte `_squeeze_one`), ruhende Order am Level mit Fill bei BERUEHRUNG, kanonischer Exit, Echtkosten, 2 Halbjahre, 95-%-Bootstrap. Variiert wird AUSSCHLIESSLICH die SL-Methode: A) PIVOT wie live: naechster M15-Pivot ∓ Puffer, geklemmt auf 1,8–2,2×ATR B) FIX schlicht 2,0×ATR — ohne jeden Level-Bezug ⚠ Feste, GETEILTE Einstiegsliste: beide Varianten laufen auf DENSELBEN Trades. Ein sequentieller Vergleich waere ungueltig — ein anderer SL laesst ANDERE Trades in den einen Slot (die Falle vom 14.08.). ⚠ M15-Pivots werden aus den M5-Bars aggregiert (3 M5 = 1 M15), Pivot-Fenster k=3 wie in `mt5_utils.pivot_low/high`. Das ist eine NAEHERUNG der Live-Funktion — sie liest direkt M15-Bars. Fuer den VERGLEICH der beiden Methoden ist das unkritisch, fuer die Absolutwerte nicht. REGEL VORAB FIXIERT: umgestellt wird nur, wenn B in BEIDEN Halbjahren mindestens gleichauf liegt (OeR nicht schlechter als A minus ein Standardfehler) UND der Worst-Case nicht groesser wird. Ist B schlechter, bleibt die Verankerung — dann ist der beobachtete Effekt der Preis fuer einen Vorteil an anderer Stelle. Aufruf: python backtest_sl_pivot.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, scan BOX_N, K, MULT_BOX, ATRMIN = 10, 0.1, 2.5, 0.06 SL_MIN, SL_MAX = 1.8, 2.2 # INIT_SL_MIN_ATR / INIT_SL_MAX_ATR PIV_K = 3 BUF = 0.02 # Puffer unter/ueber dem Pivot (Groessenordnung live) def m15_pivots(H, L): """M15-Pivots aus M5-Bars aggregiert → (Index_M5, Preis) je Pivot.""" n = len(H) // 3 hi = [max(H[3 * i:3 * i + 3]) for i in range(n)] lo = [min(L[3 * i:3 * i + 3]) for i in range(n)] ph, pl = [], [] for j in range(PIV_K, n - PIV_K): if hi[j] == max(hi[j - PIV_K:j + PIV_K + 1]): ph.append((3 * (j + PIV_K), hi[j])) # erst k Bars spaeter bekannt if lo[j] == min(lo[j - PIV_K:j + PIV_K + 1]): pl.append((3 * (j + PIV_K), lo[j])) return ph, pl def sl_pivot(i, d, entry, atr, ph, pl): """Live-Regel: naechster Pivot ∓ Puffer, dann auf 1,8–2,2×ATR geklemmt.""" kand = [p for (j, p) in (pl if d > 0 else ph) if j <= i and ((p < entry) if d > 0 else (p > entry))] if kand: piv = max(kand) if d > 0 else min(kand) sl = (piv - BUF) if d > 0 else (piv + BUF) else: sl = entry * (1 - 0.012) if d > 0 else entry * (1 + 0.012) dist = (entry - sl) if d > 0 else (sl - entry) dist = min(max(dist, SL_MIN * atr), SL_MAX * atr) return dist def lauf(ein, H, L, C, SP, ende, modus, ph, pl): rs = [] for j, d, lvl, atr in ein: f = None for x in range(j, min(j + 13, ende)): if (H[x] >= lvl) if d > 0 else (L[x] <= lvl): f = x break if f is None or f + 1 >= ende: continue dist = sl_pivot(f, d, lvl, atr, ph, pl) if modus == "pivot" else 2.0 * atr # SL-Distanz ueber `sl_atr` in EXAKT die kanonische Sim geben. par = LIVE.with_(sl_atr=dist / atr) r = simulate(lvl, d, atr, H, L, C, f + 1, par, timestop=True, use_tp=True) rs.append(r - ((SP[f] if SP[f] > 0 else 0.0225) / atr)) return rs def zeig(lbl, k): if not k: print(" {:<22} --".format(lbl)); return None ci = k.get("ci"); m = "" if ci: m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1]) + ("" if ci[0] * ci[1] > 0 else " ~0")) print(" {:<22} n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f} worst={:+.2f}{}" .format(lbl, k["n"], k["wr"], k["oer"], k["pf"], k.get("worst", 0), m)) return k def main(): n = int(sys.argv[1]) if len(sys.argv) > 1 else 60000 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) ph, pl = m15_pivots(H, L) N = len(C); mid = N // 2; ende = N - LIVE.max_hold - 1 def entries(lo, hi): out = [] scan(H, L, C, A, SP, 1.0, lo, hi, MULT_BOX, box_n=BOX_N, k=K, atr_min=ATRMIN, exit_fn=lambda *a: 0.0, on_entry=lambda j, d, lvl, atr, komp: ( out.append((j, d, lvl, atr)) if komp else None)) return out e1, e2 = entries(0, mid), entries(mid, ende) print("=" * 92) print(" SL-METHODE: Pivot-verankert gegen fix 2,0xATR — {} M5 ({} Bars)".format(sym, N)) print(" gleiche Einstiege, NUR die SL-Methode variiert · {} M15-Pivots".format(len(ph) + len(pl))) print("=" * 92) erg = {} for modus, titel in (("pivot", "A) PIVOT (= live)"), ("fix", "B) FIX 2,0xATR")): print("\n " + titel) print(" " + "-" * 88) erg[modus] = (zeig("H1", kennzahlen(lauf(e1, H, L, C, SP, ende, modus, ph, pl))), zeig("H2", kennzahlen(lauf(e2, H, L, C, SP, ende, modus, ph, pl)))) print("\n " + "=" * 88) a1, a2 = erg["pivot"]; b1, b2 = erg["fix"] if a1 and b1: print(" Differenz FIX − PIVOT: H1 {:+.3f} R H2 {:+.3f} R" .format(b1["oer"] - a1["oer"], b2["oer"] - a2["oer"])) print(" Worst-Case: PIVOT {:+.2f}/{:+.2f} FIX {:+.2f}/{:+.2f}" .format(a1.get("worst", 0), a2.get("worst", 0), b1.get("worst", 0), b2.get("worst", 0))) print(" REGEL: umstellen nur, wenn FIX beidhaelftig mindestens gleichauf") print(" liegt UND der Worst-Case nicht groesser wird.") if __name__ == "__main__": main()