#!/usr/bin/env python3 """backtest_handbook.py — die NOCH NICHT gemessenen Konzepte aus dem WTI-Handbuch (User-Vorgabe 2026-07-30 „lerne aus dem Handbuch Kontext für die Empfehlung"). Das Handbuch überschneidet sich zu großen Teilen mit bereits gemessenen (und meist verworfenen) Ideen — s. Abgleich in CLAUDE.md. Hier werden nur die **echten Lücken** getestet, alle mechanisch sauber definierbar: 1) **PDH/PDL** (Vortages-Hoch/Tief) — „WTI reagiert extrem stark". Ein ANDERER Level-Typ als unsere Pivots: zeit- statt strukturbasiert. Getestet als Reaktions-Level (prallt der Kurs ab?) und als Sweep-Setup. 2) **Asian Range H/L** (00–09 CET) — „wird oft in London/NY gesweept". 3) **Discount/Premium** („kaufe nur unter 50 % der Range, verkaufe nur darüber") — als FILTER auf das bestehende Wave-Signal, nicht als eigener Entry. 4) **Freitag-Nachmittag** („nach 16:00 CET keine neuen Positionen, höhere Loss-Rate") — ebenfalls als Filter, wie das gemessene Dead-Hours-Gate. Methodik wie im ganzen Projekt: M5, 2 Halbjahre, Live-Exit (SL 2,0×ATR · Trail 1,5 ab 0,3 · BE 1,3 · Time-Stop 120 min), Kosten = echter Bar-Spread/ATR. Filter (3/4) werden gegen die UNGEFILTERTE Basis gehalten — ein Filter taugt nur, wenn er in BEIDEN Hälften verbessert (Lehre aus 9 verworfenen Filtern). Aufruf: python backtest_handbook.py [n_bars] """ from __future__ import annotations import sys from datetime import datetime, timezone import MetaTrader5 as mt5 _SL_ATR, _TRAIL, _TRAIL_ON, _BE_ON, _TIMESTOP, _MAXH = 2.0, 1.5, 0.3, 1.3, 24, 288 _ATRMIN = 0.06 _COOL = 12 _TOUCH = 0.15 # „am Level" = innerhalb x×ATR _SWEEP_MIN, _SWEEP_MAX = 0.05, 1.0 _EMA_F, _EMA_S = 12, 50 def _atr_series(H, L, C, p=14): tr = [0.0] for i in range(1, len(C)): tr.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1]))) out = [None] * len(C); run = 0.0 for i in range(1, len(C)): run += tr[i] if i > p: run -= tr[i - p] out[i] = run / min(i, p) return out def _ema(vals, p): k = 2.0 / (p + 1); o = [vals[0]] for v in vals[1:]: o.append(v * k + o[-1] * (1 - k)) return o def _sim(entry, d, atr, H, L, C, j0): eff = entry - d * _SL_ATR * atr; hw = entry; started = False end = min(j0 + _MAXH, len(C) - 1); px = C[end] for j in range(j0, end + 1): if (L[j] <= eff) if d > 0 else (H[j] >= eff): px = eff; break hw = max(hw, H[j]) if d > 0 else min(hw, L[j]) prof = (C[j] - entry) * d if prof >= _TRAIL_ON * atr: started = True cand = hw - d * _TRAIL * atr if prof >= _BE_ON * atr: cand = max(cand, entry) if d > 0 else min(cand, entry) eff = max(eff, cand) if d > 0 else min(eff, cand) if not started and (j - j0) >= _TIMESTOP: px = C[j]; break return (px - entry) * d / atr def _rep(name, Rs, ind=" "): if not Rs or len(Rs) < 10: print(f"{ind}{name:<34} n={len(Rs):>4} (zu wenige)"); return None n = len(Rs); w = sum(1 for x in Rs if x > 0); s = sum(Rs) up = sum(x for x in Rs if x > 0); dn = -sum(x for x in Rs if x < 0) print(f"{ind}{name:<34} n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} " f"PF={up/dn if dn>0 else 9.99:.2f} ΣR={s:+.0f}") return s / n def day_levels(T, H, L, hb): """Je Bar: (PDH, PDL, AsiaH, AsiaL) des jeweils VORHERGEHENDEN Tages bzw. der heutigen, ABGESCHLOSSENEN Asia-Session (00–09 CET). Kein Look-ahead.""" n = len(T) day = [datetime.fromtimestamp(t, timezone.utc).date() for t in T] pdh = [None] * n; pdl = [None] * n; ah = [None] * n; al = [None] * n cur = day[0]; hi = H[0]; lo = L[0] prev_hi = prev_lo = None a_hi = a_lo = None; a_done_hi = a_done_lo = None for i in range(n): if day[i] != cur: # neuer Tag → Vortag einfrieren prev_hi, prev_lo = hi, lo cur = day[i]; hi, lo = H[i], L[i] a_hi = a_lo = None; a_done_hi = a_done_lo = None else: hi = max(hi, H[i]); lo = min(lo, L[i]) if hb[i] < 9: # Asia-Session läuft (00–09 CET) a_hi = H[i] if a_hi is None else max(a_hi, H[i]) a_lo = L[i] if a_lo is None else min(a_lo, L[i]) elif a_hi is not None and a_done_hi is None: a_done_hi, a_done_lo = a_hi, a_lo # Asia vorbei → einfrieren pdh[i], pdl[i] = prev_hi, prev_lo ah[i], al[i] = a_done_hi, a_done_lo return pdh, pdl, ah, al def scan_level(H, L, C, SP, A, lvl_hi, lvl_lo, lo_i, hi_i, mode): """mode 'bounce': am Level in die Gegenrichtung (Level hält). mode 'sweep' : Level kurz durchstochen, Close zurück → Gegenrichtung.""" Rs = [] def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225) / atr i = max(lo_i, 5) hi_i = min(hi_i, len(C) - _MAXH - 2) while i < hi_i: atr = A[i] if not atr or atr < _ATRMIN: i += 1; continue hit = None for lv, d in ((lvl_hi[i], -1), (lvl_lo[i], 1)): # oben→short, unten→long if lv is None: continue if mode == "bounce": if abs(C[i] - lv) <= _TOUCH * atr: hit = (lv, d); break else: over = (H[i] - lv) if d < 0 else (lv - L[i]) back = (C[i] < lv) if d < 0 else (C[i] > lv) if _SWEEP_MIN * atr <= over <= _SWEEP_MAX * atr and back: hit = (lv, d); break if hit is None: i += 1; continue _lv, d = hit Rs.append(_sim(C[i], d, atr, H, L, C, i + 1) - cost(i, atr)) i += _COOL return Rs def scan_signal(H, L, C, SP, A, EF, ES, hb, wd, lo_i, hi_i, mode): """Basis-Signal (EMA12/50-Trend, wie die Welle) mit optionalem Filter: 'base' · 'discount' (nur im günstigen Range-Drittel) · 'nofriday'.""" Rs = [] def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225) / atr i = max(lo_i, 60) hi_i = min(hi_i, len(C) - _MAXH - 2) while i < hi_i: atr = A[i] if not atr or atr < _ATRMIN: i += 1; continue d = 1 if EF[i] > ES[i] + 0.15 * atr else -1 if EF[i] < ES[i] - 0.15 * atr else 0 if d == 0: i += 1; continue if mode == "nofriday" and wd[i] == 4 and hb[i] >= 16: i += 1; continue # Freitag ab 16:00 CET if mode == "discount": # Range der letzten 60 Bars; LONG nur unter 50 %, SHORT nur darüber rh = max(H[i - 60:i]); rl = min(L[i - 60:i]) if rh <= rl: i += 1; continue pos = (C[i] - rl) / (rh - rl) if (d > 0 and pos > 0.5) or (d < 0 and pos < 0.5): i += 1; continue Rs.append(_sim(C[i], d, atr, H, L, C, i + 1) - cost(i, atr)) i += _COOL return Rs def main(): n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000 mt5.initialize(); sym = None for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD"): if mt5.symbol_info(c): sym = c; break bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n) 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] T = [int(b["time"]) for b in bars] A = _atr_series(H, L, C); EF = _ema(C, _EMA_F); ES = _ema(C, _EMA_S) # Broker UTC+3 → Berlin = Broker − 1 h hb = [(datetime.fromtimestamp(t, timezone.utc).hour - 1) % 24 for t in T] wd = [datetime.fromtimestamp(t - 3600, timezone.utc).weekday() for t in T] PDH, PDL, AH, AL = day_levels(T, H, L, hb) N = len(C); mid = N // 2 print("=" * 92) print(f" WTI-HANDBUCH — die noch NICHT gemessenen Konzepte ({sym} M5, {N} Bars)") print(f" Live-Exit · Echtkosten · 2 Halbjahre · Maßstab Squeeze ØR +0,14…+0,23") print("=" * 92) verdict = {} print("\n### 1/2 — PDH/PDL und Asian Range als Reaktions-Level") for lbl, lh, ll in (("PDH/PDL", PDH, PDL), ("Asian Range H/L", AH, AL)): for mode in ("bounce", "sweep"): print(f" {lbl} · {mode}") for hlbl, a, b in (("H1", 5, mid), ("H2", mid, N)): Rs = scan_level(H, L, C, SP, A, lh, ll, a, b, mode) verdict.setdefault((lbl, mode), []).append(_rep(f"[{hlbl}]", Rs)) print("\n### 3/4 — Discount/Premium und Freitag-Nachmittag als FILTER") print(" (Basis = EMA-Trendsignal ohne Filter; ein Filter taugt nur, wenn er") print(" in BEIDEN Hälften verbessert)") for mode, lbl in (("base", "BASIS (ohne Filter)"), ("discount", "nur Discount/Premium"), ("nofriday", "ohne Fr ab 16:00 CET")): print(f" {lbl}") for hlbl, a, b in (("H1", 60, mid), ("H2", mid, N)): Rs = scan_signal(H, L, C, SP, A, EF, ES, hb, wd, a, b, mode) verdict.setdefault(("filter", lbl), []).append(_rep(f"[{hlbl}]", Rs)) print("\n" + "=" * 92) print(" URTEIL") base = verdict.get(("filter", "BASIS (ohne Filter)")) for key, vals in verdict.items(): if len(vals) != 2 or any(v is None for v in vals): continue if key[0] == "filter": if key[1].startswith("BASIS"): continue ok = all(vals[i] > base[i] for i in (0, 1)) print(f" Filter {key[1]:<28} H1 {vals[0]:+.3f} (Basis {base[0]:+.3f}) · " f"H2 {vals[1]:+.3f} (Basis {base[1]:+.3f}) " f"{'✅ verbessert beide' if ok else '❌ nicht robust'}") else: ok = all(v > 0 for v in vals); band = all(v >= 0.14 for v in vals) mark = "✅ TRÄGT" if (ok and band) else ("⚠ positiv, unter Band" if ok else "❌ fällt durch") print(f" {key[0]} · {key[1]:<22} H1 {vals[0]:+.3f} · H2 {vals[1]:+.3f} {mark}") if __name__ == "__main__": main()