Files
AH-Oil-Trader/backtest_sl_tf_mismatch.py
T
Axel HocksandClaude Opus 4.8 75d28827e8 Initial commit: Oil Trading Bot (MT5, WTI)
Headless FastAPI-Backend (server.py + core/engine.py) mit Mobile-PWA (web/),
Strategie-/Backtest-Suite und Doku. Secrets, DB, Logs und Laufzeit-State sind
via .gitignore ausgeschlossen; Config-Vorlage: oil_widget_config.ini.example.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 08:29:23 +02:00

136 lines
5.9 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
"""SL-ATR-Timeframe-Mismatch bei Squeeze-Trades (2026-07-23, User-Frage nach
89,36-€-Nacht-Trade): Der VALIDIERTE Squeeze-Backtest (`backtest_breakout_squeeze.py`)
sizt den SL auf **M5-ATR** (2,0×) — dieselbe TF wie das Signal. LIVE sizt `trader.
_calc_sl_tp` den Initial-SL aber IMMER auf **M15-ATR** (`SL_TF=M15`, Band 1,82,2×),
unabhängig vom Signal-TF. Bei einem Squeeze (M5-Signal) kann das stark divergieren —
real letzte Nacht: M5-ATR fiel von 0,29→0,10 (Vola-Kompression, die den Squeeze
überhaupt erst auslöste!), während M15-ATR bei ~0,36 blieb → SL 0,787 statt ~0,20-0,22
bei M5-Sizing = ~3,5× zu weit für GENAU dieses Setup.
Test: Squeeze-Entries (Box/Ausbruch wie `backtest_breakout_squeeze.py`), SL/Trailing/
BE-Distanz aus ZWEI Quellen vergleichen:
LIVE = SL 2,2×ATR(M15) zum Entry-Zeitpunkt (min 1,8×, wie `_calc_sl_tp`), Trailing
bleibt M5-basiert (wie live, TF folgt dem Signal).
MODEL = SL 2,2×ATR(M5) — dieselbe TF wie das Signal (= was der Original-Backtest
validiert hat).
2 Halbjahre, Echtkosten. Verdict: nur wechseln, wenn MODEL in BEIDEN Hälften ΣR/PF
schlägt.
"""
import sys
import MetaTrader5 as mt5
_MAXH = 288; _ATRMIN = 0.12
_N = 12; _W = 24; _COOL = 12; _K = 0.1
_SL_MIN = 1.8; _SL_MAX = 2.2
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])))
out = [None]
for i in range(1, len(C)):
seg = t[max(1, i-p+1):i+1]
out.append(sum(seg)/len(seg))
return out
def sim(entry, d, sl_dist, atr_trail, H, L, C, j0, trail=1.5, trail_on=0.3, be_on=1.3):
"""sl_dist = absolute Preisdistanz (schon TF-spezifisch berechnet). Trailing/BE
laufen wie live auf atr_trail (M5, das Signal-TF) — nur der INITIALE SL variiert."""
eff = entry - d*sl_dist; hw = entry
end = min(j0+_MAXH, len(C)-1); exit_px = C[end]
for j in range(j0, end+1):
hi, lo = H[j], L[j]
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:
cand = hw - d*trail*atr_trail
if prof >= be_on*atr_trail: cand = max(cand, entry) if d > 0 else min(cand, entry)
eff = max(eff, cand) if d > 0 else min(eff, cand)
return (exit_px-entry)*d/atr_trail
def rep(name, Rs):
if not Rs: print(f" {name:<28} —"); return
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
print(f" {name:<28} n={n:>4} WR={100*w/n:>3.0f}% ØR={s/n:+.3f} PF={pf:.2f} ΣR={s:+.0f}")
def run(H, L, C, A5, A15_at, SP, lo_i, hi_i, squeeze_mult):
def cost(i, atr): return (SP[i] if SP[i] > 0 else 0.0225)/atr
live_Rs, model_Rs = [], []
i = max(lo_i, _N+15)
while i < min(hi_i, len(C)-_MAXH-1):
atr5 = A5[i]
if not atr5 or atr5 < _ATRMIN: i += 1; continue
boxHi = max(H[i-_N:i]); boxLo = min(L[i-_N:i]); box = boxHi-boxLo
if box > squeeze_mult*atr5: i += 1; continue
hit = None
for j in range(i, min(i+_W, len(C)-_MAXH-1)):
up = boxHi + _K*atr5; dn = boxLo - _K*atr5
if H[j] >= up: hit = (j, 1, up); break
if L[j] <= dn: hit = (j, -1, dn); break
if hit is None: i += 1; continue
j, d, lvl = hit
atr5_j = A5[j] or atr5
atr15_j = A15_at(j)
c = cost(j, atr5_j)
# LIVE: SL aus M15-ATR (wie trader._calc_sl_tp), Band [1.8,2.2], Trailing auf M5
if atr15_j:
sl_live = min(_SL_MAX, max(_SL_MIN, _SL_MAX)) * atr15_j # live nutzt fix 2.2 (Cap)
sl_live = _SL_MAX * atr15_j
live_Rs.append(sim(lvl, d, sl_live, atr5_j, H, L, C, j+1) - c)
# MODEL: SL aus M5-ATR (Signal-TF, wie der validierte Original-Backtest)
sl_model = _SL_MAX * atr5_j
model_Rs.append(sim(lvl, d, sl_model, atr5_j, H, L, C, j+1) - c)
i = j + _COOL
return live_Rs, model_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)
m5 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M5, 0, n+_MAXH+30)
m15 = mt5.copy_rates_from_pos(sym, mt5.TIMEFRAME_M15, 0, (n+_MAXH+30)//3+50)
si = mt5.symbol_info(sym); point = si.point
mt5.shutdown()
H = [float(b["high"]) for b in m5]; L = [float(b["low"]) for b in m5]
C = [float(b["close"]) for b in m5]; SP = [float(b["spread"])*point for b in m5]
T = [int(b["time"]) for b in m5]
A5 = _atr_series(H, L, C)
H15 = [float(b["high"]) for b in m15]; L15 = [float(b["low"]) for b in m15]
C15 = [float(b["close"]) for b in m15]; T15 = [int(b["time"]) for b in m15]
A15 = _atr_series(H15, L15, C15)
# Für jeden M5-Index den ZULETZT ABGESCHLOSSENEN M15-ATR nachschlagen (wie live
# copy_rates_from_pos "jetzt" die letzten M15-Bars holt) — simple Vorwärts-Suche.
import bisect
def A15_at(i5):
t = T[i5]
k = bisect.bisect_right(T15, t) - 1
return A15[k] if 0 <= k < len(A15) and A15[k] else None
N = len(C); mid = N//2
print("="*90)
print(f" SL-ATR-TF-Mismatch — {sym} M5 Squeeze-Entries ({N} Bars, 2 Halbjahre, Echtkosten)")
print(f" LIVE = SL 2,2×ATR(M15, fix) vs MODEL = SL 2,2×ATR(M5, Signal-TF, validierte Basis)")
print("="*90)
for lbl, lo, hi in (("H1 (alt)", 0, mid), ("H2 (neu)", mid, N)):
live_Rs, model_Rs = run(H, L, C, A5, A15_at, SP, lo, hi, 2.5)
print(f"\n {lbl}:")
rep("LIVE (M15-ATR-SL)", live_Rs)
rep("MODEL (M5-ATR-SL)", model_Rs)
if live_Rs and model_Rs:
print(f" Δ ΣR (ModelLive): {sum(model_Rs)-sum(live_Rs):+.0f}")
print(f"\n Verdict: MODEL nur übernehmen, wenn es in BEIDEN Hälften ΣR/PF schlägt.")
if __name__ == "__main__":
main()