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:
co-authored by
Claude Opus 4.8
parent
11e13cdb5a
commit
99bed4bdce
@@ -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),
|
Verwandte Timer: **`HyperliquidBookflow`** (Order-Flow, 2 Stufen ~3 Tage/~2 Wochen),
|
||||||
**`HyperliquidLeadLag`** (erledigt: Pepperstone führt), Wochenreport montags 07:30 (B4).
|
**`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
|
## Auch beachten
|
||||||
- **Startup-Schonfrist gegen Sofort-Close nach Neustart (User-Vorgabe 2026-07-20,
|
- **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
|
`[trading] startup_close_grace_s=60`):** In den ersten 60 s nach Bot-Start schließt
|
||||||
|
|||||||
@@ -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()
|
||||||
Reference in New Issue
Block a user