Slippage vs Vola-Regime gemessen + Ziel-Erreichbarkeit gebaut (v=154)
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>
This commit is contained in:
co-authored by
Claude Opus 5
parent
44955bf70a
commit
e5adfc9757
@@ -0,0 +1,144 @@
|
||||
#!/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())
|
||||
Reference in New Issue
Block a user