#!/usr/bin/env python3 """ENGER Initial-Stop fuer die ersten N Bars, danach der normale — traegt das? USER-IDEE (2026-08-13): „beim Eroeffnen den SL sehr eng setzen, so dass Trades in die falsche Richtung schnell geschlossen werden. Laeuft der Trade richtig, den SL etappenweise nachziehen." ⚠ Der zweite Teil ist bereits gebaut (die Phasen-Ratsche in `core/trailing.py`). Neu und ungemessen ist allein der erste: ein **zeitlich befristeter** enger Stop. Nicht zu verwechseln mit einem durchgehend engen Stop — DER ist zweimal gemessen (`backtest_sl_width.py` 04.08., `backtest_sl_basis.py` 05.08.) und schlaegt den Live-Stop in KEINER Variante beidhaelftig. ⚠⚠ WIE ES OHNE EXIT-NACHBAU GEHT (die Kopiererei hat 4 divergierende Exit- Modelle erzeugt — das ist die dokumentierte Falle): Lauf A = `simulate` mit sl_atr = ENG · Lauf B = `simulate` mit sl_atr = WEIT Beide laufen auf DEMSELBEN Kurspfad. Innerhalb der ersten N Bars ist der EINZIGE Unterschied zwischen A und B der SL-Abstand; jeder Nicht-SL-Ausstieg (TP, Time-Stop) faellt in beiden identisch aus. Also gilt exakt: R_gestaffelt = R_A wenn A vor/bei Bar N aussteigt, sonst R_B Kein Nachbau, keine Naeherung. ⚠⚠ DIE BINDENDE GRENZE — bitte vor der Interpretation lesen: `analyze_trail_resolution.py` (05.08.) hat gemessen, dass eine BAR-Simulation **enge Stops systematisch UEBERSCHAETZT** (Kipp-Punkt ~0,3xATR): das Modell nimmt den Bar-Hochpunkt mit und steigt erst im naechsten Bar aus. Ein POSITIVES Ergebnis fuer die enge Variante ist deshalb nach oben verzerrt und muesste abgeschlagen werden. Ein NEGATIVES ist belastbar. Dazu: die Exit-Slippage ist ABSOLUT (Median 0,017 $, Ø 0,234 $). In R gerechnet trifft sie alle Varianten gleich stark — sie verschiebt das Niveau, nicht die Rangfolge; die Spalte steht trotzdem dabei. Aufruf: python backtest_sl_staged.py [bars] """ from __future__ import annotations import random import sys from dataclasses import replace import MetaTrader5 as mt5 from core.exit_model import LIVE, simulate from core.squeeze_scan import atr_series, scan _SQ_N, _SQ_K, _SQ_MULT = 10, 0.1, 2.5 _ATRMIN = 0.06 # ⚠ LIVE-Wert; der Modul-Default 0,12 waere eine andere Population _WARTE = 8 # Live-Stop: 2,0xATR_M15 ~= 3,4xATR_M5 (Median ueber 121 echte Trades, 13.08.) _WEIT_LIVE = 3.4 def entries(H, L, C, A, SP, point, lo, hi): 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, eng, nbars, weit, slip=0.0): """Sequentiell EIN Slot. `eng=None` → durchgehend `weit` (die Basis).""" pW = replace(LIVE, sl_atr=weit) pE = None if eng is None else replace(LIVE, sl_atr=eng) Rs, n_frueh = [], 0 frei_ab, grenze = -1, len(C) - LIVE.max_hold - 2 for e in ein: i, d, lvl, a = e["j"], e["d"], e["lvl"], e["atr"] if i < frei_ab or i >= grenze or not a: continue 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 RB, xbB = simulate(lvl, d, a, H, L, C, fill + 1, pW, ret_bar=True) R, xb = RB, xbB if pE is not None: RA, xbA = simulate(lvl, d, a, H, L, C, fill + 1, pE, ret_bar=True) if xbA <= fill + nbars: # in Phase 1 ausgestiegen → A gilt R, xb = RA, xbA n_frueh += 1 Rs.append(R - SP[i] / a - (slip / a if slip else 0.0)) frei_ab = xb + 1 return Rs, n_frueh def ci(Rs, n=2500, 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, frueh=0): 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), "wr": 100 * sum(1 for x in Rs if x > 0) / len(Rs), "pf": (g / v if v else float("inf")), "worst": min(Rs), "u": u, "o": o, "fr": frueh} def zeig(name, k, basis=None): if not k: print(f" {name:<32} —"); return kib = " — " if k["u"] is None else f"[{k['u']:+.3f} … {k['o']:+.3f}]" dl = "" if basis is None else f" Δ{k['oer']-basis['oer']:+.3f}" print(f" {name:<32} n={k['n']:>4} WR {k['wr']:>4.1f}% ØR {k['oer']:>+.3f} " f"PF {k['pf']:>5.2f} worst {k['worst']:>+6.2f} früh {k['fr']:>4} {kib}{dl}") 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] A = atr_series(H, L, C) mid = len(C) // 2 e1 = entries(H, L, C, A, SP, point, 60, mid) e2 = entries(H, L, C, A, 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 · kanonischer Exit · Echtkosten · EIN Slot") print(f" Basis = durchgehend {_WEIT_LIVE}xATR_M5 (= der Live-Stop 2,0xATR_M15)") print(f" ⚠ Bar-Sim UEBERSCHAETZT enge Stops (Kipp ~0,3xATR) — ein ✅ waere " f"nach oben verzerrt\n") b1 = kenn(*lauf(e1, H, L, C, SP, None, 0, _WEIT_LIVE)) b2 = kenn(*lauf(e2, H, L, C, SP, None, 0, _WEIT_LIVE)) print(" ── BASIS (heute) ──────────────────────────────────────────────") zeig("durchgehend 3,4xATR H1", b1); zeig("durchgehend 3,4xATR H2", b2) print("\n ── GESTAFFELT: eng fuer die ersten N Bars, dann 3,4xATR ───────") besser = [] for nb in (3, 6, 12): print(f" ── N = {nb} Bars ({nb*5} min) ──") for eng in (0.5, 0.8, 1.0, 1.5): k1 = kenn(*lauf(e1, H, L, C, SP, eng, nb, _WEIT_LIVE)) k2 = kenn(*lauf(e2, H, L, C, SP, eng, nb, _WEIT_LIVE)) zeig(f"eng {eng:.1f}xATR H1", k1, b1) zeig(f"eng {eng:.1f}xATR H2", k2, b2) if k1 and k2 and k1["oer"] > b1["oer"] and k2["oer"] > b2["oer"]: besser.append((nb, eng, k1["oer"] - b1["oer"], k2["oer"] - b2["oer"])) print("\n ── KONTROLLE: durchgehend eng (= der 2x gemessene Fall) ───────") for eng in (0.8, 1.5, 2.0): k1 = kenn(*lauf(e1, H, L, C, SP, None, 0, eng)) k2 = kenn(*lauf(e2, H, L, C, SP, None, 0, eng)) zeig(f"durchgehend {eng:.1f}xATR H1", k1, b1) zeig(f"durchgehend {eng:.1f}xATR H2", k2, b2) print("\n ── URTEIL ─────────────────────────────────────────────────────") if besser: print(f" {len(besser)} Kombination(en) schlagen die Basis in BEIDEN Haelften:") for nb, eng, d1, d2 in besser: print(f" N={nb}, eng={eng}xATR Δ H1 {d1:+.3f} · H2 {d2:+.3f}") print(" ⚠ VOR einer Umsetzung: das Auflösungs-Artefakt verzerrt genau " "diese Richtung\n nach oben. Belastbar waere erst eine Tick-Simulation.") else: print(" KEINE Kombination schlaegt die Basis in beiden Haelften.") print(" ⚠ Dieses Ergebnis ist belastbar: die Verzerrung der Bar-Sim geht " "ZUGUNSTEN\n der engen Stops — sie faellt trotzdem durch.") return 0 if __name__ == "__main__": raise SystemExit(main())