Files
AH-Oil-Trader/backtest_htf_vola.py
Axel HocksandClaude Opus 5 cddcefa463 Der Engpass war nie die Mathematik: quadratische Schleife, ~9 min -> 8,9 s
Anlass war die Frage nach numpy. Statt zu vermuten wurde profiliert — und das
Ergebnis war ein anderes als erwartet: von 10,07 s Gesamtlaufzeit steckten
8,17 s in der EIGENZEIT der Backtest-Schleife, nur 0,44 s in _build.

Ursache:
    [C[j] for j in PH if i - _PIV_LOOK <= j <= i - _PIV_K]   # je BAR, ganze Liste

Das ist O(Bars x Pivots) — bei 80k Bars hunderte Millionen Vergleiche, und
genau daher kamen die 9-20-Minuten-Laeufe. PH/PL sind sortiert, also genuegen
zwei Binaersuchen fuer dieselbe Menge in O(log n).

Gemessen: backtest_signal_touchfill.py ueber 80k Bars von ~9 min auf 8,9 s
(~60x), bei 8k Bars 10,07 s -> 1,43 s. Das Ergebnis bleibt inhaltlich
identisch (alle sechs Felder negativ); die Handvoll Trades Unterschied ist die
bekannte Live-Bar-Drift, nicht der Umbau.

MIGRATIONS-REGEL ERNST GENOMMEN: ein Vorher/Nachher-Vergleich zweier LAEUFE
taugt hier nicht, weil copy_rates_from_pos am neuesten Bar ankert und
eintreffende Live-Bars das Fenster verschieben. Die Gleichheit wird deshalb
DIREKT auf der Datenstruktur bewiesen — tests/test_pivotfenster.py mit vier
Faellen: 4.000 Bars x 400 Pivots, Randfaelle, leere Liste, beidseitig
inklusive Grenzen.

Angewandt auf 7 Skripte (auto_signal_v3, breakout_gated, dist_gated, htf_vola,
metalabel, sl_width, trail_be) — mechanisch identische Transformation, alle
kompilieren, Stichprobe gegengelaufen (dist_gated 20k in 1,1 s).

Lehre: die naheliegende Antwort (numpy) war die falsche. Ein Profil kostet zwei
Minuten und haette diese Bremse jederzeit gezeigt — sie lag seit Monaten in
jedem gegateten Backtest.

17 Tests gruen, n-Falle-Scan sauber.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-07 06:21:49 +02:00

189 lines
8.2 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
"""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
from bisect import bisect_left, bisect_right
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)}
# ⚠ bisect statt linearer Filterung (2026-08-07). Die alte Fassung
# scannte je Bar die KOMPLETTE Pivot-Liste = O(Bars x Pivots) und war
# der Engpass des Projekts (80k-Lauf: ~9 min -> 8,9 s). PH/PL sind
# sortiert, die Binaersuche liefert DIESELBE Menge — bewiesen in
# tests/test_pivotfenster.py, nicht behauptet.
_a1 = bisect_left(PH, i - _PIV_LOOK); _b1 = bisect_right(PH, i - _PIVK)
_a2 = bisect_left(PL, i - _PIV_LOOK); _b2 = bisect_right(PL, i - _PIVK)
w._pb_levels = {"ph": [C[j] for j in PH[_a1:_b1]],
"pl": [C[j] for j in PL[_a2:_b2]]}
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()