"""VERIFY EXIT-16 close_confirm_sl — lente STRESS (avversariale).

Ipotesi nulla: l'edge della close-confirm-SL e' fragile a frizioni reali.
Quattro stress, tutti su segnali cache (params LIVE, hurst_max=0.55):

(1) FEE 2x: FEE_RT=0.002 (vs 0.001). Penalizza le policy che girano piu' capitale.
(2) BEAR/CRASH 2021-01..2022-12 (LUNA/FTX/19-mag-21): worst-trade + 5 peggiori
    trade della policy vs base. Lo SL disattivato lascia correre le perdite?
(3) SLIPPAGE AVVERSO 20bps sulle uscite della policy: ogni fill di USCITA paga
    +20bps contro la posizione (prezzo di uscita peggiorato). L'edge regge?
    NB: lo applico SOLO alle uscite della POLICY (la sua tesi e' "esco al close":
    il close-fill e' market, paga slippage; la base esce a livelli limite sl0/tp0).
(4) OVERLAP/TURNOVER: la policy allunga la permanenza (no stop intrabar). Conto
    i segnali SALTATI per non-overlap (i <= last_exit) base vs policy, e quanto
    capitale-tempo (somma bars in posizione) gira in piu'.

Tutto via simulate() con monkeypatch di FEE_RT e una sottoclasse engine per lo
slippage. Niente modifiche ad altri file.
"""
import sys
from pathlib import Path

import numpy as np
import pandas as pd

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))
sys.path.insert(0, str(Path(__file__).resolve().parents[3]))

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

import importlib.util
spec = importlib.util.spec_from_file_location(
    "cc16", str(Path(__file__).resolve().parent / "16_close_confirm_sl.py"))
cc16 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(cc16)
CloseConfirmSl = cc16.CloseConfirmSl

BUF = 0.5  # train-pick


def fmt(r):
    if not r:
        return "   n/a"
    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}")


def sub(cls, sleeve, g, s, e):
    return simulate(cls, sleeve, g, start_ms=s, end_ms=e)


def ms(d):
    return int(pd.Timestamp(d, tz="UTC").value // 1e6)


# ===========================================================================
# Engine "instrumented" che riproduce simulate() ma:
#  - applica uno slippage avverso (bps) su OGNI fill di USCITA (solo se policy)
#  - raccoglie la lista dei ret per-trade e i segnali SALTATI per non-overlap
#  - raccoglie capital-time (somma bars)
# Lo tengo allineato 1:1 con exit_lab.simulate (stesso ordine SL-prima-di-TP).
# ===========================================================================
def simulate_instr(policy_cls, sleeve, params=None, start_ms=None, end_ms=None,
                   exit_slip_bps=0.0):
    params = params or {}
    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 = exit_lab.FEE_RT * LEV
    slip = exit_slip_bps * 1e-4
    capital = peak = 1000.0
    max_dd = 0.0
    last_exit = -1
    trades = wins = 0
    bars_tot = 0
    skipped_overlap = 0
    rets = []           # (ret, ts_entry, bars)
    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 + 1 >= n:
            continue
        if i <= last_exit:
            skipped_overlap += 1
            continue
        entry = c[i]
        pol = policy_cls(ctx, i, d, entry, tp0, sl0, mb, **params)
        horizon = min(int(pol.horizon), HARD_CAP)
        fills = []
        remaining = 1.0
        j = i
        for step in range(1, horizon + 1):
            j = i + step
            if j >= n:
                j = n - 1
                fills.append((remaining, c[j])); 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:
                fills.append((remaining, sl)); 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):
                fills.append((remaining, c[j])); remaining = 0.0
                break
            if step == horizon:
                fills.append((remaining, c[j])); remaining = 0.0
        if remaining > 1e-9:
            fills.append((remaining, c[j]))
        # slippage avverso sull'uscita: il prezzo di uscita peggiora di slip,
        # cioe' si vende piu' basso (long) / si ricompra piu' alto (short).
        def adj(p):
            return p * (1.0 - slip) if d == 1 else p * (1.0 + slip)
        ret = sum(f * (adj(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, int(ts[i]), j - i))
    if trades == 0:
        return {}
    r = np.array([x[0] for x in 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,
        "bars_tot": bars_tot,
        "skipped_overlap": skipped_overlap,
        "rets": rets,
        "worst5": sorted(r.tolist())[:5],
    }


def main():
    data = exit_lab.load_sleeves()

    # ===================================================================
    print("=" * 104)
    print("TEST 1 — FEE 2x (FEE_RT 0.001 -> 0.002).  base vs policy buffer=0.5  (OOS, dopo 2023-11)")
    print("=" * 104)
    orig_fee = exit_lab.FEE_RT
    survive_fee = True
    for fee in (0.001, 0.002):
        exit_lab.FEE_RT = fee
        print(f"\n--- FEE_RT={fee} ---")
        for (code, asset), sleeve in data.items():
            key = f"{code.split('_')[0]} {asset}"
            b = sub(ExitPolicy, sleeve, {}, OOS_START_MS, None)
            p = sub(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None)
            tag = ""
            if fee == 0.002 and b and p:
                # regge se sharpe policy >= base (la tesi e' che migliora)
                ok = p["sharpe_t"] >= b["sharpe_t"] - 0.10
                survive_fee &= ok
                tag = "OK" if ok else "WORSE"
            print(f"  {key:<10} base {fmt(b)}")
            print(f"  {'':<10} pol  {fmt(p)} {tag}")
    exit_lab.FEE_RT = orig_fee
    print(f"\nFEE 2x: policy regge (>= base-0.10 sh su tutti gli sleeve OOS)? {survive_fee}")

    # ===================================================================
    print("\n" + "=" * 104)
    print("TEST 2 — BEAR/CRASH 2021-01..2022-12 (LUNA/FTX/19-mag): worst-trade + 5 peggiori")
    print("=" * 104)
    s2, e2 = ms("2021-01-01"), ms("2023-01-01")
    tail_worse = 0
    tail_total = 0
    for (code, asset), sleeve in data.items():
        key = f"{code.split('_')[0]} {asset}"
        b = simulate_instr(ExitPolicy, sleeve, {}, s2, e2)
        p = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, s2, e2)
        print(f"\n{key}")
        print(f"  base {fmt(b)}")
        print(f"  pol  {fmt(p)}")
        if b and p:
            bw = [f"{x*100:+.1f}%" for x in b["worst5"]]
            pw = [f"{x*100:+.1f}%" for x in p["worst5"]]
            print(f"  base 5 peggiori (ret netto): {bw}")
            print(f"  pol  5 peggiori (ret netto): {pw}")
            tail_total += 1
            # la policy peggiora la coda se il worst-trade e' piu' negativo
            if p["worst5"][0] < b["worst5"][0] - 0.005:
                tail_worse += 1
                print(f"  -> CODA PEGGIORE: worst {p['worst5'][0]*100:+.1f}% < base {b['worst5'][0]*100:+.1f}%")
            else:
                print(f"  -> coda OK: worst {p['worst5'][0]*100:+.1f}% vs base {b['worst5'][0]*100:+.1f}%")
            print(f"  DD bear: base {b['dd_pct']:.1f}%  pol {p['dd_pct']:.1f}%")
    print(f"\nBEAR: sleeve con coda PEGGIORE (worst-trade > 0.5pt sotto base): {tail_worse}/{tail_total}")

    # ===================================================================
    print("\n" + "=" * 104)
    print("TEST 3 — SLIPPAGE AVVERSO 20bps sulle uscite della POLICY (OOS).  base senza slippage")
    print("(la tesi della policy e' 'esco al close' = market fill -> paga slippage)")
    print("=" * 104)
    survive_slip = True
    for (code, asset), sleeve in data.items():
        key = f"{code.split('_')[0]} {asset}"
        b = simulate_instr(ExitPolicy, sleeve, {}, OOS_START_MS, None, exit_slip_bps=0.0)
        p0 = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None, exit_slip_bps=0.0)
        p20 = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None, exit_slip_bps=20.0)
        ok = p20 and b and p20["sharpe_t"] >= b["sharpe_t"] - 0.10
        survive_slip &= bool(ok)
        print(f"\n{key}")
        print(f"  base  (no slip)        {fmt(b)}")
        print(f"  pol   (no slip)        {fmt(p0)}")
        print(f"  pol   (+20bps exit)    {fmt(p20)}  {'OK' if ok else 'WORSE vs base'}")
    print(f"\nSLIPPAGE 20bps: policy ancora >= base-0.10 sh su tutti? {survive_slip}")
    print("(test severo: lo slippage colpisce la policy ma NON la base — asimmetria pessimistica)")

    # severita' extra: slippage anche sulla base (entrambe market) per fairness
    print("\n--- fairness: 20bps anche sulle uscite della BASE ---")
    fair = True
    for (code, asset), sleeve in data.items():
        key = f"{code.split('_')[0]} {asset}"
        b20 = simulate_instr(ExitPolicy, sleeve, {}, OOS_START_MS, None, exit_slip_bps=20.0)
        p20 = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None, exit_slip_bps=20.0)
        ok = p20 and b20 and p20["sharpe_t"] >= b20["sharpe_t"] - 0.10
        fair &= bool(ok)
        print(f"  {key:<10} base+20 {fmt(b20)}")
        print(f"  {'':<10} pol +20 {fmt(p20)}  {'OK' if ok else 'WORSE'}")
    print(f"fairness (entrambe +20bps): policy >= base-0.10 sh? {fair}")

    # ===================================================================
    print("\n" + "=" * 104)
    print("TEST 4 — OVERLAP/TURNOVER: segnali saltati per non-overlap + capital-time (OOS)")
    print("=" * 104)
    for (code, asset), sleeve in data.items():
        key = f"{code.split('_')[0]} {asset}"
        b = simulate_instr(ExitPolicy, sleeve, {}, OOS_START_MS, None)
        p = simulate_instr(CloseConfirmSl, sleeve, {"buffer": BUF}, OOS_START_MS, None)
        if b and p:
            dskip = p["skipped_overlap"] - b["skipped_overlap"]
            dbars = p["bars_tot"] - b["bars_tot"]
            print(f"  {key:<10} base: trades {b['trades']:>4} skip-overlap {b['skipped_overlap']:>4} "
                  f"bars_tot {b['bars_tot']:>6} avg {b['avg_bars']:.1f}")
            print(f"  {'':<10} pol : trades {p['trades']:>4} skip-overlap {p['skipped_overlap']:>4} "
                  f"bars_tot {p['bars_tot']:>6} avg {p['avg_bars']:.1f}")
            print(f"  {'':<10} -> +{dskip} segnali persi per overlap, "
                  f"+{dbars} bars in posizione ({dbars/max(b['bars_tot'],1)*100:+.0f}% capital-time)")


if __name__ == "__main__":
    main()
