bf617b75ff
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
1110 lines
62 KiB
Python
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()
|