From 217981ccf59a2dd2419a9e1a120f86f62500db48 Mon Sep 17 00:00:00 2001 From: Axel Hocks Date: Thu, 6 Aug 2026 12:40:15 +0200 Subject: [PATCH] =?UTF-8?q?analyze=5Fslippage=5Fholdtime.py:=20die=20offen?= =?UTF-8?q?e=20Zahl=20gemessen=20=E2=80=94=20M1=20ist=20entschieden?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- CLAUDE.md | 60 +++++++++++----- analyze_slippage_holdtime.py | 130 +++++++++++++++++++++++++++++++++++ backtest_squeeze_m1.py | 25 +++++++ 3 files changed, 199 insertions(+), 16 deletions(-) create mode 100644 analyze_slippage_holdtime.py diff --git a/CLAUDE.md b/CLAUDE.md index 7fcf748..67eb1bf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -364,22 +364,50 @@ dort bereits nachvalidiert, ØR +0,305.) **Eine** Oberfläche: ATR-Verhältnis (0,198/0,079 = 2,5). Gemessen (`analyze_execution.py`): **Median 0,017 $** (beide halten) · **Ø 0,234 $** (beide fallen, M1 weit deutlicher) · Max 4,192 $. - ⚠ **Was fehlt, ist genau die entscheidende Zahl:** die Slippage-Verteilung - **bedingt auf kurze Intraday-Haltedauern**. Ein M1-Trade lebt ≤24 min - (`LIVE.timestop`=24 Bars), hat also kaum Gap-Exposure — der Ø von 0,234 $ stammt - aus wenigen Ausreißern länger gehaltener Trades (4,192 nahe Marktschluss). Aber - **2 der 5 großen Fälle waren intraday** (30.07. 09:35 = 0,971 $ · 30.06. 15:36 = - 0,751 $), und beide liegen im 10–17-Fenster der Variante C. Überschlag: ~0,8 % - solcher Trades ergäben zusammen mit dem Median ~0,024 $ — **unter, aber nah an - der M1-Kippgrenze**. Das ist ein Überschlag, keine Messung. - ✅ **Urteil: NICHT umstellen.** Nicht weil M1 durchfällt, sondern weil (a) es je - Trade in **jeder** Variante unter M5 liegt, (b) beide um denselben EINEN - Positions-Slot konkurrieren, (c) der Vorteil ausschließlich aus der 4,3-fachen - Frequenz kommt — und genau diese Frequenz multipliziert die Slippage, deren wahre - Höhe ungemessen ist. **Der eine billige, entscheidende nächste Schritt** wäre, die - Exit-Slippage aus der echten Trade-Historie **nach Haltedauer** auszuwerten: liegt - sie für <30-min-Trades unter ~0,03 $, ist M1 ernsthaft ein Kandidat; darüber ist - es erledigt. + ✅✅ **DIE FEHLENDE ZAHL IST NACHGEMESSEN (`analyze_slippage_holdtime.py`, + 2026-08-06, 317 SL-Closes).** Die bisher zitierten „Median 0,017 · Ø 0,234" + waren die Statistik **nur über die Ausreißer** — `analyze_execution.py` zählt + ausschließlich Fälle, die schlechter als der Initial-SL endeten (`if gap > 0`). + Sauber ist die Untergrenze **E[Slippage] ≥ P(gap>0) × Ø(gap | gap>0)**; sie ist + unkontaminiert, weil das **Trailing den gap nur negativer machen kann** (es zieht + den SL in Gewinnrichtung) — ein positiver gap ist damit zwingend Slippage. + | Haltedauer | alle Stunden | **nur 10–17 Uhr** | + |---|---|---| + | 0–15 min | **0,0470 $** | **0,0058 $** | + | 15–30 min | 0,0164 $ | 0,0238 $ | + | 60–120 min | 0,0502 $ | 0,0449 $ | + | >120 min | **0,1691 $** | 0,0464 $ | + | **<30 min gewichtet** | **0,0370 $** | **0,0124 $** | + ⚠⚠ **Der Schwanz sitzt AUSSERHALB von 10–17** — bei kurzen Trades ist die + Slippage dort **8× kleiner** (0,0058 gegen 0,0470). Damit ist die + Stunden-Beschränkung nicht bloß eine Kosten-Optimierung: sie entfernt auch den + Slippage-Schwanz. Beides hat dieselbe Ursache (Buchtiefe), das ist ein + Mechanismus und kein Data-Mining. + **ENDVERGLEICH — jede Variante mit dem gemessenen Wert IHRES Fensters:** + | Variante | H1 ØR / ΣR | H2 ØR / ΣR | n (H1) | + |---|---|---|---| + | M1 A, alle Stunden · 0,0370 $ | **−0,107 / −125 ❌** | **−0,161 / −199 ❌** | 1168 | + | **M1 C, 10–17 Uhr · 0,0124 $** | +0,354 / **+150** | +0,247 / **+121** | 424 | + | M5, alle Stunden · 0,0370 $ | **+0,401** / +108 | **+0,261** / +71 | 270 | + | M5, 10–17 Uhr · 0,0124 $ | **+0,751** / +68 | **+0,444** / +45 | 90 | + ✅ **M1 über alle Stunden fällt jetzt HART durch** (PF 0,83/0,76) — die + Nachtstunden fressen es auf, exakt wie die Kosten-Arithmetik vorhergesagt hat. + Der ΣR-Vorsprung von Variante A war reines Artefakt der Annahme „keine + Exit-Slippage". + ⚠ **Auf demselben Fenster (10–17) bleibt es ein echter Zielkonflikt:** M5 liefert + je Trade **rund das Doppelte** (+0,751/+0,444 gegen +0,354/+0,247), M1 in der + Summe **rund das Doppelte** (+150/+121 gegen +68/+45) — weil es 4,7× so oft + handelt. Beide konkurrieren um denselben EINEN Positions-Slot. + ✅ **Urteil: NICHT umstellen.** Drei Gründe: (a) der entscheidende Parameter ist + nur als **Untergrenze** bekannt, und M1 C hat davon bis zur Kippgrenze nur + **~3× Luft** (0,0124 → ~0,04), M5 dagegen ~7×; (b) die Stichprobe hinter „M5 ist + je Trade doppelt so gut" ist mit n=90/101 dünn, die hinter der Slippage-Schätzung + im 10–17-Fenster mit 17 Ereignissen ebenso; (c) 80 Tage = EIN Regime. + ⚠ **Die Bedingung, unter der es kippen würde, ist benennbar** (für später): bleibt + die gemessene Exit-Slippage für <30-min-Trades im Fenster 10–17 bei größerer + Stichprobe unter ~0,02 $, dann ist der **Hybrid** (M1 während 10–17, M5 sonst) + rechnerisch ~1,9× der reinen M5-Variante (ΣR +190/+147 gegen +108/+71) und einen + kleinen Live-Test wert. Vorher nicht. ⚠ **Zwei Methodik-Lehren aus dem Lauf selbst:** (1) **Der Härtetest lief zuerst auf der SCHWÄCHSTEN Variante.** `beste = "D" if …` wählte nach Reihenfolge statt nach Ergebnis — D (+0,231/+0,130) statt C diff --git a/analyze_slippage_holdtime.py b/analyze_slippage_holdtime.py new file mode 100644 index 0000000..5d8b167 --- /dev/null +++ b/analyze_slippage_holdtime.py @@ -0,0 +1,130 @@ +#!/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()) diff --git a/backtest_squeeze_m1.py b/backtest_squeeze_m1.py index 990d183..a6f6bfe 100644 --- a/backtest_squeeze_m1.py +++ b/backtest_squeeze_m1.py @@ -271,6 +271,31 @@ def main(): lauf(K5, H5, L5, C5, lo5, hi5, exit_slip_usd=s_usd), lauf(K5, H5, L5, C5, lo6, hi6, exit_slip_usd=s_usd)) + # ── ENDVERGLEICH mit der GEMESSENEN Slippage ───────────────────────── + # `analyze_slippage_holdtime.py` (06.08., 317 SL-Closes): E[Slippage] als + # Untergrenze, bedingt auf kurze Haltedauern — + # <30 min, ALLE Stunden : 0,0370 $ (0–15 min allein sogar 0,0470) + # <30 min, NUR 10–17 Uhr : 0,0124 $ (der Schwanz sitzt ausserhalb) + # Genau das ist die Zahl, die im ersten Lauf gefehlt hat. + print() + print("=" * 94) + print(" ENDVERGLEICH mit der GEMESSENEN Exit-Slippage (analyze_slippage_holdtime.py)") + print("=" * 94) + print(" Untergrenze E[Slippage] bei kurzer Haltedauer: 0,0370 $ alle Stunden ·") + print(" 0,0124 $ nur 10–17 Uhr. Jede Variante bekommt den Wert IHRES Fensters.") + zeig(" M1 A (alle Stunden) · 0,0370 $", + lauf(K1, H1_, L1_, C1_, lo1, hi1, exit_slip_usd=0.0370), + lauf(K1, H1_, L1_, C1_, lo2, hi2, exit_slip_usd=0.0370)) + zeig(" M1 C (10–17 Uhr) · 0,0124 $", + lauf(K1, H1_, L1_, C1_, lo1, hi1, stunden=_STUNDEN_OK, exit_slip_usd=0.0124), + lauf(K1, H1_, L1_, C1_, lo2, hi2, stunden=_STUNDEN_OK, exit_slip_usd=0.0124)) + zeig(" M5-Kontrolle (alle Stunden) · 0,0370 $", + lauf(K5, H5, L5, C5, lo5, hi5, exit_slip_usd=0.0370), + lauf(K5, H5, L5, C5, lo6, hi6, exit_slip_usd=0.0370)) + zeig(" M5-Kontrolle (10–17 Uhr) · 0,0124 $", + lauf(K5, H5, L5, C5, lo5, hi5, stunden=_STUNDEN_OK, exit_slip_usd=0.0124), + lauf(K5, H5, L5, C5, lo6, hi6, stunden=_STUNDEN_OK, exit_slip_usd=0.0124)) + print() print("=" * 94) print(" NACHBAR-PARAMETER (Box / k) — kippt das Vorzeichen?")