"""TEST DECISIVO: impatto di EXIT-16 (close_confirm_sl, buffer=0.5 ATR) sul PORT06,
nel PATH CANONICO del backtest (NON exit_lab).

EXIT-16: lo SL intrabar e' DISATTIVATO; si esce al close del bar j solo se il close
ha sfondato il livello di buffer*ATR14:
  long  (d=1):  esci a close[j] se close[j] < sl0 - 0.5*atr14[j]
  short (d=-1): esci a close[j] se close[j] > sl0 + 0.5*atr14[j]
TP intrabar al livello e max_bars al close restano INVARIATI.

Metodo (come fu fatto per il loss-guard Hurst):
  1. build_everything() canonico -> equity giornaliere di TUTTI gli sleeve (cache intatta).
  2. ricostruisco le 6 equity fade in variante EXIT-16 replicando ESATTAMENTE
     fade_daily_equity/build_trades (stessi segnali fn(df,**params), trend_max=3.0,
     fee 0.10%RT*lev3, pos 0.15, compounding, non-overlap), cambiando SOLO il ramo SL.
  3. PARITA': con la SL-rule originale il replay deve riprodurre le equity canoniche.
  4. PORT06 base vs EXIT-16 con la STESSA matematica dei pesi (Portfolio.backtest):
     weighting cap, caps PAIRS 0.33, ribilancio 1D, metriche FULL e OOS.

NB: la leva 2x del portfolios.yml NON entra nel backtest (Portfolio.backtest la ignora;
e' un knob live). Le equity fade gia' includono lev=3 dentro build_trades.
"""
from __future__ import annotations

import sys
from pathlib import Path

import numpy as np
import pandas as pd

PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))

from src.data.downloader import load_data
from scripts.analysis.strategy_research import atr
from scripts.analysis.risk_management import strats_for, FEE_RT, LEV, POS, INIT
from scripts.analysis.combine_portfolio import (
    fade_daily_equity, _norm, IDX, port_returns, metrics, SPLIT, OOS_DATE,
)
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio import weighting as W

BUFFER = 0.5  # EXIT-16 promossa: close-confirm con buffer 0.5 ATR


# ---------------------------------------------------------------- engine replay
def build_trades_variant(ents, df, mode, buffer=BUFFER,
                         lev=LEV, fee_rt=FEE_RT, trend_max=3.0, ema_long=200):
    """Replica ESATTA di risk_management.build_trades, cambiando SOLO il ramo SL.

    mode="orig"   : SL intrabar al livello (SL prima del TP) == canonico.
    mode="exit16" : SL intrabar DISATTIVATO; close-confirm sul close[j]:
                    long  esci a close[j] se close[j] < sl0 - buffer*atr14[j]
                    short esci a close[j] se close[j] > sl0 + buffer*atr14[j]
                    TP intrabar al livello e max_bars al close INVARIATI.
    """
    h, l, c = df["high"].values, df["low"].values, df["close"].values
    n = len(c)
    a = atr(df, 14)
    el = pd.Series(c).ewm(span=ema_long, adjust=False).mean().values
    fee = fee_rt * lev
    out = []
    last = -1
    for e in ents:
        i, d = e["i"], e["d"]
        if i <= last or i + 1 >= n:
            continue
        if trend_max is not None and a[i] and abs(c[i] - el[i]) / a[i] > trend_max:
            continue
        entry = c[i]
        tp, sl0, mb = e["tp"], e["sl"], e["max_bars"]
        exit_p = c[min(i + mb, n - 1)]
        j = min(i + mb, n - 1)
        for k in range(1, mb + 1):
            j = i + k
            if j >= n:
                exit_p = c[n - 1]
                break
            if mode == "orig":
                hs = (d == 1 and l[j] <= sl0) or (d == -1 and h[j] >= sl0)
                ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
                if hs:
                    exit_p = sl0
                    break
                if ht:
                    exit_p = tp
                    break
                if k == mb:
                    exit_p = c[j]
            else:  # exit16: no SL intrabar; TP intrabar; poi close-confirm SL al close[j]
                ht = (d == 1 and h[j] >= tp) or (d == -1 and l[j] <= tp)
                if ht:
                    exit_p = tp
                    break
                aj = a[j] if np.isfinite(a[j]) else 0.0
                confirm = (d == 1 and c[j] < sl0 - buffer * aj) or \
                          (d == -1 and c[j] > sl0 + buffer * aj)
                if confirm:
                    exit_p = c[j]
                    break
                if k == mb:
                    exit_p = c[j]
        ret = (exit_p - entry) / entry * d * lev - fee
        out.append((i, j, ret))
        last = j
    return out


def fade_equity_variant(asset, fn, params, mode):
    """Stesso flusso di combine_portfolio.fade_daily_equity ma con build_trades_variant."""
    df = load_data(asset, "1h")
    ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
    trades = build_trades_variant(fn(df, **params), df, mode=mode, trend_max=3.0)
    n = len(df)
    eq = np.full(n, INIT, dtype=float)
    cap = INIT
    for i, j, ret in sorted(trades, key=lambda t: t[1]):
        cap = max(cap + cap * POS * ret, 10.0)
        eq[j:] = cap
    s = pd.Series(eq, index=ts).resample("1D").last().reindex(IDX).ffill().bfill()
    return _norm(s)


# ---------------------------------------------------------------- pesi PORT06
def port_metrics(members: dict[str, pd.Series], weights: dict[str, float]):
    dr = port_returns(members, weights)
    return metrics(dr), metrics(dr, lo=SPLIT)


def main():
    p = PORTFOLIOS["PORT06"]
    fade_ids = [s.sid for s in p.sleeves if s.sid.startswith("MR")]
    print("=" * 96)
    print("  TEST DECISIVO EXIT-16 (close_confirm_sl buffer=0.5 ATR) su PORT06 — path canonico")
    print(f"  fade sleeve: {fade_ids}")
    print("=" * 96)

    # --- 1. equity canoniche di TUTTI gli sleeve (cache intatta) ---
    print("\n[1] build_everything() canonico (pesante, ~2-3 min)...")
    from src.portfolio.sleeves import all_sleeve_equities
    eq_base = dict(all_sleeve_equities())  # {sid: equity giornaliera}
    print(f"    sleeve totali: {len(eq_base)}")

    # --- 2. PARITA': replay 'orig' deve riprodurre le equity canoniche ---
    print("\n[2] PARITA' replay (mode=orig) vs canonico (fade_daily_equity):")
    print(f"    {'sleeve':<10s}{'corr':>10s}{'ret_canon%':>14s}{'ret_replay%':>14s}{'diff%':>9s}")
    parity_ok = True
    eq_orig, eq_e16 = {}, {}
    for asset in ("BTC", "ETH"):
        for nm, (fn, params) in strats_for(asset).items():
            sid = f"{nm}_{asset}"
            if sid not in fade_ids:
                continue
            eq_orig[sid] = fade_equity_variant(asset, fn, params, mode="orig")
            eq_e16[sid] = fade_equity_variant(asset, fn, params, mode="exit16")
            base = eq_base[sid]
            rep = eq_orig[sid]
            corr = base.pct_change().fillna(0).corr(rep.pct_change().fillna(0))
            rb = (base.iloc[-1] / base.iloc[0] - 1) * 100
            rr = (rep.iloc[-1] / rep.iloc[0] - 1) * 100
            diff = rr - rb
            flag = "" if (corr > 0.999 and abs(diff) <= max(1.0, abs(rb) * 0.01)) else "  <-- MISMATCH"
            if flag:
                parity_ok = False
            print(f"    {sid:<10s}{corr:>10.5f}{rb:>14.1f}{rr:>14.1f}{diff:>+9.2f}{flag}")
    print(f"\n    PARITA' {'OK' if parity_ok else 'FALLITA'} "
          f"(corr>0.999 e ret finale entro 1%).")
    if not parity_ok:
        print("\n    >>> Parita' non raggiunta: NON forzo. Diagnostico sopra. STOP.")
        return

    # --- 3. PORT06 base vs EXIT-16: stessi pesi cap, stessa matematica ---
    members_base = dict(eq_base)
    members_e16 = dict(eq_base)
    for sid in fade_ids:
        members_e16[sid] = eq_e16[sid]  # sostituisco SOLO le 6 colonne fade

    ids = p.sleeve_ids
    # pesi cap canonici (gli stessi che usa Portfolio.backtest)
    dr_base = pd.DataFrame({i: members_base[i].pct_change().fillna(0.0) for i in ids})
    w_base = W.weight_vector(p.weighting, ids, dr_base, weights=p.weights,
                             caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)
    dr_e16 = pd.DataFrame({i: members_e16[i].pct_change().fillna(0.0) for i in ids})
    w_e16 = W.weight_vector(p.weighting, ids, dr_e16, weights=p.weights,
                            caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback)

    f_b, o_b = port_metrics({i: members_base[i] for i in ids}, w_base)
    f_e, o_e = port_metrics({i: members_e16[i] for i in ids}, w_e16)

    print("\n" + "=" * 96)
    print(f"  [3] PORT06 — pesi={p.weighting} caps={p.caps} | OOS da {OOS_DATE} | leva3x interna fade, pos0.15")
    print("=" * 96)
    print(f"  {'variante':<14s}{'FULL Sh':>9s}{'FULL DD%':>10s}{'FULL CAGR':>11s}"
          f"   | {'OOS Sh':>8s}{'OOS DD%':>9s}{'OOS CAGR':>10s}")
    print("  " + "-" * 90)
    print(f"  {'BASE':<14s}{f_b['sharpe']:>9.2f}{f_b['dd']:>10.2f}{f_b['cagr']:>10.0f}%"
          f"   | {o_b['sharpe']:>8.2f}{o_b['dd']:>9.2f}{o_b['cagr']:>9.0f}%")
    print(f"  {'EXIT-16':<14s}{f_e['sharpe']:>9.2f}{f_e['dd']:>10.2f}{f_e['cagr']:>10.0f}%"
          f"   | {o_e['sharpe']:>8.2f}{o_e['dd']:>9.2f}{o_e['cagr']:>9.0f}%")
    print("  " + "-" * 90)
    print(f"  {'DELTA':<14s}{f_e['sharpe']-f_b['sharpe']:>+9.2f}{f_e['dd']-f_b['dd']:>+10.2f}"
          f"{f_e['cagr']-f_b['cagr']:>+10.0f}%   | {o_e['sharpe']-o_b['sharpe']:>+8.2f}"
          f"{o_e['dd']-o_b['dd']:>+9.2f}{o_e['cagr']-o_b['cagr']:>+9.0f}%")

    # --- per-sleeve fade: differenze principali ---
    print("\n  Per-sleeve fade (equity FULL ret%, EXIT-16 vs orig-replay):")
    print(f"    {'sleeve':<10s}{'orig ret%':>12s}{'exit16 ret%':>14s}{'delta%':>10s}"
          f"{'orig DD%':>10s}{'e16 DD%':>10s}")
    for sid in fade_ids:
        ro = eq_orig[sid]; re = eq_e16[sid]
        def _dd(s):
            pk = s.cummax(); return float(((pk - s) / pk).max() * 100)
        rro = (ro.iloc[-1] / ro.iloc[0] - 1) * 100
        rre = (re.iloc[-1] / re.iloc[0] - 1) * 100
        print(f"    {sid:<10s}{rro:>12.1f}{rre:>14.1f}{rre-rro:>+10.1f}"
              f"{_dd(ro):>10.1f}{_dd(re):>10.1f}")

    # --- GATE ---
    print("\n" + "=" * 96)
    print("  GATE (stesso del loss-guard): PROMOSSO se OOS Sharpe migliora/pari E DD non peggiora")
    print("              materialmente, E in FULL non degrada.")
    print("=" * 96)
    oos_sh_ok = o_e['sharpe'] >= o_b['sharpe'] - 0.02
    oos_dd_ok = o_e['dd'] <= o_b['dd'] + 0.20            # no peggioramento materiale DD
    full_ok = f_e['sharpe'] >= f_b['sharpe'] - 0.02 and f_e['dd'] <= f_b['dd'] + 0.20
    promoted = oos_sh_ok and oos_dd_ok and full_ok
    print(f"  OOS Sharpe {o_b['sharpe']:.2f} -> {o_e['sharpe']:.2f} "
          f"({'OK' if oos_sh_ok else 'KO'})")
    print(f"  OOS DD%    {o_b['dd']:.2f} -> {o_e['dd']:.2f} "
          f"({'OK' if oos_dd_ok else 'KO'})")
    print(f"  FULL Sharpe {f_b['sharpe']:.2f} -> {f_e['sharpe']:.2f} | "
          f"FULL DD {f_b['dd']:.2f} -> {f_e['dd']:.2f} ({'OK' if full_ok else 'KO'})")
    print("\n  VERDETTO: " + (">>> PROMOSSO <<<" if promoted else ">>> BOCCIATO <<<"))


if __name__ == "__main__":
    main()
