"""VERIFICA AVVERSARIALE — lente LOOK-AHEAD/ESEGUIBILITA' su EXIT-02 (k=1.5).

Ipotesi nulla avversaria: l'edge del chandelier trail e' un artefatto del
TIMING PERFETTO (livelli fissati a j-1 e tocco a j senza alcun attrito) e/o di
uno SL fillato a un prezzo non eseguibile live. Provo a confutarla.

Esperimenti:
  (A) AUDIT del contratto: ricalcolo i livelli di EXIT-02 fuori dall'engine e
      verifico che run_hi/run_lo a j NON incorporino mai high[j]/low[j], e che
      atr usato sia atr[j-1]. (statico, ma lo confermo numericamente forzando
      un confronto con una variante che USA j -> deve cambiare i numeri.)

  (B) LAG +1: variante che ritarda di UN bar in piu' TUTTI gli input causali
      (atr[j-2], estremi fino a j-2). Se l'edge collassa -> appeso al timing.

  (C) ESEGUIBILITA' SL: lo SL fillato a `sl` (prezzo del livello) e' ottimistico?
      Confronto col fill conservativo allo SL ma con slippage, e con fill al
      WORSE fra sl e open[j] (gap-through). Stima costo.

  (D) ESEGUIBILITA' HORIZON/CLOSE: gli exit a max_bars escono a close[j]. Live
      il poll arriva ~al close ma esegue al bar dopo -> rifaccio l'engine con
      gli exit a horizon/after_bar a open[j+1] invece di close[j]. Costo?
      (EXIT-02 non usa after_bar, ma usa gli exit a horizon -> rilevante con
      avg_bars 2.5: il turnover alto AMPLIFICA il costo per-exit.)

Tutto a leva 3, fee 0.10% RT, OOS_START 2023-11-01.
"""
import sys
from pathlib import Path

import numpy as np

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
import exit_lab as EL  # noqa: E402
from exit_lab import ExitPolicy, load_sleeves, simulate, OOS_START_MS, LEV, POS, FEE_RT  # noqa: E402
from importlib import import_module  # noqa: E402

mod = import_module("02_trail_atr_keep_tp")
TrailATRKeepTP = mod.TrailATRKeepTP

KPICK = 1.5
DATA = load_sleeves()


# ---------------------------------------------------------------- (A) AUDIT statico
class TrailLeak(TrailATRKeepTP):
    """VARIANTE SPORCA: usa run_hi/run_lo fino a j (incl. bar j) e atr[j].
    Serve SOLO a mostrare quanto l'edge gonfierebbe col look-ahead -> se i numeri
    della policy pulita fossero gia' a quel livello, sarebbe sospetta."""
    name = "trail_LEAK"

    def levels(self, j: int):
        self._update_running(j)            # SPORCO: incorpora bar j
        a = self.atr[j]                    # SPORCO: atr del bar j
        if a is None or a != a:
            return self.tp0, self.sl0, 1.0
        if self.d == 1:
            chand = self.run_hi - self.k * a
            sl = max(self.sl0, chand)
        else:
            chand = self.run_lo + self.k * a
            sl = min(self.sl0, chand)
        return self.tp0, sl, 1.0


# ---------------------------------------------------------------- (B) LAG +1
class TrailLag1(TrailATRKeepTP):
    """Ritarda di UN bar in piu': estremi fino a j-2, atr[j-2]."""
    name = "trail_LAG1"

    def levels(self, j: int):
        self._update_running(j - 2)        # estremi solo <= j-2
        idx = j - 2
        a = self.atr[idx] if idx >= 0 else None
        if a is None or a != a:
            return self.tp0, self.sl0, 1.0
        if self.d == 1:
            chand = self.run_hi - self.k * a
            sl = max(self.sl0, chand)
        else:
            chand = self.run_lo + self.k * a
            sl = min(self.sl0, chand)
        return self.tp0, sl, 1.0


# ------------------------------------- (C)/(D) engine alternativo con attriti exec
def simulate_exec(policy_cls, sleeve, params, *, start_ms=None, end_ms=None,
                  sl_slip_bps=0.0, sl_gap=False, horizon_open=False):
    """Clone di EL.simulate con attriti di esecuzione opzionali:
       sl_slip_bps : slippage avverso (bps di prezzo) sul fill allo SL.
       sl_gap      : fill allo SL = worse(sl, open[j]) (gap-through realistico).
       horizon_open: exit a horizon/after_bar a open[j+1] invece di close[j].
    """
    params = params or {}
    o = sleeve["open"]; h, l, c, ts = sleeve["high"], sleeve["low"], sleeve["close"], sleeve["ts_ms"]
    n = len(c)
    ctx = dict(sleeve)
    policy_cls.prepare(ctx, **params)
    fee = FEE_RT * LEV
    capital = peak = 1000.0
    max_dd = 0.0
    last_exit = -1
    trades = wins = 0
    bars_tot = 0
    rets = []

    for (i, d, tp0, sl0, mb) in sleeve["signals"]:
        if start_ms is not None and ts[i] < start_ms:
            continue
        if end_ms is not None and ts[i] >= end_ms:
            continue
        if i <= last_exit or i + 1 >= n:
            continue
        entry = c[i]
        pol = policy_cls(ctx, i, d, entry, tp0, sl0, mb, **params)
        horizon = min(int(pol.horizon), EL.HARD_CAP)
        fills = []
        remaining = 1.0
        j = i
        for step in range(1, horizon + 1):
            j = i + step
            if j >= n:
                j = n - 1
                px = o[min(j + 1, n - 1)] if horizon_open else c[j]
                fills.append((remaining, px)); remaining = 0.0
                break
            tp, sl, tpfrac = pol.levels(j)
            hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
            hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
            if hit_sl:
                fill_px = sl
                if sl_gap:
                    # gap-through: se la barra apre gia' oltre lo SL, fill all'open
                    fill_px = min(sl, o[j]) if d == 1 else max(sl, o[j])
                if sl_slip_bps:
                    fill_px = fill_px * (1 - d * sl_slip_bps / 1e4)  # avverso
                fills.append((remaining, fill_px)); remaining = 0.0
                break
            if hit_tp:
                f = min(max(tpfrac, 0.0), 1.0) * remaining
                if f > 0:
                    fills.append((f, tp)); remaining -= f
                if remaining <= 1e-9:
                    break
                pol.on_partial(j, tp, remaining)
            if pol.after_bar(j):
                px = o[min(j + 1, n - 1)] if horizon_open else c[j]
                fills.append((remaining, px)); remaining = 0.0
                break
            if step == horizon:
                px = o[min(j + 1, n - 1)] if horizon_open else c[j]
                fills.append((remaining, px)); remaining = 0.0
        if remaining > 1e-9:
            fills.append((remaining, c[j]))

        ret = sum(f * (p - entry) for f, p in fills) / entry * d * LEV - fee
        capital = max(capital + capital * POS * ret, 10.0)
        peak = max(peak, capital)
        max_dd = max(max_dd, (peak - capital) / peak)
        last_exit = j
        trades += 1
        wins += ret > 0
        bars_tot += j - i
        rets.append(ret)

    if trades == 0:
        return {}
    r = np.array(rets)
    return {
        "ret_pct": (capital / 1000.0 - 1) * 100,
        "dd_pct": max_dd * 100,
        "trades": trades,
        "win_pct": wins / trades * 100,
        "sharpe_t": float(r.mean() / r.std() * np.sqrt(len(r))) if r.std() else 0.0,
        "avg_bars": bars_tot / trades,
    }


def oos(cls, sleeve, sim=simulate, **kw):
    return sim(cls, sleeve, {"k": KPICK}, start_ms=OOS_START_MS, **kw)


def line(tag, r):
    return (f"{tag:<26} ret{r.get('ret_pct',0):>7.0f}% dd{r.get('dd_pct',0):>5.1f} "
            f"sh{r.get('sharpe_t',0):>5.2f} n{r.get('trades',0):>4} "
            f"win{r.get('win_pct',0):>4.0f}% bars{r.get('avg_bars',0):>4.1f}")


print("=" * 90)
print("OOS (2023-11-01+) k=1.5 — clean / LEAK(look-ahead) / LAG1(+1 bar) per sleeve")
print("=" * 90)
agg = {"clean": [], "leak": [], "lag1": []}
for (code, asset), sleeve in DATA.items():
    key = f"{code.split('_')[0]} {asset}"
    rc = oos(TrailATRKeepTP, sleeve)
    rk = oos(TrailLeak, sleeve)
    rl = oos(TrailLag1, sleeve)
    agg["clean"].append(rc.get("ret_pct", 0))
    agg["leak"].append(rk.get("ret_pct", 0))
    agg["lag1"].append(rl.get("ret_pct", 0))
    print(f"\n{key}")
    print("  " + line("clean", rc))
    print("  " + line("LEAK", rk))
    print("  " + line("LAG1", rl))

print("\n" + "-" * 90)
print(f"OOS ret medio  clean={np.mean(agg['clean']):.0f}%  "
      f"LEAK={np.mean(agg['leak']):.0f}%  LAG1={np.mean(agg['lag1']):.0f}%")
print(f"LAG1/clean ratio per sleeve: "
      f"{[f'{a/ b:.2f}' if b else 'na' for a, b in zip(agg['lag1'], agg['clean'])]}")

print("\n" + "=" * 90)
print("ESECUZIONE — attriti su OOS k=1.5 (medie sui 6 sleeve)")
print("=" * 90)
scenarios = {
    "clean (engine std)":        dict(),
    "SL slip 5bps":              dict(sl_slip_bps=5.0),
    "SL slip 10bps":             dict(sl_slip_bps=10.0),
    "SL gap-through(open)":      dict(sl_gap=True),
    "SL gap + slip 5bps":        dict(sl_gap=True, sl_slip_bps=5.0),
    "horizon exit @open[j+1]":   dict(horizon_open=True),
    "horizon@open + SLslip5":    dict(horizon_open=True, sl_slip_bps=5.0),
}
for tag, kw in scenarios.items():
    rets, dds, shs = [], [], []
    for (code, asset), sleeve in DATA.items():
        r = oos(TrailATRKeepTP, sleeve, sim=simulate_exec, **kw)
        rets.append(r.get("ret_pct", 0)); dds.append(r.get("dd_pct", 0)); shs.append(r.get("sharpe_t", 0))
    print(f"{tag:<28} OOS ret medio {np.mean(rets):>6.0f}%  dd {np.mean(dds):>4.1f}  "
          f"sh {np.mean(shs):>4.2f}  (min ret {min(rets):>5.0f}%)")

# confronto baseline per contesto
print("\nBaseline (exit fissa) OOS ret medio per riferimento:")
brets = [simulate(ExitPolicy, s, {}, start_ms=OOS_START_MS).get("ret_pct", 0)
         for s in DATA.values()]
print(f"  base OOS ret medio {np.mean(brets):.0f}%")


# ---------------------------------------------------------------- (E) frequenza gap-through SL
print("\n" + "=" * 90)
print("FREQUENZA gap-through allo SL (OOS k=1.5): quanti fill SL aprono OLTRE il livello?")
print("=" * 90)


def gap_stats(sleeve, start_ms=OOS_START_MS):
    params = {"k": KPICK}
    o = sleeve["open"]; h, l, c, ts = sleeve["high"], sleeve["low"], sleeve["close"], sleeve["ts_ms"]
    n = len(c)
    ctx = dict(sleeve); TrailATRKeepTP.prepare(ctx, **params)
    last_exit = -1
    sl_hits = gaps = 0
    gap_bps = []
    for (i, d, tp0, sl0, mb) in sleeve["signals"]:
        if ts[i] < start_ms or i <= last_exit or i + 1 >= n:
            continue
        entry = c[i]
        pol = TrailATRKeepTP(ctx, i, d, entry, tp0, sl0, mb, **params)
        horizon = min(int(pol.horizon), EL.HARD_CAP)
        j = i
        for step in range(1, horizon + 1):
            j = i + step
            if j >= n:
                break
            tp, sl, _ = pol.levels(j)
            hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
            hit_tp = tp is not None and ((d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp))
            if hit_sl:
                sl_hits += 1
                # gap-through: l'open e' gia' oltre lo SL (peggio del livello)?
                worse = (d == 1 and o[j] < sl) or (d == -1 and o[j] > sl)
                if worse:
                    gaps += 1
                    gap_bps.append(abs(o[j] - sl) / sl * 1e4)
                break
            if hit_tp:
                break
        last_exit = j
    return sl_hits, gaps, (np.mean(gap_bps) if gap_bps else 0.0), (np.median(gap_bps) if gap_bps else 0.0)


tot_h = tot_g = 0
for (code, asset), sleeve in DATA.items():
    sh_, g_, m_, md_ = gap_stats(sleeve)
    tot_h += sh_; tot_g += g_
    print(f"  {code.split('_')[0]} {asset}: SL hits {sh_:>3}  gap-through {g_:>3} "
          f"({100*g_/max(sh_,1):>4.0f}%)  gap medio {m_:>5.1f}bps  mediano {md_:>5.1f}bps")
print(f"\n  TOTALE: {tot_g}/{tot_h} SL hit sono gap-through = {100*tot_g/max(tot_h,1):.0f}%")


# ------------------------------------------------- (F) gap pessimista vs baseline per sleeve
print("\n" + "=" * 90)
print("VERDETTO ESEGUIBILITA': EXIT-02 con SL gap-through vs BASELINE (exit fissa), OOS, per sleeve")
print("=" * 90)
print(f"{'sleeve':<10}{'base ret':>10}{'base dd':>9}{'base sh':>9}   "
      f"{'trail gap ret':>14}{'gap dd':>8}{'gap sh':>8}   verdetto")
n_better = 0
for (code, asset), sleeve in DATA.items():
    key = f"{code.split('_')[0]} {asset}"
    b = simulate(ExitPolicy, sleeve, {}, start_ms=OOS_START_MS)
    g = oos(TrailATRKeepTP, sleeve, sim=simulate_exec, sl_gap=True, sl_slip_bps=5.0)
    better = g.get("sharpe_t", 0) >= b.get("sharpe_t", 0)
    dd_better = g.get("dd_pct", 99) <= b.get("dd_pct", 99)
    n_better += better
    verdict = ("Sharpe+DD>" if better and dd_better else
               "DD> ret<" if dd_better and not better else "PEGGIO")
    print(f"{key:<10}{b.get('ret_pct',0):>9.0f}%{b.get('dd_pct',0):>8.1f}{b.get('sharpe_t',0):>9.2f}   "
          f"{g.get('ret_pct',0):>13.0f}%{g.get('dd_pct',0):>7.1f}{g.get('sharpe_t',0):>8.2f}   {verdict}")
print(f"\n  sleeve con Sharpe trail-gap >= baseline: {n_better}/6")


# ----------------------- (G) CONFRONTO EQUO: baseline E trail entrambi con gap-through SL
print("\n" + "=" * 90)
print("CONFRONTO EQUO (gap-through SL su ENTRAMBI) — la tesi 'SL fisso distrugge valore' regge?")
print("=" * 90)
print(f"{'sleeve':<10}{'BASE-gap ret':>13}{'dd':>6}{'sh':>6}   {'TRAIL-gap ret':>14}{'dd':>6}{'sh':>6}   verdetto")
trail_wins = 0
agg_b = []; agg_t = []
for (code, asset), sleeve in DATA.items():
    key = f"{code.split('_')[0]} {asset}"
    b = oos(ExitPolicy, sleeve, sim=simulate_exec, sl_gap=True, sl_slip_bps=5.0)
    t = oos(TrailATRKeepTP, sleeve, sim=simulate_exec, sl_gap=True, sl_slip_bps=5.0)
    agg_b.append(b.get("sharpe_t", 0)); agg_t.append(t.get("sharpe_t", 0))
    win = t.get("sharpe_t", 0) >= b.get("sharpe_t", 0)
    trail_wins += win
    print(f"{key:<10}{b.get('ret_pct',0):>12.0f}%{b.get('dd_pct',0):>6.1f}{b.get('sharpe_t',0):>6.2f}   "
          f"{t.get('ret_pct',0):>13.0f}%{t.get('dd_pct',0):>6.1f}{t.get('sharpe_t',0):>6.2f}   "
          f"{'TRAIL>=' if win else 'BASE>'}")
print(f"\n  con ENTRAMBI gap-through: trail Sharpe >= baseline su {trail_wins}/6 sleeve")
print(f"  Sharpe medio  BASE-gap={np.mean(agg_b):.2f}  TRAIL-gap={np.mean(agg_t):.2f}")
