Files
AH-Oil-Trader/backtest_handbook.py
T
Axel HocksandClaude Opus 4.8 99bed4bdce 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>
2026-07-30 20:42:36 +02:00

226 lines
9.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()