#!/usr/bin/env python """r0828_senza_gtaa.py — cosa cambia nel portafoglio di RICERCA se si toglie GTAA01. PERCHE'. Il 28/08 si e' misurato che il gate (A) della banda GTAA01 non ha risoluzione, e che lo sleeve gira su una finestra che rotola da sola (r0828_gtaa_band_phase). Prima di decidere cosa fare del gate conviene sapere quanto pesa lo sleeve: se togliendolo non cambia niente, la domanda sul gate e' accademica. ⚠️ QUESTO NON E' IL LIBRO LIVE. Il book che gira e' TP01+SKH01 75/25 su Deribit: GTAA01 non c'e'. Qui si guarda il portafoglio a 5 sleeve di `sleeves.active_sleeves` (33/15/12/20/20), che e' la serie su cui stanno quasi tutti i numeri di portafoglio del progetto. LE DUE LENTI, ed e' la seconda quella che decide (M6: un diversificatore a BASSO CAGR si giudica a iso-rischio, mai a iso-nozionale; M5: ogni claim "meno drawdown" si testa a iso-rischio prima di crederci, o si sta comprando de-levering e chiamandolo diversificazione): · ISO-NOZIONALE — si toglie GTAA01 e si ridistribuisce il suo 20% agli altri quattro in proporzione. E' cio' che succede materialmente. · ISO-RISCHIO — la stessa serie, riscalata alla volatilita' del portafoglio a 5. Risponde alla domanda giusta: a parita' di rischio preso, quanto rende in piu' o in meno? Si stampano FULL e HOLD-OUT (la data e' quella del progetto, `portfolio.HOLDOUT`), il per-anno (M9: un contributo si scompone per anno prima di crederci) e la correlazione di GTAA01 col resto. Verdetto calcolato a runtime coi criteri dichiarati qui sotto. CRITERIO, dichiarato prima: GTAA01 "porta qualcosa" se, A ISO-RISCHIO, toglierlo peggiora lo Sharpe FULL di almeno SOGLIA_SHARPE **e** l'hold-out non lo smentisce. Sotto quella soglia il contributo e' dentro lo spread fra le tre baseline che il progetto gia' si porta dietro (0.12 di Sharpe, §2 del CLAUDE.md), quindi non e' distinguibile dalla scelta della lente. uv run python scripts/research/r0828_senza_gtaa.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) from src.portfolio.portfolio import HOLDOUT, combine_outer, metrics, to_daily, yearly # noqa: E402 from src.portfolio.sleeves import active_sleeves # noqa: E402 SOGLIA_SHARPE = 0.12 # spread fra le tre baseline "libro 75/25" (§2): sotto, non si vede FUORI = "GTAA01_eq_trend" def serie_sleeve() -> tuple[dict, dict]: cols, w = {}, {} for s in active_sleeves(): cols[s.name] = s.daily().dropna() w[s.name] = float(s.weight) return cols, w def senza(w: dict, fuori: str) -> dict: """I pesi degli altri, rinormalizzati: il 20% di GTAA01 va agli altri in proporzione.""" resto = {k: v for k, v in w.items() if k != fuori} tot = sum(resto.values()) return {k: v / tot for k, v in resto.items()} def riga(nome: str, m: dict) -> str: return (f" {nome:<22} Sh {m['sharpe']:>5.2f} CAGR {m['cagr']*100:>+6.1f}% " f"maxDD {m['maxdd']*100:>5.1f}% vol {m.get('vol', float('nan'))*100:>5.1f}% n {m['n']}") def con_vol(s: pd.Series) -> dict: m = metrics(s) m["vol"] = float(np.asarray(s.dropna().values, float).std() * np.sqrt(365.0)) return m def main() -> int: print(__doc__.split("\n\n")[0]) cols, w5 = serie_sleeve() if FUORI not in cols: print(f"!! {FUORI} non e' fra gli sleeve attivi: niente da togliere") return 2 w4 = senza(w5, FUORI) cols4 = {k: v for k, v in cols.items() if k != FUORI} print(f"\n pesi CON : " + " ".join(f"{k.split('_')[0]} {v:.0%}" for k, v in w5.items())) print(f" pesi SENZA: " + " ".join(f"{k.split('_')[0]} {v:.0%}" for k, v in w4.items())) con = combine_outer(cols, w5) sen = combine_outer(cols4, w4) # allineate sullo STESSO calendario: senza questo si confronterebbero due finestre diverse J = pd.concat({"con": con, "senza": sen}, axis=1, join="inner").dropna() con, sen = J["con"], J["senza"] m_con, m_sen = con_vol(con), con_vol(sen) k = m_con["vol"] / m_sen["vol"] if m_sen["vol"] > 0 else 1.0 sen_iso = sen * k m_iso = con_vol(sen_iso) print("\n" + "=" * 96) print(f" FULL ({con.index[0].date()} -> {con.index[-1].date()})") print("=" * 96) print(riga("CON GTAA01", m_con)) print(riga("SENZA (iso-nozionale)", m_sen)) print(riga(f"SENZA (iso-rischio ×{k:.2f})", m_iso)) d_full = m_con["sharpe"] - m_iso["sharpe"] print(f"\n Δ Sharpe a iso-rischio (con − senza): {d_full:+.3f} " f"[soglia dichiarata {SOGLIA_SHARPE}]") print(f" Δ maxDD a iso-rischio: {(m_con['maxdd']-m_iso['maxdd'])*100:+.2f} pp") print("\n" + "=" * 96) print(f" HOLD-OUT ({HOLDOUT.date()}+)") print("=" * 96) hc, hs = con[con.index >= HOLDOUT], sen[sen.index >= HOLDOUT] mhc, mhs = con_vol(hc), con_vol(hs) kh = mhc["vol"] / mhs["vol"] if mhs["vol"] > 0 else 1.0 mhi = con_vol(hs * kh) print(riga("CON GTAA01", mhc)) print(riga(f"SENZA (iso-rischio ×{kh:.2f})", mhi)) d_hold = mhc["sharpe"] - mhi["sharpe"] print(f"\n Δ Sharpe a iso-rischio: {d_hold:+.3f}") print("\n" + "=" * 96) print(" PER ANNO — un contributo positivo si scompone prima di crederci (M9)") print("=" * 96) yc, yi = yearly(con), yearly(sen * k) print(f" {'anno':<6} {'CON ret':>9} {'SENZA ret':>10} {'Δ ret':>8} {'CON DD':>7} {'SENZA DD':>9}") pos = 0 for a in sorted(set(yc) & set(yi)): d = yc[a]["ret"] - yi[a]["ret"] pos += d > 0 print(f" {a:<6} {yc[a]['ret']*100:>+8.1f}% {yi[a]['ret']*100:>+9.1f}% {d*100:>+7.1f}pp " f"{yc[a]['dd']*100:>6.1f}% {yi[a]['dd']*100:>8.1f}%") n_anni = len(set(yc) & set(yi)) print(f"\n anni in cui GTAA01 aiuta: {pos}/{n_anni}") print("\n" + "=" * 96) print(" CORRELAZIONE — quanto e' davvero 'altro' rispetto al resto") print("=" * 96) g = cols[FUORI] altro = combine_outer(cols4, w4) Jg = pd.concat({"g": g, "altro": altro}, axis=1, join="inner").dropna() print(f" GTAA01 vs resto del portafoglio: corr {Jg['g'].corr(Jg['altro']):+.3f} " f"({len(Jg)} giorni in comune)") print(f" GTAA01 standalone: " + riga("", con_vol(g)).strip()) # ---------------------------------------------------------------- la banda di FASE sul Δ print("\n" + "=" * 96) print(" LA FASCIA DI FASE SUL Δ — il Δ e' una misura, o una proprieta' della notte?") print("=" * 96) print(" GTAA01 ribilancia su `i % 5` a partire dalla PRIMA barra del file, e la prima barra") print(" di SPY scivola ogni notte (finestra IB rotolante a 30 anni: r0828_gtaa_band_phase).") print(" Quindi il Δ qui sopra va letto accanto alla sua escursione sulle 5 fasi.") sys.path.insert(0, str(ROOT / "scripts" / "research")) try: from r0726_loo_deluck import gtaa01_at # replica ESATTA dello sleeve, per fase except Exception as e: # noqa: BLE001 print(f" non misurabile qui: {type(e).__name__}: {e}") gtaa01_at = None if gtaa01_at is not None: base = gtaa01_at(0) j0 = pd.concat({"a": base, "b": cols[FUORI]}, axis=1, join="inner").dropna() d0 = float((j0["a"] - j0["b"]).abs().max()) print(f"\n [SANITY] fase 0 == sleeve di produzione: max|Δ| = {d0:.2e} " f"-> {'OK' if d0 < 1e-12 else 'DIVERGE (i numeri sotto non valgono)'}") deltas = [] for ph in range(5): c = dict(cols); c[FUORI] = gtaa01_at(ph) cc = combine_outer(c, w5) Jp = pd.concat({"con": cc, "senza": sen}, axis=1, join="inner").dropna() mc = con_vol(Jp["con"]) ms = con_vol(Jp["senza"]) kk = mc["vol"] / ms["vol"] if ms["vol"] > 0 else 1.0 d = mc["sharpe"] - con_vol(Jp["senza"] * kk)["sharpe"] deltas.append(d) print(f" fase {ph}: Sharpe CON {mc['sharpe']:.3f} Δ a iso-rischio {d:+.3f}" + (" <- la fase che gira stanotte" if ph == 0 else "")) lo, hi = min(deltas), max(deltas) print(f"\n Δ su 5 fasi: da {lo:+.3f} a {hi:+.3f} (ampiezza {hi-lo:.3f}, " f"mediana {float(np.median(deltas)):+.3f})") sopra = sum(d >= SOGLIA_SHARPE for d in deltas) print(f" fasi in cui il Δ supera la soglia {SOGLIA_SHARPE}: {sopra}/5") if 0 < sopra < 5: print(" ⚠️ IL VERDETTO DIPENDE DALLA FASE: non e' una proprieta' di GTAA01,") print(" e' una proprieta' della notte in cui lo si legge.") print("\n" + "=" * 96) print(" VERDETTO — criterio dichiarato in testa, calcolato adesso") print("=" * 96) porta = d_full >= SOGLIA_SHARPE and d_hold > -SOGLIA_SHARPE print(f" Δ Sharpe FULL a iso-rischio {d_full:+.3f} >= {SOGLIA_SHARPE}? " f"{'SI' if d_full >= SOGLIA_SHARPE else 'NO'}") print(f" l'hold-out non lo smentisce (Δ {d_hold:+.3f} > {-SOGLIA_SHARPE})? " f"{'SI' if d_hold > -SOGLIA_SHARPE else 'NO'}") if porta: print("\n -> GTAA01 PORTA: toglierlo costa piu' dello spread fra le baseline.") return 0 print("\n -> il contributo di GTAA01 NON si distingue dalla scelta della lente.") print(" Non e' 'GTAA01 e' inutile': e' 'con questi dati non si vede'. E cio' che") print(" non si vede non giustifica di tenere in piedi un gate che non decide.") return 1 if __name__ == "__main__": raise SystemExit(main())