Files
AH-Oil-Trader/backtest_htf_vola.py
T
Axel HocksandClaude Opus 5 7432be0e8c M30-Filter nach Vola-Regime geprueft — Hypothese invertiert, keine Aenderung
Anlass: Am 04.08. fiel WTI -4,12 $ (davon -4,16 $ in EINER Stunde), der Bot stand
zu 93 % auf WARTEN, htf_counter blockte 160-mal. Hypothese: die nachlaufende
M30-EMA sperrt in schnellen Umkehrungen das Richtige.

Die Messung invertiert die Hypothese. Geblockte Signale, kanonischer Exit,
Echtkosten, nach ATR-Terzil x 2 Halbjahre:

  Terzil    geblockt H1  geblockt H2  durchgel. H1  durchgel. H2
  niedrig      -0,086       +0,107       -0,136        -0,134
  mittel       -0,172       +0,067       -0,198        -0,069
  hoch         -0,625       -0,329       -0,061        +0,014

Im HOHEN Terzil sind die geblockten Signale mit Abstand die schlechtesten des
gesamten Datensatzes (PF 0,15 / 0,42) - der Filter ist dort am wertvollsten,
nicht am schaedlichsten. Die durchgelassenen Hoch-Vola-Signale in H2 sind das
einzige positive Feld der ganzen Tabelle. In den unteren Terzilen kippen die
geblockten Signale zwischen den Haelften = Rauschen.

Den Filter in schnellen Bewegungen zu lockern waere die schlechtestmoegliche
Aenderung gewesen. 22. verworfener Eingriff.

Auflösung des scheinbaren Widerspruchs: die 160 geblockten Signale und die 17
manuellen Gewinn-Trades desselben Tages sind VERSCHIEDENE Grundgesamtheiten. Die
manuellen Trades liefen alle auf rec=WARTEN, stammen also gar nicht aus der
Signal-Logik - der Filter hat den User nie gebremst. "Das Gate blockte X" und
"der Mensch verdiente Y" gehoeren nur zusammen, wenn Y aus X stammt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 16:20:18 +02:00

181 lines
7.7 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
"""Kostet der M30-Gegen-Trend-Filter in HOHER VOLATILITÄT Geld? (2026-08-04)
Anlass: Am 04.08. fiel WTI um 4,12 $ (davon 4,16 $ in EINER Stunde). Der Bot
stand zu 93 % auf WARTEN; `htf_counter` blockte dabei **160-mal** — also genau
die Richtung, in die der Markt lief. Die M30-EMA läuft nach und steht in einem
schnellen Absturz noch „auf".
Der Filter ist über die GESAMTstichprobe belegt (Ø-Edge ×2, `backtest_htf_filter.py`).
Die Frage ist enger: **trägt er auch im oberen Vola-Terzil, oder kommt sein Nutzen
allein aus ruhigen Phasen?**
Aufbau: gegatete Live-Population, aber der HTF-Filter wird NICHT in `_build`
angewandt (htf_trend=0) — stattdessen wird je Signal festgehalten, ob der Filter
es geblockt HÄTTE. Die geblockten Signale werden mit dem kanonischen Exit
simuliert. Auswertung nach ATR-Terzil × 2 Halbjahre.
⚠ Regel VORAB: Der Filter wird nur dann im Hoch-Vola-Bereich gelockert, wenn die
GEBLOCKTEN Signale dort in BEIDEN Hälften ØR>0 UND PF>1 liefern — und das
mittlere Terzil das nicht widerlegt. Ein einzelnes positives Feld zählt nicht.
⚠ Hohe Beweislast: 21 Signal-Eingriffe sind hier bereits gescheitert, und dieser
würde ein GEMESSEN POSITIVES Gate schwächen.
Aufruf: python backtest_htf_vola.py [bars]
"""
import sys
import MetaTrader5 as mt5
from core.analysis import calc_trend_angle
from core.exit_model import LIVE, simulate
from core.wave_rec import (WaveRecommender, _ANGLE_LR, _EMA_FAST, _EMA_SLOW,
_HTF_DEADBAND, _ema_series)
_ATRMIN = 0.06
_PIVK = 3
_PIV_LOOK = 300
class _TU:
def snapshot(self):
return {}
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 _htf_sign(C, i, n, atr):
lo = max(0, i - n * (_EMA_SLOW + 5))
sub = C[lo:i + 1:n]
if len(sub) < _EMA_SLOW + 2 or not atr:
return 0
d = (_ema_series(sub, _EMA_FAST)[-1] - _ema_series(sub, _EMA_SLOW)[-1]) / atr
return 0 if abs(d) < _HTF_DEADBAND else (1 if d > 0 else -1)
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 zeile(lbl, s):
if not s:
return f" {lbl:<24} — (keine Trades)"
return (f" {lbl:<24} n={s['n']:>5} WR={s['wr']:>3.0f}% ØR={s['oR']:+.3f} "
f"PF={s['pf']:>4.2f} ΣR={s['sum']:>+7.0f}")
def main():
n_req = 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_req)
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]
N = len(C); mid = N // 2
A = _atr_series(H, L, C)
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
print("Vorberechnung ...", flush=True)
AD = [90.0] * N
for i in range(_ANGLE_LR + 2, N):
AD[i] = calc_trend_angle(C[i - (_ANGLE_LR + 1):i + 1], _ANGLE_LR)
HTF = [0] * N; H1 = [0] * N
for i in range(N):
if A[i]:
HTF[i] = _htf_sign(C, i, 6, A[i]); H1[i] = _htf_sign(C, i, 12, A[i])
PH = [j for j in range(_PIVK, N - _PIVK) if H[j] == max(H[j - _PIVK:j + _PIVK + 1])]
PL = [j for j in range(_PIVK, N - _PIVK) if L[j] == min(L[j - _PIVK:j + _PIVK + 1])]
gueltig = sorted(A[i] for i in range(200, N) if A[i] and A[i] >= _ATRMIN)
t1 = gueltig[len(gueltig) // 3]; t2 = gueltig[2 * len(gueltig) // 3]
print(f"ATR-Terzil-Grenzen: niedrig < {t1:.3f} ≤ mittel < {t2:.3f} ≤ hoch\n")
w = WaveRecommender(_TU(), mt5.TIMEFRAME_M5)
w.set_reversal_enabled(False); w.set_entry_room(0.6)
w.set_dead_hours([]); w.set_breakout_k(0.3)
# Signale OHNE HTF-Filter erzeugen (htf_trend=0), Blockade separat feststellen
geblockt = {}; durchgelassen = {}
pend = None
i = _PIV_LOOK + _EMA_SLOW + 5
while i < N - LIVE.max_hold - 2:
atr = A[i]
if not atr or atr < _ATRMIN:
i += 1; continue
w._pb_feats = {"atr": max(atr, 0.12)}
w._pb_levels = {"ph": [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIVK],
"pl": [C[j] for j in PL if i - _PIV_LOOK <= j <= i - _PIVK]}
rec, _ = w._build(EF[i], ES[i], C[i], atr, "M5", 5,
htf_trend=0, h1_trend=H1[i], angle=AD[i]) # Filter AUS
sig = rec.get("signal")
if sig in ("LONG", "SHORT"):
d = 1 if sig == "LONG" else -1
if pend is None or pend["dir"] != d:
pend = {"dir": d, "level": C[i] + d * 0.3 * atr,
"invalid": C[i] - d * 0.3 * atr, "confirmed": False, "bar": i}
if not pend["confirmed"]:
broke = (C[i] >= pend["level"]) if d > 0 else (C[i] <= pend["level"])
if broke:
pend["confirmed"] = True
else:
if ((C[i] <= pend["invalid"]) if d > 0 else (C[i] >= pend["invalid"])) \
or (i - pend["bar"]) > 12:
pend = {"dir": d, "level": C[i] + d * 0.3 * atr,
"invalid": C[i] - d * 0.3 * atr,
"confirmed": False, "bar": i}
i += 1; continue
rec = w._room_gate(rec, C[i])
if rec.get("signal") in ("LONG", "SHORT"):
r, xj = simulate(C[i], d, atr, H, L, C, i + 1, LIVE,
timestop=True, use_tp=True, ret_bar=True)
R = r - (SP[i] if SP[i] > 0 else 0.0225) / atr
terz = "niedrig" if atr < t1 else ("mittel" if atr < t2 else "hoch")
haelfte = "H1" if i < mid else "H2"
# Hätte der M30-Filter das Signal geblockt?
ziel = geblockt if (HTF[i] != 0 and HTF[i] != d) else durchgelassen
ziel.setdefault((terz, haelfte), []).append(R)
pend = None
i = xj + 3
continue
elif sig == "WARTEN":
pend = None
i += 1
print("=" * 92)
print(f" M30-FILTER nach VOLA-REGIME — {sym} M5 ({N} Bars, kanonischer Exit, Echtkosten)")
print(" GEBLOCKT = Signale, die der Filter live verwirft. Ist ihr ØR positiv,")
print(" kostet der Filter in diesem Regime Geld.")
print("=" * 92)
for terz in ("niedrig", "mittel", "hoch"):
print(f"\n ATR-Terzil {terz.upper()}")
for h in ("H1", "H2"):
print(zeile(f"GEBLOCKT {h}", st(geblockt.get((terz, h)))))
for h in ("H1", "H2"):
print(zeile(f"durchgelassen {h}", st(durchgelassen.get((terz, h)))))
g1 = st(geblockt.get((terz, "H1"))); g2 = st(geblockt.get((terz, "H2")))
if g1 and g2:
ok = g1["oR"] > 0 and g2["oR"] > 0 and g1["pf"] > 1 and g2["pf"] > 1
print(f" → geblockte Signale beidhälftig positiv? "
f"{'JA — Filter kostet hier' if ok else 'nein — Filter zu Recht'}")
print("\n Regel (vorab): Lockerung nur, wenn die GEBLOCKTEN Signale im hohen Terzil")
print(" in BEIDEN Hälften ØR>0 und PF>1 liefern und das mittlere Terzil nicht")
print(" widerspricht. Hohe Beweislast: 21 Signal-Eingriffe sind hier gescheitert.")
print()
if __name__ == "__main__":
main()