Der M1-Squeeze hing an der Exit-Slippage bei kurzer Haltedauer. Die bisher zitierten "Median 0,017 / Ø 0,234" waren die Statistik NUR ueber die Ausreisser — analyze_execution.py zaehlt ausschliesslich Faelle, die schlechter als der Initial-SL endeten. Sauber ist E[Slippage] >= P(gap>0) x Ø(gap|gap>0). Unkontaminiert, weil das Trailing den gap nur negativer machen kann: ein positiver gap ist zwingend Slippage. Ueber 317 SL-Closes, <30 min: alle Stunden 0,0370 $ (0-15 min allein 0,0470) nur 10-17 Uhr 0,0124 $ (0-15 min allein 0,0058) Der Schwanz sitzt ausserhalb von 10-17 — bei kurzen Trades dort 8x kleiner. Gleiche Ursache wie beim Spread (Buchtiefe), also Mechanismus statt Zufall. Endvergleich mit dem gemessenen Wert je Fenster: M1 alle Stunden -0,107/-0,161 PF 0,83/0,76 FAELLT HART DURCH M1 10-17 +0,354/+0,247 ΣR +150/+121 M5 alle Stunden +0,401/+0,261 ΣR +108/+71 M5 10-17 +0,751/+0,444 ΣR +68/+45 Der ΣR-Vorsprung von M1 ueber alle Stunden war reines Artefakt der Annahme "keine Exit-Slippage". Auf demselben Fenster bleibt ein echter Zielkonflikt: M5 doppelt so gut je Trade, M1 doppelt so gut in der Summe — bei EINEM Slot. Urteil: nicht umstellen. Der entscheidende Parameter ist nur als Untergrenze bekannt und M1 hat davon nur ~3x Luft bis zur Kippgrenze (M5 ~7x); die Stichproben sind duenn; 80 Tage = ein Regime. Die Bedingung fuer ein spaeteres Ja ist in CLAUDE.md benannt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
131 lines
5.6 KiB
Python
131 lines
5.6 KiB
Python
#!/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())
|