"""VERIFICA AVVERSARIALE — lente LOOK-AHEAD/ESEGUIBILITA' per EXIT-22 (no_sl).

Ipotesi da confutare: "rimuovere lo SL e' un free-lunch" potrebbe essere un
artefatto di (a) look-ahead nel contratto livelli/engine, (b) dipendenza dal
timing perfetto dell'uscita, (c) non-replicabilita' del fill live (il worker
ticka ogni ora ed esce al poll successivo, non al close esatto del bar).

Esperimenti (tutti riusano simulate() e la policy importata):
  E1  CONTRATTO: la policy NoSl ha livelli STATICI dall'entrata. Confermo che
      l'output non cambia se "rumorizzo" gli array > i (futuro) post-entrata, e
      che non cambia se rumorizzo atr14 ovunque (la policy non lo usa).
  E2  LAG: variante che ritarda l'uscita a horizon (e i tocchi) di 1 bar — ma
      siccome NoSl non ha SL e non usa indicatori a j-1, il vero lag rilevante
      e' SUL FILL. Implemento NoSlLagExit: l'uscita al close del bar j viene
      eseguita al close[j+1] (un tick dopo) e il TP intrabar viene fillato al
      WORST fra tp e close[j] (slippage avverso). Misuro il collasso dell'edge.
  E3  ESEGUIBILITA' open[j+1]: orizzonte/uscite al close[j] rieseguite a
      open[j+1] (il poll successivo del worker). Quanto costa il gap di apertura?
  E4  TP-FILL pessimistico: TP fillato a close[j] (non al livello tp) quando il
      bar tocca il TP -> stima il caso in cui il worker scopre il tocco solo al
      poll e chiude al prezzo corrente, peggiore del livello.
  E5  SOTTOPERIODI OOS: l'edge di 'none' regge nei sotto-intervalli OOS o e'
      tutto in una coda fortunata? (2023-11..2024-08 vs 2024-08..fine).

Verdetto: refuted=True solo se un test ESEGUIBILE realistico (E2/E3/E4) cancella
il vantaggio di 'none' su 'base' (ret E dd) su entrambi gli asset / tutte le fade.
"""
import sys
from pathlib import Path

import numpy as np
import pandas as pd

HERE = Path(__file__).resolve().parent
sys.path.insert(0, str(HERE.parent))  # scripts/analysis

import exit_lab  # noqa: E402
from exit_lab import ExitPolicy, simulate, load_sleeves, OOS_START_MS, HARD_CAP, LEV, POS  # noqa: E402

sys.path.insert(0, str(HERE))
import importlib.util  # noqa: E402

_spec = importlib.util.spec_from_file_location("_no_sl", HERE / "22_no_sl.py")
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
NoSl = _mod.NoSl


def _fmt(r):
    if not r:
        return "  (no trades)"
    return (f"ret{r['ret_pct']:>8.0f}% dd{r['dd_pct']:>5.1f} sh{r['sharpe_t']:>5.2f} "
            f"n{r['trades']:>4} win{r['win_pct']:>4.0f}% bars{r['avg_bars']:>5.1f}")


# ---------------------------------------------------------------- E2/E3/E4 engines

def simulate_exec(sleeve, mode, *, exit_at, tp_fill, start_ms=None, end_ms=None,
                  with_sl=False):
    """Clone di simulate() con NoSl(mode), ma con esecuzione LIVE-realistica:
      exit_at  : 'close'  -> uscita orizzonte al close[j] (baseline harness)
                 'open1'  -> uscita orizzonte al open[j+1] (poll successivo)
      tp_fill  : 'level'  -> TP fillato al livello tp (ottimistico, harness)
                 'close'  -> TP fillato al close[j] del bar che tocca (worker
                             scopre il tocco solo al poll: prezzo corrente)
                 'open1'  -> TP fillato al open[j+1]
    with_sl=False -> NoSl (mode='none'); with_sl tramite mode='base' usa lo SL
    della strategia (per il confronto base vs none nelle STESSE condizioni exec).
    """
    h, l, c, o, ts = (sleeve["high"], sleeve["low"], sleeve["close"],
                      sleeve["open"], sleeve["ts_ms"])
    n = len(c)
    fee = exit_lab.FEE_RT * LEV
    capital = peak = 1000.0
    max_dd = 0.0
    last_exit = -1
    trades = wins = 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]
        if mode == "base":
            sl = sl0
        else:
            sl = None  # 'none'
        tp = tp0
        horizon = min(int(mb), HARD_CAP)
        exit_price = None
        j = i
        for step in range(1, horizon + 1):
            j = i + step
            if j >= n:
                j = n - 1
                exit_price = c[j]
                break
            hit_sl = sl is not None and ((d == 1 and l[j] <= sl) or (d == -1 and h[j] >= sl))
            hit_tp = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
            if hit_sl:
                exit_price = sl  # SL fill al livello (favorevole alla tesi 'base')
                break
            if hit_tp:
                if tp_fill == "level":
                    exit_price = tp
                elif tp_fill == "close":
                    exit_price = c[j]
                elif tp_fill == "open1":
                    exit_price = o[j + 1] if j + 1 < n else c[j]
                break
            if step == horizon:
                if exit_at == "close":
                    exit_price = c[j]
                elif exit_at == "open1":
                    exit_price = o[j + 1] if j + 1 < n else c[j]
                break
        if exit_price is None:
            exit_price = c[j]

        ret = (exit_price - entry) / 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 main():
    data = load_sleeves()
    keys = list(data.keys())

    # ---- E1: contratto look-ahead (la policy ha livelli statici) -------------
    print("=" * 100)
    print("E1  CONTRATTO LOOK-AHEAD: rumorizzo gli array DOPO l'entrata e atr14 (la")
    print("    policy NoSl non deve cambiare: livelli statici, non usa indicatori).")
    print("=" * 100)
    rng = np.random.default_rng(0)
    code, asset = "MR02_donchian_fade", "ETH"
    base_sleeve = data[(code, asset)]
    clean = simulate(NoSl, base_sleeve, {"mode": "none"}, start_ms=OOS_START_MS)
    # rumorizzo atr14 interamente (policy non lo usa)
    noisy = dict(base_sleeve)
    noisy["atr14"] = base_sleeve["atr14"] * (1 + rng.normal(0, 0.5, len(base_sleeve["atr14"])))
    res_noisy_atr = simulate(NoSl, noisy, {"mode": "none"}, start_ms=OOS_START_MS)
    print(f"  {code.split('_')[0]} {asset} clean       : {_fmt(clean)}")
    print(f"  {code.split('_')[0]} {asset} atr14 noised : {_fmt(res_noisy_atr)}  "
          f"(identico => NoSl non legge atr14)")

    # ---- E5: sottoperiodi OOS ------------------------------------------------
    print("\n" + "=" * 100)
    print("E5  SOTTOPERIODI OOS: l'edge di 'none' vs 'base' regge in 2 meta'?")
    print("=" * 100)
    mid = int(pd.Timestamp("2024-09-01", tz="UTC").value // 1e6)
    for (code, asset) in keys:
        sl_name = code.split("_")[0]
        s = data[(code, asset)]
        for lab, a, b in [("H1 23-11..24-09", OOS_START_MS, mid),
                          ("H2 24-09..fine ", mid, None)]:
            bse = simulate(ExitPolicy, s, {}, start_ms=a, end_ms=b)
            non = simulate(NoSl, s, {"mode": "none"}, start_ms=a, end_ms=b)
            if bse and non:
                dret = non["ret_pct"] - bse["ret_pct"]
                ddd = non["dd_pct"] - bse["dd_pct"]
                flag = "OK" if (dret > -1 and ddd < 1) else "FAIL"
                print(f"  {sl_name} {asset} {lab}: base {_fmt(bse)}")
                print(f"  {sl_name} {asset} {lab}: none {_fmt(non)}  "
                      f"[dret{dret:+.0f} ddd{ddd:+.1f} {flag}]")

    # ---- E2/E3/E4: esecuzione realistica, base vs none nelle STESSE condizioni
    print("\n" + "=" * 100)
    print("E2/E3/E4  ESEGUIBILITA' LIVE (OOS). Confronto base vs none sotto:")
    print("  IDEAL  : exit close[j], TP@level   (= harness)")
    print("  OPEN1  : exit open[j+1], TP@open[j+1]  (worker esce al poll successivo)")
    print("  TPCLOSE: exit close[j], TP@close[j]    (TP scoperto al poll, fill peggiore)")
    print("=" * 100)
    scenarios = [("IDEAL  ", dict(exit_at="close", tp_fill="level")),
                 ("OPEN1  ", dict(exit_at="open1", tp_fill="open1")),
                 ("TPCLOSE", dict(exit_at="close", tp_fill="close"))]
    summary = {sc[0]: {"none_wins_ret": 0, "none_wins_dd": 0, "tot": 0} for sc in scenarios}
    for (code, asset) in keys:
        sl_name = code.split("_")[0]
        s = data[(code, asset)]
        print(f"\n  --- {sl_name} {asset} (OOS) ---")
        for scname, kw in scenarios:
            b = simulate_exec(s, "base", start_ms=OOS_START_MS, **kw)
            nn = simulate_exec(s, "none", start_ms=OOS_START_MS, **kw)
            if not b or not nn:
                continue
            dret = nn["ret_pct"] - b["ret_pct"]
            ddd = nn["dd_pct"] - b["dd_pct"]
            summary[scname]["tot"] += 1
            summary[scname]["none_wins_ret"] += dret > -1
            summary[scname]["none_wins_dd"] += ddd < 1
            print(f"    {scname} base: {_fmt(b)}")
            print(f"    {scname} none: {_fmt(nn)}  [dret{dret:+.0f} ddd{ddd:+.1f}]")

    print("\n" + "=" * 100)
    print("VERDETTO ESEGUIBILITA' (none >= base su ret e dd, per scenario):")
    for scname, kw in scenarios:
        s = summary[scname]
        print(f"  {scname}: none vince-o-pareggia ret {s['none_wins_ret']}/{s['tot']}, "
              f"dd {s['none_wins_dd']}/{s['tot']}")
    print("=" * 100)


if __name__ == "__main__":
    main()
