diff --git a/CLAUDE.md b/CLAUDE.md index 537772a..9e563af 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1654,6 +1654,51 @@ hatte am 2026-07-30 bereits **2213 ausgewertete Zeilen**, ohne dass jemand hinsa Verwandte Timer: **`HyperliquidBookflow`** (Order-Flow, 2 Stufen ~3 Tage/~2 Wochen), **`HyperliquidLeadLag`** (erledigt: Pepperstone führt), Wochenreport montags 07:30 (B4). +## WTI-Trading-Handbuch — Abgleich mit dem Gemessenen (2026-07-30) + +Der User reichte ein 19-seitiges WTI-Handbuch ein („lerne daraus Kontext für die +Empfehlung"). Ergebnis des Abgleichs: **der belegbare Teil ist längst umgesetzt, der +Rest ist in diesem Projekt bereits gemessen — und fiel durch.** Kein Konzept aus dem +Handbuch wurde neu ins Signal aufgenommen. + +**Bereits UMGESETZT (Handbuch bestätigt den Ist-Zustand):** +| Handbuch | im Bot | +|---|---| +| „Nur mit dem HTF-Trend" | M30-Gegen-Trend-Filter (gemessen: Edge ×2) + Entry-Checkliste | +| „Keine Trades vor EIA" | EIA-Blackout Mi 15:30–16:30 (gemessen: beidhälftig negativ) | +| ATR-basierter Stop + Puffer | Initial-SL-Band 1,8–2,2×ATR (gemessen besser als 1,5) | +| „Nach TP1 trailen" | Trailing 1,5×ATR + Breakeven 1,3 (gemessen optimal) | +| „Nach 3 Losses Pause" | Mental-Check in der Entry-Checkliste (Verlust-Serie des Tages) | +| Journal + Kennzahlen | `trades`-DB, Statistik-Tab, Wochenreport (WR/PF/Verhältnis) | +| NY-Session am besten | gemessen: 21–22 Uhr sind die einzig robust positiven Stunden | + +**Bereits GEMESSEN und VERWORFEN (Handbuch empfiehlt es trotzdem):** +Liquidity Sweeps (Equal Highs/Lows) · Order-Block-Klasse = Pivot-Zonen (6×) · +Double Top/Bottom · H&S/Triangle/Flag/Cup (Ziel-Trefferquote nur 13–38 %) · +Wyckoff Accumulation/Distribution · False Breakout · DXY-Korrelation +(`backtest_intermarket.py`) · Elliott (nur Verdict-Stimme, Gewicht 1,0). + +**NEU gemessen (die echten Lücken, `backtest_handbook.py`) — alle durchgefallen:** +| Konzept | H1 | H2 | Urteil | +|---|---|---|---| +| **PDH/PDL** Bounce („WTI reagiert extrem stark") | −0,335 | −0,199 | ❌ beidseitig negativ | +| **PDH/PDL** Sweep | −0,462 | −0,184 | ❌ schlechtestes Ergebnis überhaupt | +| **Asian Range H/L** Bounce | −0,134 | −0,005 | ❌ | +| **Asian Range H/L** Sweep | −0,178 | −0,040 | ❌ | +| **Discount/Premium**-Filter | −0,097 (Basis −0,135) | +0,041 (Basis +0,053) | ❌ nicht robust, wirft 70 % Volumen weg | +| **Freitag ab 16:00 CET meiden** | −0,179 (Basis −0,135) | +0,020 (Basis +0,053) | ❌ **verschlechtert BEIDE** | + +⚠ Zwei Befunde sind bemerkenswert: (a) **PDH/PDL ist der am klarsten schädliche +Level-Typ**, den das Projekt je gemessen hat — obwohl das Handbuch ihn als besonders +stark bewirbt. (b) Der **Freitagnachmittags-Filter verschlechtert beide Hälften**, die +dort geblockten Trades waren also überdurchschnittlich — dieselbe Lehre wie beim +Dead-Hours-Gate: pauschale Zeitfilter kosten mehr, als sie sparen. + +**Nicht umgesetzt (bewusste User-Entscheidungen, nicht Messfrage):** 1–2 % Risk/Trade +und 3 % Tages-Limit (User fährt 95 % Margin, Circuit Breaker am 2026-07-30 erneut +abgelehnt) · Mindest-R:R 1:2 (der Bot nutzt Trailing statt festem Ziel — gemessen +besser) · Teil-Exit bei 1:1 (`_PARTIAL_TP_FRAC=0`, User-Vorgabe). + ## Auch beachten - **Startup-Schonfrist gegen Sofort-Close nach Neustart (User-Vorgabe 2026-07-20, `[trading] startup_close_grace_s=60`):** In den ersten 60 s nach Bot-Start schließt diff --git a/backtest_handbook.py b/backtest_handbook.py new file mode 100644 index 0000000..0b6f1cd --- /dev/null +++ b/backtest_handbook.py @@ -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** (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()