MESSUNG (analyze_slippage_vola.py, 337 SL-Closes): die Exit-Slippage steigt MONOTON ueber alle vier Delta-ATR-Quartile: 0,39-0,91 Schaetzer 0,0200 $ 0,91-1,08 0,0390 $ 1,08-1,26 0,0645 $ 1,26-2,39 0,0839 $ Faktor 4,19x Mechanismus stimmig: die Slip-RATE bleibt flach (20-23 %), die GROESSE waechst (0,094 -> 0,396 $). Es rutscht nicht oefter, sondern weiter. ABER: der Bootstrap auf die DIFFERENZ oben-unten ergibt [-0,012 ... +0,183] - enthaelt die Null. Bei n=85 je Quartil NICHT statistisch belegt. Meine vorab fixierte Regel (Faktor >=2 UND monoton) war damit unterspezifiziert - sie haette ein Konfidenzintervall verlangen muessen. Konsequenz: gebaut als reiner HINWEIS im Kosten-Gate, kein Gate, und die Beschriftung sagt "historisch ~4x hoeher", nicht "belegt". ZIEL-ERREICHBARKEIT (Vorschlag 3, Decay-Clock): reine Geometrie auf dem kalibrierten Kegel - passt das Gegenlevel noch in das 80-%-Band des jeweiligen Horizonts? Live: Ziel 77,784 (0,401 $) liegt im 120'-Kegel, nicht im 30'/60'. Die im Vorschlag mitgedachte Annahme "je laenger, desto eher bricht es" ist BEWUSST NICHT eingebaut - sie ist nicht belegt (Touch-Zahl: umgekehrtes U auf 4 Live-Tagen, im Backtest +0,010 AUC, nach dem Nachtrainieren gar nichts mehr). Umsetzung ohne zweiten MT5-Fetch: _vola_regime puffert den ATR_M15 aus dem ohnehin laufenden Turn-Fetch (deque, ~5 h). Kaltstart liefert datr=null statt zu raten - live verifiziert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
145 lines
6.1 KiB
Python
145 lines
6.1 KiB
Python
#!/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())
|