Files
PythagorasGoal/scripts/research/r0828_senza_gtaa.py
T
Adriano Dal Pastro 21fe289383 research(gtaa): cosa cambia togliendo GTAA01 — +0.11 di Sharpe a iso-rischio, sul filo
Misura chiesta prima di decidere del gate (A): se togliere lo sleeve non
cambiasse niente, la domanda sul gate sarebbe accademica. Portafoglio di
RICERCA a 5 sleeve (33/15/12/20/20), NON il book live (dove GTAA01 non c'e').

Due lenti, ed e' la seconda che decide (M6: un diversificatore a basso CAGR si
giudica a iso-rischio, mai a iso-nozionale; M5: "meno drawdown" si testa a
iso-rischio o si sta comprando de-levering):
  · iso-nozionale: CON Sh 2.28 CAGR +19.2% DD 6.1% vol 7.8%
                   SENZA Sh 2.16 CAGR +23.8% DD 7.8% vol 10.1%
    -> togliendolo si guadagna DRIFT e si compra VOLATILITA'.
  · iso-rischio (×0.77): SENZA Sh 2.16 CAGR +18.0% DD 6.0%
    -> Δ Sharpe +0.121, Δ maxDD +0.02pp.
Hold-out 2025+: Δ +0.247. Per anno: GTAA01 aiuta in 6/8. Correlazione col resto
del portafoglio +0.087 — e' davvero altro. Standalone Sh 1.06, CAGR +5.8%.

⚠️ E LA BANDA DI FASE, che e' la ragione per cui questo numero va citato con la
sua incertezza: il Δ sulle 5 fasi va da +0.095 a +0.124 e supera la soglia
dichiarata (0.12) in 2 fasi su 5. Il SEGNO e' stabile — GTAA01 aiuta in ogni
fase — ma il verdetto BINARIO dipende dalla notte in cui lo si legge. Sanity:
la fase 0 riproduce lo sleeve di produzione bit-exact (max|Δ| = 0.00e+00).

Lettura: il contributo e' reale e sempre positivo, e vale ~+0.11 di Sharpe, cioe'
ESATTAMENTE quanto lo spread fra le tre baseline che il progetto gia' si porta
dietro (0.12, §2). Non e' "GTAA01 e' inutile": e' "sta al limite di cio' che
questi dati risolvono".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-28 12:34:44 +00:00

205 lines
9.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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())