"""VERIFICA AVVERSARIALE — lente STRESS sul sopravvissuto EXIT-22 (mode='none', SL RIMOSSO).

Ipotesi avversaria: il vantaggio di togliere lo SL (ret/DD/Sharpe migliori) e' un
ARTEFATTO del regime calmo OOS 2024-25 e/o si dissolve sotto stress realistici. Senza
SL la coda non e' limitata a leva 3x: un trade puo' perdere fino al movimento avverso
intero entro max_bars (24h). Testo 4 stress:

  (1) FEE 2x  (FEE_RT=0.002): la policy ABBASSA il turnover (avg_bars 9->12-13, meno
      trade) quindi la fee 2x dovrebbe penalizzarla MENO della base -> non e' la sua
      debolezza, ma lo misuro comunque per onesta'.
  (2) SOTTOPERIODO BEAR 2021-01..2022-12 (crash 2021-05-19, LUNA mag-2022, FTX nov-2022):
      qui lo SL dovrebbe servire. Confronto ret/DD + WORST trade + 5 peggiori trade
      none vs base. E' il test decisivo: se none esplode in DD o ha code mostruose qui,
      il survivor e' confutato (l'OOS calmo nascondeva il rischio).
  (3) SLIPPAGE AVVERSO 20bps sulle USCITE: ogni uscita (TP/horizon) pagata 20bps peggio
      (prezzo di uscita penalizzato del 20bps contro la posizione). Penalizza di piu' le
      policy che escono al close/horizon (none esce piu' spesso a horizon, non al TP-limit).
  (4) OVERLAP/CHURN: la policy allunga la permanenza -> conto i segnali SCARTATI per
      non-overlap (i<=last_exit) in piu' rispetto alla base, e l'effetto sul capitale.

Confuto SOLO con evidenza numerica concreta.
"""
import sys
from pathlib import Path

import numpy as np
import pandas as pd

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

# import della policy dal suo file
import importlib.util
_spec = importlib.util.spec_from_file_location(
    "policy_22", str(Path(__file__).resolve().parent / "22_no_sl.py"))
_mod = importlib.util.module_from_spec(_spec)
_spec.loader.exec_module(_mod)
NoSl = _mod.NoSl

BEAR_START = int(pd.Timestamp("2021-01-01", tz="UTC").value // 1e6)
BEAR_END = int(pd.Timestamp("2023-01-01", tz="UTC").value // 1e6)  # esclusivo
OOS_START = exit_lab.OOS_START_MS

DATA = load_sleeves()


def line(tag, key, r):
    if not r:
        print(f"  {tag:<14}{key:<10} (no trades)")
        return
    print(f"  {tag:<14}{key:<10} ret{r['ret_pct']:>7.0f}% dd{r['dd_pct']:>5.1f} "
          f"sh{r['sharpe_t']:>5.2f} n{r['trades']:>4} win{r['win_pct']:>4.0f}% "
          f"bars{r['avg_bars']:>5.1f}")


# ----------------------------------------------------------- (1) FEE 2x
def test_fee2x():
    print("\n=== (1) FEE 2x  (0.10%->0.20% RT x leva 3) ===")
    orig = exit_lab.FEE_RT
    for fee, label in [(0.001, "fee1x"), (0.002, "fee2x")]:
        exit_lab.FEE_RT = fee
        print(f"  -- {label} --")
        n_better_none = 0
        for (code, asset), sleeve in DATA.items():
            key = f"{code.split('_')[0]} {asset}"
            b = simulate(ExitPolicy, sleeve, start_ms=OOS_START)
            no = simulate(NoSl, sleeve, {"mode": "none"}, start_ms=OOS_START)
            flag = "  <none>=base" if (no and b and no["ret_pct"] >= b["ret_pct"]
                                       and no["dd_pct"] <= b["dd_pct"]) else ""
            if flag:
                n_better_none += 1
            print(f"    {key:<10} base ret{b['ret_pct']:>6.0f}% dd{b['dd_pct']:>5.1f} | "
                  f"none ret{no['ret_pct']:>6.0f}% dd{no['dd_pct']:>5.1f}{flag}")
        print(f"    -> none >= base (ret&dd) su {n_better_none}/6 sleeve")
    exit_lab.FEE_RT = orig


# ----------------------------------------------------------- (2) BEAR 2021-22
def worst_trades(policy_cls, sleeve, params, start_ms, end_ms, k=5):
    """Replica la logica di simulate ma raccoglie i ret per-trade per
    estrarre i k peggiori e il worst. Stessa identica meccanica."""
    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
    last_exit = -1
    rets = []
    for (i, d, tp0, sl0, mb) in sleeve["signals"]:
        if ts[i] < start_ms or 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), 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]))
        ret = sum(f * (p - entry) for f, p in fills) / entry * d * LEV - fee
        rets.append(ret * 1e4)  # bps
        last_exit = j
    rets = np.array(sorted(rets))
    return rets


def test_bear():
    print("\n=== (2) SOTTOPERIODO BEAR 2021-01..2022-12 (LUNA/FTX/19-may-2021) ===")
    print("  (ret/DD su periodo + WORST trade e media 5-peggiori, bps; leva 3x)")
    agg_none_dd = []
    agg_base_dd = []
    for (code, asset), sleeve in DATA.items():
        key = f"{code.split('_')[0]} {asset}"
        b = simulate(ExitPolicy, sleeve, start_ms=BEAR_START, end_ms=BEAR_END)
        no = simulate(NoSl, sleeve, {"mode": "none"}, start_ms=BEAR_START, end_ms=BEAR_END)
        rb = worst_trades(ExitPolicy, sleeve, {}, BEAR_START, BEAR_END)
        rn = worst_trades(NoSl, sleeve, {"mode": "none"}, BEAR_START, BEAR_END)
        wb = rb[0] if len(rb) else float("nan")
        wn = rn[0] if len(rn) else float("nan")
        w5b = rb[:5].mean() if len(rb) >= 5 else float("nan")
        w5n = rn[:5].mean() if len(rn) >= 5 else float("nan")
        if b:
            agg_base_dd.append(b["dd_pct"])
        if no:
            agg_none_dd.append(no["dd_pct"])
        print(f"  {key:<10}")
        print(f"     base ret{b.get('ret_pct',0):>6.0f}% dd{b.get('dd_pct',0):>5.1f} "
              f"n{b.get('trades',0):>3} | worst{wb:>8.0f}bps  5worst{w5b:>8.0f}bps")
        print(f"     none ret{no.get('ret_pct',0):>6.0f}% dd{no.get('dd_pct',0):>5.1f} "
              f"n{no.get('trades',0):>3} | worst{wn:>8.0f}bps  5worst{w5n:>8.0f}bps  "
              f"{'TAIL+' if wn < wb - 1 else ''}")
    print(f"  -> DD medio bear: base {np.mean(agg_base_dd):.1f}%  none {np.mean(agg_none_dd):.1f}%")
    print(f"     worst-DD bear:  base {np.max(agg_base_dd):.1f}%  none {np.max(agg_none_dd):.1f}%")


# ----------------------------------------------------------- (3) SLIPPAGE 20bps exit
class NoSlSlip(NoSl):
    """none + slippage avverso 20bps su OGNI uscita (sia TP che horizon/close).
    Implementato penalizzando i livelli: tp eseguito 20bps peggio, e l'uscita al
    close subisce un haircut equivalente applicato nel ret. Per semplicita' e
    fedelta', applico lo slippage al RET finale come costo per-trade extra: ogni
    trade paga 20bps di slippage sul notional (una uscita per trade)."""
    name = "no_sl_slip"


class BaseSlip(ExitPolicy):
    name = "base_slip"


def simulate_slip(policy_cls, sleeve, params, start_ms, slip_bps=20.0):
    """simulate con costo di uscita avverso slip_bps (in bps di prezzo) applicato
    al ret per-trade (oltre alla fee). Penalizza uscite al close E al TP."""
    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 = slip_bps / 1e4 * LEV
    capital = peak = 1000.0
    max_dd = 0.0
    last_exit = -1
    trades = wins = 0
    rets = []
    for (i, d, tp0, sl0, mb) in sleeve["signals"]:
        if ts[i] < start_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), 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]))
        ret = sum(f * (p - entry) for f, p in fills) / entry * d * LEV - fee - slip
        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
        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}


def test_slippage():
    print("\n=== (3) SLIPPAGE AVVERSO 20bps su ogni uscita (OOS 2023-11+) ===")
    print("  confronto: none-noslip vs none-slip20 vs base-slip20")
    n_survive = 0
    for (code, asset), sleeve in DATA.items():
        key = f"{code.split('_')[0]} {asset}"
        no0 = simulate(NoSl, sleeve, {"mode": "none"}, start_ms=OOS_START)
        nos = simulate_slip(NoSl, sleeve, {"mode": "none"}, OOS_START, 20.0)
        bas = simulate_slip(ExitPolicy, sleeve, {}, OOS_START, 20.0)
        surv = nos and bas and nos["ret_pct"] >= bas["ret_pct"] and nos["dd_pct"] <= bas["dd_pct"]
        if surv:
            n_survive += 1
        print(f"  {key:<10} none ret{no0['ret_pct']:>6.0f}% | none+slip ret{nos['ret_pct']:>6.0f}%"
              f" dd{nos['dd_pct']:>5.1f} sh{nos['sharpe_t']:>5.2f} | base+slip ret{bas['ret_pct']:>6.0f}%"
              f" dd{bas['dd_pct']:>5.1f}  {'EDGE' if surv else 'lost'}")
    print(f"  -> none+slip >= base+slip (ret&dd) su {n_survive}/6 sleeve")


# ----------------------------------------------------------- (4) OVERLAP / CHURN
def count_dropped(policy_cls, sleeve, params, start_ms):
    """Conta i segnali scartati per non-overlap (i<=last_exit) e quelli eseguiti."""
    h, l, c, ts = sleeve["high"], sleeve["low"], sleeve["close"], sleeve["ts_ms"]
    n = len(c)
    ctx = dict(sleeve)
    policy_cls.prepare(ctx, **params)
    last_exit = -1
    dropped = executed = 0
    for (i, d, tp0, sl0, mb) in sleeve["signals"]:
        if ts[i] < start_ms or i + 1 >= n:
            continue
        if i <= last_exit:
            dropped += 1
            continue
        entry = c[i]
        pol = policy_cls(ctx, i, d, entry, tp0, sl0, mb, **params)
        horizon = min(int(pol.horizon), HARD_CAP)
        remaining = 1.0
        j = i
        for step in range(1, horizon + 1):
            j = i + step
            if j >= n:
                j = n - 1
                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:
                break
            if hit_tp:
                f = min(max(tpfrac, 0.0), 1.0) * remaining
                remaining -= f
                if remaining <= 1e-9:
                    break
                pol.on_partial(j, tp, remaining)
            if pol.after_bar(j):
                break
            if step == horizon:
                break
        last_exit = j
        executed += 1
    return executed, dropped


def test_overlap():
    print("\n=== (4) OVERLAP / CHURN (OOS 2023-11+) — segnali persi per non-overlap ===")
    tot_extra = 0
    for (code, asset), sleeve in DATA.items():
        key = f"{code.split('_')[0]} {asset}"
        eb, db = count_dropped(ExitPolicy, sleeve, {}, OOS_START)
        en, dn = count_dropped(NoSl, sleeve, {"mode": "none"}, OOS_START)
        extra = dn - db
        tot_extra += extra
        print(f"  {key:<10} base exec{eb:>4} drop{db:>3} | none exec{en:>4} drop{dn:>3} "
              f"| extra-drop {extra:+d}")
    print(f"  -> segnali extra persi per overlap (none vs base): {tot_extra:+d} totali")
    print("     (capitale gira meno: trade piu' lunghi, ma none rende comunque di piu' net)")


if __name__ == "__main__":
    test_fee2x()
    test_bear()
    test_slippage()
    test_overlap()
    print("\n[fatto] verifica stress EXIT-22 no_sl completata.")
