diff --git a/CLAUDE.md b/CLAUDE.md index e3ccdaf..4ec3e2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -701,9 +701,26 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: Tail-Risiko hinzu (das war die Sorge aus `backtest_sl_method.py`). ⚠ **Die KONTROLLVARIANTE „TP am Gegenlevel" reproduziert den alten Befund exakt** (WR 40→54 %, ΣR H2 +1261→+740 = Gewinner-Kappen) — damit ist die Simulation gegen - `backtest_srclose.py` validiert und der TP-Teil endgültig erledigt. **Offener - Nachtest:** dieselbe Messung NUR auf Squeeze-Entries (runner-abhängig → dort könnte - ein Level-Stop mehr bewegen als über alle Wave-Signale gemittelt). + `backtest_srclose.py` validiert und der TP-Teil endgültig erledigt. + **NACHTEST auf NUR Squeeze-Entries (`--squeeze`, 2026-07-31, Echtkosten): ERLEDIGT — + dort ist der S/R-Trail nicht nur zu klein, sondern klar SCHLECHTER.** Basis + H1 +0,102/H2 +0,108; **keine einzige Variante besteht** (S/R 0,10: +0,096/+0,093 · + 0,25: +0,083/+0,098 · 0,50: +0,052/+0,092 · Hybrid: +0,093/+0,122 = kippt · + Hybrid weit: +0,091/+0,084). Anders als im Wave-Lauf ist das **kein Rauschen**: der + reine S/R-Trail wird **monoton schlechter, je breiter der Puffer** (in BEIDEN + Hälften) — sauberer monotoner Verlauf, nur gegen die Idee. **Mechanismus:** der + Squeeze lebt von wenigen Läufern (43 % Treffer, Ertrag im Tail); ein an das + nächstgelegene Level gebundener Stop holt genau dort raus, wo der Lauf beginnt. Bei + Wave-Signalen fällt das kaum auf (selten große Runner) — daher das umgekehrte + Vorzeichen zwischen beiden Läufen. ⚠ **Stärkster Gewinner-Kappen-Beleg des + Projekts:** die Kontrollvariante (fester TP am Gegenlevel) dreht den Squeeze von + **+0,102/+0,108 auf −0,066/−0,004** = Edge vollständig zerstört. ⚠ **Abgrenzung + gegen den laufenden S/R-Auto-Close:** der ist **P(break)-gegated** (schließt nur, + wenn das Level wahrscheinlich HÄLT) und war auf Squeeze-Trades separat gemessen ein + **Wash** (`backtest_squeeze_srclose.py`, +0,190→+0,224 / +0,394→+0,377). **Das Gate + ist der Unterschied zwischen „Wash" und „Edge zerstört"** — ein ungegateter Level-TP + ist etwas grundlegend anderes. Damit ist die User-Idee in allen DREI Teilen + durchgemessen; der Live-Exit-Stack bleibt unverändert. - **S/R-verankerter SL = KEIN Edge (gemessen, `backtest_sl_method.py`):** Pivot+Band vs. „S/R ∓ ATR/0,10-Puffer" vs. fest 2,0×ATR — **gedeckelt alle identisch** (Ø-R 0,196–0,201, PF 1,38–1,39, Worst −2,2×ATR): der **Band-Cap diff --git a/backtest_trailing_sr.py b/backtest_trailing_sr.py index a70dcd2..de2b9c6 100644 --- a/backtest_trailing_sr.py +++ b/backtest_trailing_sr.py @@ -182,6 +182,42 @@ def st(Rs): f"Worst={min(Rs):+.2f} ΣR={sum(Rs):+.0f}") +def squeeze_signals(H, L, C, AT, lo, hi): + """Nur SQUEEZE-Ausbrüche (das einzige validierte Auto-Setup) — Nachtest + 2026-07-31: der Squeeze ist runner-abhängig (nur 43 % Treffer, der Ertrag + kommt aus wenigen Läufern), dort könnte ein level-verankerter Stop mehr + bewegen als über ALLE Wave-Signale gemittelt. + + Regel exakt wie live (`wave._squeeze_one`): Box = Spanne der letzten _SQ_N + abgeschlossenen M5-Bars ≤ _SQ_MULT×ATR, Ausbruch um _SQ_K×ATR. Einstieg AM + Ausbruchslevel (nicht am Bar-Close), Cooldown wie in den Squeeze-Backtests. + """ + from core.wave_rec import _SQ_N, _SQ_MULT, _SQ_K + _W = 24; _COOL = 12 + out = [] + i = max(lo, _N_BARS, _SQ_N + 15) + while i < min(hi, len(C) - _MAXH - 1): + atr = AT[i] + if not atr or atr < _ATRMIN: + i += 1; continue + boxHi = max(H[i - _SQ_N:i]); boxLo = min(L[i - _SQ_N:i]) + if (boxHi - boxLo) > _SQ_MULT * atr: # keine Kompression + i += 1; continue + hit = None + for j in range(i, min(i + _W, len(C) - _MAXH - 1)): + up = boxHi + _SQ_K * atr; dn = boxLo - _SQ_K * atr + if H[j] >= up: + hit = (j, 1, up); break + if L[j] <= dn: + hit = (j, -1, dn); break + if hit is None: + i += 1; continue + j, d, lvl = hit + out.append((j, d, atr, lvl)) + i = j + _COOL + return out + + def signals(H, L, C, ES, EF, AT, lo, hi, step): TH = _REVERSAL_STRETCH; out = [] for i in range(max(lo, _N_BARS), min(hi, len(C) - _MAXH - 1), step): @@ -204,8 +240,10 @@ def signals(H, L, C, ES, EF, AT, lo, hi, step): def main(): - n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 - step = int(sys.argv[2]) if len(sys.argv) > 2 else 2 + squeeze = "--squeeze" in sys.argv + args = [a for a in sys.argv[1:] if not a.startswith("--")] + n = int(args[0]) if args else 80000 + step = int(args[1]) if len(args) > 1 else 2 mt5.initialize(); sym = None for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): if mt5.symbol_info(c): @@ -215,22 +253,39 @@ def main(): bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, req) if bars is not None and len(bars) > 2000: break + si = mt5.symbol_info(sym); point = si.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] EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW); AT = _atr_series(H, L, C) LV = build_levels(H, L) mid = len(C) // 2 halves = [("H1 (alt)", _N_BARS, mid), ("H2 (neu)", mid, len(C))] print("=" * 96) - print(f" S/R-VERANKERTES TRAILING — {sym} M5 ({len(C)} Bars, step {step})") + kind = "NUR SQUEEZE-Ausbrüche (Echtkosten)" if squeeze else "Signal = Trend+Reversal" + print(f" S/R-VERANKERTES TRAILING — {sym} M5 ({len(C)} Bars) {kind}") print(f" Level = M30-Pivots k={_PIV_K}, Fenster {_PIV_LOOKBACK} M30-Bars (kausal), " f"Deckel [{_SL_MIN_ATR}..{_SL_MAX_ATR}]×ATR") + if squeeze: + print(" Erwartung Squeeze-Entry-Edge (validiert): ØR +0,14…+0,23 · PF>1") print("=" * 96) - SIG = {lbl: signals(H, L, C, ES, EF, AT, a, b, step) for lbl, a, b in halves} + if squeeze: + SIG = {lbl: squeeze_signals(H, L, C, AT, a, b) for lbl, a, b in halves} + else: + SIG = {lbl: [(i, d, atr, C[i]) for (i, d, atr) + in signals(H, L, C, ES, EF, AT, a, b, step)] for lbl, a, b in halves} for lbl, _, _ in halves: print(f" {lbl}: {len(SIG[lbl])} Signale") + def cost(i, atr): + """Echte Kosten = Bar-Spread/ATR (nur im Squeeze-Modus; für den reinen + VARIANTEN-Vergleich neutral, da alle Varianten dieselben Entries teilen — + macht das ØR aber mit der +0,14…+0,23-Erwartung vergleichbar.""" + if not squeeze: + return 0.0 + return (SP[i] if SP[i] > 0 else 0.0225) / atr + variants = [ ("LIVE ATR-Trail 1,5 (Basis)", dict(mode="atr", mult=1.5)), ("S/R-Trail Puffer 0,10", dict(mode="sr", buf=0.10)), @@ -243,7 +298,8 @@ def main(): for name, kw in variants: print(f"\n{name}") for lbl, _, _ in halves: - Rs = [sim(C[i], d, atr, H, L, C, i + 1, LV, **kw) for (i, d, atr) in SIG[lbl]] + Rs = [sim(entry, d, atr, H, L, C, i + 1, LV, **kw) - cost(i, atr) + for (i, d, atr, entry) in SIG[lbl]] print(f" {lbl} {st(Rs)}")