"""MAT01-b — il vantaggio di GTAA6 su MAT18 e' EDGE o SCOMMESSA DI REGIME sull'equity US? CONTESTO (da r0725_mat01_multiasset_trend.py): * ALL18 (universo multi-classe, ZERO liberta' di selezione) fa OOS Sharpe 0.80 / DD -6.9%. * GTAA6 (incumbent) fa OOS Sharpe 0.89 / DD -8.2% -> l'espansione NON migliora lo Sharpe. * MA la curva di ampiezza e' monotona (mediana OOS 0.44 a k=1 -> 0.80 a k=18, DD -16% -> -6.9%): l'ampiezza FUNZIONA come meccanismo. * E GTAA6 sta al 95° percentile dei 6-subset casuali PUR CONTENENDO TLT, il peggiore dei 18 (Sh OOS 0.03) -> non e' un best-of cherry-picked: e' la sua COMPOSIZIONE a essere forte. DOMANDA: quella composizione (3 gambe su 6 = equity US: SPY+QQQ+IWM, piu' GLD e HYG) e' un edge stabile, o e' beta azionario US travestito da trend, premiato da un OOS 2015-2026 che e' stato un toro azionario US quasi ininterrotto? La distinzione decide se lo 0.89 e' una stima onesta del forward o un massimo di regime — esattamente la domanda che il progetto si e' gia' posto sulle ancore temporali (anchor-luck) e sui pesi (weights_tilt_null). TEST 1) FINESTRE DISGIUNTE (nessun overlap, ~7 anni ciascuna): se GTAA6 vince in tutte -> edge di composizione; se vince solo dove l'equity US corre -> regime. 2) BETA AZIONARIO: regressione di ciascun paniere su SPY buy&hold (beta, alpha annualizzata, corr). Un "trend difensivo multi-asset" con beta alto e' equity long travestito. 3) COMPORTAMENTO NEI BEAR AZIONARI (dot-com, GFC, COVID, 2022): il valore dichiarato dello sleeve e' il taglio del drawdown -> e' li' che si giudica, non nel toro. 4) MAT18-CLS: aggregazione per CLASSE invece che per asset (7 classi equipesate). NON e' selezione: e' la costruzione multi-asset canonica (l'EW per asset sovrappesa meccanicamente commodity 4/18 ed equity US 4/18 contro real_estate 1/18). 1 variante in piu' -> 3 trial totali. 5) PARI VOLATILITA': MAT18 ha vol piu' bassa; il confronto CAGR va fatto a vol uguagliata (lo Sharpe e' scale-invariante, il CAGR no) — quanto costa/rende davvero il cambio. 6) DIVERSIFICAZIONE DEL BOOK: correlazione di ciascun paniere alla parte CRYPTO del book. Il compito di questo sleeve non e' fare Sharpe da solo, e' essere scorrelato dal crypto. uv run python scripts/research/r0725_mat01b_regime.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)) sys.path.insert(0, str(ROOT / "scripts" / "research")) from r0725_mat01_multiasset_trend import (ALL18, CLASSES, GTAA6, _cagr, _dd, _sh, basket, gated, _close) ANN = np.sqrt(252.0) # finestre disgiunte, ~7 anni ciascuna, definite sul calendario (non sui risultati) WINDOWS = [("1998-2004", "1998-01-01", "2004-12-31"), ("2005-2011", "2005-01-01", "2011-12-31"), ("2012-2018", "2012-01-01", "2018-12-31"), ("2019-2026", "2019-01-01", "2026-12-31")] # bear azionari US, date a priori (nessuna ottimizzazione) BEARS = [("dot-com 2000-09/2002-10", "2000-09-01", "2002-10-31"), ("GFC 2007-10/2009-03", "2007-10-01", "2009-03-31"), ("COVID 2020-02/2020-03", "2020-02-15", "2020-03-31"), ("2022 2022-01/2022-10", "2022-01-01", "2022-10-31")] def _win(r: pd.Series, lo: str, hi: str) -> pd.Series: return r[(r.index >= pd.Timestamp(lo, tz="UTC")) & (r.index <= pd.Timestamp(hi, tz="UTC"))] def class_basket(end) -> pd.Series: """MAT18-CLS: media EW delle 7 CLASSI (ogni classe = media EW dei suoi asset disponibili).""" per_class = {} for cls, syms in CLASSES.items(): cols = {s: gated(s) for s in syms} per_class[cls] = pd.concat(cols, axis=1, sort=True).sort_index().mean(axis=1, skipna=True) r = pd.concat(per_class, axis=1, sort=True).sort_index().mean(axis=1, skipna=True).dropna() return r[r.index <= end] def spy_bh(end) -> pd.Series: c = _close("SPY") return c.pct_change().dropna()[lambda s: s.index <= end] def main() -> None: print("=" * 100) print(" MAT01-b — GTAA6 vs MAT18: edge di composizione o scommessa di regime?") print("=" * 100) end = min(_close(s).index[-1] for s in ALL18) r6, r18 = basket(GTAA6, end), basket(ALL18, end) rcls = class_basket(end) idx = r6.index.intersection(r18.index).intersection(rcls.index) r6, r18, rcls = r6[idx], r18[idx], rcls[idx] books = [("GTAA6", r6), ("MAT18", r18), ("MAT18-CLS", rcls)] # ---------------- 1) finestre disgiunte print("\n" + "-" * 100) print(" (1) FINESTRE DISGIUNTE — Sharpe (e maxDD) per periodo; nessun overlap") print("-" * 100) print(f" {'periodo':<12}" + "".join(f"{n:>22}" for n, _ in books) + f"{'SPY B&H':>12}") spy = spy_bh(end) wins18 = wins_cls = 0 for nm, lo, hi in WINDOWS: cells = [] for _, r in books: w = _win(r, lo, hi) cells.append(f"{_sh(w):>10.2f} / {_dd(w)*100:>6.1f}%" if len(w) > 60 else f"{'—':>19}") s6, s18, scl = (_sh(_win(r, lo, hi)) for _, r in books) wins18 += int(s18 > s6) wins_cls += int(scl > s6) print(f" {nm:<12}" + "".join(f"{c:>22}" for c in cells) + f"{_sh(_win(spy, lo, hi)):>12.2f}") print(f"\n MAT18 batte GTAA6 in {wins18}/{len(WINDOWS)} finestre disgiunte; " f"MAT18-CLS in {wins_cls}/{len(WINDOWS)}") # ---------------- 2) beta azionario print("\n" + "-" * 100) print(" (2) BETA AZIONARIO US — regressione del paniere su SPY buy&hold (giorni comuni)") print("-" * 100) print(f" {'paniere':<12}{'corr SPY':>10}{'beta':>8}{'alpha ann':>12}{'volAnn':>9}{'Sh FULL':>9}") for nm, r in books: j = pd.concat({"r": r, "m": spy}, axis=1, sort=True).dropna() beta = float(np.cov(j["r"], j["m"])[0, 1] / np.var(j["m"])) alpha = float((j["r"] - beta * j["m"]).mean() * 252) print(f" {nm:<12}{j['r'].corr(j['m']):>10.2f}{beta:>8.2f}{alpha*100:>11.2f}%" f"{r.std()*ANN*100:>8.2f}%{_sh(r):>9.2f}") print(" lettura: beta alto + alpha bassa = equity long travestito da trend.") # ---------------- 3) bear azionari print("\n" + "-" * 100) print(" (3) BEAR AZIONARI — dove lo sleeve deve guadagnarsi lo stipendio (ritorno cumulato)") print("-" * 100) print(f" {'bear':<28}" + "".join(f"{n:>12}" for n, _ in books) + f"{'SPY B&H':>12}") for nm, lo, hi in BEARS: cells = [] for _, r in books: w = _win(r, lo, hi) cells.append(f"{(np.prod(1+w.values)-1)*100:>11.1f}%" if len(w) > 5 else f"{'—':>12}") ws = _win(spy, lo, hi) print(f" {nm:<28}" + "".join(cells) + f"{(np.prod(1+ws.values)-1)*100:>11.1f}%") # ---------------- 4/5) pari volatilita' print("\n" + "-" * 100) print(" (4/5) CONFRONTO A PARI VOLATILITA' (scalato alla vol di GTAA6; Sharpe invariato)") print("-" * 100) v6 = float(r6.std()) print(f" {'paniere':<12}{'scala':>8}{'Sh FULL':>10}{'Sh OOS':>9}{'CAGR@vol6':>12}{'maxDD@vol6':>12}") for nm, r in books: k = v6 / float(r.std()) rs = r * k o = rs[rs.index >= pd.Timestamp("2015-01-01", tz="UTC")] print(f" {nm:<12}{k:>8.2f}{_sh(rs):>10.2f}{_sh(o):>9.2f}{_cagr(rs)*100:>11.2f}%{_dd(rs)*100:>11.1f}%") # ---------------- 6) diversificazione del book print("\n" + "-" * 100) print(" (6) IL VERO COMPITO: scorrelazione dalla parte CRYPTO del book") print("-" * 100) try: from src.portfolio.portfolio import combine_outer from src.portfolio.sleeves import active_sleeves cry = [s for s in active_sleeves() if "GTAA" not in s.name.upper()] wsum = sum(s.weight for s in cry) crypto = combine_outer({s.name: s.daily() for s in cry}, {s.name: s.weight / wsum for s in cry}) print(f" crypto del book = {[s.name for s in cry]} (pesi rinormalizzati)") print(f" {'paniere':<12}{'corr->crypto':>14}{'Sh 50/50':>11}{'DD 50/50':>11}") for nm, r in books: j = pd.concat({"e": r, "c": crypto}, axis=1, sort=True).dropna() if len(j) < 100: print(f" {nm:<12}{'(overlap corto)':>14}") continue bl = 0.5 * j["e"] + 0.5 * j["c"] print(f" {nm:<12}{j['e'].corr(j['c']):>14.3f}{_sh(bl):>11.2f}{_dd(bl)*100:>10.1f}%") except Exception as e: print(f" [skip: {e.__class__.__name__}: {e}]") print("\n" + "=" * 100) if __name__ == "__main__": main()