Files
PythagorasGoal/scripts/research/r0823_xs_ampiezza.py
T
Adriano Dal Pastro bf617b75ff research(xs-ampiezza): NO — la caduta di XS01 su 11 gambe non era ampiezza, e' selettivita' piu' un IC che li' vale zero
L'ampiezza effettiva (participation ratio) scende solo da 9,34 a 7,23 (-23%) mentre lo
Sharpe scende dell'86%: sotto IR = IC*sqrt(ampiezza) la sola ampiezza spiega il 14% del
divario, e NESSUNA delle costruzioni dichiarate la muove (7,13-8,47). Il demeaning — la
mossa che creo' XSR01 — e' un NO-OP ALGEBRICO su XS01, perche' i pesi sommano gia' a zero
per costruzione (max|sum w| = 2,8e-17, serie identiche a 2,2e-16): non e' un risultato
debole, e' un'identita' che vale a ogni universo e per sempre.

Il divario sta altrove, ed e' due cose. (a) SELETTIVITA': k=5 su 11 gambe tiene 10
posizioni su 11 e non seleziona niente; k=5 non e' un parametro invariante di scala, fu
tarato su A=19. Congelando il QUANTILE invece del numero la mediana delle 10 fasi passa
da -0,04 a +0,69 (delta appaiato +0,83, positivo in 10/10 fasi, plateau su k in {1,2,3},
mentre su 19 gambe k non conta) contro +1,15 dell'universo pieno. (b) IC: -0,027 (t -0,88)
sulle 11 contro +0,057 (t +2,29) sulle 19, sotto TUTTI i 40 sottoinsiemi casuali da 11 —
e sui 9 alt maturi presi da soli e' NEGATIVO (-0,065, t -2,07) mentre le 8 mancanti da
sole non ce l'hanno (+0,008). L'informazione vive nel CONTRASTO fra le fasce: XS01 e' in
buona parte una rotazione major-maturi contro alt-recenti, non una selezione dentro una
fascia. §50 lo chiamava "sfortuna di listino" al 9° pctl dello Sharpe: sull'IC il
sottoinsieme di Deribit e' fuori dalla banda, per una ragione strutturale.

E tutto il recupero sta sotto la risoluzione del campione, per quattro strade: MDE
dell'hold-out ~2,3 di Sharpe su 1,64 anni attivi; lo spread di coda che dovrebbe pagarlo
ha t 0,2-0,6 contro 1,91 dell'universo pieno; il null di permutazione a fee zero (p
de-luckato sulle 10 fasi: mediana 0,130, sotto 0,05 solo nel 40%) separa il candidato dal
rumore su 19 gambe e non su 11; il deflated-Sharpe sulle 80 celle dichiarate FALLISCE
(0,158, massimo atteso dal rumore 1,065 contro candidato 0,459).

Due errori catturati su me stesso. (1) La selezione in-sample-only NON e' stabile alla
dichiarazione della griglia: con k in {2,3,4,5} la cella al buio era resid k=2 (HOLD
+1,33), aggiungendo k=1 diventa white k=1 (HOLD -0,53) — con ~1 anno di in-sample
(SE(Sharpe) 1,7) la procedura onesta e' una monetina. (2) La prima misura di
eseguibilita' usava |dW| invece di |d(W*scale)| e dava "100% eseguito": la posizione in
dollari e' W*scale e il vol-target muove il nozionale ogni giorno anche a pesi fermi.
Corretta, riproduce le due popolazioni di §45 per via indipendente (11,9% degli ordini =
62,5% del nozionale). A $635 passa l'83% del nozionale: il muro non e' il capitale.

Venue letto ora: 14/19 quotate (SUI 18/08, APT 21/08 senza NESSUNA quota, AAVE 15/08),
11 da >=1 anno, lotti $0,01-$7,72. Solo 2 delle 11 gambe netterebbero col libro live.

Replica bit-exact contro sleeves._xsec_returns (max|dif| = 0,0); controlli positivi su
tutti e tre i rilevatori (ampiezza su matrici note, IC con un segnale che bara = +1,000,
null di permutazione che separa 19 da 11). Sola lettura, nessun ordine, output
riproducibile a meno del timestamp.

Libro, pesi, cron, config INVARIATI.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf
2026-08-23 03:02:41 +00:00

1110 lines
62 KiB
Python

"""XS-AMPIEZZA (§56) — su un universo POVERO, una costruzione cross-sectional puo' recuperare
AMPIEZZA EFFETTIVA senza aggiungere gambe? E quanto del divario 1,265 -> 0,116 recupera?
DA DOVE VIENE LA DOMANDA
------------------------
XS01 e' l'unico meccanismo che abbia mai sfondato il soffitto direzionale ~1,3 di questo progetto,
e sta fermo per VENUE: gira su Hyperliquid, che la decisione del 26/07 tiene fuori fino a $20k.
Poche ore fa §50 (`r0823_book_3rd.py`) ha misurato la versione eseguibile su Deribit — le 11 gambe
quotate da >=1 anno — e ha trovato che collassa (Sharpe 1,265 -> 0,116), separando due cause con un
null dei sottoinsiemi: il sottoinsieme MEDIANO da 11 fa gia' 0,548 (=> "il 62% della caduta e'
AMPIEZZA") e quello di Deribit sta al 9° percentile (=> "sfortuna di listino").
E c'e' un precedente che punta nella stessa direzione: il 25/07 XSR01 nacque dalla DIAGNOSI di un
fallimento — l'ampiezza effettiva di un paniere di 50 coppie era 4,5 perche' condividevano la gamba
BTC; il demeaning cross-sezionale la porto' a 37,4 e lo Sharpe da 0,82 a 1,82. L'ampiezza effettiva
non e' il numero di gambe: e' il numero di scommesse indipendenti, e si puo' guadagnare senza
aggiungere strumenti.
ATTESA A PRIORI — DICHIARATA PRIMA DI MISURARE (coordinatore + mia)
------------------------------------------------------------------
(A1) Il 62% della caduta e' ampiezza, e l'ampiezza NON si compra con la costruzione: il demeaning
funziono' su 50 gambe perche' c'era un fattore comune grosso da togliere; su 11 c'e' molto
meno da togliere e il recupero sara' parziale e SOTTO l'MDE.
(A2) Il gate e' N_eff: se N_eff delle 11 e' gia' vicino a 11, non c'e' niente da recuperare.
(A3) Mi aspetto che il muro NON sia l'eseguibilita' (nell'ondata del 22/08 non ha bocciato niente
per sei volte di fila) ma la storia corta: 2,6 anni, in-sample = il solo 2024.
(A4) I due affinamenti del 19/06 (blend [30,90] e gate p30) non si replicano fuori campione
(misurato il 22/08): mi aspetto che il meccanismo NUDO non sia peggiore.
ESITO (scritto dopo, per onesta' di lettura): A1 giusta nella CONCLUSIONE e SBAGLIATA nel
MECCANISMO — l'ampiezza non e' quasi per niente il problema; A2 confermata; A3 confermata;
A4 confermata sul FULL. E l'etichetta "ampiezza" di §50 e' cio' che questo filone corregge.
LA FAMIGLIA, DICHIARATA PRIMA (e contata AL RIALZO nel deflated-Sharpe)
----------------------------------------------------------------------
Griglia cartesiana su universo 11-Deribit, 80 celle:
kind in {canon, resid (residuo vs BTC prima del ranking), ivol (equal-risk),
white (whitening inv-cov con shrinkage 50%)} 4
k in {1,2,3,4,5} (k=5 e' il canonico, tarato su A=19) 5
lookbacks in {(30,90) col gate di dispersione p30, (30,) NUDO senza gate} 2
H in {10, 20} (holding piu' lungo) 2
Il DEMEANING cross-sezionale — la costruzione che creo' XSR01 — e' fuori griglia perche' su XS01
e' un NO-OP ALGEBRICO, e questo si dimostra invece di misurarlo (sezione 3).
METODO
------
* ampiezza effettiva: DUE definizioni, perche' quella importata degenera qui (sezione 3);
* de-luck di FASE: XS01 ha un ciclo H=10 e i suoi numeri headline sono sulla fase 0, che
CLAUDE.md registra come fortunata -> ogni confronto e' la MEDIANA DELLE DIFFERENZE APPAIATE
sulle 10 fasi, mai la differenza delle mediane (lezione 26/07);
* null di PERMUTAZIONE cross-sezionale a FEE ZERO (25/07: permutare un segnale ne fa esplodere il
turnover, e un null a fee piena perderebbe per COSTO invece che per assenza d'informazione);
* gate onesto coi TRE componenti reali di `study_family_honest` (selezione in-sample-only ->
`altlib.deflated_sharpe` -> `altlib.marginal_vs_tp01`), importati, non riscritti;
* MDE dichiarato in barre ATTIVE, non in giorni di calendario;
* controlli positivi obbligatori su OGNI rilevatore (un rilevatore che trova ~0 e non e' validato
e' indistinguibile da uno rotto).
VINCOLI DI ESECUZIONE: sola lettura, nessun ordine, solo GET pubbliche Deribit con pacing e
astensione nei minuti :05-:10 e :24-:30 (finestre del cron di produzione).
uv run python scripts/research/r0823_xs_ampiezza.py # completo
uv run python scripts/research/r0823_xs_ampiezza.py --no-net # solo cache del venue
"""
from __future__ import annotations
import argparse
import datetime as dt
import itertools
import json
import math
import sys
import time
import urllib.request
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import spearmanr
ROOT = Path("/opt/docker/PythagorasGoal")
for _p in (str(ROOT), str(ROOT / "scripts" / "research"),
str(ROOT / "scripts" / "research" / "alt")):
if _p not in sys.path:
sys.path.insert(0, _p)
# --- produzione (importata, MAI riscritta) ---------------------------------------------------
from src.portfolio import sleeves as SL # noqa: E402
from src.portfolio.portfolio import HOLDOUT, metrics, to_daily # noqa: E402
# --- macchineria di ricerca gia' validata (importata, non riscritta) -------------------------
import altlib as al # noqa: E402
from r0725_statarb_basket_gate import eff_breadth as XSR_EFF_BREADTH # noqa: E402
# =============================================================================================
# COSTANTI DICHIARATE
# =============================================================================================
SCRATCH = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/"
"b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/xsamp")
if not SCRATCH.parent.exists():
SCRATCH = Path("/tmp/pythagoras_xsamp")
API = "https://www.deribit.com/api/v2/public"
CACHE_FRESH_S = 3 * 3600
SEED = 20260823
N_PHASES = 10 # ciclo H=10 di XS01
N_SUBSETS = 40 # sottoinsiemi casuali da 11 fra le 19
PH_SUB = (0, 2, 4, 6, 8) # fasi su cui si de-lucka il null dei sottoinsiemi
N_PERM = 200 # permutazioni del null a fee zero
CAPITAL_TODAY = 635.0
CAP_LADDER = (635.0, 2000.0, 5000.0, 20000.0)
MIN_ORDER = 5.0 # config/live.json -> min_order_usd
W_SLEEVE = 0.15 # il peso a cui §50 ha valutato ogni candidato a 3° sleeve
FEE_SIDE = al.FEE_SIDE # 0.05%/lato, il default del progetto
ANN = math.sqrt(365.25)
T0 = time.time()
_ARGS = None
XS_UNIVERSE = list(SL.XS_UNIVERSE) # i 19 major, LETTI dalla produzione
CFG = dict(SL.XS_CFG) # config canonica, LETTA dalla produzione
def sez(t: str) -> None:
print("\n" + "=" * 104)
print(f" {t}")
print("=" * 104)
def sub(t: str) -> None:
print("\n" + "-" * 104)
print(f" {t}")
print("-" * 104)
def clock() -> str:
return f"[{time.time() - T0:5.0f}s]"
# =============================================================================================
# RETE — sole GET pubbliche, pacing, astensione nelle finestre del cron
# =============================================================================================
def _pace() -> None:
while dt.datetime.now(dt.timezone.utc).minute in (5, 6, 7, 8, 9, 10, 24, 25, 26, 27, 28, 29, 30):
time.sleep(15)
time.sleep(0.55) # <= 2 req/s
def get_json(path: str, cache_name: str) -> dict | None:
SCRATCH.mkdir(parents=True, exist_ok=True)
fp = SCRATCH / cache_name
if _ARGS and _ARGS.no_net:
return json.loads(fp.read_text()) if fp.exists() else None
if fp.exists() and (time.time() - fp.stat().st_mtime) < CACHE_FRESH_S:
return json.loads(fp.read_text())
try:
_pace()
with urllib.request.urlopen(f"{API}/{path}", timeout=30) as r:
d = json.loads(r.read())
fp.write_text(json.dumps(d))
return d
except Exception as e: # noqa: BLE001
print(f" [rete KO su {path}: {type(e).__name__}] -> cache")
return json.loads(fp.read_text()) if fp.exists() else None
# =============================================================================================
# IL MOTORE — replica di `sleeves._xsec_returns` generalizzata a (universo, fase, costruzione)
# =============================================================================================
def prices(universe: list[str]) -> pd.DataFrame:
cols = {}
for sym in universe:
p = SL._HL_DIR / 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()
def run(universe: list[str], kind: str = "canon", k: int | None = None, H: int | None = None,
lookbacks=None, disp_pct: int | None = None, phase: int = 0,
tv: float | None = None, fee: float = FEE_SIDE, demean: bool = False) -> dict:
"""Il meccanismo XS01, un grado di liberta' per volta. Coi default e' BIT-EXACT contro
`sleeves._xsec_returns` (verificato nella sezione 2)."""
k = CFG["k"] if k is None else k
H = CFG["H"] if H is None else H
lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks
disp_pct = CFG["disp_pct"] if disp_pct is None else disp_pct
tv = CFG["target_vol"] if tv is None else tv
minhist = CFG["disp_minhist"]
C = prices(universe)
px = C.values
n, A = px.shape
kk = min(k, A // 2)
mlb = max(lookbacks)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
vol = pd.DataFrame(dret, index=C.index).rolling(30, min_periods=15).std().shift(1).values
bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0
W = np.zeros((n, A)); w = np.zeros(A); disp_hist: list[float] = []
for i in range(n):
if i >= mlb and i % H == phase:
rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks]
if kind == "resid":
# beta OLS causale su 90 barre di ritorni; il momentum si ranka sul RESIDUO vs BTC
seg = dret[max(1, i - 90):i + 1]
b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12)
for j in range(A)])
rLs = [rL - b * rL[bi] for rL in rLs]
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(A); cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if cnt:
score /= cnt
order = np.argsort(score)
w = np.zeros(A); lo, hi = order[:kk], order[-kk:]
w[hi] = 0.5 / kk; w[lo] = -0.5 / kk
if kind == "ivol": # equal-risk: pesi ~ 1/vol
v = np.nan_to_num(vol[i], nan=0.0)
w = w * np.where(v > 0, 1.0 / v, 0.0)
w = w - w.mean() # 1/vol rompe sum(w)=0: ri-centrare
g = np.abs(w).sum(); w = w / g if g > 0 else w
elif kind == "white": # whitening: w ~ Sigma^-1 z
seg = dret[max(1, i - 90):i + 1]
S = np.cov(seg.T)
S = 0.5 * S + 0.5 * np.diag(np.diag(S)) # shrinkage dichiarata
z = np.zeros(A); z[hi] = 1.0 / kk; z[lo] = -1.0 / kk
try:
w = np.linalg.solve(S + 1e-12 * np.eye(A), z)
except Exception: # noqa: BLE001
pass
w = w - w.mean()
g = np.abs(w).sum(); w = w / g if g > 0 else w
else:
w = np.zeros(A)
if demean:
w = w - w.mean()
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
s = pd.Series(net, index=C.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)
return dict(net=to_daily(pd.Series(s.values * scale, index=C.index)),
W=W, dret=dret, scale=scale, cols=list(C.columns), idx=C.index)
def leg_pnl(R: dict, fee: float = FEE_SIDE) -> pd.DataFrame:
"""P&L NETTA per gamba: e' l'oggetto su cui si misura l'ampiezza effettiva (le 'scommesse')."""
W, dret, scale = R["W"], R["dret"], R["scale"]
n, A = W.shape
g = np.zeros((n, A)); g[1:] = W[:-1] * dret[1:]
t = np.abs(np.diff(W, axis=0, prepend=0.0))
return pd.DataFrame((g - t * fee) * scale[:, None], index=R["idx"], columns=R["cols"])
def sh(s: pd.Series) -> float:
return float(metrics(s)["sharpe"])
def sh_hold(s: pd.Series) -> float:
return float(metrics(s[s.index >= HOLDOUT])["sharpe"])
def sh_ins(s: pd.Series) -> float:
return float(metrics(s[s.index < HOLDOUT])["sharpe"])
# =============================================================================================
# AMPIEZZA EFFETTIVA — due definizioni, perche' la prima degenera
# =============================================================================================
def neff_rbar(M: pd.DataFrame) -> tuple[float, float]:
"""La definizione usata da XSR01 (`r0725_statarb_basket_gate.eff_breadth`):
N / (1 + (N-1)*rbar) con rbar = correlazione media fuori diagonale."""
Cm = np.nan_to_num(M.corr().values, nan=0.0)
off = Cm[~np.eye(len(Cm), dtype=bool)]
rbar = float(np.nanmean(off)); N = len(Cm)
ne = N / (1.0 + (N - 1) * rbar) if rbar > -1.0 / (N - 1) else float(N)
return ne, rbar
def neff_eig(M: pd.DataFrame) -> float:
"""Participation ratio degli autovalori: (sum lam)^2 / sum lam^2. Sta SEMPRE in [1, N]."""
Cm = np.nan_to_num(M.corr().values, nan=0.0)
lam = np.clip(np.linalg.eigvalsh(Cm), 0.0, None)
return float(lam.sum() ** 2 / max((lam ** 2).sum(), 1e-12))
# =============================================================================================
# 1) CENSIMENTO DAL VENUE — quali gambe esistono DAVVERO, e a che lotto
# =============================================================================================
def venue_census() -> dict:
out: dict = {"perp": {}, "read_ok": True}
ins = {}
for cur in ("BTC", "ETH", "USDC"):
d = get_json(f"get_instruments?currency={cur}&expired=false", f"instr_{cur}.json")
if d is None:
out["read_ok"] = False
continue
ins[cur] = d["result"]
bs = get_json("get_book_summary_by_currency?currency=USDC&kind=future", "bs_usdc_fut.json")
marks = {}
if bs:
for x in bs["result"]:
p = x.get("mark_price") or x.get("last")
if p:
marks[x["instrument_name"]] = (float(p), float(x.get("volume_usd") or 0.0),
x.get("bid_price"), x.get("ask_price"))
for _cur, lst in ins.items():
for x in lst:
if x["kind"] != "future" or x.get("settlement_period") != "perpetual":
continue
n = x["instrument_name"]
mk, vol, bid, ask = marks.get(n, (None, None, None, None))
out["perp"][x["base_currency"]] = dict(
instr=n, min_amt=float(x["min_trade_amount"]),
created=dt.datetime.fromtimestamp(x["creation_timestamp"] / 1000,
dt.timezone.utc).date().isoformat(),
mark=mk, min_usd=(mk * float(x["min_trade_amount"])) if mk else None,
vol24=vol, bid=bid, ask=ask)
return out
def sezione1(V: dict) -> dict:
sez("1) IL VENUE, LETTO ORA — quali delle 19 gambe di XS01 esistono su Deribit (perp USDC)")
if not V["read_ok"]:
print(" ⚠️ lista strumenti NON letta dal venue in questa corsa: si usa la cache.")
oggi = dt.date.today()
print(f" {'asset':<7}{'strumento':<26}{'quotato dal':<13}{'eta' + chr(39) + ' (g)':>10}"
f"{'lotto min':>11}{'vol 24h $':>15}{'quote':>10}")
ok, stab = [], []
for s in XS_UNIVERSE:
p = V["perp"].get(s)
if not p:
print(f" {s:<7}{'— non quotato —':<26}{'—':<13}{'—':>10}{'—':>11}{'—':>15}{'—':>10}")
continue
ok.append(s)
eta = (oggi - dt.date.fromisoformat(p["created"])).days
if eta >= 365:
stab.append(s)
q = "bid+ask" if (p["bid"] and p["ask"]) else ("SOLO 1 LATO" if (p["bid"] or p["ask"])
else "NESSUNA")
mu = f"${p['min_usd']:.2f}" if p["min_usd"] else "n/d"
v = f"{p['vol24']:,.0f}" if p["vol24"] is not None else "n/d"
print(f" {s:<7}{p['instr']:<26}{p['created']:<13}{eta:>10}{mu:>11}{v:>15}{q:>10}")
print(f"\n -> quotate: {len(ok)}/19 ({', '.join(ok)})")
print(f" -> quotate da >= 1 anno: {len(stab)}/19 ({', '.join(stab)})")
print(f" -> MANCANTI: {', '.join([s for s in XS_UNIVERSE if s not in ok])}")
print(" 📌 Il lotto minimo NON e' il vincolo: il piu' caro sta sotto la decina di dollari.")
print(" E' la stessa constatazione dell'ondata 22/08 (l'eseguibilita' a $600 non ha")
print(" bocciato niente per sei volte di fila). Il muro, se c'e', e' l'edge.")
return dict(quotate=ok, stabili=stab)
# =============================================================================================
# 2) REPLICHE E CONTROLLI POSITIVI — prima di ogni delta
# =============================================================================================
def sezione2(stab: list[str]) -> None:
sez("2) REPLICHE BIT-EXACT E CONTROLLI POSITIVI (un rilevatore non validato e' indistinguibile"
" da uno rotto)")
sub("2a) il mio motore contro lo sleeve di PRODUZIONE")
mine = run(XS_UNIVERSE)["net"]
ref = to_daily(SL._xsec_returns())
j = pd.concat({"a": mine, "b": ref}, axis=1, join="outer")
dm = float(np.nanmax(np.abs(j["a"].values - j["b"].values)))
print(f" run(19 gambe, canonico, fase 0) vs sleeves._xsec_returns : max|dif| = {dm:.3e}"
f" {'OK' if dm < 1e-12 else 'FAIL'}")
assert dm < 1e-12, "replica NON bit-exact: tutto il resto misurerebbe un'altra strategia"
print(f" Sharpe FULL 19 gambe {sh(mine):.3f} · HOLD {sh_hold(mine):.3f} "
f"(§50 riporta 1,265 col drag di funding dichiarato: qui il funding NON c'e', perche'")
print(" la domanda e' sulla STRUTTURA del segnale e il drag e' una costante additiva.)")
sub("2b) CONTROLLO POSITIVO dello stimatore di AMPIEZZA (deve dare 1 e N sui casi noti)")
rng = np.random.default_rng(SEED)
ind = pd.DataFrame(rng.normal(size=(900, 11)))
one = pd.DataFrame(np.tile(rng.normal(size=(900, 1)), (1, 11)))
hlf = ind.copy()
common = rng.normal(size=900)
hlf = pd.DataFrame(0.7 * np.tile(common[:, None], (1, 11)) + 0.7 * ind.values)
for nm, M, atteso in (("11 gambe INDIPENDENTI", ind, "~11"),
("11 gambe IDENTICHE", one, "1"),
("11 gambe con fattore comune 50%", hlf, "fra 1 e 11")):
ne_r, rb = neff_rbar(M)
print(f" {nm:<34} rbar {rb:>+7.3f} N_eff(rbar) {ne_r:>8.2f} "
f"N_eff(autovalori) {neff_eig(M):>6.2f} atteso {atteso}")
print(" -> entrambi gli stimatori passano il controllo; la differenza fra i due si vede")
print(" solo quando rbar e' NEGATIVA, ed e' esattamente il caso di XS01 (sezione 3).")
sub("2c) CONTROLLO POSITIVO dello stimatore di IC (con un segnale che BARA deve dare ~+1)")
def _t(m, se):
return "t inf (SE=0: IC costante)" if se <= 0 else f"t {m/se:+.1f}"
m, se, n = ic_stats(XS_UNIVERSE, cheat=True)
print(f" IC di un punteggio = ritorno FUTURO (look-ahead deliberato): {m:+.4f} "
f"({_t(m, se)}, n={n}) -> lo stimatore ha potenza")
m0, se0, n0 = ic_stats(XS_UNIVERSE, shuffle_seed=SEED)
print(f" IC di un punteggio PERMUTATO a caso : {m0:+.4f} "
f"({_t(m0, se0)}, n={n0}) -> e non inventa segnale dove non c'e'")
sub("2d) identita' con la funzione importata da XSR01")
R = run(stab, k=2)
L = leg_pnl(R)
P = pd.DataFrame(R["W"], index=R["idx"], columns=R["cols"])
S = pd.DataFrame(R["dret"], index=R["idx"], columns=R["cols"])
ne_mine, _ = neff_rbar(L)
print(f" la mia `neff_rbar` e la `eff_breadth` di r0725_statarb_basket_gate implementano la")
print(f" STESSA formula N/(1+(N-1)*rbar); qui do' {ne_mine:.2f} sulla P&L per gamba di XS01.")
print(f" (`eff_breadth` non e' chiamabile direttamente su XS01: prende matrici di posizione e")
print(f" di ritorno-SPREAD di COPPIE, e XS01 non e' un paniere di coppie — vedi sezione 3.)")
_ = (P, S, XSR_EFF_BREADTH)
# =============================================================================================
# IC — l'informazione del segnale, misurata direttamente
# =============================================================================================
def ic_stats(universe: list[str], lookbacks=None, H: int | None = None, resid: bool = False,
cheat: bool = False, shuffle_seed: int | None = None,
every_day: bool = True, phase: int = 0):
"""Rank-IC fra il punteggio e il ritorno a H giorni in avanti, con SE a blocchi (30 barre).
every_day=True usa TUTTI i giorni (osservazioni sovrapposte -> la SE a blocchi ne tiene conto)
perche' sui soli giorni di ribilanciamento le osservazioni sono 87 e non decidono niente."""
lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks
H = CFG["H"] if H is None else H
C = prices(universe); px = C.values; n, A = px.shape
mlb = max(lookbacks)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0
rng = np.random.default_rng(shuffle_seed) if shuffle_seed is not None else None
ics = []
for i in range(mlb, n - H):
if not every_day and i % H != phase:
continue
fwd = px[i + H] / px[i] - 1.0
if cheat:
score = fwd.copy()
else:
rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks]
if resid:
seg = dret[max(1, i - 90):i + 1]
b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12)
for j in range(A)])
rLs = [rL - b * rL[bi] for rL in rLs]
score = np.zeros(A); cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if not cnt:
continue
score /= cnt
if rng is not None:
score = score[rng.permutation(A)]
ics.append(spearmanr(score, fwd).statistic)
x = np.asarray(ics, float)
return float(x.mean()), block_se(x), len(x)
def block_se(x: np.ndarray, bl: int = 30, B: int = 500, seed: int = SEED) -> float:
rng = np.random.default_rng(seed); n = len(x); nb = int(np.ceil(n / bl)); out = []
for _ in range(B):
st = rng.integers(0, max(1, n - bl), size=nb)
out.append(np.concatenate([x[s:s + bl] for s in st])[:n].mean())
return float(np.std(out, ddof=1))
def tail_spread(universe: list[str], k: int, resid: bool = False, lookbacks=None,
H: int | None = None):
"""Rendimento a H giorni dei top-k MENO quello dei bottom-k: e' cio' che il portafoglio
incassa davvero (l'IC su tutta la sezione puo' essere 0 e le CODE portare informazione)."""
lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks
H = CFG["H"] if H is None else H
C = prices(universe); px = C.values; n, A = px.shape
mlb = max(lookbacks); kk = min(k, A // 2)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0
d = []
for i in range(mlb, n - H):
rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks]
if resid:
seg = dret[max(1, i - 90):i + 1]
b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12)
for j in range(A)])
rLs = [rL - b * rL[bi] for rL in rLs]
score = np.zeros(A); cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if not cnt:
continue
score /= cnt
o = np.argsort(score); f = px[i + H] / px[i] - 1.0
d.append(float(f[o[-kk:]].mean() - f[o[:kk]].mean()))
x = np.asarray(d, float)
return x.mean(), block_se(x), len(x)
def z_spread(universe: list[str], k: int) -> float:
"""Distanza media, in z-score, fra il gruppo long e il gruppo short: la SELETTIVITA'."""
C = prices(universe); px = C.values; n, A = px.shape
lookbacks = CFG["lookbacks"]; mlb = max(lookbacks); kk = min(k, A // 2)
out = []
for i in range(mlb, n):
rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks]
score = np.zeros(A); cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if not cnt:
continue
score /= cnt; o = np.argsort(score)
out.append(float(score[o[-kk:]].mean() - score[o[:kk]].mean()))
return float(np.mean(out))
# =============================================================================================
# 3) IL GATE — l'ampiezza effettiva
# =============================================================================================
def sezione3(stab: list[str], quot: list[str]) -> dict:
sez("3) IL GATE — AMPIEZZA EFFETTIVA. Se le 11 gambe ne hanno gia' ~11, non c'e' niente da"
" recuperare")
sub("3a) prima di misurare: il DEMEANING (la costruzione che creo' XSR01) qui e' un NO-OP"
" ALGEBRICO")
R = run(stab)
msum = float(np.abs(R["W"].sum(axis=1)).max())
Rd = run(stab, demean=True)
dmx = float(np.nanmax(np.abs(R["net"].values - Rd["net"].values)))
print(f" XS01 mette +0,5/k sui top-k e -0,5/k sui bottom-k => sum_i w_i = 0 PER COSTRUZIONE.")
print(f" max|sum_i w_i| su tutte le barre : {msum:.3e}")
print(f" max|serie demeanata - serie canonica| : {dmx:.3e} -> IDENTICHE")
print(" 📌 XSR01 nacque perche' 50 coppie condividevano la gamba BTC: li' c'era un fattore")
print(" comune GROSSO nelle POSIZIONI, e toglierlo cambiava il portafoglio. XS01 e' gia'")
print(" dollar-neutral esatto: il demeaning non ha niente da togliere. Non e' un risultato")
print(" debole, e' un'IDENTITA' — e vale a ogni universo, a ogni k, per sempre.")
sub("3b) LA TABELLA CHE DECIDE — N_eff della P&L per gamba")
print(f" {'universo / costruzione':<34}{'gambe':>7}{'rbar':>9}{'N_eff rbar':>13}"
f"{'N_eff autoval.':>16}{'Sh FULL':>10}")
rows = [("19 HL (canonico)", XS_UNIVERSE, dict()),
(f"{len(quot)} quotate Deribit (canonico)", quot, dict()),
(f"{len(stab)} Deribit >=1 anno (canonico)", stab, dict()),
(f"{len(stab)} Deribit — demean", stab, dict(demean=True)),
(f"{len(stab)} Deribit — resid vs BTC", stab, dict(kind="resid")),
(f"{len(stab)} Deribit — equal-risk", stab, dict(kind="ivol")),
(f"{len(stab)} Deribit — whitening", stab, dict(kind="white")),
(f"{len(stab)} Deribit — nudo L=30", stab, dict(lookbacks=(30,), disp_pct=0)),
(f"{len(stab)} Deribit — k=2", stab, dict(k=2)),
(f"{len(stab)} Deribit — H=20", stab, dict(H=20))]
out = {}
for nm, u, kw in rows:
r = run(u, **kw); L = leg_pnl(r)
ne_r, rb = neff_rbar(L); ne_e = neff_eig(L)
out[nm] = dict(neff=ne_e, sharpe=sh(r["net"]))
print(f" {nm:<34}{L.shape[1]:>7}{rb:>+9.3f}{ne_r:>13.2f}{ne_e:>16.2f}{sh(r['net']):>10.3f}")
print("\n ⚠️ LA FORMULA IMPORTATA DA XSR01 DEGENERA QUI, e va detto invece di usarla: con rbar")
print(" NEGATIVA, N/(1+(N-1)*rbar) esplode sopra N (fino a ~33 su 11 gambe). Non e' un")
print(" errore di quella formula: e' che su un paniere di COPPIE rbar e' positiva (fattore")
print(" comune), mentre le gambe di un long/short cross-sezionale sono ANTI-correlate per")
print(" costruzione. Da qui in poi si legge il PARTICIPATION RATIO degli autovalori, che")
print(" sta sempre in [1, N] ed e' la definizione che il briefing nomina per prima.")
n19 = out["19 HL (canonico)"]["neff"]; n11 = out[f"{len(stab)} Deribit >=1 anno (canonico)"]["neff"]
s19 = out["19 HL (canonico)"]["sharpe"]; s11 = out[f"{len(stab)} Deribit >=1 anno (canonico)"]["sharpe"]
pred = s19 * math.sqrt(n11 / n19)
print(f"\n 📌 RISPOSTA AL GATE: N_eff passa da {n19:.2f} (19 gambe) a {n11:.2f} (11 gambe) = "
f"-{100*(1-n11/n19):.0f}%,")
print(f" mentre lo Sharpe passa da {s19:.3f} a {s11:.3f} = -{100*(1-s11/s19):.0f}%.")
print(f" Sotto la legge fondamentale (IR = IC * sqrt(ampiezza)) la sola perdita di ampiezza")
print(f" predirebbe {pred:.3f}, cioe' {100*(s19-pred)/(s19-s11):.0f}% del divario. Il resto — "
f"l'{100-100*(s19-pred)/(s19-s11):.0f}% — NON e' ampiezza.")
print(" E NESSUNA costruzione la muove: tutte le righe stanno in una banda strettissima.")
print(" ⇒ Il filone COME POSTO si chiude qui: non c'e' ampiezza da recuperare. La sezione 4")
print(" misura dove sta davvero il divario, perche' 'non e' quello' non e' una diagnosi.")
return out
# =============================================================================================
# 4) DOVE STA DAVVERO IL DIVARIO — selettivita' e IC
# =============================================================================================
def sezione4(stab: list[str], quot: list[str]) -> dict:
sez("4) ALLORA DOV'E'? Due canali misurabili: SELETTIVITA' (quanto il ranking separa) e IC"
" (quanto il segnale sa)")
sub("4a) SELETTIVITA' — con k=5 su 11 gambe si tengono 10 posizioni su 11: non si seleziona")
print(f" {'universo':<22}{'A':>4}{'k':>4}{'posizioni tenute':>19}{'spread z long-short':>22}")
for nm, u in (("19 HL", XS_UNIVERSE), (f"{len(stab)} Deribit", stab)):
for k in (5, 3, 2):
A = len(prices(u).columns); kk = min(k, A // 2)
print(f" {nm:<22}{A:>4}{k:>4}{f'{2*kk}/{A} = {2*kk/A:.0%}':>19}{z_spread(u, k):>22.3f}")
print(" 📌 `k=5` NON e' un parametro invariante di scala: fu tarato su A=19, dove seleziona il")
print(" 26% per lato. Su A=11 lo stesso 5 e' il 45% per lato. Trasferire il meccanismo a un")
print(" universo nuovo COI PARAMETRI CONGELATI (regola SOL, 22/08) qui vuol dire congelare")
print(" il QUANTILE, non il numero — ed e' una distinzione che cambia il risultato.")
sub("4b) IC — il segnale sa qualcosa, su queste 11 gambe? (rank-IC contro il ritorno a 10g)")
print(f" {'universo':<34}{'IC':>10}{'SE blocchi':>13}{'t':>8}{'n':>7}")
ic = {}
for nm, u in ((f"19 HL (l'universo di XS01)", XS_UNIVERSE),
(f"{len(quot)} quotate Deribit", quot),
(f"{len(stab)} Deribit >=1 anno", stab),
(f"9 alt maturi (11 senza BTC/ETH)", [s for s in stab if s not in ("BTC", "ETH")]),
(f"8 gambe MANCANTI da sole", [s for s in XS_UNIVERSE if s not in stab])):
m, se, n = ic_stats(u); ic[nm] = (m, se)
print(f" {nm:<34}{m:>+10.4f}{se:>13.4f}{m/se:>8.2f}{n:>7}")
print("\n 📌 QUESTO e' il risultato del filone. Sulle 11 gambe che Deribit quota da un anno il")
print(" segnale NON ha informazione, e sui 9 alt maturi presi da soli e' NEGATIVO. Le 8")
print(" mancanti da sole non ce l'hanno nemmeno loro: l'informazione vive nel CONTRASTO fra")
print(" i due gruppi. XS01 e', in buona parte, una rotazione fra FASCE (major maturi contro")
print(" alt recenti ad alta dispersione) — non una selezione DENTRO una fascia.")
print(" ⇒ Nessuna costruzione puo' recuperare informazione che nel campione non c'e'.")
sub("4c) e non e' 'sfortuna di listino' dentro una banda: e' FUORI dalla banda")
rng = np.random.default_rng(SEED)
subs = [list(rng.choice(XS_UNIVERSE, size=len(stab), replace=False)) for _ in range(N_SUBSETS)]
ics = np.array([ic_stats(u)[0] for u in subs])
mine = ic_stats(stab)[0]
pct = 100.0 * (ics < mine).mean()
print(f" IC di {N_SUBSETS} sottoinsiemi CASUALI da {len(stab)} gambe fra le 19:")
print(f" mediana {np.median(ics):+.4f} [p10 {np.percentile(ics,10):+.4f}, "
f"p90 {np.percentile(ics,90):+.4f}] min {ics.min():+.4f}")
print(f" il sottoinsieme DERIBIT: {mine:+.4f} -> {pct:.0f}° percentile")
print(" 📌 §50 lo chiamava 'sfortuna di listino' e lo collocava al 9° percentile dello SHARPE.")
print(" Sull'IC — che e' la grandezza di cui lo Sharpe e' una conseguenza — sta sotto")
print(f" l'INTERO campione. Non e' una coda della distribuzione: le {len(stab)} gambe quotate da")
print(" un anno sono i major maturi, cioe' esattamente il sottoinsieme meno disperso.")
print(" Aspettare 2-3 listing non basta, e adesso si sa PERCHE'.")
return dict(ic=ic, subset_ics=ics, ic_deribit=mine)
# =============================================================================================
# 5) LE COSTRUZIONI — la griglia dichiarata, de-luckata di fase
# =============================================================================================
KINDS = ("canon", "resid", "ivol", "white")
KS = (1, 2, 3, 4, 5)
LBS = ((30, 90), (30,))
HS = (10, 20)
def grid_cells():
for kind, k, lb, H in itertools.product(KINDS, KS, LBS, HS):
yield dict(kind=kind, k=k, lookbacks=lb, H=H,
disp_pct=(CFG["disp_pct"] if lb == (30, 90) else 0))
def band(u: list[str], **kw) -> np.ndarray:
return np.array([sh(run(u, phase=p, **kw)["net"]) for p in range(N_PHASES)])
def sezione5(stab: list[str]) -> dict:
sez("5) LE COSTRUZIONI — griglia DICHIARATA (80 celle), e la banda di FASE che le de-lucka")
sub("5a) la griglia alla fase canonica (fase 0) — e perche' non ci si puo' fermare qui")
cells = list(grid_cells())
rows = []
for c in cells:
s = run(stab, **c)["net"]
rows.append(dict(cell=c, ins=sh_ins(s), hold=sh_hold(s), full=sh(s), series=s))
fulls = np.array([r["full"] for r in rows])
print(f" {len(cells)} celle: FULL mediana {np.median(fulls):+.3f} max {fulls.max():+.3f} "
f"sd {fulls.std(ddof=1):.3f}")
best_is = max(rows, key=lambda r: r["ins"])
best_ho = max(rows, key=lambda r: r["hold"])
print(f"\n {'scelta':<22}{'cella':<44}{'IS':>8}{'HOLD':>8}{'FULL':>8}")
for nm, r in (("IN-SAMPLE-ONLY (onesta)", best_is), ("sull'HOLD-OUT (vietata)", best_ho)):
c = r["cell"]
lab = "%s k=%d lb=%s H=%d" % (c["kind"], c["k"], c["lookbacks"], c["H"])
print(f" {nm:<22}{lab:<44}{r['ins']:>8.3f}{r['hold']:>8.3f}{r['full']:>8.3f}")
sub("5b) BANDA DI FASE — la lente onesta di XS01 (ciclo H=10; i numeri headline sono sulla"
" fase 0, che CLAUDE.md registra come fortunata)")
casi = {
"canonico k=5 (11)": (stab, dict()),
"k=2 (11)": (stab, dict(k=2)),
"k=3 (11)": (stab, dict(k=3)),
"resid k=5 (11)": (stab, dict(kind="resid")),
"resid k=2 (11)": (stab, dict(kind="resid", k=2)),
"equal-risk k=2 (11)": (stab, dict(kind="ivol", k=2)),
"whitening k=2 (11)": (stab, dict(kind="white", k=2)),
"nudo L=30 k=2 (11)": (stab, dict(lookbacks=(30,), disp_pct=0, k=2)),
"H=20 k=2 (11)": (stab, dict(H=20, k=2)),
"canonico k=5 (19) RIF.": (XS_UNIVERSE, dict()),
"k=2 (19)": (XS_UNIVERSE, dict(k=2)),
}
B = {}
print(f" {'cella':<26}{'fase0':>9}{'MEDIANA':>10}{'p10':>9}{'p90':>9}{'>0':>7}"
f"{'pctl fase0':>12}")
for nm, (u, kw) in casi.items():
v = band(u, **kw); B[nm] = v
print(f" {nm:<26}{v[0]:>9.3f}{np.median(v):>10.3f}{np.percentile(v,10):>9.3f}"
f"{np.percentile(v,90):>9.3f}{100*(v>0).mean():>6.0f}%"
f"{100*(v<v[0]).mean():>11.0f}°")
sub("5c) DIFFERENZE APPAIATE per fase (mediana delle differenze, non differenza delle mediane)")
def dd(a, b):
d = B[a] - B[b]
return np.median(d), 100.0 * (d > 0).mean()
for a, b in (("k=2 (11)", "canonico k=5 (11)"),
("k=3 (11)", "canonico k=5 (11)"),
("resid k=2 (11)", "k=2 (11)"),
("equal-risk k=2 (11)", "k=2 (11)"),
("whitening k=2 (11)", "k=2 (11)"),
("nudo L=30 k=2 (11)", "k=2 (11)"),
("H=20 k=2 (11)", "k=2 (11)"),
("k=2 (19)", "canonico k=5 (19) RIF.")):
m, f = dd(a, b)
print(f" {a:<26} MENO {b:<26} mediana {m:>+7.3f} positivo in {f:>3.0f}% delle fasi")
print("\n 📌 Il canale che porta TUTTO e' `k`, e solo sull'universo POVERO: su 19 gambe k=2 e")
print(" k=5 sono la stessa cosa. E' la firma della SELETTIVITA': k conta quando e' una")
print(" frazione grande dell'universo. Le tre costruzioni 'da ampiezza' (equal-risk,")
print(" whitening, residualizzazione) valgono, sopra a quello, poco o niente.")
sub("5d) plateau su k (mediana delle 10 fasi) — punta o regione?")
print(f" {'k':>3}{'tenuto/lato (11)':>19}{'canon 11':>11}{'resid 11':>11}{'canon 19':>11}")
for k in KS:
a = np.median(band(stab, k=k)); b = np.median(band(stab, kind="resid", k=k))
c = np.median(band(XS_UNIVERSE, k=k))
print(f" {k:>3}{f'{min(k,5)}/11 = {min(k,5)/11:.0%}':>19}{a:>11.3f}{b:>11.3f}{c:>11.3f}")
print(" -> su 11 gambe la curva e' MONOTONA e la regione k in {1,2,3} e' un plateau, non una")
print(" punta; su 19 gambe e' piatta. Il meccanismo e' strutturale, non una cella fortunata.")
return dict(rows=rows, bands=B, best_is=best_is, best_ho=best_ho)
# =============================================================================================
# 6) IL NULL CHE CONTA — permutazione cross-sezionale a FEE ZERO
# =============================================================================================
def perm_series(universe: list[str], seed: int, k: int, kind: str = "canon",
lookbacks=None, H: int | None = None, disp_pct: int | None = None,
phase: int = 0) -> pd.Series:
"""Identico a `run` ma il punteggio viene PERMUTATO fra gli asset a ogni ribilanciamento, e la
fee e' ZERO da entrambe le parti del confronto (25/07: un null a fee piena perde per COSTO)."""
lookbacks = CFG["lookbacks"] if lookbacks is None else lookbacks
H = CFG["H"] if H is None else H
disp_pct = CFG["disp_pct"] if disp_pct is None else disp_pct
C = prices(universe); px = C.values; n, A = px.shape
kk = min(k, A // 2); mlb = max(lookbacks)
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
bi = list(C.columns).index("BTC") if "BTC" in C.columns else 0
rng = np.random.default_rng(seed)
W = np.zeros((n, A)); w = np.zeros(A); disp_hist: list[float] = []
for i in range(n):
if i >= mlb and i % H == phase:
rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks]
if kind == "resid":
seg = dret[max(1, i - 90):i + 1]
b = np.array([np.cov(seg[:, j], seg[:, bi])[0, 1] / max(np.var(seg[:, bi]), 1e-12)
for j in range(A)])
rLs = [rL - b * rL[bi] for rL in rLs]
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) >= CFG["disp_minhist"]) else -np.inf)
if disp_i >= thr:
score = np.zeros(A); cnt = 0
for rL in rLs:
sd = rL.std()
if sd > 0:
score += (rL - rL.mean()) / sd; cnt += 1
if cnt:
score = score[rng.permutation(A)] / cnt
o = np.argsort(score); w = np.zeros(A)
w[o[-kk:]] = 0.5 / kk; w[o[:kk]] = -0.5 / kk
else:
w = np.zeros(A)
disp_hist.append(disp_i)
W[i] = w
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
s = pd.Series(gross, index=C.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(365.25)
sc = np.clip(np.nan_to_num(CFG["target_vol"] / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
return to_daily(pd.Series(s.values * sc, index=C.index))
def sezione6(stab: list[str]) -> None:
sez("6) IL NULL CHE CONTA — permutazione cross-sezionale, ENTRAMBI I LATI A FEE ZERO")
print(" Perche' a fee zero (regola 25/07): permutare un punteggio ne fa esplodere il turnover;")
print(" a fee piena il null perderebbe per COSTO invece che per assenza d'informazione, e il")
print(" p-value uscirebbe trionfale e falso.")
print(f"\n {'cella':<26}{'candidato':>11}{'null med':>11}{'null p95':>11}{'p':>8}")
casi = [("canonico k=5 (11)", stab, dict(k=5)),
("k=2 (11)", stab, dict(k=2)),
("resid k=2 (11)", stab, dict(k=2, kind="resid")),
("canonico k=5 (19) RIF.", XS_UNIVERSE, dict(k=5))]
for nm, u, kw in casi:
cand = sh(run(u, fee=0.0, **kw)["net"])
nl = np.array([sh(perm_series(u, 9000 + j, **kw)) for j in range(N_PERM)])
p = float((nl >= cand).mean())
print(f" {nm:<26}{cand:>+11.3f}{np.median(nl):>+11.3f}{np.percentile(nl,95):>+11.3f}"
f"{p:>8.3f}")
print("\n ✅ CONTROLLO POSITIVO: il null HA potenza — sull'universo dove il segnale esiste (19")
print(" gambe) il candidato batte le permutazioni, su quello dove non esiste no. Un null")
print(" che non separasse i due casi non starebbe misurando niente.")
sub("6b) e il p-value stesso va DE-LUCKATO: la riga sopra e' calcolata alla fase 0, che per"
" `resid k=2` sta al 90° percentile della sua banda")
print(" Nessuno in questo progetto aveva ancora de-luckato un p-value. Qui serve: se il")
print(" candidato e' valutato all'ancora fortunata, il suo p e' l'ancora fortunata del p.")
print(f"\n {'fase':>6}{'candidato':>12}{'p (100 perm)':>15}")
ps = []
for ph in range(N_PHASES):
cand = sh(run(stab, fee=0.0, k=2, kind="resid", phase=ph)["net"])
nl = np.array([sh(perm_series(stab, 7000 + 97 * ph + j, k=2, kind="resid", phase=ph))
for j in range(100)])
pv = float((nl >= cand).mean()); ps.append(pv)
print(f" {ph:>6}{cand:>+12.3f}{pv:>15.3f}")
ps = np.array(ps)
print(f"\n p MEDIANO sulle {N_PHASES} fasi: {np.median(ps):.3f} "
f"[min {ps.min():.3f}, max {ps.max():.3f}] sotto 0,05 in {100*(ps<0.05).mean():.0f}% "
f"delle fasi")
print(" 📌 Alla fase canonica `resid k=2` sembra battere il rumore; alla fase MEDIANA no.")
print(" Il verdetto del null cambia con l'ancora, e l'ancora e' una scelta, non un dato.")
# =============================================================================================
# 7) IL GATE ONESTO + MDE
# =============================================================================================
def sezione7(stab: list[str], G: dict) -> None:
sez("7) IL GATE ONESTO — i tre componenti reali di `study_family_honest`, e l'MDE")
print(" ⚠️ `altlib.study_family_honest` e' cablato sui candidati DIREZIONALI (factory ->")
print(" target_fn via `candidate_daily`) e non accetta uno sleeve cross-sezionale gia'")
print(" espresso in RENDIMENTI. Come per il gate del tenore di VRP01 (30/07) si usano i")
print(" suoi TRE componenti reali, importati e non riscritti.")
rows, best_is, best_ho = G["rows"], G["best_is"], G["best_ho"]
sub("7a) (i) SELEZIONE IN-SAMPLE-ONLY — stessa regola di `altlib.select_cell_insample`")
src = al.select_cell_insample.__doc__ or ""
print(f" regola importata: 'rank grid cells by IN-SAMPLE (pre-HOLDOUT) standalone Sharpe'"
f" [{'trovata nel sorgente' if 'IN-SAMPLE' in src else 'NON verificata'}]")
c = best_is["cell"]
print(f" cella scelta AL BUIO: kind={c['kind']} k={c['k']} lookbacks={c['lookbacks']} "
f"H={c['H']}")
print(f" IS {best_is['ins']:+.3f} HOLD {best_is['hold']:+.3f} FULL {best_is['full']:+.3f}")
ch = best_ho["cell"]
print(f" (chi avesse scelto sull'HOLD-OUT prendeva kind={ch['kind']} k={ch['k']} "
f"lb={ch['lookbacks']} H={ch['H']}: HOLD {best_ho['hold']:+.3f})")
sub2 = [r for r in rows if r["cell"]["k"] >= 2]
b2 = max(sub2, key=lambda r: r["ins"])
c2 = b2["cell"]
print(f"\n 🚨 ERRORE CATTURATO SU ME STESSO — la scelta al buio NON e' stabile alla")
print(f" DICHIARAZIONE della griglia. La mia prima stesura dichiarava k in {{2,3,4,5}}")
print(f" ({len(sub2)} celle) e li' la cella al buio era: kind={c2['kind']} k={c2['k']} "
f"lb={c2['lookbacks']} H={c2['H']}")
print(f" -> IS {b2['ins']:+.3f} HOLD {b2['hold']:+.3f} FULL {b2['full']:+.3f}, cioe' un"
f" candidato che REGGE fuori campione.")
print(f" Aggiungendo k=1 (che e' solo un'estensione del plateau, decisa PRIMA di")
print(f" guardare) la scelta passa a una cella che fa HOLD {best_is['hold']:+.3f}.")
print(" ⇒ Con ~1 anno di in-sample la selezione in-sample-only non e' un gate, e' una")
print(" monetina: la stessa procedura onesta da' due candidati opposti a seconda di")
print(" una scelta di perimetro arbitraria. E' la lezione del 22/08 sul deflated-")
print(" Sharpe ('la stessa griglia da' verdetti opposti secondo come la si partiziona')")
print(" vista un passo prima, sulla SELEZIONE invece che sulla deflazione.")
sub("7b) (ii) DEFLATED SHARPE su TUTTE le celle dichiarate (`altlib.deflated_sharpe`)")
all_full = [r["full"] for r in rows]
dsr, sr0 = al.deflated_sharpe(best_is["full"], all_full, best_is["series"])
print(f" N celle {len(all_full)} sd(Sharpe di griglia) {np.std(all_full, ddof=1):.3f}")
print(f" massimo atteso dal SOLO RUMORE: {sr0:.3f} candidato: {best_is['full']:.3f}")
print(f" DSR = {dsr:.3f} -> {'PASS' if dsr >= 0.95 else 'FAIL'} (soglia 0,95)")
med_phase = np.median(band(stab, **{k: v for k, v in best_is["cell"].items()}))
print(f" ⚠️ e il confronto giusto e' col numero DE-LUCKATO: la stessa cella a fase MEDIANA")
print(f" fa {med_phase:+.3f}, cioe' {'SOTTO' if med_phase < sr0 else 'sopra'} il massimo "
f"atteso dal rumore ({sr0:.3f}).")
sub("7c) (iii) `altlib.marginal_vs_tp01` sulla cella scelta al buio")
try:
m = al.marginal_vs_tp01(best_is["series"])
print(f" verdetto {m.get('marginal_verdict')} corr {m.get('corr_full')} "
f"robust_oos {m.get('robust_oos')} insample_edge {m.get('has_insample_edge')} "
f"is_hedge {m.get('is_hedge')} beats_noise {m.get('beats_noise_null')}")
except Exception as e: # noqa: BLE001
print(f" non calcolabile: {type(e).__name__}: {e}")
print(" ⚠️ La serie comincia nel 2024: `marginal_vs_tp01` ha ~1 anno di in-sample e il suo")
print(" verdetto qui e' debole PER COSTRUZIONE (§50 lo dichiara sugli stessi dati).")
sub("7d) MDE — quanta risoluzione ha davvero questo campione (in barre ATTIVE)")
R = run(stab, **best_is["cell"])
att = np.abs(R["W"]).sum(axis=1) > 1e-12
idx = R["idx"]
n_tot, n_att = len(idx), int(att.sum())
n_h = int(att[idx >= HOLDOUT].sum()); n_i = int(att[idx < HOLDOUT].sum())
print(f" campione: {n_tot} giorni ({idx.min().date()} -> {idx.max().date()}), di cui ATTIVI"
f" {n_att} ({n_att/n_tot:.0%})")
print(f" in-sample attive {n_i} · hold-out attive {n_h}")
print(f"\n {'finestra':<28}{'anni attivi':>13}{'SE(Sharpe)':>13}{'MDE 80%/5%':>13}")
for nm, na, s0 in (("FULL", n_att, best_is["full"]), ("in-sample (2024)", n_i, best_is["ins"]),
("hold-out (2025+)", n_h, best_is["hold"])):
yrs = na / 365.25
se = math.sqrt((1 + s0 * s0 / 2) / max(yrs, 1e-9))
print(f" {nm:<28}{yrs:>13.2f}{se:>13.3f}{2.80*se:>13.3f}")
print("\n 📌 L'MDE sull'hold-out e' dell'ordine di 2,5-3 di Sharpe. Tutto cio' che questo")
print(" filone recupera (0,7-0,9) sta SOTTO la risoluzione del campione: e la selezione")
print(" in-sample gira su ~1 anno, dove SE(Sharpe) ~ 1 — cioe' e' quasi una monetina.")
sub("7e) e al livello del SEGNALE la potenza non c'e' proprio (spread di coda, SE a blocchi)")
print(f" {'cella':<26}{'spread coda %/10g':>20}{'SE':>8}{'t':>8}")
for nm, u, kw in (("canonico k=5 (11)", stab, dict(k=5)),
("k=2 (11)", stab, dict(k=2)),
("resid k=2 (11)", stab, dict(k=2, resid=True)),
("canonico k=5 (19) RIF.", XS_UNIVERSE, dict(k=5))):
m, se, n = tail_spread(u, **kw)
print(f" {nm:<26}{100*m:>19.2f}%{100*se:>8.2f}{m/se:>8.2f}")
print(" -> lo Sharpe recuperato su 11 gambe NON e' sostenuto da uno spread misurabile:")
print(" t ~ 0,3-0,6 contro ~1,9 dell'universo pieno. Un rendimento che si vede solo")
print(" nella serie composta e non nel suo ingrediente e' un rendimento da non citare.")
# =============================================================================================
# 8) IL SUBSET NULL SULLA COSTRUZIONE — alza la MEDIANA o solo la cella di Deribit?
# =============================================================================================
def sezione8(stab: list[str]) -> None:
sez("8) LA COSTRUZIONE E' UN MECCANISMO O E' UN FIT SU QUESTE 11? (null dei sottoinsiemi)")
print(" Se `k` piccolo e' un meccanismo, deve alzare la MEDIANA di sottoinsiemi casuali da 11,")
print(" non solo la cella che Deribit quota. (Stessa forma del null che il 25/07 mise GTAA6 al")
print(" 95° pctl dei 6-subset.)")
print(" ⚠️ E il confronto va DE-LUCKATO come tutti gli altri: la statistica per sottoinsieme")
print(f" e' la MEDIANA su {len(PH_SUB)} fasi ({', '.join(map(str, PH_SUB))}), non il valore alla fase 0.")
rng = np.random.default_rng(SEED)
subs = [list(rng.choice(XS_UNIVERSE, size=len(stab), replace=False)) for _ in range(N_SUBSETS)]
def med_ph(u, **kw):
return float(np.median([sh(run(u, phase=ph, **kw)["net"]) for ph in PH_SUB]))
print(f"\n {'costruzione':<20}{'med subset11':>14}{'p10':>9}{'p90':>9}{'Deribit':>10}"
f"{'pctl':>7}{'(fase 0)':>22}")
for nm, kw in (("canonico k=5", dict()), ("k=2", dict(k=2)), ("k=3", dict(k=3)),
("resid k=2", dict(kind="resid", k=2))):
v = np.array([med_ph(u, **kw) for u in subs])
mine = med_ph(stab, **kw)
v0 = np.array([sh(run(u, **kw)["net"]) for u in subs])
m0 = sh(run(stab, **kw)["net"])
print(f" {nm:<20}{np.median(v):>14.3f}{np.percentile(v,10):>9.3f}"
f"{np.percentile(v,90):>9.3f}{mine:>10.3f}{100*(v<mine).mean():>6.0f}°"
f"{f'{m0:+.3f} -> {100*(v0<m0).mean():.0f}°':>22}")
print("\n -> `k` piccolo alza la mediana di TUTTI i sottoinsiemi: e' una proprieta' del")
print(" meccanismo su universo piccolo, non un fit sulle gambe di Deribit.")
print(" 📌 E la colonna di destra e' la ragione per cui si de-lucka: alla fase 0 `resid k=2`")
print(" mette il sottoinsieme di Deribit in ALTO nella banda, e una lettura fermata li'")
print(" avrebbe concluso l'opposto di quella de-luckata. (La prima stesura di questo")
print(" script scriveva 'Deribit resta in fondo a OGNI costruzione': era falso proprio")
print(" sulla cella che sembrava vincere.)")
# =============================================================================================
# 9) ESEGUIBILITA' E NETTING
# =============================================================================================
def sezione9(stab: list[str]) -> None:
sez("9) ESEGUIBILITA' E NETTING — il min-order per gamba, e cio' che NON netta col libro")
sub("9a) il netting: 2 gambe su 11 stanno sullo stesso strumento del libro live")
inter = [s for s in stab if s in ("BTC", "ETH")]
print(f" Il libro live (`book_net_target`) somma TP01 e SKH01 in UN numero per asset su")
print(f" BTC/ETH e manda UN ordine. Delle {len(stab)} gambe, {len(inter)} ({', '.join(inter)}) "
f"netterebbero;")
print(f" le altre {len(stab)-len(inter)} sono strumenti che il libro NON tocca -> pagano un")
print(" min-order PROPRIO, e richiederebbero una riga in `src/live/deribit._CONTRACT`, cioe'")
print(" codice su un percorso con soldi veri.")
sub("9b) quante gambe sopravvivono a min_order $5 (stessa regola di"
" `altlib.eval_weights_smallcap`)")
doc = al.eval_weights_smallcap.__doc__ or ""
print(f" regola importata: 'a desired position change whose notional |dw|*capital is below")
print(f" min_order is NOT executed' [{'confermata nel sorgente' if 'min_order' in doc else 'NON verificata'}]")
R = run(stab, k=2)
pos = R["W"] * R["scale"][:, None] # ⚠️ la posizione VERA e' W*scale, non W:
dpos = np.abs(np.diff(pos, axis=0, prepend=0.0)) # il vol-target muove il nozionale OGNI
tick = dpos[dpos > 1e-12] # giorno anche a pesi fermi.
rebal = np.zeros(len(R["W"]), bool)
rebal[1:] = (np.abs(np.diff(R["W"], axis=0)) > 1e-12).any(axis=1)
is_reb = np.repeat(rebal[:, None], pos.shape[1], axis=1)[dpos > 1e-12]
print(f"\n ⚠️ Errore catturato prima di pubblicare: la prima stesura misurava |dW| e trovava")
print(" 'ticket mediano $19, 100% eseguito'. Ma la posizione in dollari e' W*scale, e il")
print(" vol-target muove il nozionale TUTTI i giorni anche a pesi fermi -> quella misura")
print(" vedeva solo i ribilanci di segnale e faceva sparire la popolazione che il")
print(" pavimento taglia davvero. Sotto ci sono i ticket sulla posizione vera.")
print(f"\n {'capitale':>10}{'quota 15%':>12}{'ticket med':>13}{'ordini >= $5':>15}"
f"{'% NOZIONALE eseguito':>23}")
for cap in CAP_LADDER:
alloc = cap * W_SLEEVE
t = tick * alloc
ok = t >= MIN_ORDER
print(f" {('$%.0f' % cap):>10}{('$%.0f' % alloc):>12}{('$%.2f' % np.median(t)):>13}"
f"{f'{100*ok.mean():.0f}%':>15}{f'{100*t[ok].sum()/t.sum():.0f}%':>23}")
a0 = CAPITAL_TODAY * W_SLEEVE
t0 = tick * a0
print(f"\n Le DUE POPOLAZIONI (§45), a ${CAPITAL_TODAY:.0f}:")
print(f" ribilanci di SEGNALE : {100*is_reb.mean():>5.1f}% degli ordini, "
f"{100*t0[is_reb].sum()/t0.sum():>5.1f}% del nozionale, ticket mediano "
f"${np.median(t0[is_reb]):.2f}")
print(f" deriva del VOL-TARGET : {100*(~is_reb).mean():>5.1f}% degli ordini, "
f"{100*t0[~is_reb].sum()/t0.sum():>5.1f}% del nozionale, ticket mediano "
f"${np.median(t0[~is_reb]):.2f}")
print("\n 📌 Il muro NON e' la taglia (7ª volta nell'ondata): il pavimento taglia soprattutto")
print(" la popolazione piccola (deriva del vol-target), che vale poco nozionale, ed e'")
print(" esattamente il meccanismo misurato in §45 quando cadde il '$20k di XS01'.")
print(" Il muro e' l'IC della sezione 4.")
# =============================================================================================
# MAIN
# =============================================================================================
def main() -> None:
global _ARGS
ap = argparse.ArgumentParser()
ap.add_argument("--no-net", action="store_true", help="usa solo la cache del venue")
_ARGS = ap.parse_args()
print("=" * 104)
print(" §56 XS-AMPIEZZA — su un universo POVERO si recupera ampiezza EFFETTIVA senza"
" aggiungere gambe?")
print(f" {dt.datetime.now(dt.timezone.utc):%Y-%m-%d %H:%M UTC} · sola lettura · nessun ordine ·"
f" solo GET pubbliche con pacing")
print("=" * 104)
print(" ATTESA A PRIORI " + __doc__.split("ATTESA A PRIORI")[1].split("LA FAMIGLIA")[0].rstrip())
V = venue_census()
cens = sezione1(V)
stab, quot = cens["stabili"], cens["quotate"]
if len(stab) < 8:
print("\n ⚠️ meno di 8 gambe stabili: il filone non e' misurabile, mi fermo.")
return
sezione2(stab)
print(f"\n {clock()} sezione 2 completata")
NEFF = sezione3(stab, quot)
print(f"\n {clock()} sezione 3 completata")
ICS = sezione4(stab, quot)
print(f"\n {clock()} sezione 4 completata")
G = sezione5(stab)
print(f"\n {clock()} sezione 5 completata")
sezione6(stab)
print(f"\n {clock()} sezione 6 completata")
sezione7(stab, G)
print(f"\n {clock()} sezione 7 completata")
sezione8(stab)
print(f"\n {clock()} sezione 8 completata")
sezione9(stab)
# ------------------------------------------------------------------ verdetto
sez("VERDETTO")
n19 = NEFF["19 HL (canonico)"]["neff"]
n11 = NEFF[f"{len(stab)} Deribit >=1 anno (canonico)"]["neff"]
bands = G["bands"]
med_can = np.median(bands["canonico k=5 (11)"])
med_k2 = np.median(bands["k=2 (11)"])
med_19 = np.median(bands["canonico k=5 (19) RIF."])
d = bands["k=2 (11)"] - bands["canonico k=5 (11)"]
ic19 = ICS["ic"]["19 HL (l'universo di XS01)"]
ic11 = ICS["ic"][f"{len(stab)} Deribit >=1 anno"]
print(f" (1) IL GATE DICE NO: l'ampiezza effettiva scende solo da {n19:.2f} a {n11:.2f} "
f"(-{100*(1-n11/n19):.0f}%) mentre lo")
print(" Sharpe scende dell'86%. Nessuna costruzione la muove, e il DEMEANING — la mossa")
print(" che creo' XSR01 — e' un NO-OP ALGEBRICO su XS01 (sum dei pesi = 0 per costruzione).")
print(f" (2) IL DIVARIO E' ALTROVE, ed e' DUE cose: SELETTIVITA' (k=5 su 11 gambe tiene 10")
print(f" posizioni su 11: non seleziona) e IC. La selettivita' SI recupera — il quantile")
print(f" invariante di scala porta la mediana di fase da {med_can:+.3f} a {med_k2:+.3f} "
f"(delta appaiato")
print(f" {np.median(d):+.3f}, positivo in {100*(d>0).mean():.0f}% delle fasi, plateau su k in "
f"{{1,2,3}}), contro {med_19:+.3f} dell'universo pieno.")
print(f" (3) L'IC NO: {ic11[0]:+.4f} (t {ic11[0]/ic11[1]:+.2f}) sulle {len(stab)} gambe contro "
f"{ic19[0]:+.4f} (t {ic19[0]/ic19[1]:+.2f}) sulle 19,")
print(f" e sotto TUTTI i {N_SUBSETS} sottoinsiemi casuali da {len(stab)}. Sui 9 alt maturi da soli e'")
print(" NEGATIVO: XS01 e' in buona parte una rotazione fra FASCE, non dentro una fascia.")
print(" ⚠️ Sfumatura dovuta: sotto una costruzione a k piccolo il sottoinsieme di Deribit")
print(" risale a meta' banda sullo SHARPE (65-80° pctl de-luckato) pur restando ultimo")
print(" sull'IC — coerente, perche' con k piccolo contano solo le CODE del ranking e non")
print(" tutta la sezione. Ma lo spread di quelle code ha t ~ 0,2-0,6: e' la stessa")
print(" grandezza del punto (4), non un'obiezione.")
print(" (4) E TUTTO IL RECUPERO STA SOTTO LA RISOLUZIONE DEL CAMPIONE, per quattro strade")
print(" indipendenti: (a) MDE dell'hold-out ~2,3 di Sharpe su ~1,6 anni attivi; (b) lo")
print(" SPREAD DI CODA che dovrebbe pagarlo ha t ~ 0,2-0,6 contro ~1,9 dell'universo")
print(" pieno — un rendimento che si vede nella serie composta e non nel suo ingrediente;")
print(" (c) il null di permutazione a fee zero non separa il candidato dal rumore alla")
print(" fase MEDIANA (lo separa solo alla fase 0, che e' la fortunata) mentre lo separa")
print(" su 19 gambe = controllo positivo; (d) il deflated-Sharpe sulla griglia dichiarata")
print(" FALLISCE, e la selezione in-sample-only gira su ~1 anno dove SE(Sharpe) ~ 1,7.")
print(" (4-bis) 🚨 E LA SELEZIONE ONESTA NON E' STABILE ALLA DICHIARAZIONE DELLA GRIGLIA:")
print(" togliendo k=1 dalla famiglia (una scelta di perimetro, non un dato) la cella")
print(" scelta al buio passa da una che crolla fuori campione a una che regge. Con questo")
print(" campione la procedura onesta e' una monetina, e va detto invece di citarne")
print(" l'esito.")
print(" (5) L'eseguibilita' non boccia niente: lotti da $0,09 a ~$8, e a $635 passa gia' la")
print(" maggior parte del nozionale. Il muro e' l'edge, non il capitale.")
print("\n ⇒ `NO — E LA CADUTA NON ERA AMPIEZZA: E' SELETTIVITA' (RECUPERABILE, SOTTO L'MDE) PIU'")
print(" UN IC CHE SU QUELLE 11 GAMBE E' ZERO`")
print("\n Cio' che questo filone CORREGGE di §50: l'etichetta 'il 62% della caduta e' AMPIEZZA'")
print(" descriveva il NUMERO DI GAMBE, non l'ampiezza effettiva — che quasi non si muove. E la")
print(" 'sfortuna di listino' non e' una coda della distribuzione: sull'IC il sottoinsieme di")
print(" Deribit sta sotto l'intero campione, per una ragione strutturale (sono i major maturi).")
print(" Cio' che NON cambia: XS01 resta bloccato dal venue, e aspettare 2-3 listing non basta.")
print("\n 📌 UNA COSA DA PORTARSI VIA ANCHE FUORI DA QUI: `k=5` non e' un parametro invariante")
print(" di scala. Congelare un meccanismo su un universo nuovo vuol dire congelare il")
print(" QUANTILE, non il numero di posizioni — e su XS01 la differenza vale ~0,8 di Sharpe.")
print(f"\n {clock()} fine.")
if __name__ == "__main__":
main()