#!/usr/bin/env python3 """Steigt die Exit-Slippage mit dem Vola-REGIME? — die offene Ausführungs-Frage. ANLASS (2026-08-08, User-Vorschlag 4/2). Das Vola-Regime ist als RICHTUNGS-Signal gemessen tot (`backtest_pbreak_regime.py`: Bruchrate über 5 ATR-Quintile flach, 37,0–39,2 %; das Regime-Merkmal fiel durch die vorab fixierte Regel). Als **Ausführungskosten-Warnung** ist es aber nie gemessen worden — und das ist eine ganz andere Behauptung: nicht „wohin läuft der Kurs", sondern „wie teuer wird das Verlassen der Position". METHODE — bewusst die aus `analyze_slippage_holdtime.py`, damit die Zahlen vergleichbar bleiben: gap = (sl_at_entry − exit_price) × Richtung, nur `closed_by='sl'` gap > 0 heisst „schlechter als der Initial-SL" ⚠ Warum ausgerechnet dieser gap unkontaminiert ist: das **Trailing kann ihn nur NEGATIVER machen** (es zieht den SL in Gewinnrichtung). Ein positiver gap kann also nicht vom Trailing stammen — er ist zwingend Slippage. Schätzer (Untergrenze): E[Slippage] ≥ P(gap>0) × Ø(gap | gap>0) NEUE ACHSE: ΔATR = ATR_M15(Exit-Zeitpunkt) / Median(ATR_M15 der letzten 20 Bars). > 1 heisst „Vola zieht gerade an". ⚠ KAUSAL: der Median endet VOR dem Exit-Bar. ⚠⚠ ENTSCHEIDUNGSREGEL, VORAB: ein Warn-Flag im Kosten-Gate ist nur dann gerechtfertigt, wenn die Slippage im obersten ΔATR-Bucket **mindestens doppelt so hoch** ist wie im untersten UND der Zusammenhang **monoton** verläuft. Ein einzelner Ausreisser-Bucket reicht nicht — dieselbe Nachbar-Regel, an der ORB und die VWAP-Bänder gescheitert sind. ⚠ Stichprobe: es gibt insgesamt ~317 SL-Closes. Über 4–5 Buckets sind das ~60–80 je Zelle — dünn. Ein ❌ ist belastbar, ein ✅ nur ein Hinweis. Aufruf: python analyze_slippage_vola.py """ from __future__ import annotations import bisect import sqlite3 import statistics as st import MetaTrader5 as mt5 DB = "oil_widget_history.db" _BROKER_OFF = 10800 # Brokerzeit = UTC+3 _MA = 20 # Fenster fuer den ATR-Median _M15_SEK = 900 def atr_series(H, L, C, p=14): tr = [0.0] for i in range(1, len(C)): tr.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 = tr[max(1, i - p + 1):i + 1] out.append(sum(seg) / len(seg)) return out def main(): db = sqlite3.connect(DB); db.row_factory = sqlite3.Row rows = [] for r in db.execute( "SELECT exit_time, exit_price, sl_at_entry, direction, entry_time " "FROM trades WHERE closed_by='sl' AND sl_at_entry IS NOT NULL " "AND exit_price IS NOT NULL AND exit_time IS NOT NULL"): d = 1 if (r["direction"] or "").upper() in ("BUY", "LONG") else -1 rows.append({"t": int(r["exit_time"]), "gap": (r["sl_at_entry"] - r["exit_price"]) * d, "hold": (int(r["exit_time"]) - int(r["entry_time"] or 0)) / 60.0}) db.close() if len(rows) < 60: print(f"nur {len(rows)} SL-Closes — zu wenig"); return 1 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_M15, 0, 26000) mt5.shutdown() T = [int(b["time"]) - _BROKER_OFF for b in bars] H = [float(b["high"]) for b in bars]; L = [float(b["low"]) for b in bars] C = [float(b["close"]) for b in bars] A = atr_series(H, L, C) # ΔATR je Trade — kausal: der Median endet VOR dem Exit-Bar dat = [] for r in rows: i = bisect.bisect_right(T, r["t"]) - 1 if i < _MA + 15 or i >= len(A) or not A[i]: continue fenster = [a for a in A[i - _MA:i] if a] if len(fenster) < _MA // 2: continue med = st.median(fenster) if med <= 0: continue dat.append({**r, "datr": A[i] / med, "atr": A[i]}) print("=" * 84) print(" EXIT-SLIPPAGE gegen das VOLA-REGIME (ΔATR = ATR / Median der letzten 20)") print("=" * 84) print(f" {len(rows)} SL-Closes, davon {len(dat)} mit M15-Abdeckung") if len(dat) < 60: print(" zu wenige mit Abdeckung — keine Aussage"); return 1 ds = sorted(x["datr"] for x in dat) print(f" ΔATR: Median {ds[len(ds)//2]:.2f} 10-Perz. {ds[len(ds)//10]:.2f} " f"90-Perz. {ds[int(0.9*len(ds))]:.2f}") # ── Quartile, damit die Zellen nicht zu duenn werden ────────────────── kanten = [ds[0], ds[len(ds)//4], ds[len(ds)//2], ds[3*len(ds)//4], ds[-1] + 1e-9] print(f"\n {'ΔATR-Bereich':<18}{'n':>5}{'slip>0':>8}{'Rate':>7}" f"{'Ø gap|>0':>11}{'Schaetzer':>12}") werte = [] for a, b in zip(kanten, kanten[1:]): grp = [x for x in dat if a <= x["datr"] < b] if len(grp) < 15: print(f" {f'{a:.2f}–{b:.2f}':<18}{len(grp):>5}{'':>8}{'zu wenig':>7}") werte.append(None); continue pos = [x["gap"] for x in grp if x["gap"] > 0] rate = len(pos) / len(grp) m = sum(pos) / len(pos) if pos else 0.0 sch = rate * m werte.append(sch) print(f" {f'{a:.2f}–{b:.2f}':<18}{len(grp):>5}{len(pos):>8}{100*rate:>6.0f}%" f"{m:>11.4f}{sch:>12.4f}") gut = [w for w in werte if w is not None] print("\n " + "-" * 78) if len(gut) < 3: print(" zu wenige belegte Buckets fuer ein Urteil"); return 0 unten, oben = gut[0], gut[-1] monoton = all(gut[i] <= gut[i + 1] for i in range(len(gut) - 1)) faktor = (oben / unten) if unten > 0 else float("inf") print(f" unterstes Quartil {unten:.4f} $ → oberstes {oben:.4f} $ " f"Faktor {faktor:.2f}×") print(f" monoton steigend: {'JA' if monoton else 'NEIN'}") print(f" Regel (Faktor ≥2 UND monoton): " f"{'ERFUELLT — Warn-Flag gerechtfertigt' if (faktor >= 2 and monoton) else 'VERFEHLT — kein Flag'}") print("\n ⚠ Zum Vergleich (analyze_slippage_holdtime): Median-Spread 0,0280 $;") print(" <30-min-Trades 0,0370 $ ueber alle Stunden, 0,0124 $ im Fenster 10–17.") return 0 if __name__ == "__main__": raise SystemExit(main())