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>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user