"""TAIL-RISK AUDIT of EXIT-22 (no_sl) and EXIT-16 (close_confirm_sl).

Hypothesis under test (to REFUTE if possible): removing/softening the intrabar SL
is an artifact whose hidden cost is catastrophic tail risk in crash regimes.

Sections:
 (1) Per-trade MAE (maximum adverse excursion, intrabar, leverage 3, % of notional
     ret terms == same units as engine `ret`) for base vs no_sl vs close_confirm.
     Distribution p50/p95/p99/max per sleeve.
 (2) Crash windows: 2020-03-12, 2021-05-19, 2022-06, 2022-11 (FTX). Trades OPEN in
     those windows: realized loss + MAE under base / no_sl / close_confirm.
 (3) Live worker -2% fallback: with no_sl the strategy emits tp but sl=0 -> the
     worker branch `if self.tp and self.sl` is False, falls to `elif self.max_bars`
     (fade have max_bars) -> PURE horizon exit, the -2% `else` branch NEVER fires.
     So no_sl LIVE has NO stop at all. We simulate what a -2% price stop WOULD have
     capped, to quantify the protection that is in fact ABSENT.
 (4) Disaster SL at 3x / 4x the base SL distance, intrabar: does it keep almost all
     the no_sl gain while cutting the tail?

Run: cd /opt/docker/PythagorasGoal && PYTHONPATH=. uv run python \
     scripts/analysis/exit_policies/verify_tail_risk.py
"""
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 as EL  # noqa: E402
from exit_lab import ExitPolicy, simulate, load_sleeves, LEV, OOS_START_MS  # noqa: E402

DATA = load_sleeves()
SLEEVES = list(DATA.items())


# --------------------------------------------------------------------------- helpers

def _replay_trades(policy_cls, sleeve, params=None, start_ms=None, end_ms=None):
    """Re-run the engine logic but COLLECT per-trade detail incl. MAE.

    MAE = worst adverse excursion (in `ret` units == leverage*frac move on notional)
    measured on the bars the trade is actually OPEN, from entry bar+1 up to and
    INCLUDING the exit bar (the exit bar's wick counts: it's where SL would trigger).
    """
    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 = EL.FEE_RT * LEV
    last_exit = -1
    out = []
    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
        worst = 0.0  # most negative excursion in ret units
        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
            # adverse excursion of THIS bar (before any exit): worst intrabar price
            adverse = l[j] if d == 1 else h[j]
            exc = (adverse - entry) / entry * d * LEV
            worst = min(worst, exc)
            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
        last_exit = j
        out.append({
            "i": i, "j": j, "d": d, "entry": entry,
            "ts_entry": ts[i], "ts_exit": ts[j],
            "ret": ret, "mae": worst, "bars": j - i,
        })
    return out


def _pct(arr, q):
    return np.percentile(arr, q) if len(arr) else float("nan")


# --------------------------------------------------------------------------- (1) MAE dist

def section1():
    print("=" * 100)
    print("(1) MAE DISTRIBUTION per sleeve (ret units = leverage*move on notional; "
          "fee NOT included). Negative = adverse.")
    print("    MAE is the worst intrabar excursion BEFORE exit. For base, SL caps it; "
          "for no_sl/close_confirm it can run.")
    print("=" * 100)
    policies = _load_policies()
    hdr = f"{'sleeve':<11}{'policy':<16}{'n':>5}{'p50':>9}{'p95':>9}{'p99':>9}{'max':>9}{'realP99':>10}{'realMax':>10}"
    for (code, asset), sleeve in SLEEVES:
        key = f"{code.split('_')[0]} {asset}"
        print(f"\n--- {key}")
        print(hdr)
        for pname, (cls, prm) in policies.items():
            tr = _replay_trades(cls, sleeve, prm)
            mae = np.array([t["mae"] for t in tr]) * 100  # to %
            rets = np.array([t["ret"] for t in tr]) * 100
            print(f"{'':<11}{pname:<16}{len(tr):>5}"
                  f"{_pct(mae,50):>9.2f}{_pct(mae,5):>9.2f}{_pct(mae,1):>9.2f}{mae.min():>9.2f}"
                  f"{_pct(rets,1):>10.2f}{rets.min():>10.2f}")


def _load_policies():
    """Return {name: (cls, params)} for base, no_sl, close_confirm(buf0)."""
    p22 = Path(__file__).resolve().parents[0] / "22_no_sl.py"
    p16 = Path(__file__).resolve().parents[0] / "16_close_confirm_sl.py"
    import importlib.util

    def _load(path, attr):
        spec = importlib.util.spec_from_file_location(path.stem, path)
        mod = importlib.util.module_from_spec(spec)
        spec.loader.exec_module(mod)
        return getattr(mod, attr)

    NoSl = _load(p22, "NoSl")
    CloseConfirm = _load(p16, "CloseConfirmSl")
    return {
        "base": (ExitPolicy, {}),
        "no_sl": (NoSl, {"mode": "none"}),
        "close_confirm0": (CloseConfirm, {"buffer": 0.0}),
    }


# --------------------------------------------------------------------------- (2) crashes

CRASHES = {
    "2020-03-12 covid": ("2020-03-10", "2020-03-16"),
    "2021-05-19 leverage": ("2021-05-17", "2021-05-23"),
    "2022-06 3AC/Luna": ("2022-06-10", "2022-06-20"),
    "2022-11 FTX": ("2022-11-07", "2022-11-12"),
}


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


def section2():
    print("\n" + "=" * 100)
    print("(2) CRASH WINDOWS — trades OPEN (entry inside window) per policy: realized "
          "loss (ret%) and MAE%.")
    print("=" * 100)
    policies = _load_policies()
    for label, (a, b) in CRASHES.items():
        lo, hi = _ms(a), _ms(b)
        print(f"\n### {label}  [{a} .. {b}]")
        any_trade = False
        for (code, asset), sleeve in SLEEVES:
            key = f"{code.split('_')[0]} {asset}"
            for pname, (cls, prm) in policies.items():
                tr = _replay_trades(cls, sleeve, prm)
                win = [t for t in tr if lo <= t["ts_entry"] <= hi]
                if not win:
                    continue
                any_trade = True
                rets = np.array([t["ret"] for t in win]) * 100
                mae = np.array([t["mae"] for t in win]) * 100
                worst = min(win, key=lambda t: t["ret"])
                print(f"  {key:<11}{pname:<16}n{len(win):>3}  "
                      f"sumRet{rets.sum():>8.2f}%  worstRet{rets.min():>8.2f}%  "
                      f"worstMAE{mae.min():>8.2f}%  avgBars{np.mean([t['bars'] for t in win]):>5.1f}")
        if not any_trade:
            print("  (no trades opened in window across sleeves)")


# --------------------------------------------------------------------------- (3) -2% fallback

def section3():
    print("\n" + "=" * 100)
    print("(3) LIVE -2% FALLBACK on no_sl. NOTE: with no_sl the worker has NO stop at "
          "all (branch analysis in module docstring).")
    print("    Below = what a -2% PRICE stop (==-6% ret at lev3) WOULD cap if it WERE "
          "wired. Compares no_sl ret vs a synthetic no_sl+2%stop.")
    print("=" * 100)
    NoSl = _load_policies()["no_sl"][0]
    stop_ret = -0.02 * LEV  # -2% price move * leverage = -6% on notional in ret units
    hdr = f"{'sleeve':<11}{'no_sl ret%':>12}{'+2%stop ret%':>14}{'Δret pp':>10}{'n capped':>10}{'worst no_sl':>13}{'worst +2%':>11}"
    print(hdr)
    for (code, asset), sleeve in SLEEVES:
        key = f"{code.split('_')[0]} {asset}"
        tr = _replay_trades(NoSl, sleeve, {"mode": "none"})
        # synthetic: if MAE breaches stop_ret, realize at stop_ret (approx; ignores
        # that price may recover — that's the point: a -2% stop locks the loss).
        base_rets = np.array([t["ret"] for t in tr])
        capped = []
        n_cap = 0
        fee = EL.FEE_RT * LEV
        for t in tr:
            if t["mae"] <= stop_ret:
                capped.append(stop_ret - fee)  # stopped at -2% price, pay fee
                n_cap += 1
            else:
                capped.append(t["ret"])
        capped = np.array(capped)

        def _compound(rets):
            cap = 1000.0
            for r in rets:
                cap = max(cap + cap * EL.POS * r, 10.0)
            return (cap / 1000.0 - 1) * 100

        r_nosl = _compound(base_rets)
        r_stop = _compound(capped)
        print(f"{key:<11}{r_nosl:>12.0f}{r_stop:>14.0f}{r_stop - r_nosl:>10.1f}"
              f"{n_cap:>10}{base_rets.min()*100:>13.2f}{capped.min()*100:>11.2f}")


# --------------------------------------------------------------------------- (4) disaster SL

class DisasterSl(ExitPolicy):
    """no_sl behaviour + a FAR intrabar disaster stop at `mult` x base SL distance."""
    name = "disaster_sl"

    def __init__(self, ctx, i, d, entry, tp0, sl0, mb, **params):
        super().__init__(ctx, i, d, entry, tp0, sl0, mb, **params)
        mult = float(params.get("mult", 3.0))
        self.sl = entry + mult * (sl0 - entry)

    def levels(self, j: int):
        return self.tp0, self.sl, 1.0


def _full_metrics(cls, sleeve, prm):
    full = simulate(cls, sleeve, prm)
    oos = simulate(cls, sleeve, prm, start_ms=OOS_START_MS)
    return full, oos


def section4():
    print("\n" + "=" * 100)
    print("(4) DISASTER SL — no_sl + far intrabar stop at 3x / 4x base SL distance. "
          "Keep the gain, cut the tail?")
    print("=" * 100)
    NoSl = _load_policies()["no_sl"][0]
    hdr = (f"{'sleeve':<11}{'policy':<13}"
           f"{'FULLret%':>10}{'FULLdd%':>9}{'FULLsh':>8}"
           f"{'OOSret%':>9}{'OOSdd%':>8}{'OOSsh':>7}{'worstMAE%':>11}{'nStop':>7}")
    print(hdr)
    for (code, asset), sleeve in SLEEVES:
        key = f"{code.split('_')[0]} {asset}"
        rows = [
            ("base", ExitPolicy, {}),
            ("no_sl", NoSl, {"mode": "none"}),
            ("disaster3x", DisasterSl, {"mult": 3.0}),
            ("disaster4x", DisasterSl, {"mult": 4.0}),
        ]
        print(f"--- {key}")
        for pname, cls, prm in rows:
            full, oos = _full_metrics(cls, sleeve, prm)
            tr = _replay_trades(cls, sleeve, prm)
            mae = np.array([t["mae"] for t in tr]) * 100
            # count trades that hit the disaster stop (ret near the stop level)
            n_stop = 0
            if pname.startswith("disaster"):
                mult = prm["mult"]
                for t, raw in zip(tr, sleeve["signals"]):
                    pass
                # simpler: a stop hit shows up as a large negative ret roughly = stop level
                n_stop = int(np.sum(mae <= -2.0 * mult * LEV / LEV * 0))  # placeholder
            print(f"{'':<11}{pname:<13}"
                  f"{full.get('ret_pct',0):>10.0f}{full.get('dd_pct',0):>9.2f}{full.get('sharpe_t',0):>8.2f}"
                  f"{oos.get('ret_pct',0):>9.0f}{oos.get('dd_pct',0):>8.2f}{oos.get('sharpe_t',0):>7.2f}"
                  f"{mae.min():>11.2f}{'':>7}")


# --------------------------------------------------------------------------- aggregate

def section5_aggregate():
    """Equal-weight aggregate across the 6 sleeves: does no_sl's tail blow up the
    PORTFOLIO worst-trade vs disaster3x? Sum of per-sleeve compounded won't aggregate
    DD honestly, so we report the WORST single-trade ret across all sleeves and the
    99th pct of the pooled trade distribution."""
    print("\n" + "=" * 100)
    print("(5) POOLED TRADE DISTRIBUTION across all 6 sleeves (the real tail metric).")
    print("=" * 100)
    NoSl = _load_policies()["no_sl"][0]
    cfgs = [
        ("base", ExitPolicy, {}),
        ("no_sl", NoSl, {"mode": "none"}),
        ("close_confirm0", _load_policies()["close_confirm0"][0], {"buffer": 0.0}),
        ("disaster3x", DisasterSl, {"mult": 3.0}),
        ("disaster4x", DisasterSl, {"mult": 4.0}),
    ]
    print(f"{'policy':<16}{'n':>6}{'retP1%':>9}{'retMin%':>9}{'maeP1%':>9}{'maeMin%':>9}{'<-15%cnt':>10}")
    for pname, cls, prm in cfgs:
        allret, allmae = [], []
        for (code, asset), sleeve in SLEEVES:
            tr = _replay_trades(cls, sleeve, prm)
            allret += [t["ret"] * 100 for t in tr]
            allmae += [t["mae"] * 100 for t in tr]
        ar = np.array(allret); am = np.array(allmae)
        n_bad = int(np.sum(ar < -15.0))
        print(f"{pname:<16}{len(ar):>6}{_pct(ar,1):>9.2f}{ar.min():>9.2f}"
              f"{_pct(am,1):>9.2f}{am.min():>9.2f}{n_bad:>10}")


if __name__ == "__main__":
    section1()
    section2()
    section3()
    section4()
    section5_aggregate()
