"""Analisi di ACCORPAMENTO degli sleeve: le strategie possono essere raggruppate
meglio o diversamente rispetto all'attuale "per famiglia"?

Costruisce le 17 sleeve daily (FADE 6 + HONEST 3 + PAIRS 5 + TSM01 + SHAPE 2),
e risponde con evidenza a:
  1. CORRELAZIONE: matrice completa -> quali sleeve sono ridondanti (corr alta)?
  2. CLUSTER: clustering gerarchico sulla distanza 1-corr -> i gruppi NATURALI
     coincidono con le famiglie o no?
  3. RISCHIO: contributo di ogni sleeve alla volatilita' del portafoglio equal-weight
     -> chi domina il rischio (e va cappato)?
  4. PESI: confronto equal-weight vs inverse-vol vs risk-parity (per cluster) su
     ritorno/DD/Sharpe FULL e OOS.

Tutto netto fee, leva 3x, finestra comune 2021-2026, OOS = ultimo 30%.
Run:  uv run python scripts/analysis/sleeve_clustering.py
"""
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 scipy.cluster.hierarchy import linkage, fcluster
from scipy.spatial.distance import squareform

from scripts.analysis.report_families import build_everything
from scripts.analysis.combine_portfolio import port_returns, metrics, SPLIT


def daily_matrix(sleeves: dict) -> pd.DataFrame:
    return pd.DataFrame({k: v.pct_change().fillna(0.0) for k, v in sleeves.items()})


def risk_contributions(dr: pd.DataFrame, w: np.ndarray) -> np.ndarray:
    """Contributo % di ogni sleeve alla varianza del portafoglio (w'Σ)."""
    cov = dr.cov().values
    port_var = float(w @ cov @ w)
    mrc = cov @ w                      # marginal risk contribution
    rc = w * mrc                       # risk contribution (somma = port_var)
    return rc / port_var * 100 if port_var > 0 else rc


def inv_vol(dr: pd.DataFrame) -> np.ndarray:
    v = dr.std().values
    inv = np.where(v > 0, 1.0 / v, 0.0)
    return inv / inv.sum()


def cluster_risk_parity(dr: pd.DataFrame, labels: np.ndarray) -> dict:
    """Peso: equal fra i CLUSTER, poi inverse-vol DENTRO ogni cluster.
    Diversifica per gruppo-naturale invece che per sleeve -> non sovrappesa cluster affollati."""
    cols = list(dr.columns)
    w = np.zeros(len(cols))
    clusters = sorted(set(labels))
    per_cluster = 1.0 / len(clusters)
    for cl in clusters:
        idx = [i for i, lb in enumerate(labels) if lb == cl]
        sub = dr.iloc[:, idx]
        iv = inv_vol(sub)
        for j, i in enumerate(idx):
            w[i] = per_cluster * iv[j]
    return {cols[i]: w[i] for i in range(len(cols))}


def main():
    print("Costruzione 17 sleeve (~2-3 min)...\n")
    S, pairs, tsm, shape = build_everything()
    all_sl = {**S, **pairs, **tsm, **shape}
    dr = daily_matrix(all_sl)
    cols = list(dr.columns)
    n = len(cols)

    fam_of = {}
    for k in cols:
        if k.startswith("MR"):
            fam_of[k] = "FADE"
        elif k.startswith("PR_"):
            fam_of[k] = "PAIRS"
        elif k.startswith("SH_"):
            fam_of[k] = "SHAPE"
        elif k == "TSM01":
            fam_of[k] = "TSM"
        else:
            fam_of[k] = "HONEST"

    # ---------- 1. correlazione ----------
    print("=" * 100)
    print("  (1) MATRICE DI CORRELAZIONE daily fra sleeve")
    print("=" * 100)
    corr = dr.corr()
    short = [c.replace("_", "")[:8] for c in cols]
    print("        " + "".join(f"{s[:6]:>7s}" for s in short))
    for i, c in enumerate(cols):
        print(f"  {short[i]:<6s}" + "".join(f"{corr.iloc[i, j]:>7.2f}" for j in range(n)))

    # coppie piu' correlate (candidati all'accorpamento)
    print("\n  Coppie piu' correlate (>0.5 -> ridondanza potenziale):")
    pairs_corr = []
    for i in range(n):
        for j in range(i + 1, n):
            pairs_corr.append((corr.iloc[i, j], cols[i], cols[j]))
    pairs_corr.sort(reverse=True)
    for cc, a, b in pairs_corr[:12]:
        flag = "  <-- stessa famiglia" if fam_of[a] == fam_of[b] else "  <-- CROSS-famiglia"
        print(f"    {a:<11s} {b:<11s} {cc:+.2f}{flag if cc > 0.5 else ''}")

    # ---------- 2. cluster ----------
    print("\n" + "=" * 100)
    print("  (2) CLUSTERING GERARCHICO (distanza = 1-corr) — i gruppi naturali")
    print("=" * 100)
    dist = 1.0 - corr.values
    np.fill_diagonal(dist, 0.0)
    dist = (dist + dist.T) / 2
    Z = linkage(squareform(dist, checks=False), method="average")
    for thr in (0.85, 0.95):
        labels = fcluster(Z, t=thr, criterion="distance")
        groups: dict[int, list] = {}
        for c, lb in zip(cols, labels):
            groups.setdefault(lb, []).append(c)
        print(f"\n  taglio a distanza {thr} (corr>{1-thr:.2f}) -> {len(groups)} cluster:")
        for lb, members in sorted(groups.items()):
            fams = {fam_of[m] for m in members}
            print(f"    C{lb}: {', '.join(members)}   [{'/'.join(sorted(fams))}]")

    # ---------- 3. rischio ----------
    print("\n" + "=" * 100)
    print("  (3) CONTRIBUTO AL RISCHIO (equal-weight) — chi domina la volatilita'")
    print("=" * 100)
    w_eq = np.ones(n) / n
    rc = risk_contributions(dr, w_eq)
    order = np.argsort(rc)[::-1]
    print(f"  {'sleeve':<12s}{'peso%':>7s}{'risk%':>7s}  famiglia")
    for i in order:
        print(f"  {cols[i]:<12s}{w_eq[i]*100:>7.1f}{rc[i]:>7.1f}  {fam_of[cols[i]]}")
    # rischio per famiglia
    print("\n  contributo al rischio per FAMIGLIA (equal-weight sleeve):")
    fam_rc: dict[str, float] = {}
    for i, c in enumerate(cols):
        fam_rc[fam_of[c]] = fam_rc.get(fam_of[c], 0.0) + rc[i]
    for f, v in sorted(fam_rc.items(), key=lambda x: -x[1]):
        print(f"    {f:<8s} {v:>5.1f}%")

    # ---------- 4. schemi di peso ----------
    print("\n" + "=" * 100)
    print("  (4) SCHEMI DI PESO a confronto | FULL ret/DD/Sharpe | OOS ret/DD/Sharpe")
    print("=" * 100)
    labels95 = fcluster(Z, t=0.95, criterion="distance")

    schemes = {
        "equal-weight": {c: 1.0 / n for c in cols},
        "inverse-vol": {cols[i]: inv_vol(dr)[i] for i in range(n)},
        "cluster-risk-parity": cluster_risk_parity(dr, labels95),
    }
    print(f"  {'schema':<22s}{'Ret%':>9s}{'DD%':>7s}{'Shrp':>7s}   | {'oRet%':>9s}{'oDD%':>7s}{'oShrp':>7s}")
    print("  " + "-" * 78)
    for nm, w in schemes.items():
        dserved = port_returns(all_sl, w)
        f, o = metrics(dserved), metrics(dserved, lo=SPLIT)
        print(f"  {nm:<22s}{f['ret']:>+9.0f}{f['dd']:>7.1f}{f['sharpe']:>7.2f}   | "
              f"{o['ret']:>+9.0f}{o['dd']:>7.1f}{o['sharpe']:>7.2f}")

    print("\n  Lettura: se i cluster naturali != famiglie, conviene pesare per CLUSTER (rischio)")
    print("  invece che per famiglia. Se inverse-vol/risk-parity battono equal-weight in OOS,")
    print("  l'accorpamento attuale (equal-weight per sleeve) e' migliorabile.")


if __name__ == "__main__":
    main()
