"""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*(v11.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*(v6.0f}°" f"{f'{m0:+.3f} -> {100*(v022}") 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()