Files
AH-Oil-Trader/backtest_breakout_canon.py
Axel HocksandClaude Opus 5 80a5be7fe5 Vollpruefung aller 87 Skripte + Nachrechnung der verworfenen Modelle
Phase 1 - Lauffaehigkeit: 87 von 87 Skripten laufen fehlerfrei (8000 Bars,
Timeout 900 s). Kein Defekt durch die heutigen wave_rec-Aenderungen. Werkzeug:
run_all_backtests.py, probiert die uneinheitlichen Aufruf-Signaturen adaptiv
durch (Bar-Zahl / TF+Bars / zwei TFs+Bars).

Phase 2a - breakout_k: die dokumentierte Begruendung REPRODUZIERT NICHT.
k=0,3 war mit dem LEGACY_BE10-Exit (Breakeven 1,0) validiert. Erstmals mit dem
kanonischen Exit nachgerechnet (backtest_breakout_canon.py, echte
_confirm_breakout-Mechanik, 80k M5, 2 Halbjahre, Echtkosten):
  k=0,0  H1 -0,180/-569   H2 -0,054/-250
  k=0,3  H1 -0,170/-356   H2 -0,069/-217   (LIVE)
  k=0,5  H1 -0,142/-262   H2 -0,074/-208
  k=0,8  H1 -0,112/-166   H2 -0,032/ -73
  k=1,0  H1 -0,101/-130   H2 -0,004/  -8
k=0,5/0,8/1,0 schlagen den Live-Wert in BEIDEN Haelften (bis +226/+209 SR).

TROTZDEM keine Aenderung empfohlen: (a) alle Werte negativ (PF 0,70-0,99), es
ist eine Wahl zwischen Verlusten; (b) Optimum lag am Rand -> Gegenprobe bis
k=3,0 zeigt kein sauberes Optimum (H2: -8 -> -17 -> -20 -> -19); (c) gemessen
wurde das UNGEGATETE Signal, live laufen min_conf/HTF/Entry-Raum davor. Naechster
Schritt waere eine gegatete Neumessung, bevor ein Gate angefasst wird, das 63 %
der WARTEN erzeugt.

Phase 2b - verworfene Modelle bei vollen Bars: ALLE bleiben verworfen.
14 Klassen (ORB, Doppeltop, Volume Profile, Liquidity Sweeps, Konsolidierung,
Handbuch-Level, Marktstruktur, Momentum, Inter-Market, Chop-Gate,
Reversal-Lockout, S/R-Close-Signal, MACD/ADX/RSI, HTF-Winkel) - keine kippt
ins Positive. Bei ~14 Tests waere bei 5 % Fehlalarmquote ein falsch-positives
Ergebnis zu erwarten gewesen; es gab keins. Regel vorab fixiert.

Kein Freibrief fuer regelmaessiges Nachrechnen - die Begruendung gegen
woechentliche Laeufe steht unveraendert. Anlass war die einmalige
Signalaenderung.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 12:15:33 +02:00

142 lines
5.9 KiB
Python
Raw Permalink 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
"""`breakout_k` mit dem KANONISCHEN Exit nachgerechnet (2026-08-04).
Offener Punkt aus CLAUDE.md: „**`breakout_k=0,3` ruht weiter auf dem
`LEGACY_BE10`-Exit** (Breakeven 1,0 statt live 1,3). Der Legacy-Recheck hat EIA
und Dead-Hours nachgerechnet, `breakout_k` nicht. Kandidat für den nächsten
Recheck." — Das wiegt schwer, weil es ein AKTIVES Gate ist: die erste
`block_reason`-Auswertung zeigte `breakout_pending` als Ursache von **63 %**
aller WARTEN-Zustände.
Aufbau: echte `_confirm_breakout`-Mechanik (stateful `_pend`, Neuverankerung bei
Richtungswechsel/Timeout) auf dem EMA12/50-Signal, Entry erst nach Bestätigung.
Exit = `core/exit_model.LIVE` (SL 2,0 · Trail 1,0 · BE 1,3 · Time-Stop · TP),
Echtkosten = Bar-Spread/ATR, sequentielle 1-Positions-Sim, 2 Halbjahre.
⚠ VORAB festgelegte Regel (gegen Mehrfachvergleich): ein anderer k-Wert wird nur
dann empfohlen, wenn er in BEIDEN Hälften mehr ΣR liefert als der Live-Wert 0,3
UND seine Nachbarwerte mithalten. Ein einzelner Ausreißer zählt nicht.
Aufruf: python backtest_breakout_canon.py [bars]
"""
import sys
import MetaTrader5 as mt5
from core.exit_model import LIVE, simulate
from core.wave_rec import _EMA_FAST, _EMA_SLOW, _TREND_DEADBAND, _ema_series
_ATRMIN = 0.06
_COOL = 3
_PEND_TIMEOUT = 24 # Bars, bis eine unbestätigte Anwartschaft verfällt
def _atr_series(H, L, C, p=14):
t = [0.0]
for i in range(1, len(C)):
t.append(max(H[i] - L[i], abs(H[i] - C[i - 1]), abs(L[i] - C[i - 1])))
return [(sum(t[max(1, i - p + 1):i + 1]) / max(1, len(t[max(1, i - p + 1):i + 1])))
if i else None for i in range(len(C))]
def st(Rs):
if not Rs:
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)
return dict(n=n, wr=100 * w / n, oR=s / n, pf=(up / dn if dn > 0 else 9.99), sum=s)
def run(H, L, C, A, SP, EF, ES, lo, hi, k):
"""k = geforderte Bewegung in ATR, bevor das Signal gehandelt wird. k=0 → aus."""
def cost(i, atr):
return (SP[i] if SP[i] > 0 else 0.0225) / atr
Rs = []
pend = None # {"dir": ±1, "level": float, "bar": int}
i = max(lo, _EMA_SLOW + 5)
while i < hi:
atr = A[i]
if not atr or atr < _ATRMIN:
i += 1; continue
sep = (EF[i] - ES[i]) / atr
d = 0 if abs(sep) < _TREND_DEADBAND else (1 if sep > 0 else -1)
if d == 0:
pend = None; i += 1; continue
if k <= 0: # Bestätigung aus → sofort handeln
trigger = True
else:
if pend is None or pend["dir"] != d:
pend = {"dir": d, "level": C[i], "bar": i}
i += 1; continue
if i - pend["bar"] > _PEND_TIMEOUT: # Timeout → neu verankern
pend = {"dir": d, "level": C[i], "bar": i}
i += 1; continue
fortschritt = (C[i] - pend["level"]) * d / atr
if fortschritt <= -k: # gegen die Richtung → neu
pend = {"dir": d, "level": C[i], "bar": i}
i += 1; continue
trigger = fortschritt >= k
if not trigger:
i += 1; continue
r, xj = simulate(C[i], d, max(atr, _ATRMIN), H, L, C, i + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
Rs.append(r - cost(i, max(atr, _ATRMIN)))
pend = None
i = xj + _COOL
return Rs
def main():
n = int(sys.argv[1]) if len(sys.argv) > 1 else 80000
mt5.initialize()
sym = next((c for c in ("SpotCrude", "USOIL", "WTI", "XTIUSD")
if mt5.symbol_info(c)), None)
bars = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n)
point = mt5.symbol_info(sym).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]
A = _atr_series(H, L, C)
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
N = len(C); mid = N // 2
print("=" * 92)
print(f" breakout_k — KANONISCHER Exit — {sym} M5 ({N} Bars, 2 Halbjahre, Echtkosten)")
print(f" Exit: SL {LIVE.sl_atr}× · Trail {LIVE.mult}× · BE {LIVE.be}× · Time-Stop · TP")
print(f" ⚠ Die Validierung von k=0,3 stammt aus `backtest_breakout.py` mit BE 1,0")
print(f" (LEGACY_BE10) — hier erstmals mit dem Live-Exit gegengerechnet.")
print("=" * 92)
erg = {}
for k in (0.0, 0.2, 0.3, 0.5, 0.8, 1.0):
s1 = st(run(H, L, C, A, SP, EF, ES, 0, mid, k))
s2 = st(run(H, L, C, A, SP, EF, ES, mid, N - LIVE.max_hold - 1, k))
erg[k] = (s1, s2)
tag = " ← LIVE" if abs(k - 0.3) < 1e-9 else ""
print(f"\n k = {k:.1f}{tag}")
for lbl, s in (("H1 (alt)", s1), ("H2 (neu)", s2)):
if s:
print(f" {lbl} n={s['n']:>5} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} "
f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+7.0f}")
else:
print(f" {lbl} — keine Trades")
live = erg.get(0.3)
print("\n" + "=" * 92)
print(" Vergleich gegen den Live-Wert k=0,3 (ΣR-Differenz je Hälfte):")
for k, (s1, s2) in sorted(erg.items()):
if abs(k - 0.3) < 1e-9 or not (s1 and s2 and live[0] and live[1]):
continue
print(f" k={k:.1f}: H1 {s1['sum'] - live[0]['sum']:+7.0f} "
f"H2 {s2['sum'] - live[1]['sum']:+7.0f}"
+ (" << in BEIDEN besser" if (s1['sum'] > live[0]['sum']
and s2['sum'] > live[1]['sum']) else ""))
print("\n Regel (vorab): Wechsel nur, wenn ein k in BEIDEN Hälften besser ist")
print(" UND seine Nachbarn mithalten. Ein einzelner Ausreißer zählt nicht.")
print()
if __name__ == "__main__":
main()