#!/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())
