Files
AH-Oil-Trader/backtest_sl_basis.py
T
Axel HocksandClaude Opus 5 a6c43183e2 SL-Basis M5 vs M15 auf Level-Entries gemessen: Tail-Frage, keine Ertragsfrage
Strukturfund vorweg: exit_model.simulate nimmt EINEN atr fuer SL, TP, Trail und
Breakeven - und alle Backtests uebergeben ATR_M5. Live kommt der SL aus ATR_M15
(Median 1,80x groesser). Praktisch jede Projekt-Messung modelliert damit
2,0xATR_M5, waehrend live ~4,0xATR_M5 gilt. Einzige bisherige Ausnahme war
backtest_sl_width.py.

Gemessen auf der NEUEN Level-Entry-Population (80k, 2 Halbjahre, nur die
SL-Basis variiert):

  SQUEEZE: Ertrag ist ein Wash - alle Varianten innerhalb eines Standardfehlers,
  und die einzige beidhaelftig bessere Zelle (2,5) hat schlechtere Nachbarn.
  WELLE:   der weite Live-SL ist in BEIDEN Haelften der beste, jede Verengung
  kostet.

Der einzige robuste Unterschied ist das Tail-Risiko: Worst-Case -8,83 xATR
(live, Squeeze H2) gegen -2,94 bei 2,5xATR_M5 = Faktor 3. Bei 3,5 Lots sind das
~-890 EUR gegen ~-300 EUR im schlechtesten Einzeltrade.

Antwort auf die Frage: vertretbar ja, aber als Risiko- und nicht als
Ertragsentscheidung. Saubere Umsetzung waere quellen-abhaengig (M5-ATR nur bei
auto_squeeze) - strukturell begruendet, nicht data-mined. Nicht gebaut.

Reproduziert NICHT backtest_sl_tf_mismatch.py (23.07.): dort Einstieg am
Bar-Close, hier am Level - der Einstiegspreis hat den Befund gedreht.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-05 17:34:23 +02:00

173 lines
7.5 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-Basis M5 gegen M15 — auf der NEUEN Level-Entry-Population (2026-08-05).
FRAGE (User): „Wäre SL-Breite aus M5-ATR vertretbar?"
⚠⚠ BEIM PRÜFEN AUFGEFALLEN — eine Lücke zwischen Messung und Betrieb:
`exit_model.simulate()` nimmt EINEN `atr` für SL, TP, Trail und Breakeven. **Alle
Backtests übergeben ATR_M5.** Live kommt der SL aber aus **ATR_M15**
(`mt5_utils.atr_value(sym)` mit `SL_TF`), im Median **1,80× so groß**. Damit
modellieren praktisch alle Messungen des Projekts einen SL von 2,0×ATR_M5 —
während live 1,82,2×ATR_M15 ≈ **3,24,0×ATR_M5** gilt.
Einzige Ausnahme: `backtest_sl_width.py` (04.08.) hat genau das explizit gemessen
und die Verengung verworfen (H2 bevorzugte den weiten Stop klar: +48 gegen 18).
WARUM ES SICH TROTZDEM ZU WIEDERHOLEN LOHNT: jene Messung lief auf Entries **am
Bar-Close**. Seit dem 05.08. steigen beide Auto-Pfade über ruhende Stop-Orders
**am Level** ein — eine andere Einstiegs-Population mit anderem Verlauf danach
(ØR kippte dadurch von 0,09 auf +0,35). Ob der weite Stop auch dort noch besser
ist, ist ungemessen.
MESSUNG: dieselben Level-Entries wie in `backtest_trail_be.py`, Trail/BE/TP
unverändert auf M5-ATR (wie in allen Backtests), variiert wird AUSSCHLIESSLICH die
SL-Basis. Der Live-Fall wird pro Trade exakt nachgebildet: `sl_atr = 2.2 ×
(ATR_M15 / ATR_M5)` an genau diesem Bar.
VORAB FIXIERTE ENTSCHEIDUNGSREGEL:
Umstellen auf M5 nur, wenn es den Live-Stand in ΣR **und** ØR in BEIDEN
Halbjahren und auf BEIDEN Populationen schlägt. Der Worst-Case darf sich dabei
nicht verschlechtern (er wird bei engerem SL ohnehin besser).
⚠ Wird die Regel verfehlt, ist „vertretbar" trotzdem eine eigene Frage: ein
engerer Stop kauft Tail-Sicherheit gegen Ertrag. Diese Abwägung ist NICHT
gemessen entscheidbar — die Zahlen liefern nur den Preis.
Aufruf: python backtest_sl_basis.py [bars]
"""
import sys
import MetaTrader5 as mt5
from backtest_cost_gate import _atr_series, kandidaten
from backtest_trail_be import _ema_series, _htf_sign, wellen_entries
from core.analysis import calc_trend_angle
from core.exit_model import LIVE, simulate
from core.wave_rec import (WaveRecommender, _EMA_FAST, _EMA_SLOW, _N_BARS,
_ANGLE_LR, _ANGLE_DEAD)
_PIV_K, _PIV_LOOK = 3, 300
_SL_LIVE = 2.2 # `INIT_SL_MAX_ATR` — der live wirksame Deckel
def atr_m15_auf_m5(H, L, C):
"""ATR(14) auf M15 (3 M5-Bars aggregiert), zurückgemappt auf den M5-Index."""
oh, ol, oc, idx = [], [], [], []
for j in range(0, len(C) - 2, 3):
oh.append(max(H[j:j + 3])); ol.append(min(L[j:j + 3]))
oc.append(C[j + 2]); idx.append(j + 2)
tr = [0.0] + [max(oh[i] - ol[i], abs(oh[i] - oc[i - 1]), abs(ol[i] - oc[i - 1]))
for i in range(1, len(oc))]
a15 = [None] + [sum(tr[max(1, i - 13):i + 1]) / len(tr[max(1, i - 13):i + 1])
for i in range(1, len(oc))]
out = [None] * len(C)
for k, j in enumerate(idx):
for x in range(j, min(j + 3, len(C))):
out[x] = a15[k]
# Vorlauf auffuellen
letzte = None
for i in range(len(out)):
if out[i] is None:
out[i] = letzte
else:
letzte = out[i]
return out
def lauf(K, H, L, C, A15, lo, hi, basis):
"""basis: float = feste SL-Weite in ×ATR_M5 · 'live' = 2,2×ATR_M15 je Trade."""
rs = []
frei = lo
weiten = []
for k in K:
if not (lo <= k["i"] < hi) or k["i"] < frei:
continue
if basis == "live":
a15 = A15[k["i"]] or k["atr"]
sl = _SL_LIVE * (a15 / k["atr"]) # in ×ATR_M5 ausgedrueckt
else:
sl = float(basis)
weiten.append(sl)
p = LIVE.with_(sl_atr=sl)
r, xb = simulate(k["lvl"], k["d"], k["atr"], H, L, C, k["i"] + 1, p,
timestop=True, use_tp=True, ret_bar=True)
rs.append(r - k["kosten"])
frei = xb + 1
return rs, (sum(weiten) / len(weiten) if weiten else 0.0)
def kenn(rs):
if not rs:
return None
n = len(rs); s = sum(rs)
g = sum(x for x in rs if x > 0); v = -sum(x for x in rs if x < 0)
return {"n": n, "wr": 100 * sum(1 for x in rs if x > 0) / n, "oer": s / n,
"sr": s, "pf": (g / v) if v > 0 else float("inf"), "worst": min(rs)}
def main():
n_bars = 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_bars)
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]; T = [int(b["time"]) for b in bars]
SP = [float(b["spread"]) * point for b in bars]
A = _atr_series(H, L, C)
A15 = atr_m15_auf_m5(H, L, C)
EF = _ema_series(C, _EMA_FAST); ES = _ema_series(C, _EMA_SLOW)
AD = [0.0] * len(C)
for i in range(_N_BARS, len(C)):
AD[i] = calc_trend_angle(C[i - _N_BARS:i + 1], _ANGLE_LR) - 90.0
if abs(AD[i]) < _ANGLE_DEAD:
AD[i] = 0.0
HTF = [_htf_sign(C, i, 6, A[i] or 0.1) for i in range(len(C))]
H1S = [_htf_sign(C, i, 12, A[i] or 0.1) for i in range(len(C))]
PH = [j for j in range(_PIV_K, len(C) - _PIV_K)
if H[j] == max(H[j - _PIV_K:j + _PIV_K + 1])]
PL = [j for j in range(_PIV_K, len(C) - _PIV_K)
if L[j] == min(L[j - _PIV_K:j + _PIV_K + 1])]
w = WaveRecommender(lambda: None)
w.set_reversal_enabled(False); w.set_entry_room(0.6); w.set_dead_hours("")
mid = len(C) // 2
v = [A15[i] / A[i] for i in range(len(C)) if A[i] and A15[i] and A[i] > 0.05]
v.sort()
print("=" * 100)
print(f" SL-BASIS M5 gegen M15 — Level-Entries · {sym} M5, {len(C)} Bars")
print(f" Verhaeltnis ATR_M15/ATR_M5: Median {v[len(v)//2]:.2f} · "
f{sum(v)/len(v):.2f} → live-SL entspricht ~{_SL_LIVE*v[len(v)//2]:.1f}xATR_M5")
print(" Trail/BE/TP bleiben auf M5-ATR (wie in allen Backtests) — variiert wird NUR der SL.")
print("=" * 100)
pops = [("SQUEEZE", kandidaten(H, L, C, T, SP, A)),
("WELLE (gegated)",
wellen_entries(w, H, L, C, A, SP, EF, ES, AD, HTF, H1S, PH, PL))]
for name, K in pops:
print(f"\n {name}{len(K)} Entries")
print(f" {'SL-Basis':<22}{'Ø Weite':>9}"
f"{'H1 OeR':>9}{'H1 SigmaR':>11}{'H1 Worst':>10}"
f"{'H2 OeR':>9}{'H2 SigmaR':>11}{'H2 Worst':>10}")
for basis, lbl in (("live", "LIVE (2,2xATR_M15)"), (2.0, "2,0xATR_M5"),
(2.5, "2,5xATR_M5"), (3.0, "3,0xATR_M5"),
(3.6, "3,6xATR_M5 (~live)"), (1.5, "1,5xATR_M5")):
r1, w1 = lauf(K, H, L, C, A15, 0, mid, basis)
r2, w2 = lauf(K, H, L, C, A15, mid, len(C), basis)
k1, k2 = kenn(r1), kenn(r2)
if not (k1 and k2):
continue
print(f" {lbl:<22}{(w1+w2)/2:>9.2f}"
f"{k1['oer']:>+9.3f}{k1['sr']:>+11.1f}{k1['worst']:>+10.2f}"
f"{k2['oer']:>+9.3f}{k2['sr']:>+11.1f}{k2['worst']:>+10.2f}")
print()
print(" LESART: 'Ø Weite' = tatsaechliche SL-Distanz in xATR_M5. Die LIVE-Zeile")
print(" zeigt, was der Bot heute macht; alle anderen Zeilen sind Alternativen.")
print(" ⚠ Enger SL verbessert den Worst-Case IMMER (er deckelt frueher) — die")
print(" Frage ist, was er an Ertrag kostet.")
print()
if __name__ == "__main__":
main()