Files
AH-Oil-Trader/analyze_slippage_holdtime.py
Axel HocksandClaude Opus 5 217981ccf5 analyze_slippage_holdtime.py: die offene Zahl gemessen — M1 ist entschieden
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>
2026-08-06 12:40:15 +02:00

131 lines
5.6 KiB
Python
Raw Permalink 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
"""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 1017 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())