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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
d8a243612a
commit
a6c43183e2
@@ -0,0 +1,172 @@
|
||||
#!/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,8–2,2×ATR_M15 ≈ **3,2–4,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()
|
||||
Reference in New Issue
Block a user