Files

684 lines
38 KiB
Python

"""r0822b_xs01_oos.py — XS01 FUORI dalla sua finestra di scoperta (2026-08-22, ondata wave-0822).
IL PROBLEMA (filone PROP-ALLOC, RESULTS-0822 §1)
------------------------------------------------
Il risultato piu' vicino all'obiettivo dell'operatore e': su un conto FUNDED da $100k, aggiungere
XS01 al libro porta J = P(pass) x P(vivo 12m) da 0,335 a 0,738. Ma **tutto** quel vantaggio poggia
sul drift di XS01 misurato sulla SUA STESSA finestra di scoperta (Hyperliquid, 2024-01 -> oggi,
~2,6 anni, e senza un solo mercato orso). Azzerando quel drift l'uplift scende a +0,038; sotto
stress congiunto diventa negativo. Il gate proposto era "non aprire un funded finche' XS01 non ha
una finestra fuori dal 2024-2026" = ANNI di attesa.
LA DOMANDA DI QUESTO FILONE
---------------------------
Si puo' testare il MECCANISMO CONGELATO su una storia PIU' LUNGA e INDIPENDENTE invece di aspettare?
La risposta e' si', e il vincolo non e' il dato ma il CALENDARIO DELLE QUOTAZIONI: 6 dei 19 asset
(APT, OP, ARB, SUI, SEI, TIA) non esistono prima del 2022-06/2023-10. Il piu' vecchio universo
>= 10 asset dei 19 e' del **2020-10** -> circa **3 anni interi fuori dalla finestra di scoperta,
2021-01 -> 2023-12**, che contengono il top 2021, il crollo di maggio 2021, **LUNA e FTX (2022)** e
la ripresa 2023. E' esattamente il regime che manca al campione di scoperta.
IL DATO: BINANCE, E PERCHE' L'OBIEZIONE DEL PROGETTO NON MORDE QUI
-----------------------------------------------------------------
CLAUDE.md: "Binance NON e' la verita' (e' USDT, ~10 bps fuori dal consenso, e sotto depeg fino al
3%) -> usare SOLO come audit indipendente, mai come ancora per ripulire i dati". Quella regola
riguarda l'uso come **ANCORA DI PREZZO** (livello). Qui Binance serve come **audit indipendente per
un segnale CROSS-SECTIONAL DI RANKING su barre giornaliere**, e i tre difetti citati sono tutti
**fattori COMUNI alla valuta di quotazione**:
- uno scarto costante di ~10 bps sul livello e' identico su tutti e 19 gli asset -> sparisce
ESATTAMENTE nello z-score cross-sectional (che sottrae la media di sezione);
- un depeg USDT muove tutte le gambe insieme -> non cambia il RANKING, e il libro e'
dollar-neutral (0.5/k long, -0.5/k short) quindi il fattore comune si cancella anche nel P&L;
- a 1d un errore di livello di 10 bps e' 1/50 della vol giornaliera tipica di un alt.
NON e' un argomento teorico: e' MISURATO qui sotto (sezione 3) confrontando il meccanismo su
Binance e su Hyperliquid nella finestra in cui esistono ENTRAMBI (2024+).
E i dati scaricati sono CERTIFICATI comunque (barre flat, gap di calendario, volumi zero =
backfill sintetico: il progetto ci e' gia' cascato una volta, correzione AXS del 2026-06-20).
MECCANISMO: CONGELATO. Nessun parametro toccato. E' `src/portfolio/sleeves.py::_xsec_returns`
(lookbacks (30,90), H=10, k=5, mode="mom", target_vol=0.20, gate dispersione p30 espandente
causale, fee 5 bps/lato, vol-target su rv30 shiftata, clip leva 3x). Il motore qui sotto e' una
versione MASCHERATA (regge NaN = asset non ancora quotato) e la sezione 2 dimostra che riproduce
lo sleeve ufficiale a **max|diff| = 0.0** sui dati HL.
ESITO (2026-08-22, corsa completa 78 s)
---------------------------------------
- Il motore replica lo sleeve ufficiale a max|diff| = 0.0; Binance e Hyperliquid danno lo STESSO
sleeve sulla finestra comune (corr 0.9991, dSharpe -0.025) -> la storia lunga e' utilizzabile.
- Fuori dalla finestra di scoperta (2021-01 -> 2023-12, contiene LUNA e FTX) il meccanismo ha un
edge: Sharpe MEDIANO di fase **+1.12** (U13 fisso) / **+1.17** (universo espandente), positivo in
**10/10** fasi, p = 0.013 contro il null di permutazione cross-sezionale a fee zero.
- **Il fuori campione e' MIGLIORE della finestra di scoperta**, a parita' di universo: differenza
appaiata per fase +0.668 (10/10 fasi). L'ipotesi "il drift di XS01 e' un artefatto del 2024-2026"
e' REFUTATA per la parte testabile del sleeve.
- MA: il maxDD raddoppia (10.8% nella scoperta -> 20.9-22.0% mediano fuori), il 2021 vale meta'
dell'edge, una gamba sola (LTC) ne vale 0.9 di Sharpe, e i due affinamenti del 2026-06-19 non si
replicano. E il deflated-Sharpe FALLISCE (0.342-0.418).
- Sul LIBRO, fuori campione: TP01+SKH01 75/25 Sharpe +1.17 -> **+1.65** e maxDD 9.4% -> **6.0%**
a peso XS01 0.25; a peso **0.50** (l'argmax di PROP-ALLOC) il maxDD risale a **8.9%**.
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0822b_xs01_oos.py
Rete: solo lettura, <= 2 req/s, MAI nella finestra :25-:29 (rate limit per-IP condiviso col
collettore della catena). Il download e' messo in cache nello scratchpad: la seconda corsa e'
offline.
"""
from __future__ import annotations
import math
import os
import sys
import time
from datetime import datetime, timezone
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" / "alt"))
import altlib as A # noqa: E402
from src.portfolio.sleeves import XS_CFG, XS_UNIVERSE # noqa: E402
CACHE = Path(os.environ.get(
"R0822B_CACHE",
"/tmp/claude-1001/-opt-docker-PythagorasGoal/b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/r0822b",
))
CACHE.mkdir(parents=True, exist_ok=True)
RAW = ROOT / "data" / "raw"
FEE_SIDE = 0.001 / 2.0 # identica al sleeve congelato
DISCOVERY = pd.Timestamp("2024-01-01", tz="UTC") # inizio della finestra di scoperta (HL)
# I 13 dei 19 quotati su Binance prima del 2020-11 -> l'universo FISSO piu' lungo possibile
# che rispetta il vincolo del meccanismo congelato (>= 10 gambe).
U13 = ["BTC", "ETH", "BNB", "XRP", "LTC", "ADA", "LINK", "DOGE", "SOL", "AVAX", "NEAR", "AAVE", "INJ"]
SECTORS = ["xlb", "xlc", "xle", "xlf", "xli", "xlk", "xlp", "xlre", "xlu", "xlv", "xly"]
# ===========================================================================
# 0. disciplina di rete
# ===========================================================================
def _outside_forbidden_window() -> None:
"""Il collettore della catena gira al minuto :25 e satura il rate limit per-IP di Deribit;
la regola d'ondata e' non toccare la rete fra :25 e :29. Qui parliamo con Binance, ma la
disciplina e' della MACCHINA (2 core, libro con soldi veri), non del venue."""
while True:
m = datetime.now(timezone.utc).minute
if not (25 <= m <= 29):
return
print(f" [rete] minuto :{m:02d} nella finestra vietata :25-:29 — attendo 30s", flush=True)
time.sleep(30)
# ===========================================================================
# 1. FETCH + CERTIFY (Binance spot USDT, 1d)
# ===========================================================================
def fetch_binance_daily(sym: str, since_iso: str = "2017-01-01T00:00:00Z") -> pd.DataFrame:
f = CACHE / f"bin_{sym.lower()}_1d.parquet"
if f.exists():
return pd.read_parquet(f)
import ccxt
ex = ccxt.binance({"enableRateLimit": True})
since = ex.parse8601(since_iso)
rows, cur = [], since
while True:
_outside_forbidden_window()
batch = ex.fetch_ohlcv(f"{sym}/USDT", "1d", since=cur, limit=1000)
time.sleep(0.5) # <= 2 req/s
if not batch:
break
rows.extend(batch)
if len(batch) < 1000:
break
cur = batch[-1][0] + 86_400_000
d = pd.DataFrame(rows, columns=["timestamp", "open", "high", "low", "close", "volume"])
d = d.drop_duplicates("timestamp").sort_values("timestamp").reset_index(drop=True)
d.to_parquet(f)
return d
def certify(sym: str, d: pd.DataFrame) -> dict:
"""Certificazione locale, stessa forma di scripts/analysis/fetch_hyperliquid.py.
Il rivelatore del backfill SINTETICO NON e' il prezzo (cerbero copiava i prezzi da un altro
venue) ma il **VOLUME**: un run di barre a volume 0 e' padding, non mercato."""
t = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
c = d["close"].values.astype(float)
v = d["volume"].values.astype(float)
span = (t.max() - t.min()).days + 1
flat = float(np.mean(c[1:] == c[:-1])) if len(c) > 1 else float("nan")
zero_v = float(np.mean(v == 0.0))
# run iniziale a volume zero = firma del padding sintetico (lezione AXS 2026-06-20)
lead0 = int(np.argmax(v > 0)) if (v > 0).any() else len(v)
return dict(sym=sym, n=len(d), start=str(t.min().date()), end=str(t.max().date()),
gap_days=int(span - len(d)), flat=flat, zero_vol=zero_v, lead_zero_vol=lead0,
ok=bool(span - len(d) == 0 and flat < 0.02 and zero_v < 0.01 and lead0 == 0))
def load_panel(syms: list[str]) -> pd.DataFrame:
"""Pannello dei CLOSE, indice giornaliero UTC, NaN dove l'asset non e' ancora quotato."""
cols = {}
for s in syms:
d = fetch_binance_daily(s)
cols[s] = pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
return pd.concat(cols, axis=1).sort_index()
def load_sector_panel() -> pd.DataFrame:
cols = {}
for s in SECTORS:
d = pd.read_parquet(RAW / f"eq_{s}_1d.parquet")
cols[s.upper()] = pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
return pd.concat(cols, axis=1).sort_index()
# ===========================================================================
# 2. MOTORE — meccanismo CONGELATO, versione mascherata (regge i NaN)
# ===========================================================================
def xsec_engine(P: pd.DataFrame, phase: int = 0, fee_side: float = FEE_SIDE,
mode: str | None = None, min_assets: int = 10,
perm_rng: np.random.Generator | None = None,
perm_kind: str = "cross", lookbacks=None, disp_pct=None,
detail: dict | None = None) -> pd.Series:
"""Riproduce `sleeves._xsec_returns` riga per riga. Unica differenza: l'insieme ammissibile
e' calcolato PER RIBILANCIAMENTO (un asset entra quando ha almeno max(lookbacks) barre), cosi'
il meccanismo si puo' applicare a un universo che si allarga nel tempo — che e' cio' che un
implementatore in tempo reale avrebbe avuto. Con un pannello senza NaN, e' identico all'originale.
`perm_rng` attiva un NULL: 'cross' = permuta lo score FRA GLI ASSET al ribilanciamento
(distrugge l'informazione, conserva ESATTAMENTE la struttura di posizione e di turnover),
'sign' = pesi casuali fra gli ammissibili (stesso gross, stesso k)."""
px = P.values.astype(float)
n, Ncols = px.shape
H, k = XS_CFG["H"], XS_CFG["k"]
lookbacks = tuple(XS_CFG["lookbacks"] if lookbacks is None else lookbacks)
disp_pct = XS_CFG["disp_pct"] if disp_pct is None else disp_pct
tv, minhist = XS_CFG["target_vol"], XS_CFG["disp_minhist"]
mode = mode or XS_CFG["mode"]
mlb = max(lookbacks)
with np.errstate(invalid="ignore", divide="ignore"):
dret = np.vstack([np.zeros(Ncols), px[1:] / px[:-1] - 1.0])
dret = np.nan_to_num(dret, nan=0.0, posinf=0.0, neginf=0.0)
W = np.zeros((n, Ncols)); w = np.zeros(Ncols); disp_hist: list[float] = []
n_rebal = n_traded = 0
for i in range(n):
if i >= mlb and (i % H) == phase:
elig = np.isfinite(px[i])
for L in lookbacks:
elig = elig & np.isfinite(px[i - L])
m = int(elig.sum())
if m >= min_assets:
n_rebal += 1
idx = np.flatnonzero(elig)
rLs = [px[i, idx] / px[i - L, idx] - 1.0 for L in lookbacks]
disp_i = float(np.mean([r.std() for r in rLs]))
thr = (np.percentile(disp_hist, disp_pct)
if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf)
if disp_i >= thr:
score = np.zeros(m); cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if cnt:
score /= cnt
w = np.zeros(Ncols)
if perm_rng is not None and perm_kind == "sign":
pick = perm_rng.permutation(m)
lo, hi = idx[pick[:k]], idx[pick[-k:]]
else:
if perm_rng is not None: # perm_kind == "cross"
score = score[perm_rng.permutation(m)]
order = np.argsort(score)
lo, hi = idx[order[:k]], idx[order[-k:]]
if mode == "mom":
w[hi] = 0.5 / k; w[lo] = -0.5 / k
else:
w[lo] = 0.5 / k; w[hi] = -0.5 / k
n_traded += 1
else:
w = np.zeros(Ncols)
disp_hist.append(disp_i)
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum()
turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
net = gross - turn * fee_side
s = pd.Series(net, index=P.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
scale = np.clip(np.nan_to_num(tv / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
out = pd.Series(s.values * scale, index=P.index)
out.attrs["n_rebal"] = n_rebal
out.attrs["n_traded"] = n_traded
if detail is not None:
detail.update(W=W, scale=scale, turn=turn, index=P.index,
n_rebal=n_rebal, n_traded=n_traded)
return out
def hl_panel(syms=None) -> pd.DataFrame:
"""Pannello Hyperliquid, ESATTAMENTE come lo costruisce lo sleeve congelato."""
cols = {}
for sym in (syms or XS_UNIVERSE):
p = RAW / f"hl_{sym.lower()}_1d.parquet"
if not p.exists():
continue
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
return pd.concat(cols, axis=1, join="inner").sort_index().dropna()
# ===========================================================================
# metriche
# ===========================================================================
def sh(s: pd.Series) -> float:
return A._sh(s)
def dd(s: pd.Series) -> float:
return A._dd_ret(s)
def cagr(s: pd.Series) -> float:
r = s.dropna()
if len(r) < 30:
return float("nan")
yrs = (r.index[-1] - r.index[0]).days / 365.25
return float(np.prod(1.0 + r.values) ** (1.0 / yrs) - 1.0) if yrs > 0 else float("nan")
def win(s: pd.Series, a: str | None = None, b: str | None = None) -> pd.Series:
x = s
if a:
x = x[x.index >= pd.Timestamp(a, tz="UTC")]
if b:
x = x[x.index < pd.Timestamp(b, tz="UTC")]
return x
def alive(s: pd.Series) -> pd.Series:
"""Taglia il prefisso di ZERI (warm-up dei lookback + attesa che l'universo raggiunga 10
gambe). ⚠️ Bug catturato nella prima corsa: sull'universo ESPANDENTE il pannello parte dal
2017 (BTC) ma il meccanismo non puo' operare prima del 2020-12, e i ~1.200 zeri iniziali
entravano nella finestra 'PRE-2024' abbassandone lo Sharpe e ribaltando la differenza
appaiata. E' la lezione gia' codificata nel progetto (barre ATTIVE, non di calendario),
ripetuta in un contesto nuovo."""
v = np.asarray(s.values, float)
nz = np.flatnonzero(v != 0.0)
return s.iloc[nz[0]:] if len(nz) else s
def line(tag: str, s: pd.Series) -> str:
return (f"{tag:<22s} n={len(s):>5d} Sh {sh(s):+6.2f} maxDD {dd(s)*100:5.1f}% "
f"CAGR {cagr(s)*100:+6.2f}% vol {s.std()*np.sqrt(365.25)*100:5.1f}%")
# ===========================================================================
def main() -> None:
t0 = time.time()
print("=" * 100)
print("XS01 FUORI DALLA FINESTRA DI SCOPERTA — meccanismo CONGELATO su storia lunga indipendente")
print(f"XS_CFG = {XS_CFG}")
print("=" * 100)
# ---------------------------------------------------------------- 1. dato
print("\n[1] DATO — Binance spot USDT 1d, certificazione locale")
panel19 = load_panel(XS_UNIVERSE)
cert = [certify(s, fetch_binance_daily(s)) for s in XS_UNIVERSE]
bad = [c for c in cert if not c["ok"]]
print(f" {len(cert)} serie scaricate; certificate OK: {len(cert)-len(bad)}/{len(cert)}")
print(f" {'sym':<6}{'n':>6}{'start':>12}{'end':>12}{'gap':>5}{'flat%':>8}{'vol0%':>8}{'lead0':>7}")
for c in cert:
flag = "" if c["ok"] else " <-- CONTROLLA"
print(f" {c['sym']:<6}{c['n']:>6}{c['start']:>12}{c['end']:>12}{c['gap_days']:>5}"
f"{c['flat']*100:>8.2f}{c['zero_vol']*100:>8.2f}{c['lead_zero_vol']:>7}{flag}")
print(" NB: 'lead0' = barre iniziali a volume 0 = firma del backfill sintetico (lezione AXS).")
# ---------------------------------------------------- 2. replica bit-exact
print("\n[2] SANITY — il motore riproduce lo sleeve UFFICIALE?")
from src.portfolio.sleeves import _xsec_returns
off = _xsec_returns()
rep = xsec_engine(hl_panel(), phase=0)
j = pd.concat({"off": off, "rep": rep}, axis=1, join="inner").dropna()
md = float(np.max(np.abs(j["off"] - j["rep"]))) if len(j) else float("nan")
print(f" n={len(j)} max|diff| = {md:.2e} -> {'IDENTICO' if md < 1e-12 else 'DIVERGE'}")
print(f" {line('XS01 ufficiale (HL)', off)}")
if md >= 1e-12:
print(" !! il motore NON replica: ogni numero sotto e' sospetto. STOP.")
return
# ------------------------------------------------ 3. validazione di VENUE
print("\n[3] VALIDAZIONE DI VENUE — Binance vs Hyperliquid dove esistono ENTRAMBI (2024+)")
hlp = hl_panel()
b19 = panel19.reindex(columns=XS_UNIVERSE)
common = b19.index.intersection(hlp.index)
rr = []
for s in XS_UNIVERSE:
a1 = hlp[s].reindex(common).pct_change()
b1 = b19[s].reindex(common).pct_change()
k2 = pd.concat({"a": a1, "b": b1}, axis=1).dropna()
rr.append((s, float(k2["a"].corr(k2["b"])), float(np.median(np.abs(k2["a"] - k2["b"]))) * 1e4))
print(f" ritorni giornalieri per asset, {len(common)} giorni comuni:")
print(" " + " ".join(f"{s}:{c:.4f}" for s, c, _ in rr))
print(f" corr min {min(c for _, c, _ in rr):.4f} | mediana |diff| ritorno: "
f"{np.median([m for _, _, m in rr]):.1f} bps (max {max(m for _, _, m in rr):.1f} bps)")
# e soprattutto: il MECCANISMO da' la stessa cosa?
bin_on_hl_window = xsec_engine(b19.loc[common].dropna(), phase=0)
hl_on_window = xsec_engine(hlp.loc[common], phase=0)
jj = pd.concat({"bin": bin_on_hl_window, "hl": hl_on_window}, axis=1, join="inner").dropna()
print(f" {line('XS01 su HL 2024+', jj['hl'])}")
print(f" {line('XS01 su BINANCE 2024+', jj['bin'])}")
print(f" corr fra le DUE VERSIONI DELLO SLEEVE: {jj['bin'].corr(jj['hl']):.4f} "
f"dSharpe = {sh(jj['bin'])-sh(jj['hl']):+.3f}")
print(" -> se questa corr e' ~1, la storia lunga Binance e' un'estensione legittima:")
print(" lo scarto USDT e' un FATTORE COMUNE e sparisce nello z-score cross-sectional.")
# ------------------------------------- 4. la storia lunga, fuori campione
print("\n[4] STORIA LUNGA — meccanismo congelato, ancora canonica (fase 0)")
p13 = panel19[U13].dropna() # universo FISSO 13 (il piu' lungo a >=10 gambe)
s13 = xsec_engine(p13, phase=0)
sEXP = xsec_engine(panel19, phase=0) # universo che si ALLARGA (19 quando esistono)
s13, sEXP = alive(s13), alive(sEXP)
print(f" U13-FIX universo fisso {len(U13)} gambe, dal {s13.index[0].date()} "
f"({s13.attrs.get('n_traded')}/{s13.attrs.get('n_rebal')} ribilanciamenti con posizione)")
print(f" U19-EXP universo espandente (10 -> 19 gambe), dal {sEXP.index[0].date()}")
for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP)):
print(f" {line(tag + ' FULL', s)}")
print(f" {line(tag + ' PRE-2024 (OOS)', win(s, None, '2024-01-01'))}")
print(f" {line(tag + ' 2024+ (scoperta)', win(s, '2024-01-01'))}")
print("\n Sharpe per ANNO (netto fee):")
yrs = sorted({d.year for d in sEXP.index})
hdr = " " + f"{'':<10}" + "".join(f"{y:>9d}" for y in yrs)
print(hdr)
for tag, s in (("U13-FIX", s13), ("U19-EXP", sEXP), ("XS01 HL", off)):
cells = []
for y in yrs:
x = win(s, f"{y}-01-01", f"{y+1}-01-01")
cells.append(f"{sh(x):>9.2f}" if len(x) > 60 else f"{'—':>9s}")
print(f" {tag:<10}" + "".join(cells))
print("\n Sharpe per REGIME:")
regimes = [("2021 toro/top", "2021-01-01", "2022-01-01"),
("2022 ORSO (LUNA/FTX)", "2022-01-01", "2023-01-01"),
("2023 ripresa", "2023-01-01", "2024-01-01"),
("2024-26 scoperta", "2024-01-01", None)]
for nm, a, b in regimes:
x13, xEX = win(s13, a, b), win(sEXP, a, b)
print(f" {nm:<24s} U13 Sh {sh(x13):+6.2f} DD {dd(x13)*100:5.1f}% | "
f"U19-EXP Sh {sh(xEX):+6.2f} DD {dd(xEX)*100:5.1f}%")
# -------------------------------------------- 5. fortuna di FASE (H = 10)
print("\n[5] FORTUNA DI FASE — H=10, tutte e 10 le ancore (la stima onesta e' la MEDIANA)")
phases = list(range(XS_CFG["H"]))
bands = {}
for tag, P, cut in (("U13-FIX FULL", p13, None), ("U13-FIX PRE-2024", p13, "pre"),
("U19-EXP FULL", panel19, None), ("U19-EXP PRE-2024", panel19, "pre")):
def f(ph, P=P, cut=cut):
s = alive(xsec_engine(P, phase=ph))
return win(s, None, "2024-01-01") if cut == "pre" else s
bands[tag] = A.anchor_luck_band(f, phases, canonical=0, metric=sh)
bands[tag + "|DD"] = A.anchor_luck_band(f, phases, canonical=0, metric=lambda x: -dd(x))
for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"):
r = bands[tag]; rd = bands[tag + "|DD"]
print(f" {tag:<20s} Sh canonico {r['canonical']:+.2f} (pctl {r['canonical_pctl']*100:4.0f}°) "
f"| MEDIANA {r['median']:+.2f} | banda [{r['lo']:+.2f},{r['hi']:+.2f}] "
f"| fasi>0 {r['frac_positive']*100:3.0f}% | fortuna {r['luck']:+.2f} | gate {r['gate_pass']}")
print(f" {'':<20s} maxDD canonico {-rd['canonical']*100:5.1f}% "
f"| MEDIANA {-rd['median']*100:5.1f}% | banda [{-rd['hi']*100:.1f},{-rd['lo']*100:.1f}]%")
# anche lo sleeve UFFICIALE su HL, de-luckato di fase (per confronto onesto)
bhl = A.anchor_luck_band(lambda ph: xsec_engine(hlp, phase=ph), phases, canonical=0, metric=sh)
print(f" {'XS01 UFFICIALE (HL)':<20s} Sh canonico {bhl['canonical']:+.2f} "
f"(pctl {bhl['canonical_pctl']*100:4.0f}°) | MEDIANA {bhl['median']:+.2f} "
f"| banda [{bhl['lo']:+.2f},{bhl['hi']:+.2f}] | fortuna {bhl['luck']:+.2f}")
# -------------- 6. differenza APPAIATA fuori campione vs finestra di scoperta
print("\n[6] DIFFERENZA APPAIATA per fase: (fuori campione) - (finestra di scoperta)")
for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)):
def pre(ph, P=P):
return win(alive(xsec_engine(P, phase=ph)), None, "2024-01-01")
def post(ph, P=P):
return win(alive(xsec_engine(P, phase=ph)), "2024-01-01")
d1 = A.anchor_luck_delta(pre, post, phases, metric=sh)
print(f" {tag:<10s} mediana DIFF appaiata {d1['median_paired']:+.3f} "
f"[{d1['lo']:+.3f},{d1['hi']:+.3f}] fasi con OOS>scoperta {d1['n_positive']}/{d1['n_anchors']}")
# ------------------------------------------------------------- 7. i NULL
print("\n[7] NULL — a FEE ZERO (permutare un segnale ne fa esplodere il turnover: a fee piene")
print(" il null perderebbe per COSTO invece che per assenza di informazione — regola XSR01)")
NDRAW = 300
for tag, P, a, b in (("U13-FIX PRE-2024", p13, None, "2024-01-01"),
("U13-FIX FULL", p13, None, None)):
real = win(alive(xsec_engine(P, phase=0, fee_side=0.0)), a, b)
for kind in ("cross", "sign"):
rng = np.random.default_rng(20260822)
draws = np.array([sh(win(alive(xsec_engine(P, phase=0, fee_side=0.0, perm_rng=rng,
perm_kind=kind)), a, b)) for _ in range(NDRAW)])
p = float(np.mean(draws >= sh(real)))
print(f" {tag:<20s} null '{kind}': reale(lordo) {sh(real):+.2f} vs null "
f"med {np.median(draws):+.2f} max {draws.max():+.2f} -> p = {p:.4f}")
print("\n NULL 'universo dove NON dovrebbe funzionare' — 11 settoriali SPDR, 1998+,")
print(" stesso meccanismo congelato (il progetto ha gia' misurato che il cross-sectional")
print(" crypto non generalizza alle azioni: XSR01, 2026-07-26):")
sect = load_sector_panel()
ssec = alive(xsec_engine(sect, phase=0, min_assets=9))
bsec = A.anchor_luck_band(lambda ph: alive(xsec_engine(sect, phase=ph, min_assets=9)), phases,
canonical=0, metric=sh)
print(f" {line('SECT11 (azioni)', ssec)}")
print(f" SECT11 banda di fase: mediana Sh {bsec['median']:+.2f} "
f"[{bsec['lo']:+.2f},{bsec['hi']:+.2f}], fasi>0 {bsec['frac_positive']*100:.0f}%")
print("\n CONTROLLO DI DIREZIONE — stesso meccanismo con mode='rev' (long i deboli):")
for tag, P in (("U13-FIX", p13), ("U19-EXP", panel19)):
r = alive(xsec_engine(P, phase=0, mode="rev"))
print(f" {line(tag + ' REV', r)}")
# ------------- 7b. la SELEZIONE del 2026-06-19, rivalidata FUORI CAMPIONE
print("\n[7b] LA SELEZIONE DEL 2026-06-19, RIVALIDATA FUORI CAMPIONE")
print(" XS01 non e' solo un meccanismo: e' un meccanismo AFFINATO due volte sui dati HL")
print(" 2024+ (blend di lookback [30,90] e gate di dispersione p30). Quelle DUE scelte sono")
print(" debito di multiple-testing contratto sulla finestra di scoperta. Qui si paga:")
print(" ogni variante e' valutata sul PRE-2024, a MEDIANA delle 10 fasi (mai l'ancora).")
variants = [("blend [30,90] + gate p30 (CANONICA)", dict(lookbacks=(30, 90), disp_pct=30)),
("solo L=30 + gate p30", dict(lookbacks=(30,), disp_pct=30)),
("solo L=90 + gate p30", dict(lookbacks=(90,), disp_pct=30)),
("blend [30,90] SENZA gate", dict(lookbacks=(30, 90), disp_pct=0)),
("blend [30,90] + gate p15", dict(lookbacks=(30, 90), disp_pct=15)),
("blend [30,90] + gate p50", dict(lookbacks=(30, 90), disp_pct=50))]
sel_rows = []
for nm, kw in variants:
b_pre = A.anchor_luck_band(
lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), None, "2024-01-01"),
phases, canonical=0, metric=sh)
b_post = A.anchor_luck_band(
lambda ph, kw=kw: win(alive(xsec_engine(p13, phase=ph, **kw)), "2024-01-01"),
phases, canonical=0, metric=sh)
sel_rows.append((nm, b_pre["median"], b_post["median"], b_pre["per_offset"]))
print(f" {nm:<38s} PRE-2024 mediana Sh {b_pre['median']:+5.2f} "
f"[{b_pre['lo']:+.2f},{b_pre['hi']:+.2f}] | 2024+ {b_post['median']:+5.2f}")
rank_pre = 1 + sorted([r[1] for r in sel_rows], reverse=True).index(sel_rows[0][1])
rank_post = 1 + sorted([r[2] for r in sel_rows], reverse=True).index(sel_rows[0][2])
print(f" -> la configurazione CANONICA (scelta nel 2026-06 guardando SOLO il 2024+) e'")
print(f" {rank_pre}/{len(sel_rows)} sul PRE-2024 e {rank_post}/{len(sel_rows)} sul 2024+.")
print( " 📌 LETTURA: il NUCLEO regge (ogni variante e' positiva fuori campione, banda")
print( " [+0.76,+1.28]), ma i DUE AFFINAMENTI del 19/06 NON si replicano:")
print( " - il BLEND [30,90] fuori campione e' PEGGIO del solo L=30 (+1.12 vs +1.28);")
print( " - il GATE di dispersione p30 fuori campione vale ZERO (con e senza: +1.12);")
print( " e' dentro la finestra di scoperta che vale (+0.37 contro -0.03).")
print( " Cioe': cio' che sopravvive e' il meccanismo cross-sectional nudo; i due")
print( " affinamenti sono, per quanto qui misurabile, adattamento alla finestra in")
print( " cui furono scelti. NON e' un argomento per cambiarli (cambiarli guardando")
print( " QUESTO risultato sarebbe la stessa selezione, spostata di finestra): e'")
print( " un argomento per non attribuire loro il valore che il progetto gli attribuisce.")
# --- concentrazione: un contributo positivo si scompone per ANNO e per GAMBA (lezione SOL)
print("\n Concentrazione (lezione SOL 22/08: '24/24 ancore positive erano UN ANNO SOLO'):")
pre13 = win(s13, None, "2024-01-01")
for y in (2021, 2022, 2023):
rest = pre13[pre13.index.year != y]
print(f" PRE-2024 togliendo il {y}: Sh {sh(rest):+5.2f} "
f"(intero pre-2024 {sh(pre13):+5.2f})")
loo = {}
for a_ in U13:
sub = panel19[[x for x in U13 if x != a_]].dropna()
bb = A.anchor_luck_band(lambda ph, sub=sub: win(alive(xsec_engine(sub, phase=ph,
min_assets=10)),
None, "2024-01-01"),
phases, canonical=0, metric=sh)
loo[a_] = bb["median"]
base_med = bands["U13-FIX PRE-2024"]["median"]
worst = min(loo, key=loo.get)
print(f" Leave-one-asset-out sul PRE-2024 (mediana di fase; base {base_med:+.2f}):")
print(" " + " ".join(f"-{k2}:{v:+.2f}" for k2, v in sorted(loo.items(), key=lambda t: t[1])))
print(f" gamba piu' importante: {worst} (senza di lei {loo[worst]:+.2f}); "
f"minimo {min(loo.values()):+.2f}, tutte >0: {all(v > 0 for v in loo.values())}")
# ---------------------------------------------------------- 8. i GATE
print("\n[8] GATE")
cand = s13.copy(); cand.index = cand.index.tz_convert("UTC")
mg = A.marginal_vs_tp01(cand)
print(f" marginal_vs_tp01 (U13-FIX, 2021+): {mg.get('marginal_verdict')} "
f"corr {mg.get('corr_full')} n={mg.get('n_days')}")
for kk in ("robust_oos", "has_insample_edge", "is_hedge", "beats_noise_null",
"insample_sharpe", "tp01_beta", "alpha_ann"):
if kk in mg:
print(f" {kk:<20s} {mg[kk]}")
if "blends" in mg:
for wname, bl in mg["blends"].items():
print(f" blend {wname}: uplift_full {bl.get('uplift_full')} "
f"uplift_hold {bl.get('uplift_hold')}")
mgE = A.marginal_vs_tp01(sEXP)
print(f" marginal_vs_tp01 (U19-EXP, 2020-12+): {mgE.get('marginal_verdict')} "
f"corr {mgE.get('corr_full')}")
# deflated Sharpe: TUTTI i trial valutati, al RIALZO
all_sr = []
for tag in ("U13-FIX FULL", "U13-FIX PRE-2024", "U19-EXP FULL", "U19-EXP PRE-2024"):
all_sr += list(bands[tag]["per_offset"].values())
all_sr += list(bhl["per_offset"].values()) + list(bsec["per_offset"].values())
for _, _, _, po in sel_rows: # 6 varianti x 10 fasi della sezione 7b
all_sr += list(po.values())
all_sr += [float(v) for v in loo.values()]
cand_pool = list(all_sr) # SOLO configurazioni di XS01
ctrl = [sh(alive(xsec_engine(p13, phase=0, mode="rev"))),
sh(alive(xsec_engine(panel19, phase=0, mode="rev"))), sh(jj["bin"]), sh(jj["hl"])]
for fs in (0.0005, 0.001, 0.002):
ctrl.append(sh(alive(xsec_engine(p13, phase=0, fee_side=fs))))
dsr_val = bands["U13-FIX PRE-2024"]["median"]
med_ph = int(np.argsort(list(bands["U13-FIX PRE-2024"]["per_offset"].values()))[len(phases) // 2])
med_series = win(alive(xsec_engine(p13, phase=med_ph)), None, "2024-01-01")
d1, s1 = A.deflated_sharpe(dsr_val, cand_pool, med_series)
d2, s2 = A.deflated_sharpe(dsr_val, cand_pool + ctrl, med_series)
print(f" deflated_sharpe — headline = MEDIANA di fase OOS {dsr_val:+.2f}")
print(f" pool A = SOLE configurazioni XS01 ({len(cand_pool)} trial): DSR {d1:.3f} "
f"(max dal rumore {s1:+.2f}) -> {'PASS' if d1 >= 0.95 else 'FAIL'}")
print(f" pool B = tutto cio' che ho valutato, controlli NEGATIVI inclusi "
f"({len(cand_pool)+len(ctrl)} trial): DSR {d2:.3f} (max dal rumore {s2:+.2f}) "
f"-> {'PASS' if d2 >= 0.95 else 'FAIL'}")
print( " ⚠️ Il pool B e' il conteggio 'al rialzo' che il progetto impone, ma qui e' anche")
print( " il piu' SEVERO in modo improprio: il DSR pesa la VARIANZA dei trial, e io ci")
print( " ho messo dentro controlli costruiti per essere negativi (REV -0.97, SECT11")
print( " -0.44). Si cita il piu' basso dei due; e si dice che entrambi penalizzano un")
print( " MEDIANA-di-fase come se fosse un MAX-di-k, che e' il caso per cui il DSR e'")
print( " costruito. Il debito di selezione vero di XS01 non e' stato contratto qui:")
print( " e' quello del 2026-06-19, e la sezione 7b lo misura direttamente.")
imp = A.implausible_sharpe(s13)
print(f" implausible_sharpe: {imp['implausible']} Sh {imp.get('sharpe'):+.2f} "
f"maxDD {imp.get('maxdd', 0)*100:.1f}% perdite su barre attive "
f"{imp.get('loss_frac', float('nan'))*100:.1f}% (attive {imp.get('active_frac', 0)*100:.0f}%)"
f"{' — ' + '; '.join(imp['reasons']) if imp['reasons'] else ''}")
# causalita': i prezzi DOPO la data di decisione non devono cambiare le posizioni prima
Pc = p13.copy()
cut = len(Pc) // 2
rngc = np.random.default_rng(7)
Pd = Pc.copy()
Pd.iloc[cut:] = Pc.iloc[cut:].values * (1.0 + rngc.normal(0, 0.35, size=Pc.iloc[cut:].shape))
a_ = xsec_engine(Pc, phase=0); b_ = xsec_engine(Pd, phase=0)
# confronto solo dove il vol-target non ha ancora "visto" i dati perturbati (rv30 shiftata)
lim = cut - 35
cdiff = float(np.max(np.abs(a_.values[:lim] - b_.values[:lim])))
print(f" causalita' (prezzi futuri randomizzati da i={cut}): max|diff| prima di i={lim} "
f"= {cdiff:.2e} -> {'LEAK-FREE' if cdiff < 1e-12 else 'PERDITA DI FUTURO'}")
print(" null de-levering: NON PERTINENTE — il claim qui non e' 'meno drawdown' ma "
"'l'edge esiste fuori campione'. Il de-levering e' comunque implicito nel vol-target,")
print(" e le tabelle di fase riportano DD e Sharpe INSIEME (un k<1 abbassa entrambi).")
print(f"\n sensibilita' al COSTO (il rischio #1 di uno sleeve a 13 gambe alt):")
for fs in (0.0005, 0.001, 0.002, 0.003):
x = alive(xsec_engine(p13, phase=0, fee_side=fs))
print(f" {fs*1e4:5.0f} bps/lato: FULL Sh {sh(x):+5.2f} | "
f"PRE-2024 {sh(win(x, None, '2024-01-01')):+5.2f} | "
f"2024+ {sh(win(x, '2024-01-01')):+5.2f}")
# ------------------------------------------------- 9. impatto sul LIBRO
print("\n[9] IMPATTO SUL LIBRO — TP01+SKH01 (75/25 live) con e senza XS01, su 2021+")
from src.portfolio.sleeves import _tp01_returns, _skyhook_returns
tp = A._to_daily(_tp01_returns()); sk = A._to_daily(_skyhook_returns())
for xs_tag, xs in (("XS01 storia lunga (U13-FIX)", s13), ("XS01 ufficiale (HL 2024+)", off)):
J = pd.concat({"tp": tp, "sk": sk, "xs": A._to_daily(xs)}, axis=1, join="inner").dropna()
if len(J) < 200:
continue
base = 0.75 * J["tp"] + 0.25 * J["sk"]
for w in (0.15, 0.25, 0.50):
mix = (1 - w) * base + w * J["xs"]
print(f" [{xs_tag}] {J.index[0].date()}..{J.index[-1].date()} n={len(J)} "
f"w={w:.2f}: Sh {sh(base):+.2f} -> {sh(mix):+.2f} "
f"({sh(mix)-sh(base):+.2f}) maxDD {dd(base)*100:.1f}% -> {dd(mix)*100:.1f}% "
f"corr(XS,libro) {J['xs'].corr(base):+.3f}")
# e il numero che il PROP-ALLOC usa davvero: il libro 2021+ contiene il 2022
pre = J[J.index < DISCOVERY]
if len(pre) > 200:
bpre = 0.75 * pre["tp"] + 0.25 * pre["sk"]
print(f" -> SOLO FUORI CAMPIONE ({pre.index[0].date()}..{pre.index[-1].date()}, "
f"n={len(pre)}), sweep del peso di XS01:")
for w in (0.15, 0.25, 0.33, 0.50):
mpre = (1 - w) * bpre + w * pre["xs"]
print(f" w={w:.2f}: libro Sh {sh(bpre):+.2f} -> {sh(mpre):+.2f} "
f"({sh(mpre)-sh(bpre):+.2f}) maxDD {dd(bpre)*100:.1f}% -> "
f"{dd(mpre)*100:.1f}% corr {pre['xs'].corr(bpre):+.3f}")
print(" ⚠️ PROP-ALLOC mette XS01 al 50%: su un conto a BARRIERA cio' che si")
print(" compra e' il maxDD, quindi la riga da guardare e' quella, non lo Sharpe.")
# ---------------------------------------------------- 10. eseguibilita'
print("\n[10] ESEGUIBILITA' — la domanda del canale funded e' se il libro STA su $100k")
det: dict = {}
xsec_engine(p13, phase=0, detail=det)
W, sc = det["W"], det["scale"]
dW = np.abs(np.diff(W, axis=0)); rb = np.flatnonzero(dW.sum(axis=1) > 1e-12)
print(f" {len(U13)} gambe, {len(rb)} ribilanciamenti, gross medio "
f"{np.abs(W).sum(axis=1).mean():.2f}, leva media (vol-target) {np.mean(sc[sc > 0]):.2f}x")
print(f" {'capitale':>10} {'ticket mediano':>16} {'ticket p10':>12} {'<$5':>7} {'<$10':>7}")
for cap in (600, 5000, 20000, 100000):
parts = [dW[i][dW[i] > 1e-12] * sc[i + 1] * cap for i in rb if sc[i + 1] > 0]
tk = np.concatenate(parts) if parts else np.array([])
if not tk.size:
print(" (nessun ticket calcolabile — controlla il motore)"); break
print(f" {cap:>10,d} {np.median(tk):>15.2f}$ {np.percentile(tk, 10):>11.2f}$ "
f"{(tk < 5).mean()*100:>6.1f}% {(tk < 10).mean()*100:>6.1f}%")
print(" ⚠️ Approssimazione DICHIARATA: il modello congelato applica il vol-target alla")
print(" SERIE dei rendimenti, non alle posizioni -> qui il ticket e' calcolato sui soli")
print(" delta di RIBILANCIAMENTO (ogni 10 giorni), ignorando il micro-turnover")
print(" giornaliero che una implementazione vera del vol-target genererebbe. E' un")
print(" LIMITE INFERIORE del numero di ordini, quindi ottimistico sul lato costi.")
print(" ⚠️ E NON rimette in discussione il ~$20k citato dal progetto: quella soglia e' sui")
print(" 19 alt Hyperliquid (min-order e liquidita' del venue), non su questi 13.")
print(f"\n[fine] {time.time()-t0:.1f}s")
if __name__ == "__main__":
main()