"""STRESS verifier for EXIT-02 trail_atr_keep_tp (train-pick k=1.5).

Adversarial lens = STRESS. Hypothesis to try to REFUTE the survivor:
  (1) Fee 2x (FEE_RT=0.002): the policy raises turnover (avg_bars 9->2.5) =>
      it should be disproportionately hurt by doubling fees.
  (2) Bear/crash subperiod 2021-01..2022-12 (2021-05-19 crash, LUNA, FTX):
      does the DD/tail of the policy survive? compare worst trade + 5 worst.
  (3) Adverse slippage on the POLICY exits (+20bps against position on exit
      price): the trail exits more often near wicks -> does edge survive?
  (4) Turnover / capital-churn: the policy turns capital ~3.6x faster. Quantify
      how many distinct trades each takes and the compounding effect.

We monkeypatch exit_lab.FEE_RT and re-implement a thin per-trade collector that
mirrors exit_lab.simulate EXACTLY (same SL-before-TP, same fills, same compounding)
so we can extract per-trade rets/exit prices for tail analysis and slippage.
"""
import sys
from pathlib import Path

import numpy as np

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

# import the survivor policy
import importlib.util
spec = importlib.util.spec_from_file_location(
    "p02", str(Path(__file__).resolve().parent / "02_trail_atr_keep_tp.py"))
p02 = importlib.util.module_from_spec(spec)
spec.loader.exec_module(p02)
TrailATRKeepTP = p02.TrailATRKeepTP

import pandas as pd
BEAR_START = int(pd.Timestamp("2021-01-01", tz="UTC").value // 1e6)
BEAR_END = int(pd.Timestamp("2023-01-01", tz="UTC").value // 1e6)


def simulate_detailed(policy_cls, sleeve, params=None, start_ms=None, end_ms=None,
                      exit_slip_bps=0.0):
    """Mirror of exit_lab.simulate but returns per-trade detail and supports an
    adverse slippage applied to every fill price (against the position)."""
    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 * exit_lab.LEV
    LEV, POS = exit_lab.LEV, exit_lab.POS
    slip = exit_slip_bps * 1e-4
    capital = peak = 1000.0
    max_dd = 0.0
    last_exit = -1
    rets = []
    tdetail = []

    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), 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]))

        # adverse slippage: every exit fill price moves AGAINST the position
        # long (d=1) sells lower -> p*(1-slip); short (d=-1) buys back higher -> p*(1+slip)
        adj_fills = [(f, p * (1.0 - d * slip)) for f, p in fills]
        ret = sum(f * (p - entry) for f, p in adj_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
        rets.append(ret)
        tdetail.append({"i": i, "j": j, "d": d, "ret": ret, "bars": j - i, "ts": int(ts[i])})

    if not rets:
        return {}
    r = np.array(rets)
    return {
        "ret_pct": (capital / 1000.0 - 1) * 100,
        "dd_pct": max_dd * 100,
        "trades": len(r),
        "win_pct": (r > 0).mean() * 100,
        "avg_ret_bps": r.mean() * 1e4,
        "sharpe_t": float(r.mean() / r.std() * np.sqrt(len(r))) if r.std() else 0.0,
        "avg_bars": np.mean([t["bars"] for t in tdetail]),
        "detail": tdetail,
    }


def run_grid(data, fee_rt, slip_bps, start_ms, end_ms, label):
    orig = exit_lab.FEE_RT
    exit_lab.FEE_RT = fee_rt
    print(f"\n===== {label}  (FEE_RT={fee_rt}, slip={slip_bps}bps, "
          f"start={start_ms}, end={end_ms}) =====")
    agg = {}
    for (code, asset), sleeve in data.items():
        key = f"{code.split('_')[0]} {asset}"
        base = simulate_detailed(ExitPolicy, sleeve, {}, start_ms, end_ms, slip_bps)
        pol = simulate_detailed(TrailATRKeepTP, sleeve, {"k": 1.5}, start_ms, end_ms, slip_bps)
        if not base or not pol:
            continue
        agg[key] = (base, pol)
        print(f"{key:<10} BASE ret{base['ret_pct']:>8.0f}% dd{base['dd_pct']:>5.1f} "
              f"sh{base['sharpe_t']:>5.2f} n{base['trades']:>4} bars{base['avg_bars']:>4.1f} "
              f"| POL ret{pol['ret_pct']:>8.0f}% dd{pol['dd_pct']:>5.1f} "
              f"sh{pol['sharpe_t']:>5.2f} n{pol['trades']:>4} bars{pol['avg_bars']:>4.1f}")
    exit_lab.FEE_RT = orig
    return agg


def policy_better(agg, metric="sharpe_t"):
    """count sleeves where policy >= base on metric (and ret not collapsed)."""
    n_better_sh = n_better_dd = n_ret_ok = 0
    for key, (base, pol) in agg.items():
        if pol["sharpe_t"] >= base["sharpe_t"]:
            n_better_sh += 1
        if pol["dd_pct"] <= base["dd_pct"]:
            n_better_dd += 1
        if pol["ret_pct"] >= base["ret_pct"] * 0.5 or pol["ret_pct"] >= base["ret_pct"]:
            n_ret_ok += 1
    return n_better_sh, n_better_dd, n_ret_ok, len(agg)


def main():
    data = load_sleeves()

    # ---- LENS 1: fee 2x on full OOS
    a_base_oos = run_grid(data, 0.001, 0.0, OOS_START_MS, None, "L0 OOS baseline fee (sanity)")
    a_fee2_oos = run_grid(data, 0.002, 0.0, OOS_START_MS, None, "L1 OOS FEE 2x")

    # ---- LENS 3: adverse slippage 20bps on exits (OOS, normal fee)
    a_slip_oos = run_grid(data, 0.001, 20.0, OOS_START_MS, None, "L3 OOS +20bps adverse exit slippage")

    # ---- LENS 1+3 combined (worst case): fee 2x AND slippage
    a_both = run_grid(data, 0.002, 20.0, OOS_START_MS, None, "L1+3 OOS fee2x + 20bps slip")

    # ---- LENS 2: bear/crash subperiod 2021-2022
    a_bear = run_grid(data, 0.001, 0.0, BEAR_START, BEAR_END, "L2 BEAR 2021-2022")

    # ---- LENS 2 tail: worst trade + 5 worst, base vs policy, on bear window
    print("\n===== L2 TAIL: worst trades in 2021-2022 (base vs policy) =====")
    for (code, asset), sleeve in data.items():
        key = f"{code.split('_')[0]} {asset}"
        base = simulate_detailed(ExitPolicy, sleeve, {}, BEAR_START, BEAR_END)
        pol = simulate_detailed(TrailATRKeepTP, sleeve, {"k": 1.5}, BEAR_START, BEAR_END)
        if not base or not pol:
            continue
        bw = sorted([t["ret"] for t in base["detail"]])[:5]
        pw = sorted([t["ret"] for t in pol["detail"]])[:5]
        print(f"{key:<10} base 5-worst(bps) {[f'{x*1e4:.0f}' for x in bw]} "
              f"| pol 5-worst(bps) {[f'{x*1e4:.0f}' for x in pw]}")
        print(f"{'':<10} base worst {bw[0]*1e4:.0f}bps  pol worst {pw[0]*1e4:.0f}bps  "
              f"base n={base['trades']} pol n={pol['trades']}")

    # ---- LENS 4: turnover / capital churn quantification (OOS)
    print("\n===== L4 TURNOVER / capital churn (OOS) =====")
    for (code, asset), sleeve in data.items():
        key = f"{code.split('_')[0]} {asset}"
        base = simulate_detailed(ExitPolicy, sleeve, {}, OOS_START_MS, None)
        pol = simulate_detailed(TrailATRKeepTP, sleeve, {"k": 1.5}, OOS_START_MS, None)
        if not base or not pol:
            continue
        # bars in market total, fraction of time deployed
        n = len(sleeve["close"])
        base_bars = sum(t["bars"] for t in base["detail"])
        pol_bars = sum(t["bars"] for t in pol["detail"])
        churn = (pol["trades"] / max(base["trades"], 1))
        print(f"{key:<10} trades base{base['trades']:>4} pol{pol['trades']:>4} "
              f"(x{churn:.2f}) | bars-in-mkt base{base_bars:>5} pol{pol_bars:>5} "
              f"| avg_bars base{base['avg_bars']:.1f} pol{pol['avg_bars']:.1f} "
              f"| win% base{base['win_pct']:.0f} pol{pol['win_pct']:.0f}")

    # ---- VERDICT helpers
    print("\n===== VERDICT TALLIES =====")
    for lbl, agg in [("OOS baseline-fee", a_base_oos), ("OOS fee2x", a_fee2_oos),
                     ("OOS slip20", a_slip_oos), ("OOS fee2x+slip", a_both),
                     ("BEAR 2021-22", a_bear)]:
        nsh, ndd, nret, tot = policy_better(agg)
        print(f"{lbl:<20} policy>=base: sharpe {nsh}/{tot}  dd-better {ndd}/{tot}  "
              f"ret>=50%base {nret}/{tot}")


if __name__ == "__main__":
    main()
