#!/usr/bin/env python3 """Wie gross ist die Exit-Slippage bei KURZEN Haltedauern? ANLASS (2026-08-06): `backtest_squeeze_m1.py` hat gezeigt, dass der Squeeze auf M1 an genau einer Zahl haengt — der Exit-Slippage. M1 kippt bei **~0,04 $**, M5 erst bei **~0,09 $** (Faktor 2,2 = das ATR-Verhaeltnis; Slippage ist absolut und frisst deshalb vom kleinen Stop den doppelten Anteil). ⚠ WARUM DIE VORHANDENE ZAHL NICHT REICHT. `analyze_execution.py` meldet „Ø 0,234 · Median 0,017 · Max 4,192" — aber das ist die Statistik NUR ueber die Faelle, die schlechter als der Initial-SL endeten (`if gap > 0`). Es ist der Median der Ausreisser, nicht der aller Exits. Und der Ø wird von wenigen Gap-Faellen getragen, die lange gehaltene Trades betreffen. Ein M1-Scalp lebt ≤24 min (`LIVE.timestop`) und hat kaum Gap-Exposure — die Frage ist also, wie die Verteilung **bedingt auf kurze Haltedauern** aussieht. METHODE — und warum sie sauber ist: gap = (sl_at_entry − exit_price) × Richtung ... > 0 heisst „schlechter als der Initial-SL". Das **Trailing kann den gap nur negativer machen** (es zieht den SL in Gewinnrichtung, der Exit wird also besser als der Initial-SL). Ein POSITIVER gap ist damit **unkontaminierte Slippage** — er kann nicht vom Trailing kommen. Negative gaps mischen „Trail hat gezogen" und „guenstiger Fill" und sind deshalb nicht interpretierbar; sie werden nur gezaehlt, nicht gemittelt. Daraus die belastbare Groesse — eine UNTERGRENZE des Erwartungswerts: E[Slippage] ≥ P(gap > 0) × Ø(gap | gap > 0) Untergrenze, weil ein getrailter Stop mit Slippage negativ bleiben kann und dann nicht mitzaehlt. Aufruf: python analyze_slippage_holdtime.py """ import sqlite3 import sys from datetime import datetime from zoneinfo import ZoneInfo _BERLIN = ZoneInfo("Europe/Berlin") _DB = "oil_widget_history.db" # Kipp-Punkte aus backtest_squeeze_m1.py (Exit-Slippage in Dollar) _KIPP_M1, _KIPP_M5 = 0.04, 0.09 _EIMER = ((0, 15), (15, 30), (30, 60), (60, 120), (120, 10 ** 9)) def _q(v, p): s = sorted(v) return s[min(len(s) - 1, int(p * len(s)))] if s else 0.0 def block(titel, rows): print() print("=" * 92) print(f" {titel}") print("=" * 92) print(f" {'Haltedauer':<14} {'SL-Closes':>10} {'davon slip':>11} {'Rate':>7} " f"{'Ø|slip':>8} {'Med':>7} {'p90':>7} {'Max':>7} {'E[slip] ≥':>11}") gesamt = [] for a, b in _EIMER: grp = [r for r in rows if a <= r["min"] < b] if not grp: continue pos = [r["gap"] for r in grp if r["gap"] > 0] rate = len(pos) / len(grp) e = rate * (sum(pos) / len(pos)) if pos else 0.0 gesamt.append((a, b, len(grp), e)) lbl = f"{a}–{b} min" if b < 10 ** 8 else f">{a} min" print(f" {lbl:<14} {len(grp):>10} {len(pos):>11} {rate:>6.0%} " f"{(sum(pos)/len(pos) if pos else 0):>8.3f} {_q(pos, 0.5):>7.3f} " f"{_q(pos, 0.9):>7.3f} {(max(pos) if pos else 0):>7.3f} " f"{e:>10.4f} $") return gesamt def main(): db = sqlite3.connect(_DB) db.row_factory = sqlite3.Row rows = [] for r in db.execute( "SELECT direction, entry_time, exit_time, entry_price, exit_price, " "sl_at_entry, pnl, setup 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 str(r["direction"]).upper() in ("BUY", "LONG") else -1 rows.append({ "gap": (r["sl_at_entry"] - r["exit_price"]) * d, "min": (r["exit_time"] - r["entry_time"]) / 60.0, "std": datetime.fromtimestamp(r["entry_time"], _BERLIN).hour, "setup": r["setup"] or "", }) if not rows: print("keine SL-Closes mit Initial-SL in der DB"); return 1 print("=" * 92) print(" EXIT-SLIPPAGE NACH HALTEDAUER — die offene Zahl aus backtest_squeeze_m1.py") print("=" * 92) print(f" {len(rows)} SL-Closes mit bekanntem Initial-SL") print(" gap > 0 = schlechter als der Initial-SL = unkontaminierte Slippage") print(" (das Trailing kann den gap nur NEGATIVER machen, nie positiver)") g_alle = block("ALLE SL-Closes", rows) block("NUR 10–17 Uhr (das Fenster der M1-Variante C)", [r for r in rows if 10 <= r["std"] < 18]) print() print("=" * 92) print(" URTEIL — gegen die Kipp-Punkte aus backtest_squeeze_m1.py") print("=" * 92) print(f" M1 kippt bei ~{_KIPP_M1:.2f} $ · M5 kippt bei ~{_KIPP_M5:.2f} $") kurz = [(a, b, n, e) for a, b, n, e in g_alle if b <= 30] if kurz: n = sum(x[2] for x in kurz) e = sum(x[3] * x[2] for x in kurz) / n if n else 0.0 print(f" Kurze Trades (<30 min): n={n} · E[Slippage] ≥ {e:.4f} $") if e >= _KIPP_M1: print(f" → ❌ ueber der M1-Kippgrenze ({_KIPP_M1:.2f} $) — M1 erledigt.") elif e >= _KIPP_M1 * 0.6: print(f" → ⚠ unter der Grenze, aber im Risikobereich (>{_KIPP_M1*0.6:.3f} $).") print(" ⚠ Es ist eine UNTERGRENZE — der wahre Wert liegt darueber.") else: print(f" → ✅ klar unter der M1-Kippgrenze.") print() print(" ⚠ Vorbehalt, der bleibt: die Haltedauer eines SL-Closes ist NICHT") print(" die eines M1-Scalps. Sie stammt aus M5-/manuellen Trades mit") print(" breiterem Stop; ein M1-Trade mit halb so weitem Stop wird im") print(" Schnitt frueher getroffen. Die Zeile '<15 min' ist deshalb die") print(" naechste Naeherung, nicht der gesuchte Wert selbst.") return 0 if __name__ == "__main__": sys.exit(main())