Files
AH-Oil-Trader/analyze_slippage_vola.py
T
Axel HocksandClaude Opus 5 e5adfc9757 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>
2026-08-08 22:59:37 +02:00

145 lines
6.1 KiB
Python
Raw 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
"""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,039,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 45 Buckets sind das
~6080 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 1017.")
return 0
if __name__ == "__main__":
raise SystemExit(main())