Files
AH-Oil-Trader/backtest_auto_signal.py
T
Axel HocksandClaude Opus 4.8 d40616fbeb backtest_auto_signal.py: Auto-Entry auf die Empfehlung gemessen -> VERWORFEN
User-Wunsch "Trades bei Empfehlung automatisch eroeffnen" -> erst gemessen (B2).

Methodik: echte _build-Logik (kein Nachbau), M30-/H1-Trend aus M5 aggregiert und
in _build gefuettert, sequentielle 1-Positions-Sim, Live-Exit (SL2,0/Trail1,5/
BE1,3/Time-Stop 120min), Echtkosten (Bar-Spread/ATR), 80k M5-Bars, 2 Halbjahre.

Ergebnis: ALLE 8 Varianten (Konf 55/65/70/75 x Nacht-Sperre) fallen durch.
  H1 durchgehend NEGATIV -0.076..-0.182 (PF 0.78-0.90)
  H2 durchgehend positiv +0.112..+0.167
  -> Regime-Kippen, n=718-1167 je Haelfte (keine Stichprobenfrage).

Kontroll-Beleg: der Squeeze laeuft durch DIESELBE Exit-/Kosten-Maschinerie und
ist in H1 +0.187/PF1.29 -> die Sim funktioniert, das Wave-Signal traegt in H1 nicht.
Deckt sich mit dem Live-Dry-Run (WR 29%/PF 0.64).

Nebenbefunde: hoehere Konfidenz hilft NICHT monoton (conf_pct nicht kalibriert);
Nacht-Sperre verbessert konsistent, reicht aber nicht; ein 6k-Vorlauf zeigte
faelschlich "traegt" (n=60-99 = Rauschen) -> kleine Stichprobe haette zum
Fehl-Einbau gefuehrt.

CLAUDE.md aktualisiert (14. verworfener Signal-Eingriff).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-30 13:45:35 +02:00

247 lines
10 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_auto_signal.py — Auto-Entry auf die EMPFEHLUNG (B2, User-Wunsch
2026-07-30 „Trades bei Empfehlung automatisch eröffnen").
Kernfrage: Trägt ein AUTONOMER Entry auf dem Wave-Signal (mit den HEUTIGEN Gates)
netto in BEIDEN Halbjahren — und hält er gegen den einzigen validierten Auto-Entry
(Squeeze: ØR +0,14…+0,23, PF>1)?
Vorgeschichte (hohe Beweislast!): Der Wave-Auto-Trader lief live als Dry-Run und
war ein klarer VERLIERER (WR 29 % / PF 0,64 vs. manuell 5676 %) → am 2026-07-23
komplett entfernt. Gegenbefund: die MANUELLEN „MIT Signal"-Trades des Users sind
profitabel (+118 €, WR 56 %) — aber das ist MENSCHLICHE AUSWAHL unter den Signalen,
nicht „alle nehmen". Genau diese Differenz misst dieses Skript.
Methodik (bewusst pessimistisch, wie backtest_breakout_squeeze.py):
· Signal = die ECHTE Logik `WaveRecommender._build` (kein Nachbau), inkl.
Totband, Anti-Überdehnung, Konfidenz-Gate 55, Reversal-Zweig
· HTF = M30- UND H1-Trend aus den M5-Bars aggregiert und in `_build`
gefüttert (`htf_trend`/`h1_trend`) → der live aktive M30-Gegen-
Trend-Filter und der H1-Konfluenz-Bonus wirken mit
· Entry = am Bar-CLOSE des Signal-Bars, nur wenn FLAT (sequentiell, 1 Position)
· Exit = Live-Modell: SL 2,0×ATR · Trailing 1,5 ab 0,3 · Breakeven 1,3 ·
Time-Stop 120 min (24 M5-Bars) wenn nie Trail-Start erreicht
· Kosten = echter Bar-Spread/ATR (Ø ~0,265×ATR — NICHT die alte 0,1-Annahme)
· 2 Halbjahre (H1/H2) — eine Variante gilt nur als „trägt", wenn sie in BEIDEN
positiv ist (Dead-Hour-Lektion)
NICHT simuliert (bewusst): Entry-Raum-Gate + ATR-Breakout-Bestätigung (beide
brauchen Live-State/pb_levels, s. CLAUDE.md — Backtests bleiben gate-frei) sowie
das EIA-Blackout-Gate. Beide würden Trades ENTFERNEN, die Ergebnisse hier sind
also eher optimistisch als zu streng.
Aufruf: python backtest_auto_signal.py [n_bars] (Default 40000 M5-Bars)
"""
from __future__ import annotations
import sys
from datetime import datetime, timezone, timedelta
import MetaTrader5 as mt5
from core.wave_rec import (WaveRecommender, _atr, _ema_last,
_EMA_FAST, _EMA_SLOW, _HTF_DEADBAND)
# ── Exit-Parameter = die LIVE-Werte (core/trailing.py) ────────────────────────
_SL_ATR = 2.0 # Initial-SL (Band 1,82,2 → Ziel 2,0)
_TRAIL = 1.5 # Trailing-Abstand ×ATR (M5-Mult)
_TRAIL_ON = 0.3 # Trail-Start ab diesem Profit ×ATR
_BE_ON = 1.3 # Breakeven-Floor ab diesem Profit ×ATR
_TIMESTOP = 24 # Time-Stop: 120 min = 24 M5-Bars in Phase „Init"
_MAXH = 288 # Sim-Horizont (24 h auf M5)
_ATRMIN = 0.06 # ATR-Floor wie live (_ATR_MIN in trailing.py)
_NIGHT = (0, 1, 2, 3, 4, 5, 6, 7) # Berlin-Stunden = gemessene Kostenfalle
_HTF_N = 6 # 6 M5-Bars = 1 M30-Bar
_H1_N = 12 # 12 M30-Bars … (H1 = 12 M5-Bars)
class _NeutralTU:
"""TU ist aus der Empfehlung entfernt — im Test neutral (kein Einfluss)."""
def snapshot(self):
return {"intervals": {}}
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_series(vals, period):
k = 2.0 / (period + 1)
out = [vals[0]]
for v in vals[1:]:
out.append(v * k + out[-1] * (1.0 - k))
return out
def _htf_signs(H, L, C, step):
"""Aggregiert die M5-Bars zu einer höheren TF (step M5-Bars je HTF-Bar) und
liefert je M5-Index das Trend-Vorzeichen (+1/-1/0, Totband _HTF_DEADBAND×ATR)
— LOOK-AHEAD-frei: an Index i zählen nur HTF-Bars, die vor i abgeschlossen sind."""
# HTF-Bars bilden
hc, hh, hl = [], [], []
for s in range(0, len(C) - step + 1, step):
seg_h = H[s:s + step]; seg_l = L[s:s + step]
hh.append(max(seg_h)); hl.append(min(seg_l)); hc.append(C[s + step - 1])
if len(hc) < _EMA_SLOW + 5:
return [0] * len(C)
ef = _ema_series(hc, _EMA_FAST); es = _ema_series(hc, _EMA_SLOW)
hatr = _atr_series(hh, hl, hc)
signs_htf = []
for j in range(len(hc)):
a = hatr[j]
if j < _EMA_SLOW + 2 or not a:
signs_htf.append(0); continue
d = ef[j] - es[j]
signs_htf.append(0 if abs(d) < _HTF_DEADBAND * a else (1 if d > 0 else -1))
# auf M5-Index abbilden: bei i ist der letzte ABGESCHLOSSENE HTF-Bar (i//step)-1
out = [0] * len(C)
for i in range(len(C)):
j = i // step - 1
out[i] = signs_htf[j] if 0 <= j < len(signs_htf) else 0
return out
def _sim(entry, d, atr, H, L, C, j0):
"""Live-Exit-Modell. Rückgabe R = Profit/ATR (brutto, ohne Kosten).
Phasen-Ratsche wie live: Time-Stop greift nur, solange nie Trail-Start erreicht."""
eff = entry - d * _SL_ATR * atr
hw = entry
trail_started = False
end = min(j0 + _MAXH, len(C) - 1)
exit_px = C[end]
for j in range(j0, end + 1):
hi, lo = H[j], L[j]
# SL/Trail-Stop zuerst (konservativ: Stop schlägt Weiterlaufen im selben Bar)
if (lo <= eff) if d > 0 else (hi >= eff):
exit_px = eff; break
hw = max(hw, hi) if d > 0 else min(hw, lo)
prof = (C[j] - entry) * d
if prof >= _TRAIL_ON * atr:
trail_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)
# Time-Stop: nach 120 min noch in „Init" (nie Trail-Start) → raus
if not trail_started and (j - j0) >= _TIMESTOP:
exit_px = C[j]; break
return (exit_px - entry) * d / atr
def _rep(name, Rs, ref=None):
if not Rs:
print(f" {name:<34} -"); 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)
pf = up / dn if dn > 0 else 9.99
worst = min(Rs)
print(f" {name:<34} n={n:>4} WR={100*w/n:>3.0f}% "
f"ØR={s/n:+.3f} PF={pf:.2f} ΣR={s:+.0f} worst={worst:+.1f}")
return s / n
def scan(bars, H, L, C, SP, A, HTF, H1, EF, ES, w, lo_i, hi_i, min_conf, skip_night):
"""Sequentielle 1-Positions-Simulation: nimmt das ERSTE Signal, wenn flat."""
Rs = []
i = max(lo_i, _EMA_SLOW + 20)
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
if skip_night:
# Broker-Zeit ist UTC+3, Berlin (Sommer) UTC+2 → Berlin = Broker 1 h
hb = (datetime.fromtimestamp(int(bars[i]["time"]), timezone.utc).hour - 1) % 24
if hb in _NIGHT:
i += 1; continue
ef, es = EF[i], ES[i]
rec, _snap = w._build(ef, es, C[i], atr, "M5", 5,
htf_trend=HTF[i], h1_trend=H1[i])
sig = rec.get("signal")
if sig not in ("LONG", "SHORT") or (rec.get("conf_pct") or 0) < min_conf:
i += 1; continue
d = 1 if sig == "LONG" else -1
entry = C[i]
cost = (SP[i] if SP[i] > 0 else 0.0225) / atr
R = _sim(entry, d, atr, H, L, C, i + 1) - cost
Rs.append(R)
# nach dem Trade weiterspringen: der Exit fand irgendwo in [i+1, i+_MAXH]
# statt — konservativ mit dem Time-Stop-Fenster weiterrücken (kein Overlap)
i += _TIMESTOP
return Rs
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 40000
if not mt5.initialize():
print(f"MT5-Init fehlgeschlagen: {mt5.last_error()}"); sys.exit(1)
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 + _MAXH + 60)
si = mt5.symbol_info(sym); point = si.point
mt5.shutdown()
if bars is None or len(bars) < 5000:
print("Zu wenige Bars."); sys.exit(1)
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]
A = _atr_series(H, L, C)
HTF = _htf_signs(H, L, C, _HTF_N) # M30-Trend je M5-Bar
H1 = _htf_signs(H, L, C, _HTF_N * 2) # H1-Trend je M5-Bar (12 M5-Bars)
w = WaveRecommender(_NeutralTU(), mt5.TIMEFRAME_M5)
# EMA-Serien vorab (identisch zur Live-Rekursion, nur ohne Fenster-Neuaufbau)
EF = _ema_series(C, _EMA_FAST)
ES = _ema_series(C, _EMA_SLOW)
N = len(C); mid = N // 2
print("=" * 92)
print(f" AUTO-ENTRY auf die EMPFEHLUNG — {sym} M5 ({N} Bars) · echte _build-Logik")
print(f" Exit=live(SL2,0/Trail1,5/BE1,3/Time-Stop120min) · Kosten=Bar-Spread/ATR")
print(f" Referenz (validierter Auto-Entry Squeeze): ØR +0,14…+0,23 · PF>1")
print("=" * 92)
verdict = {}
for label, lo, hi in (("H1 (alt)", 0, mid), ("H2 (neu)", mid, N)):
print(f"\n{label}:")
for mc in (55, 65, 70, 75):
for night in (False, True):
tag = f"Konf>={mc}" + (" · Nacht-Sperre" if night else "")
Rs = scan(bars, H, L, C, SP, A, HTF, H1, EF, ES, w, lo, hi, mc, night)
oR = _rep(tag, Rs)
verdict.setdefault((mc, night), []).append(oR)
print("\n" + "=" * 92)
print(" URTEIL (trägt nur, wenn ØR in BEIDEN Hälften > 0 UND im Squeeze-Band):")
any_ok = False
for (mc, night), vals in sorted(verdict.items()):
if len(vals) != 2 or any(v is None for v in vals):
continue
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 Squeeze-Band" if ok else "❌ fällt durch")
if ok and band:
any_ok = True
tag = f"Konf>={mc}" + (" +Nacht-Sperre" if night else "")
print(f" {tag:<28} H1 ØR {vals[0]:+.3f} · H2 ØR {vals[1]:+.3f} {mark}")
print("\n " + ("→ mindestens eine Variante trägt: Einbau vertretbar (klein starten, B4-Monitor)."
if any_ok else
"→ KEINE Variante trägt beidhälftig im Squeeze-Band → NICHT einbauen."))
if __name__ == "__main__":
main()