diff --git a/backtest_brk_slonly.py b/backtest_brk_slonly.py new file mode 100644 index 0000000..8c82f64 --- /dev/null +++ b/backtest_brk_slonly.py @@ -0,0 +1,164 @@ +"""Was bleibt vom Squeeze-Edge, wenn NUR der Broker-SL schuetzt? (2026-08-19) + +FRAGE (User: "BRK soll auch eroeffnen wenn M15 aktiv ist"). Damit BRK parallel +zu einer anderen Position laufen kann, braucht es einen zweiten Positions-Slot. +Der kleine Umbau (Pendings nicht mehr stornieren, `open_*` nicht mehr bei +`self.ticket` abweisen) waere an einem Abend gemacht -- ABER der zweite Trade +haette dann nur den **Broker-SL**: `trader` haelt EIN Ticket (`self.ticket`), und +Trailing, Breakeven, Time-Stop, Notfall-Stop und S/R-Close haengen alle daran +(29 Stellen in `engine.py`). + +⚠⚠ DAS IST NICHT NUR EIN SCHUTZ-, SONDERN EIN EDGE-PROBLEM. Die dokumentierten +Squeeze-Zahlen (OeR +0,14…+0,23) sind MIT dem kanonischen Exit gemessen. Der +Squeeze lebt von LAEUFERN (43 % Treffer, Ertrag im Tail) -- ein reiner SL-Exit +ist eine ANDERE, nie gemessene Konfiguration. Sie ohne Messung zu betreiben +waere Deployment-Drift per Konstruktion (neunmal dokumentiert). + +DIESE MESSUNG ENTSCHEIDET DIE BAUWEISE: + traegt SL-only -> der kleine Umbau reicht, BRK kann sofort parallel laufen + traegt es nicht -> es braucht das Trailing JE TICKET, also den grossen Umbau + +AUFBAU. Echte `core.squeeze_scan`-Regel (kein Nachbau), LIVE-Parameter, ruhende +Stop-Order am Level mit Fill bei BERUEHRUNG, Echtkosten aus dem Bar-Spread, +2 Halbjahre, 95-%-Bootstrap je Zelle. + +⚠ KEINE Exit-Kopie: die SL-only-Variante entsteht aus `LIVE.with_(...)`, indem +Trail-Start und Breakeven unerreichbar gesetzt werden -- der Trade bleibt damit +in Phase "Init" und verlaesst sie nur ueber den SL oder `max_hold`. Es laeuft +also derselbe kanonische Code, nur ohne die Phasen. Eine eigene Schleife waere +das fuenfte Exit-Modell gewesen. + +⚠ Zwischenstufen bewusst mitgemessen, weil die Entscheidung nicht binaer ist: + (b) SL + Time-Stop -- der Time-Stop haengt NICHT am Trailing-Zustand, + er koennte also billig nachgeruestet werden + (c) SL + Initial-TP -- `place_stop` koennte einen TP mitschicken + (d) SL + Time-Stop + TP -- was ohne echtes Trailing maximal erreichbar ist + +REGEL VORAB FIXIERT: SL-only gilt als tragfaehig, wenn es in BEIDEN Haelften +OeR > 0 UND PF > 1 liefert und das 95-%-KI die Null ausschliesst. Alles darunter +heisst: grosser Umbau, nicht kleiner. + +Aufruf: python backtest_brk_slonly.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 + +# LIVE-Parameter -- ⚠ NICHT die Modul-Defaults. Der ATR-Floor wurde gemessen von +# 0,12 auf 0,06 gesenkt; ihn zu vergessen erzeugt eine ANDERE Population (real +# passiert am 13.08.: H1 127 gegen H2 483 Einstiege). +BOX_N, K, MULT, ATRMIN = 10, 0.1, 2.5, 0.06 + +# SL-only: Trail-Start und Breakeven unerreichbar -> der Trade bleibt in Phase +# "Init" und verlaesst sie nur ueber den Initial-SL oder `max_hold`. +SL_ONLY = LIVE.with_(trail_start=999.0, be=999.0) + +VARIANTEN = [ + ("A) kanonisch (heute)", LIVE, True, True), + ("B) SL only", SL_ONLY, False, False), + ("C) SL + Time-Stop", SL_ONLY, True, False), + ("D) SL + TP", SL_ONLY, False, True), + ("E) SL + Time + TP", SL_ONLY, True, True), +] + + +def einstiege(H, L, C, A, SP, lo, hi): + """Squeeze-Ausbrueche in [lo,hi) -- nur die KOMPRIMIERTEN.""" + out = [] + scan(H, L, C, A, SP, 1.0, lo, hi, MULT, + 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 + + +def lauf(ein, H, L, C, SP, ende, params, timestop, use_tp): + rs = [] + for j, d, lvl, atr in ein: + # ruhende Stop-Order am Level: fuellt bei der ersten BERUEHRUNG + 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 + r = simulate(lvl, d, atr, H, L, C, f + 1, params, + timestop=timestop, use_tp=use_tp) + rs.append(r - ((SP[f] if SP[f] > 0 else 0.0225) / atr)) + return rs + + +def zeile(lbl, k1, k2): + def f(k): + if not k: + return " --" + ci = k.get("ci") + m = "" + if ci: + m = (" [{:+.3f} .. {:+.3f}]".format(ci[0], ci[1]) + + ("" if ci[0] * ci[1] > 0 else " ~0")) + return ("n={:>4} WR={:>3.0f}% OeR={:+.3f} PF={:.2f} SR={:+7.1f}{}" + .format(k["n"], k["wr"], k["oer"], k["pf"], k["sr"], m)) + print(" {:<22} H1 {}".format(lbl, f(k1))) + print(" {:<22} H2 {}".format("", f(k2))) + ok = (k1 and k2 and k1["oer"] > 0 and k2["oer"] > 0 + and k1["pf"] > 1 and k2["pf"] > 1 + and (k1.get("ci") or (0, 0))[0] > 0 and (k2.get("ci") or (0, 0))[0] > 0) + print(" {:<22} {}".format("", "✅ Regel erfuellt" if ok + else "❌ Regel NICHT erfuellt")) + print("") + return ok + + +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() + 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("=" * 96) + print(" BRK OHNE TRAILING -- was bleibt vom Edge? {} M5 ({} Bars)".format(sym, N)) + print(" Fill bei BERUEHRUNG · Echtkosten · 2 Halbjahre · 95-%-KI") + print("=" * 96) + + # ⚠ Feste, GETEILTE Einstiegslisten: alle Varianten laufen auf DENSELBEN + # Trades, sonst vergleicht man Populationen statt Exits (der Fehler, der + # am 31.07. in `backtest_exit_combo.py` korrigiert wurde). + e1 = einstiege(H, L, C, A, SP, 0, mid) + e2 = einstiege(H, L, C, A, SP, mid, ende) + print(" Einstiege: H1 {} H2 {}\n".format(len(e1), len(e2))) + + erg = {} + for lbl, par, ts, tp in VARIANTEN: + k1 = kennzahlen(lauf(e1, H, L, C, SP, ende, par, ts, tp)) + k2 = kennzahlen(lauf(e2, H, L, C, SP, ende, par, ts, tp)) + erg[lbl] = (k1, k2, zeile(lbl, k1, k2)) + + print(" " + "=" * 92) + a1, a2, _ = erg["A) kanonisch (heute)"] + b1, b2, bok = erg["B) SL only"] + if a1 and b1: + print(" Preis des fehlenden Trailings: H1 {:+.3f} R H2 {:+.3f} R je Trade" + .format(b1["oer"] - a1["oer"], b2["oer"] - a2["oer"])) + print("") + print(" LESART: besteht B) die Regel, reicht der KLEINE Umbau (Pendings") + print(" nicht mehr stornieren). Sonst braucht es das Trailing JE TICKET.") + print(" " + "=" * 92) + + +if __name__ == "__main__": + main()