backtest_handbook.py: WTI-Handbuch abgeglichen -- alle Luecken gemessen, alle durch

User reichte ein 19-seitiges WTI-Trading-Handbuch ein ("lerne daraus Kontext fuer
die Empfehlung"). Systematischer Abgleich statt naivem Einbau:

BEREITS UMGESETZT (Handbuch bestaetigt den Ist-Zustand): HTF-Trend-Filter,
EIA-Blackout, ATR-Stop mit Puffer, Trailing nach TP1, Verlust-Serien-Check,
Journal/Kennzahlen, NY-Session-Fokus.

BEREITS GEMESSEN + VERWORFEN: Liquidity Sweeps, Order-Block-Klasse (Pivot-Zonen,
6x), Double Top/Bottom, H&S/Triangle/Flag/Cup, Wyckoff, False Breakout, DXY.

NEU GEMESSEN (die echten Luecken) -- alle durchgefallen:
  PDH/PDL bounce   H1 -0.335 / H2 -0.199   (beidseitig negativ)
  PDH/PDL sweep    H1 -0.462 / H2 -0.184   (schlechtestes Ergebnis im Projekt)
  Asian Range bnc  H1 -0.134 / H2 -0.005
  Asian Range swp  H1 -0.178 / H2 -0.040
  Discount/Premium: nicht robust (H1 besser, H2 schlechter), -70% Volumen
  Freitag-ab-16h  : verschlechtert BEIDE Haelften

Bemerkenswert: PDH/PDL ist der am klarsten schaedliche Level-Typ, den das Projekt
je gemessen hat -- obwohl das Handbuch ihn als besonders stark bewirbt. Und der
Freitagnachmittags-Filter kostet Geld, die dort geblockten Trades waren
ueberdurchschnittlich (gleiche Lehre wie beim Dead-Hours-Gate).

Kein Konzept aus dem Handbuch neu ins Signal aufgenommen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Axel Hocks
2026-07-30 20:42:36 +02:00
co-authored by Claude Opus 4.8
parent 11e13cdb5a
commit 99bed4bdce
2 changed files with 270 additions and 0 deletions
+225
View File
@@ -0,0 +1,225 @@
#!/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** (0009 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 (0009 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 (0009 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()