Files
PythagorasGoal/scripts/research/r0725_mat01b_regime.py
T
Adriano Dal Pastro 9af6693e4f research: MAT01 e generalizzazione STATARB — 2 refutazioni pulite + addendum al gate 27/09
MAT01 (r0725_mat01_multiasset_trend.py, r0725_mat01b_regime.py) — SCARTATO.
Allargare il trend difensivo da 6 a 18 ETF multi-asset-class a meccanismo GTAA
CONGELATO: MAT18 perde 3/4 finestre disgiunte e, a PARI VOLATILITA', il suo
minor drawdown si INVERTE (-20.6% vs -15.4%) -> era solo de-levering. Terza
occorrenza del null de-levering dopo VRP-DD e TP01xDVOL. Corr->crypto invariata
(0.110 vs 0.116): zero guadagno di diversificazione.
Risultato positivo conservato: la curva d'ampiezza e' monotona (mediana OOS 0.44
a k=1 -> 0.80 a k=18, DD -16.0% -> -6.9%, satura a k~10-12), ma GTAA6 sta gia' al
95° pctl dei 6-subset casuali — e non e' cherry-picked (contiene TLT, il peggiore
dei 18) -> niente piu' ampiezza da raccogliere su quello sleeve.

STATARB-MULTI (r0725_statarb_multi.py) — il meccanismo congelato (W=45, sgn=+1)
su tutte le 50 coppie alt/BTC, out-of-pair-sample. REGGE: 82% Sharpe>0, 0/50
degeneri (mono 53%), perm p<0.05 nel 18% (atteso 5%). E ETH/BTC e' al rango
22/50 (58° pctl): la coppia scopritrice NON e' un outlier -> per il gate 27/09
l'ipotesi "fortuna di una coppia" e' refutata. NON e' uno sleeve: ampiezza
effettiva ~4.5 (corr media 0.204, gamba BTC condivisa), paniere IC95%
[-0.12, 1.72] con t 1.31.
Errore di metodo corretto in-sessione: il primo null statico usava
sign(mean(segnale)) sull'intero campione = look-ahead; rifatto causale + a priori.

STATARB-EQ (r0725_statarb_eq.py) — SCARTATO. Stesso meccanismo su 12 coppie ETF
a priori, 28.5 anni, ampiezza effettiva 9.9: paniere Sharpe -1.00 (t -5.33),
negativo in 4/4 decadi. Ma la lettura sta nel LORDO (-0.17, non -1.00): due terzi
sono drag di turnover -> non esiste strategia specchio. Il segno lordo dice che
sulle azioni il residuo REVERTE mentre sul crypto CONTINUA: meccanismo
plausibilmente crypto-specifico. Il gate 27/09 non puo' appoggiarsi
all'argomento "fenomeno universale".

r0724_statarb_deploy_gate.py: addendum PRE-REGISTRATO oggi (forward-day 26 di 90,
64 giorni prima della decisione) — riporta anche lo Sharpe della statica
"sempre short ETH vs BTC" a pari vol-target sulla stessa finestra. Le soglie del
2026-07-24 NON sono toccate: e' diagnostica non binding. Lettura odierna
(26 barre): STATARB +5.70 vs benchmark -4.98, posizione corrente LONG lo spread
= opposta alla statica.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 09:47:47 +00:00

180 lines
8.5 KiB
Python

"""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()