21fe289383
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>
205 lines
9.5 KiB
Python
205 lines
9.5 KiB
Python
#!/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())
|