Files
PythagorasGoal/scripts/research/r0726_xsr_equity.py
T
Adriano Dal Pastro 2c3882b60c research(wave): ondata 3 filoni — esecuzione SKH01, DVOLSPREAD, XSR01 fuori dal crypto
Tre questioni che il progetto aveva lasciato aperte e decidibili oggi.

T1 — ESECUZIONE SKH01. Testato l'unico meccanismo che non e' un cron: ordini
resting on-book (TP=limit al livello per costruzione + fee maker, SL=stop-market).
A offset 0 recupera il 94% del degrado, ma l'audit 02/07 aveva de-luckato i
numeri headline di SKH01 e NON il degrado. Sulla banda appaiata dei 23 offset il
degrado recuperabile e' +0.054 Sh di book, non -0.35 di sleeve.

Errore di metodo corretto in sessione: mediana(A)-mediana(B) fra offset confronta
offset diversi; serve la mediana delle DIFFERENZE appaiate. Col fix il verdetto
si ribalta: solo TP a limite = +0.054 FULL / +0.061 HOLD, positivo in 19/23 e
21/23 offset; lo SL on-book PEGGIORA (mediana -0.010, positivo in 11/23) perche'
cristallizza la perdita mentre l'exit ritardata incassa il rimbalzo.
Raccomandazione NON eseguita: TP a limite si, SL strategico on-book no.

T2 — DVOLSPREAD esce dal limbo (fermo dal 21/06). Passato ai due gate che allora
non esistevano. La griglia dichiarata "72 celle" ne contiene 729: valutate tutte.
Plateau reale (729/729 hold-out positivo). Selection-on-holdout confermata ma
mite: cella pubblicata 83a/729 sull'hold-out, 471a/729 in-sample. Cella onesta
FULL 0.68 / HOLD 0.69 (non 0.93) e DSR 0.953 PASS; la pubblicata fallisce 0.947.
Promosso a forward-monitor coi parametri onesti, NON nel book (hold-out attivo
1.6 anni, DSR sul filo, weights_tilt_null mai affrontato).

T3 — XSR01 non generalizza. Meccanismo congelato su 9 settoriali (1998+) e 28 ETF
(30 anni), versione DEMEANATA (il test del 25/07 era a coppie). Lordo +0.24
p=0.193 e -0.14 p=0.747 vs null a fee zero. Il risultato che conta e' l'ampiezza:
il demean fa 4.5->37.4 (8x) sul crypto ma 5.3->6.2 (1.2x) sulle azioni, perche'
sul crypto il residuo-vs-BTC lascia un enorme fattore comune e sulle azioni il
residuo-vs-SPY e' gia' indipendente. XSR01 e' crypto-specifico; NON e' falso.
Soglie del gate 23/10 non toccate.

Book/pesi/cron INVARIATI. Suite 245 verdi.

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

215 lines
9.9 KiB
Python

#!/usr/bin/env python
"""r0726_xsr_equity — T3 dell'ondata 2026-07-26: XSR01 regge fuori dal crypto e fuori dai
2.6 anni monoregime? Test di FALSIFICAZIONE su 26+ anni di ETF settoriali.
PERCHE'. XSR01 (candidato in forward-monitor dal 25/07, gate pre-registrato al 2026-10-23) ha
superato tutto quello che gli e' stato chiesto — marginale ADDS, deflated-Sharpe 0.985, corr ~0
a tutti e 5 gli sleeve, tre null a fee-neutrale — MA la sua debolezza numero uno e' dichiarata:
"storia 2.6 anni MONOREGIME e CRESCENTE (Sharpe 2024 1.03 / 2025 1.98 / 2026 3.11):
l'edge e' recente."
Con 2.6 anni non si distingue un meccanismo reale da un regime fortunato. L'unico modo di
guadagnare potenza statistica senza aspettare anni e' portare il meccanismo CONGELATO su un
pannello **diverso per asset class e lungo decenni**, e vedere se sopravvive. Se regge su 26 anni
di settoriali US, l'ipotesi "e' un artefatto del bear alt 2024-2026" e' molto piu' difficile da
sostenere. Se non regge, il gate del 23/10 deve saperlo.
NON E' IL TEST DEL 25/07. `r0725_statarb_eq.py` ha gia' portato il meccanismo sulle azioni ed e'
stato SCARTATO (paniere Sharpe -1.00 netta, -0.17 lorda) — ma quello era il meccanismo a **COPPIE**
(12 coppie a priori, ampiezza effettiva 9.9). XSR01 non e' un paniere di coppie: e' la versione
**DEMEANATA cross-sezionalmente**, ed e' proprio il demeaning che ha cambiato tutto (ampiezza
effettiva 4.5 -> 37.4) annullando algebricamente la gamba comune. Testare il demean su equity e'
quindi una domanda DIVERSA, mai posta. Il risultato lordo del 25/07 (-0.17, cioe' il residuo
REVERTE debolmente sulle azioni mentre CONTINUA sul crypto) e' l'ipotesi nulla di questo test.
DISEGNO
* meccanismo CONGELATO importato da r0725_statarb_multi (W=45, sgn=+1, residuo OLS causale su
un fattore comune, z-score, tanh, vol-target 20%, cap 2x) — nessuna reimplementazione;
* demeaning cross-sezionale giornaliero, identico a r0725_statarb_basket_gate;
* fattore comune = **SPY** (analogo di BTC: il fattore che il demeaning deve annullare);
* due universi, entrambi a priori:
SECT9 = i 9 settoriali SPDR classici (1998+, ~26 anni) — cross-section OMOGENEA, il
parallelo piu' stretto degli alt (un solo fattore comune forte);
ALL28 = tutti gli ETF su disco tranne SPY — parallelo del "ALL50" di XSR01;
* split NON aggiustati riparati in lettura (eqlib.load_eq -> src/data/eq_splits) — senza questo
IWM/EFA avvelenerebbero il pannello (difetto trovato il 25/07);
* LORDO **e** netto: la lettura del 25/07 e' che sulle azioni il segno vive nel lordo e il
turnover se lo mangia. Un venue a PAVIMENTO FISSO (IB) e' anche peggio di una fee
proporzionale a questa taglia (lezione 25/07) -> il netto proporzionale e' gia' generoso;
* null di permutazione cross-sezionale **A FEE ZERO** (lezione XSR01: permutare un segnale ne
fa esplodere il turnover, quindi un null a fee piena perderebbe per COSTO invece che per
assenza d'informazione, e regalerebbe un p-value trionfale e falso);
* decomposizione per DECENNIO: 26 anni servono a questo, non a fare una media unica.
Uso: `uv run python scripts/research/r0726_xsr_equity.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 eqlib import SECTORS_CLASSIC, load_eq # noqa: E402
from r0725_statarb_multi import (FEE_LEG, SGN, W, _dd, _sh, signal) # noqa: E402
RAW = ROOT / "data" / "raw"
BASE = "SPY"
MIN_BARS = 400
N_PERM = 300
BLOCK = 20
SEED = 20260726
ANN = np.sqrt(252.0) # ⚠️ giorni di BORSA, non 365 (lezione 25/07 sull'annualizzazione)
def all_symbols() -> list[str]:
out = []
for p in sorted(RAW.glob("eq_*_1d.parquet")):
s = p.stem.replace("eq_", "").replace("_1d", "").upper()
if s != BASE:
out.append(s)
return out
def _px(sym: str) -> pd.Series:
s = load_eq(sym)["close"].astype(float)
return s[~s.index.duplicated(keep="last")].sort_index()
def build(cols: list[str]) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Posizioni e spread per ogni gamba, col meccanismo congelato vs SPY."""
base = _px(BASE)
P, S = {}, {}
for sym in cols:
try:
t = _px(sym)
except FileNotFoundError:
continue
ix = base.index.intersection(t.index)
if len(ix) < MIN_BARS:
continue
p, s = signal(base[ix], t[ix])
P[sym] = pd.Series(p, index=ix)
S[sym] = pd.Series(s, index=ix)
return (pd.concat(P, axis=1, sort=True).sort_index(),
pd.concat(S, axis=1, sort=True).sort_index())
def pnl_fee(pos: np.ndarray, spread: np.ndarray, fee_leg: float) -> np.ndarray:
held = np.concatenate([[0.0], pos[:-1]])
turn = np.abs(np.diff(held, prepend=0.0))
return held * spread - 2.0 * fee_leg * turn
def basket(P: pd.DataFrame, S: pd.DataFrame, demean: bool, fee_leg: float) -> pd.Series:
Q = P.sub(P.mean(axis=1), axis=0) if demean else P
out = {c: pd.Series(pnl_fee(Q[c].to_numpy(float), S[c].to_numpy(float), fee_leg), index=Q.index)
for c in Q.columns}
return pd.concat(out, axis=1, sort=True).mean(axis=1, skipna=True).dropna()
def eff_breadth(P: pd.DataFrame, S: pd.DataFrame, demean: bool) -> float:
Q = P.sub(P.mean(axis=1), axis=0) if demean else P
M = pd.concat({c: pd.Series(pnl_fee(Q[c].to_numpy(float), S[c].to_numpy(float), 0.0),
index=Q.index) for c in Q.columns}, axis=1)
C = M.corr().values
n = C.shape[0]
off = (C.sum() - np.trace(C)) / (n * (n - 1))
return float(n / (1.0 + (n - 1) * off))
def sh(r) -> float:
r = np.asarray(r, float)
r = r[np.isfinite(r)]
return float(r.mean() / r.std() * ANN) if len(r) > 30 and r.std() > 0 else 0.0
def main() -> None:
print("=" * 100)
print(" T3 — XSR01 fuori dal crypto: il meccanismo congelato su 26 anni di ETF settoriali")
print("=" * 100)
print(f"\n meccanismo: W={W}, sgn={SGN:+d}, residuo OLS causale vs {BASE}, z-score, tanh,")
print(" vol-target 20%, cap 2x, demean cross-sezionale giornaliero [CONGELATO, importato]")
print(f" annualizzazione: sqrt(252) = giorni di BORSA (NON 365)")
universes = {
"SECT9 9 settoriali SPDR (1998+)": SECTORS_CLASSIC,
"ALL28 tutti gli ETF su disco": all_symbols(),
}
store = {}
for name, cols in universes.items():
P, S = build(cols)
store[name] = (P, S)
print("\n" + "-" * 100)
print(f" {name}{P.shape[1]} gambe, {len(P)} barre, "
f"{P.index[0].date()} -> {P.index[-1].date()}")
print("-" * 100)
print(f" {'variante':>28} {'ampiezza eff.':>14} {'Sharpe LORDA':>13} "
f"{'Sharpe NETTA':>13} {'maxDD':>8} {'ret/anno':>9}")
for lab, dm in (("coppie (no demean)", False), ("DEMEAN (= XSR01)", True)):
g = basket(P, S, dm, 0.0)
n = basket(P, S, dm, FEE_LEG)
eb = eff_breadth(P, S, dm)
cagr = float(np.nanmean(n.values)) * 252.0
print(f" {lab:>28} {eb:>14.1f} {sh(g):>13.2f} {sh(n):>13.2f} "
f"{_dd(n.values):>7.1%} {cagr:>8.1%}")
# ------------------------------------------------------------------ per decennio
print("\n" + "-" * 100)
print(" A. PER DECENNIO — 26 anni servono a questo (variante DEMEAN, la stessa di XSR01)")
print("-" * 100)
print(f" {'universo':>34} {'periodo':>14} {'anni':>5} {'Sh LORDA':>9} {'Sh NETTA':>9} {'giorni':>7}")
for name, (P, S) in store.items():
g = basket(P, S, True, 0.0)
n = basket(P, S, True, FEE_LEG)
for lo, hi in (("1998", "2005"), ("2005", "2012"), ("2012", "2019"), ("2019", "2027")):
kg = (g.index >= lo) & (g.index < hi)
kn = (n.index >= lo) & (n.index < hi)
if kg.sum() < 250:
continue
print(f" {name.split()[0]:>34} {lo + '-' + hi:>14} {kg.sum() / 252:>5.1f} "
f"{sh(g[kg]):>9.2f} {sh(n[kn]):>9.2f} {int(kg.sum()):>7d}")
# ------------------------------------------------------------------ null a fee ZERO
print("\n" + "-" * 100)
print(" B. NULL DI PERMUTAZIONE CROSS-SEZIONALE — a FEE ZERO (lezione XSR01)")
print("-" * 100)
print(" Permutare un segnale ne fa esplodere il turnover: a fee piena il null perderebbe")
print(" per COSTO invece che per assenza d'informazione -> p-value trionfale e falso.")
rng = np.random.default_rng(SEED)
print(f"\n {'universo':>34} {'candidato LORDO':>16} {'null medio':>11} {'null p95':>9} "
f"{'null max':>9} {'p-value':>8}")
for name, (P, S) in store.items():
cand = sh(basket(P, S, True, 0.0))
Pv = P.to_numpy(float)
draws = []
for _ in range(N_PERM):
idx = rng.permutation(Pv.shape[1])
Pp = pd.DataFrame(Pv[:, idx], index=P.index, columns=P.columns)
draws.append(sh(basket(Pp, S, True, 0.0)))
d = np.array(draws)
pval = float((d >= cand).mean())
print(f" {name.split()[0]:>34} {cand:>16.2f} {d.mean():>11.2f} "
f"{np.percentile(d, 95):>9.2f} {d.max():>9.2f} {pval:>8.3f}")
# ------------------------------------------------------------------ confronto col crypto
print("\n" + "-" * 100)
print(" C. CONFRONTO DIRETTO col candidato crypto (numeri del 25/07, non ricalcolati)")
print("-" * 100)
print(" XSR01 (50 alt HL, 2.6 anni): ampiezza eff. 37.4 | LORDA 2.70 | NETTA 1.82 | DD -2.6%")
print(" STATARB-EQ a COPPIE (25/07): ampiezza eff. 9.9 | LORDA -0.17 | NETTA -1.00")
print("\n La domanda del test: il DEMEAN — che sul crypto ha fatto 4.5 -> 37.4 di ampiezza e")
print(" ha creato l'edge — fa la stessa cosa sulle azioni, o li' il residuo resta reverting?")
print("\n" + "=" * 100)
if __name__ == "__main__":
main()