Files
Adriano Dal Pastro 9cb3465374 research(volvol): la vol-of-vol e' una variabile NUOVA che guarda INDIETRO — SCARTATA, quinto lato del filone DVOL chiuso
§48 VOLVOL. Il DVOL era stato usato in quattro modi, tutti sul LIVELLO o su una differenza
di livelli (VRP01 gate IV-rank, DVOLSPREAD, TP01xDVOL, DVOL-direzionale). Mai il secondo
momento. Qui si apre e si chiude.

ORTOGONALITA' (il gate, misurato PRIMA di costruire qualsiasi strategia): PASS con margine.
Max |corr| = 0.396 su 6 referenze x 6 celle; contro il LIVELLO del DVOL sta a 0.13-0.21 e
contro l'IV-rank di VRP01 a 0.01-0.24. **La meta' "ridondante" dell'attesa a priori e'
REFUTATA**: e' davvero una quinta variabile, non una quarta riscritta.

LEAD-LAG: e' un termometro, e peggio di quanto l'attesa dicesse. Il picco di
corr(VoV_t, |r|_{t+k}) non e' a lag 0 ma a lag **-5/-10**, con la curva monotona da +0.17
a lag -10 fino a +0.02 a lag +10: non accompagna il movimento, lo INSEGUE. E al netto di
RV_t e DVOL_t la correlazione con la vol futura a 10g e' **NEGATIVA** (-0.069 BTC /
-0.090 ETH). Controllo positivo del rilevatore superato 2/2 nei due versi.

GRIGLIA dichiarata prima e contata al rialzo: 3 finestre x 3 soglie x 4 usi = 36 celle
(27 direzionali + 9 sull'arm VRP). Tenuta piccola di proposito: su 168 trial il massimo
atteso dal puro rumore e' Sharpe 1.572, sopra il soffitto direzionale.
- study_family_honest: cella scelta in-sample-only SIZE w=10 p=0.50, marginal NEUTRAL,
  **DSR 0.448 FAIL** (0.441/0.381/0.228 a N=36/72/168) -> earns_slot_honest=False.
- La spia T1 in chiaro: la cella scelta ha **corr->TP01 0.995 SULL'HOLD-OUT** (0.667 sul
  pieno). E' TP01 con un nome diverso. Per uso: RISKOFF/SIZE ereditano lo Sharpe del trend
  (mediana IS 0.40/0.66), **DIR — l'unico uso in cui la variabile decide da sola — ha
  mediana IS -0.24 e 5 celle su 9 con FULL negativo**.
- Arm VRP01, replica del sleeve **bit-exact 2/2** prima di ogni delta: 5/9 celle battono il
  canonico (moneta), maxDD giu' in **9/9** = de-levering puro, e contro gate CASUALI che
  saltano lo stesso numero di settimane la mediana e' 0.71/0.72 con **0/9 celle al 95° pctl**.
  5° fallimento consecutivo di un gate nuovo su VRP01 dopo i 4 del 03/07.

IL NUMERO CHE CHIUDE, e non e' quello della griglia: il candidato migliore batte TP01 nudo
di **+0.034 di Sharpe su una finestra il cui MDE e' 1.51 — fattore 44**. Su questo dato la
domanda non e' rispondibile in positivo nemmeno in linea di principio. A chiudere sono le
due misure che hanno potenza e non dipendono da nessuna cella scelta: la parziale negativa
e il **controllo NON CAUSALE** (stessa cella con vol-of-vol che sbircia: **-0.292** sotto
TP01) -> non e' che la si stima male, e' che la variabile non contiene l'informazione.

Errori catturati su me stesso: (a) la lettura di §1 era CABLATA e diceva "il legame piu'
forte e' con la vol realizzata" mentre la tabella diceva SPREAD -> ora e' calcolata;
(b) il null del gate casuale estraeva le settimane INDIPENDENTEMENTE per gamba mentre il
gate vero e' guidato da due DVOL correlati -> bracciato coi due estremi, e la differenza
e' risultata immateriale (0.71 vs 0.72, sovrapposizione vera 0.29), ma andava misurata e
non assunta (nel 25/07 lo stesso errore valeva 2-3x); (c) un conteggio off-by-one su DIR.

Eseguibilita' a $635 NON e' il vincolo (haircut -0.002, turnover 4-5/anno): 8ª volta
nell'ondata che muore sull'edge e non sulla taglia del conto. causality_ok OK, oracolo OK.

Book, pesi, cron, config INVARIATI. Nessuna scrittura, nessuna rete, 20s di corsa.

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

1013 lines
57 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""r0823_volvol.py — §48 VOLVOL: il QUINTO lato del filone DVOL, e l'unico mai aperto.
LA DOMANDA
==========
Il progetto ha usato il DVOL (indice di vol implicita Deribit) in QUATTRO modi, tutti sul
**livello** o su una **differenza di livelli**:
1. VRP01 — gate IV-rank sul LIVELLO (percentile espandente causale del DVOL).
2. DVOLSPREAD — differenza di livelli CROSS-ASSET (log DVOL_btc log DVOL_eth), in
forward-monitor dal 25/07. ⚠ vedi CORREZIONE sotto.
3. TP01xDVOL — il DVOL come DENOMINATORE del vol-target (SCARTATO 26/06: de-levering).
4. DVOL-direz. — buy-the-fear sul livello (SCARTATO 29/06: is_hedge=True, non alpha).
Mai il SECONDO MOMENTO: la **volatilita' del DVOL stesso**. E' una quantita' diversa — misura
quanto e' instabile l'*aspettativa* di rischio, non quanto e' alto il rischio — ed e' in
letteratura il piu' classico predittore di cambio di regime.
Domanda decidibile: **la vol-of-vol contiene informazione che il livello e lo spread NON
contengono, e vale abbastanza da meritare uno slot?**
⚠️ CORREZIONE AL BRIEF (fatta prima di misurare, leggendo il codice in produzione).
Il brief chiedeva di correlare con «lo spread DVOLRV di DVOLSPREAD». Ma il DVOLSPREAD che sta
in forward-monitor NON e' implicita-meno-realizzata: `scripts/research/r0726_dvolspread_gate.py`
:make_book calcola `ldvr = log(dvol_btc) log(dvol_eth)` — e' un **relative-value CROSS-ASSET**
fra le due implicite, non un premio a termine. Qui si misura l'ortogonalita' contro ENTRAMBI:
il segnale realmente monitorato (ratio BTC/ETH) e il concetto DVOLRV (che e' il gate `gate_vrp`
di VRP01, `sleeves.py:167-171`). Correlare contro quello sbagliato avrebbe dato un "ortogonale"
gratuito contro una variabile che nessuno traderebbe.
ATTESA A PRIORI — DICHIARATA NEL BRIEF **PRIMA** DI QUALUNQUE MISURA
====================================================================
Quella del coordinatore, riportata alla lettera: *«sara' un termometro contemporaneo o
ridondante col trend»*. Sono due affermazioni distinte e questo script le separa, perche' i
dati le trattano in modo opposto:
- RIDONDANZA → REFUTATA (§1: |corr| max 0.37, e con il livello DVOL solo 0.16-0.20).
- TERMOMETRO → CONFERMATA, e in forma PIU' FORTE del previsto (§2: il picco della
cross-correlazione non e' a lag 0 — e' a lag **NEGATIVO**: la vol-of-vol
non accompagna il movimento, lo **insegue**).
Chi scrive non aveva un'attesa indipendente da registrare: le due sonde di §1 e §2 sono state
girate prima di scrivere questo file, e si dichiara — non si finge un a-priori a posteriori.
LE DUE TRAPPOLE GIA' PAGATE SU QUESTO DATO, escluse esplicitamente
==================================================================
(T1) RIDONDANZA COL TREND — 5 filoni indipendenti (macro-regime-gate 29/06 corr→TP01 0.989,
TP01xDVOL 26/06, funding-gate 01/07, velocita'-trend-regime 02/07, WK-COND 17/07) hanno
trovato che un gate di rischio e' TP01 travestito, perche' TP01 e' GIA' flat nei crash.
→ §1 misura corr con l'esposizione di TP01; §3 riporta `marginal_vs_tp01`.
(T2) TERMOMETRO CONTEMPORANEO — TERM-STRUCTURE (§7 ondata 22/08) e SKEW (§5) sono morti cosi':
la variabile si muove INSIEME al prezzo, non prima.
→ §2 misura la cross-correlazione a lag negativi E positivi e dice dove sta il picco.
METODO
======
§1 ORTOGONALITA' — il gate che decide se il filone ha senso (Pearson E rango, 6 referenze).
§2 LEAD-LAG — cross-correlazione a lag ±, correlazione PARZIALE, controlli positivi
nei DUE versi (un rilevatore di lead va provato su un lead E su un lag).
§3 GRIGLIA DICHIARATA PRIMA (36 celle) → `study_family_honest` (selezione in-sample-only →
deflated_sharpe → marginal_vs_tp01), importato da altlib, non riscritto.
§4 ARM VRP01 — replica BIT-EXACT del sleeve prima di ogni delta, poi il gate VoV.
§5 NULL DEL DE-LEVERING, MDE, turnover, eseguibilita' a $635.
§6 CONTROLLI POSITIVI del macchinario (oracolo + VoV NON-CAUSALE): un rilevatore che non
trova nulla e non e' validato e' indistinguibile da uno rotto.
Uso: nice -n 19 timeout 900 uv run python scripts/research/r0823_volvol.py
Non tocca nulla: nessuna scrittura, nessuna rete (DVOL e OHLC sono su disco).
"""
from __future__ import annotations
import bisect
import math
import sys
from pathlib import Path
import numpy as np
import pandas as pd
from scipy.stats import norm, spearmanr
_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(_ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(_ROOT))
import altlib as al # noqa: E402
from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402
ASSETS = ("BTC", "ETH")
TF = "1d"
DPY = 365.25
HOLDOUT = al.HOLDOUT
CAPITAL = 635.0 # il conto vero
MIN_ORDER = 5.0
# --- GRIGLIA DICHIARATA PRIMA DI GUARDARE (contata al RIALZO) ---------------------------
W_GRID = (10, 20, 60) # finestra di stima della vol-of-vol, in giorni
P_GRID = (0.20, 0.50, 0.80) # soglia sul percentile ESPANDENTE CAUSALE
USES = ("RISKOFF", "DIR", "SIZE", "VRPGATE")
N_CELLS_DECLARED = len(W_GRID) * len(P_GRID) * len(USES) # = 36
N_CELLS_DIRECTIONAL = len(W_GRID) * len(P_GRID) * 3 # = 27 (RISKOFF/DIR/SIZE)
N_CELLS_VRP = len(W_GRID) * len(P_GRID) # = 9
PCTL_MIN_OBS = 250 # warm-up del percentile espandente: 1 anno. Dichiarato.
SEP = "=" * 104
SUB = "-" * 104
# =========================================================================================
# UTILITA'
# =========================================================================================
def expanding_pctl(x: np.ndarray, min_obs: int = PCTL_MIN_OBS) -> np.ndarray:
"""Percentile ESPANDENTE CAUSALE: q[t] = frazione di x[:t] STRETTAMENTE minore di x[t].
Nessun dato futuro entra: al tempo t si guarda solo il passato stretto. Stessa convenzione
dell'IV-rank di VRP01 in produzione (`sleeves.py:174`, `(dvf[:i] < dvf[i]).mean()`) — usare
una convenzione diversa da quella del sleeve renderebbe le due misure non confrontabili.
NaN finche' non ci sono `min_obs` osservazioni finite precedenti.
"""
out = np.full(len(x), np.nan)
hist: list[float] = []
for i, v in enumerate(x):
if len(hist) >= min_obs and np.isfinite(v):
out[i] = bisect.bisect_left(hist, v) / len(hist)
if np.isfinite(v):
bisect.insort(hist, v)
return out
_VOV_CACHE: dict = {}
def vov_and_pctl(df: pd.DataFrame, asset: str, w: int):
"""(vol-of-vol, percentile espandente causale) allineati alle barre di `df`.
vol-of-vol = deviazione standard rotolante a `w` giorni delle VARIAZIONI LOG del DVOL,
annualizzata. Backward-looking per costruzione. La chiave di cache include l'ULTIMO
timestamp e la lunghezza di df, cosi' la troncatura di `causality_ok` NON pesca la
versione a lunghezza piena (sarebbe un look-ahead invisibile introdotto dalla cache).
"""
key = (asset, len(df), int(df["timestamp"].iloc[-1]), w)
if key in _VOV_CACHE:
return _VOV_CACHE[key]
dv = al.dvol(df, asset)
ldv = np.full(len(dv), np.nan)
with np.errstate(invalid="ignore", divide="ignore"):
ldv[1:] = np.log(dv[1:] / dv[:-1])
ldv = np.where(np.isfinite(ldv), ldv, np.nan)
vov = pd.Series(ldv).rolling(w, min_periods=w).std().values * math.sqrt(DPY)
q = expanding_pctl(vov)
_VOV_CACHE[key] = (vov, q)
return vov, q
_TP_CACHE: dict = {}
def tp01_target(df: pd.DataFrame, asset: str) -> np.ndarray:
"""Bersaglio canonico di TP01 sulle barre di `df` (stessa cache-key sicura di sopra)."""
key = (asset, len(df), int(df["timestamp"].iloc[-1]))
if key not in _TP_CACHE:
_TP_CACHE[key] = TrendPortfolio(**CANONICAL).target_series(df)
return _TP_CACHE[key]
def sh(s) -> float:
r = np.asarray(pd.Series(s).dropna().values, float)
return float(np.mean(r) / np.std(r) * math.sqrt(DPY)) if len(r) > 2 and np.std(r) > 0 else 0.0
def maxdd(s) -> float:
eq = np.cumprod(1.0 + np.asarray(pd.Series(s).dropna().values, float))
pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk)) if len(eq) else 0.0
def ccf(x: np.ndarray, y: np.ndarray, lags) -> dict:
"""corr(x_t, y_{t+k}). k>0 => x ANTICIPA y. k<0 => y anticipa x (x insegue)."""
out = {}
for k in lags:
a, b = (x[:len(x) - k], y[k:]) if k >= 0 else (x[-k:], y[:len(y) + k])
m = np.isfinite(a) & np.isfinite(b)
out[k] = float(np.corrcoef(a[m], b[m])[0, 1]) if m.sum() > 50 else np.nan
return out
def partial_corr(y: np.ndarray, x: np.ndarray, controls: list[np.ndarray]) -> float:
"""corr(y, x) al netto dei `controls` (residui OLS di entrambi). NaN-safe."""
cols = [y, x] + controls
m = np.all([np.isfinite(c) for c in cols], axis=0)
if m.sum() < 50:
return float("nan")
X = np.column_stack([np.ones(m.sum())] + [c[m] for c in controls])
ry = y[m] - X @ np.linalg.lstsq(X, y[m], rcond=None)[0]
rx = x[m] - X @ np.linalg.lstsq(X, x[m], rcond=None)[0]
return float(np.corrcoef(ry, rx)[0, 1])
# =========================================================================================
# §1 — ORTOGONALITA'. E' IL GATE: se la vol-of-vol e' >0.8 con una referenza gia' usata,
# il filone e' chiuso qui e lo si dice, invece di scoprirlo dopo 36 celle.
# =========================================================================================
def sezione_1_ortogonalita() -> dict:
print(SEP)
print(" §1 ORTOGONALITA' — IL GATE. Soglia dichiarata: |corr| > 0.80 con UNA referenza")
print(" gia' usata dal progetto => filone CHIUSO qui.")
print(SEP)
print(" Referenze (le quattro gia' usate + i due controlli di ridondanza col trend):")
print(" LIVELLO = DVOL (uso 3-4) IVRANK = pctl espandente causale (VRP01)")
print(" DVOL-RV = premio implicita-realizzata SPREAD = log DVOL_btc - log DVOL_eth")
print(" (gate `gate_vrp` di VRP01) (il DVOLSPREAD in monitor)")
print(" RV30 = vol realizzata 30g TP01 = |esposizione| di TP01")
print()
# SPREAD e' cross-asset: lo si costruisce una volta sull'indice comune e lo si riallinea.
dfb, dfe = al.get("BTC", TF), al.get("ETH", TF)
sb = pd.Series(al.dvol(dfb, "BTC"), index=pd.to_datetime(dfb["datetime"], utc=True))
se = pd.Series(al.dvol(dfe, "ETH"), index=pd.to_datetime(dfe["datetime"], utc=True))
with np.errstate(invalid="ignore", divide="ignore"):
spread = np.log(sb) - np.log(se)
worst = 0.0
worst_lbl = ""
rows = []
for a in ASSETS:
df = al.get(a, TF)
idx = pd.to_datetime(df["datetime"], utc=True)
c = df["close"].values.astype(float)
dv = al.dvol(df, a)
rv = al.realized_vol(al.simple_returns(c), 30, DPY)
ivr = expanding_pctl(dv, min_obs=60) # convenzione di VRP01
spr = spread.reindex(idx).values
tp = np.abs(tp01_target(df, a))
refs = {"LIVELLO": dv, "IVRANK": ivr, "DVOL-RV": dv / 100.0 - rv,
"SPREAD": spr, "RV30": rv, "TP01": tp}
for w in W_GRID:
vov, _ = vov_and_pctl(df, a, w)
line = {"asset": a, "w": w}
for lbl, ref in refs.items():
m = np.isfinite(vov) & np.isfinite(ref)
p = float(np.corrcoef(vov[m], ref[m])[0, 1]) if m.sum() > 50 else np.nan
s_ = float(spearmanr(vov[m], ref[m]).statistic) if m.sum() > 50 else np.nan
line[lbl] = (p, s_, int(m.sum()))
if np.isfinite(p) and abs(p) > worst:
worst, worst_lbl = abs(p), f"{a} w={w} vs {lbl} (Pearson {p:+.3f})"
if np.isfinite(s_) and abs(s_) > worst:
worst, worst_lbl = abs(s_), f"{a} w={w} vs {lbl} (rango {s_:+.3f})"
rows.append(line)
hdr = f" {'asset':<5} {'w':>3} {'n':>5} " + " ".join(f"{k:>16}" for k in
("LIVELLO", "IVRANK", "DVOL-RV", "SPREAD", "RV30", "TP01"))
print(hdr)
print(" " + SUB[:len(hdr) - 2])
for r in rows:
cells = " ".join(f"{r[k][0]:+.3f}/{r[k][1]:+.3f}".rjust(16)
for k in ("LIVELLO", "IVRANK", "DVOL-RV", "SPREAD", "RV30", "TP01"))
print(f" {r['asset']:<5} {r['w']:>3} {r['LIVELLO'][2]:>5} {cells}")
print(" (ogni cella = Pearson / rango di Spearman)")
print()
passed = worst <= 0.80
print(f" MASSIMO |corr| su tutte le celle e tutte le referenze: {worst:.3f} [{worst_lbl}]")
print(f" GATE ORTOGONALITA': {'PASS' if passed else 'FAIL'} (soglia 0.80)")
print()
# ⚠️ La lettura e' CALCOLATA dalla tabella, non cablata: una nota di diagnosi cablata e'
# peggio di nessuna nota (regola 29/07) — manderebbe sulla pista sbagliata sembrando una
# misura. Errore commesso in prima stesura: il testo diceva "il legame piu' forte e' con
# la vol realizzata" quando la tabella diceva SPREAD.
per_ref = {}
for k in ("LIVELLO", "IVRANK", "DVOL-RV", "SPREAD", "RV30", "TP01"):
vals = [max(abs(r[k][0]), abs(r[k][1])) for r in rows]
per_ref[k] = (min(vals), max(vals))
order = sorted(per_ref.items(), key=lambda kv: -kv[1][1])
print(" LETTURA. |corr| massimo per referenza (il massimo fra Pearson e rango, su 6 celle):")
for k, (lo, hi) in order:
print(f" {k:<9} {lo:.3f} - {hi:.3f}")
print(f" Il legame piu' forte e' con {order[0][0]} ({order[0][1][1]:.3f}), il piu' debole con "
f"{order[-1][0]} ({order[-1][1][1]:.3f}).")
print(" Contro il LIVELLO del DVOL — la variabile che il progetto usa in 3 modi su 4 — la")
print(" vol-of-vol sta a 0.13-0.21, e contro l'IV-rank di VRP01 a 0.01-0.24. Il legame")
print(" residuo con RV30 e SPREAD e' STRUTTURALE, non informativo: quando il mercato si")
print(" muove molto anche l'implicita si muove molto, e lo scarto BTC-vs-ETH e' esso stesso")
print(" una differenza di implicite. Tutto ben sotto la soglia dichiarata.")
print(" ⇒ **La meta' `ridondante` dell'attesa a priori e' REFUTATA.** E' davvero una quinta")
print(" variabile, non una quarta riscritta. Il filone ha diritto di proseguire.")
return dict(passed=passed, worst=worst, worst_lbl=worst_lbl, per_ref=per_ref)
# =========================================================================================
# §2 — LEAD-LAG. Termometro o predittore? Con controlli positivi NEI DUE VERSI.
# =========================================================================================
def sezione_2_leadlag() -> dict:
print()
print(SEP)
print(" §2 LEAD-LAG — la vol-of-vol ANTICIPA o INSEGUE? (T2: qui sono morti SKEW e")
print(" TERM-STRUCTURE). Convenzione: corr(VoV_t, Y_{t+k}); k>0 => VoV ANTICIPA Y.")
print(SEP)
lags = [-20, -10, -5, -3, -1, 0, 1, 3, 5, 10, 20]
out = {}
for a in ASSETS:
df = al.get(a, TF)
c = df["close"].values.astype(float)
r = al.simple_returns(c)
absr = np.abs(r)
dv = al.dvol(df, a)
rv = al.realized_vol(r, 30, DPY)
# vol realizzata FUTURA a 10 giorni, costruita in avanti (solo per diagnosi, mai in una
# strategia): media di |r| su [t+1, t+10].
fwd10 = pd.Series(absr).rolling(10, min_periods=10).mean().shift(-10).values
print(f"\n --- {a} (w=20, la finestra centrale della griglia) ---")
vov, _ = vov_and_pctl(df, a, 20)
for lbl, y in (("|r| (ampiezza)", absr), ("r (direzione)", r)):
cc = ccf(vov, y, lags)
best = max((k for k in lags if np.isfinite(cc[k])), key=lambda k: abs(cc[k]))
print(f" VoV_t vs {lbl:<16} " + " ".join(f"{k:+d}:{cc[k]:+.3f}" for k in lags))
print(f" -> |picco| a lag {best:+d} ({cc[best]:+.3f})")
out[(a, lbl)] = (cc, best)
praw = partial_corr(fwd10, vov, [])
ppar = partial_corr(fwd10, vov, [rv, dv])
print(f" corr(VoV_t, vol-realizzata-FUTURA-10g) grezza {praw:+.3f}"
f" | PARZIALE al netto di RV_t e DVOL_t: {ppar:+.3f}")
out[(a, "partial")] = (praw, ppar)
print()
print(" ⚠️ PERCHE' NON SI GUARDA `VoV_t vs RV_{t+k}` COME SE FOSSE UNA PREVISIONE.")
print(" RV e' una media ROTOLANTE ALL'INDIETRO a 30 giorni: RV_{t+10} contiene ancora i")
print(" giorni t-20..t. La sua cross-correlazione sale con k per pura sovrapposizione di")
print(" finestre e sembrerebbe un lead. La lente onesta e' |r|_{t+k}, che e' un giorno")
print(" solo e non si sovrappone a niente — ed e' quella riportata sopra.")
print()
# ---- CONTROLLI POSITIVI DEL RILEVATORE, NEI DUE VERSI --------------------------------
print(SUB)
print(" CONTROLLO POSITIVO del rilevatore di lead (obbligatorio: un rilevatore che trova")
print(" ~zero e non e' validato e' indistinguibile da uno rotto — e la convenzione di segno")
print(" di una cross-correlazione e' esattamente il genere di cosa che si inverte in")
print(" silenzio). Due sonde sintetiche, una per verso, sullo STESSO codice `ccf`.")
rng = np.random.default_rng(20260823)
df = al.get("BTC", TF)
absr = np.abs(al.simple_returns(df["close"].values.astype(float)))
n = len(absr)
ok_both = True
for name, shift_k in (("X ANTICIPA di 3g", +3), ("X INSEGUE di 3g", -3)):
src = pd.Series(absr).shift(-shift_k).values # x_t = |r|_{t+shift_k}
x = src + rng.normal(0, np.nanstd(absr) * 0.5, n)
cc = ccf(x, absr, lags)
best = max((k for k in lags if np.isfinite(cc[k])), key=lambda k: abs(cc[k]))
good = (best == shift_k)
ok_both &= good
print(f" {name:<18} atteso picco a lag {shift_k:+d} -> trovato {best:+d} "
f"({cc[best]:+.3f}) {'OK' if good else '*** ROTTO ***'}")
print(f" CONTROLLO POSITIVO: {'SUPERATO 2/2' if ok_both else 'FALLITO'}")
print(SUB)
print()
print(" 📌 LETTURA — E' UN TERMOMETRO, E PEGGIO DI QUANTO L'ATTESA DICESSE.")
print(" Il brief dava per chiuso il filone se il picco fosse caduto a lag 0 o -1. Cade")
print(" a lag **-10/-5**: la curva scende MONOTONA da ~+0.17 a lag -10 fino a ~+0.02 a")
print(" lag +10. Cioe' la vol-of-vol non accompagna il movimento — lo INSEGUE con una")
print(" settimana di ritardo, il che e' la firma meccanica di una deviazione standard")
print(" rotolante all'indietro su una serie che ha appena avuto uno scossone.")
print(" E la prova che chiude: **al netto di RV_t e DVOL_t la correlazione con la vol")
print(" futura e' NEGATIVA (-0.07/-0.09)**. Chi gia' conosce la vol realizzata di oggi e")
print(" il livello dell'implicita di oggi — due variabili che il progetto ha gia' e usa")
print(" — non impara NULLA dalla vol-of-vol sul regime di domani.")
print(" ⇒ **La meta' `termometro` dell'attesa a priori e' CONFERMATA.**")
return out
# =========================================================================================
# §3 — GRIGLIA DICHIARATA + GATE ONESTO
# =========================================================================================
def make_target(tf: str, use: str, w: int, p: float, peek: bool = False):
"""Fabbrica di celle. `target_fn(df, asset)` -> posizione per barra decisa a close[i].
Convenzione dichiarata: **posizione 0 prima dell'inizio del DVOL (2021-03)**, come ogni
sleeve del progetto che parte quando parte il suo dato (VRP01 dal 2021, XS01 dal 2024).
`peek=True` costruisce la stessa cella con una vol-of-vol NON CAUSALE (§6): serve solo
come controllo positivo dell'impianto, non e' una variante.
Segni DICHIARATI A PRIORI (non si prova anche il verso opposto: una griglia che contiene
entrambi i segni fabbrica celle "significative" per costruzione — regola (c) dell'ondata
22/08. Il verso opposto e' riportato in §5 come DIAGNOSTICA, non come cella).
RISKOFF: alta vol-of-vol = aspettative instabili -> FLAT.
SIZE : idem ma taglio a meta' invece che a zero.
DIR : long quando le aspettative sono CALME, short quando sono instabili.
"""
def _fn(df: pd.DataFrame, asset: str) -> np.ndarray:
vov, q = vov_and_pctl(df, asset, w)
if peek:
# NON causale di proposito: centra la finestra guardando in avanti.
vov2 = pd.Series(vov).shift(-w // 2).values
q = expanding_pctl(vov2)
calm = np.isfinite(q) & (q <= p) # aspettative "stabili"
known = np.isfinite(q)
if use == "RISKOFF":
return np.where(calm, tp01_target(df, asset), 0.0)
if use == "SIZE":
return np.where(known, tp01_target(df, asset) * np.where(calm, 1.0, 0.5), 0.0)
if use == "DIR":
d = np.where(known, np.where(calm, 1.0, -1.0), 0.0)
return al.vol_target(d, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
raise ValueError(use)
return _fn
def sezione_3_griglia() -> dict:
print()
print(SEP)
print(" §3 GRIGLIA DICHIARATA PRIMA DI GUARDARE, CONTATA AL RIALZO")
print(SEP)
print(f" finestra vol-of-vol w in {W_GRID} -> {len(W_GRID)}")
print(f" soglia (pctl caus.) p in {P_GRID} -> {len(P_GRID)}")
print(f" uso {USES} -> {len(USES)}")
print(f" asset NON e' un asse: ogni gamba usa il PROPRIO DVOL e il")
print(f" candidato e' il book 50/50 (convenzione di altlib).")
print(f" Trattarlo come asse selezionabile porterebbe a 72 celle:")
print(f" la sensibilita' del verdetto a quel conteggio e' pubblicata.")
print(f" ---------------------------------------------------------------------------")
print(f" CELLE DICHIARATE {N_CELLS_DECLARED} ({N_CELLS_DIRECTIONAL} direzionali qui + {N_CELLS_VRP} sull'arm VRP in §4)")
print()
print(" ⚠️ ARITMETICA CHE UCCIDE GLI SCREEN LARGHI (ondata 22/08 §8b): su 168 trial il")
print(" massimo atteso dal PURO RUMORE e' Sharpe 1.572, sopra il soffitto direzionale")
print(" misurato (~1.3, ~1.15 col funding dentro) -> uno screen largo su BTC/ETH")
print(" direzionale non puo' passare il proprio gate per aritmetica. Per questo la")
print(" griglia qui e' tenuta a 27+9 e non allargata.")
print()
grid = [dict(use=u, w=w, p=p) for u in ("RISKOFF", "DIR", "SIZE") for w in W_GRID for p in P_GRID]
assert len(grid) == N_CELLS_DIRECTIONAL, "la griglia costruita non e' quella dichiarata"
rep = al.study_family_honest("VOLVOL-DIR", make_target, grid, tfs=(TF,))
ch = rep["chosen"]
print(SUB)
print(f" CELLA SCELTA IN-SAMPLE-ONLY (nessuna occhiata all'hold-out): {ch['params']}")
print(f" Sharpe in-sample {ch['insample_sharpe']:+.3f} Sharpe FULL {ch['full_sharpe']:+.3f}")
print(f" deflated-Sharpe {rep['deflated_sharpe']} (soglia 0.95) "
f"massimo-nullo atteso {rep['expected_null_max']} -> DSR {'PASS' if rep['dsr_pass'] else 'FAIL'}")
m = rep["marginal"]["marginal"]
print(f" marginal_vs_tp01: {rep['marginal']['marginal_verdict']} corr->TP01 full "
f"{m.get('corr_full')} / hold {m.get('corr_hold')} beta {m.get('beta_to_tp01')}")
print(f" uplift blend w25: full {m['blends']['w25']['uplift_full']:+.3f} "
f"hold {m['blends']['w25']['uplift_hold']} robust_oos={m.get('robust_oos')} "
f"insample_edge={m.get('has_insample_edge')} is_hedge={m.get('is_hedge')}")
print(f" ==> earns_slot_honest = {rep['earns_slot_honest']}")
print(SUB)
print("\n TUTTE LE CELLE, ordinate per Sharpe IN-SAMPLE (la selezione onesta guarda questa"
"\n colonna e nessun'altra):")
print(f" {'uso':<8} {'w':>3} {'p':>5} {'Sh in-sample':>13} {'Sh FULL':>9}")
for r in rep["rows"]:
pr = r["params"]
print(f" {pr['use']:<8} {pr['w']:>3} {pr['p']:>5.2f} {r['insample_sharpe']:>13.3f} {r['full_sharpe']:>9.3f}")
# `study_family_honest` non ri-espone `all_full_sharpe`; le righe valide lo contengono e
# qui TUTTE le 27 celle sono valide (nessuna scartata), quindi le due liste coincidono.
allsh = [r["full_sharpe"] for r in rep["rows"]]
assert len(allsh) == N_CELLS_DIRECTIONAL, f"celle valutate {len(allsh)} != dichiarate {N_CELLS_DIRECTIONAL}"
# ---- IL RIASSUNTO PER USO E' LA LETTURA VERA DELLA TABELLA -------------------------
print()
print(" 📌 RIASSUNTO PER USO — e qui si vede la trappola T1 (ridondanza col trend):")
print(f" {'uso':<8} {'celle':>6} {'Sh IS mediano':>14} {'Sh IS max':>10} {'FULL>0':>8}")
for u in ("RISKOFF", "SIZE", "DIR"):
rr = [r for r in rep["rows"] if r["params"]["use"] == u]
med = float(np.median([r["insample_sharpe"] for r in rr]))
mx = max(r["insample_sharpe"] for r in rr)
npos_ = sum(1 for r in rr if r["full_sharpe"] > 0)
print(f" {u:<8} {len(rr):>6} {med:>14.3f} {mx:>10.3f} {npos_:>6}/{len(rr)}")
_dir = [r for r in rep["rows"] if r["params"]["use"] == "DIR"]
_dneg = sum(1 for r in _dir if r["full_sharpe"] <= 0)
print(" RISKOFF e SIZE sono TP01 con un interruttore sopra: ereditano lo Sharpe del")
print(" trend (~0.8 in-sample su questa finestra) e per questo occupano la testa della")
print(" classifica. **DIR e' l'unico uso in cui la vol-of-vol decide DA SOLA che")
print(" posizione avere**, ed e' li' che si legge il contenuto della variabile: mediana")
print(f" in-sample NEGATIVA e {_dneg} celle su {len(_dir)} con FULL negativo, cioe' una monetina.")
print(" Prendere la testa della classifica per un risultato sarebbe attribuire alla")
print(" vol-of-vol lo Sharpe di TP01 — l'errore che 5 filoni indipendenti hanno gia'")
print(" pagato (macro-regime-gate 29/06: corr->TP01 0.989).")
ch_hold = m.get("corr_hold")
if ch_hold is not None and ch_hold > 0.9:
print()
print(f" 🚨 E la spia piu' netta: la cella scelta ha corr->TP01 **{ch_hold}** SULL'HOLD-OUT")
print(f" (contro {m.get('corr_full')} sul campione pieno). Sull'hold-out il gate quasi non")
print(" scatta piu': il candidato **E' TP01**, con un nome diverso. Non e' un difetto")
print(" della cella, e' cosa succede quando si mette un interruttore rumoroso sopra")
print(" una strategia che funziona: nei tratti in cui l'interruttore tace, si sta")
print(" misurando la strategia sottostante.")
print(f"\n Celle con Sharpe FULL > 0: {sum(1 for s in allsh if s > 0)}/{len(allsh)}"
f" | mediana {np.median(allsh):+.3f} max {max(allsh):+.3f} min {min(allsh):+.3f}")
# ---- SENSIBILITA' DEL VERDETTO AL CONTEGGIO (regola 30/07) --------------------------
print()
print(" SENSIBILITA' DEL DEFLATED-SHARPE AL CONTEGGIO DEI TRIAL (regola 30/07: si dichiara")
print(" la griglia prima E si pubblica quanto il verdetto dipende da quel numero).")
fn = make_target(tf=ch["tf"], **ch["params"])
daily = al.candidate_daily(fn, tf=ch["tf"])
for N, why in ((len(allsh), "famiglia direzionale (quella dichiarata)"),
(N_CELLS_DECLARED, "griglia dichiarata intera, arm VRP compreso"),
(72, "se l'asset fosse un asse selezionabile"),
(168, "conteggio di SCREEN dell'ondata 22/08")):
pad = list(np.resize(np.asarray(allsh, float), N))
d, s0 = al.deflated_sharpe(sh(daily), pad, daily)
print(f" N={N:>4} ({why:<44}) DSR {d:.3f} nullo-max {s0:+.3f} "
f"{'PASS' if d >= 0.95 else 'FAIL'}")
print(" ⚠️ Il padding ALZA N e NON la varianza della griglia (lezione BOCPD, 22/08 §10b):")
print(" e' la forma piu' generosa del conteggio, non la piu' severa. Il vero sr0 di")
print(" screen andrebbe calcolato sulla varianza di TUTTE le famiglie provate")
print(" nell'ondata, che questo script non ha. Si dichiara il limite invece di")
print(" spacciare il numero piu' comodo.")
print(" ⚠️ E c'e' un secondo motivo per non fidarsi di un DSR generoso qui: le 27 celle NON")
print(" sono indipendenti (RISKOFF e SIZE sono lo STESSO segnale a due intensita'), la")
print(" varianza degli Sharpe di griglia e' quindi piccola, e sr0 con lei. Una famiglia")
print(" omogenea rende il DSR facile da passare: e' la ragione per cui qui il verdetto")
print(" NON si appoggia al DSR.")
return dict(rep=rep, chosen=ch, daily=daily, fn=fn, all_sh=allsh)
# =========================================================================================
# §4 — ARM VRP01: replica BIT-EXACT prima di ogni delta, poi il gate vol-of-vol.
# =========================================================================================
def vrp_weekly(asset: str, gate_q: float | None = None, w: int = 20):
"""Replica di `sleeves._vrp_weekly_asset` con UN grado di liberta' in piu': saltare la
settimana anche quando il percentile causale della vol-of-vol supera `gate_q`.
A `gate_q=None` DEVE riprodurre il sleeve di produzione BIT-EXACT: e' il controllo che
rende questo un re-test dello stesso oggetto e non di una sua approssimazione (30/07).
A priori dichiarato: non vendere vol quando l'ASPETTATIVA di vol e' instabile.
"""
from src.portfolio.sleeves import (VRP_CFG, _bs_put, _strike_from_delta, # noqa: N813
load_data, resample_1d)
df = resample_1d(load_data(asset, "1h"))
s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"]))
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
dv = pd.read_parquet(_ROOT / "data" / "raw" / f"dvol_{asset.lower()}.parquet")
d = pd.Series(dv["close"].values.astype(float),
index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
J = pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna()
px = J["px"].values
dvf = J["dvol"].values / 100.0
idx = J.index
qv = None
if gate_q is not None:
ld = np.full(len(dvf), np.nan)
ld[1:] = np.log(dvf[1:] / dvf[:-1])
vv = pd.Series(ld).rolling(w, min_periods=w).std().values * math.sqrt(DPY)
qv = expanding_pctl(vv)
cfg = VRP_CFG
tn = cfg["tenor_d"]
T = tn / DPY
rets, gated_idx = {}, []
i = 60
while i + tn < len(px):
S0, sig = px[i], dvf[i]
skip = False
if cfg["gate_vrp"] and i >= 31:
rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * math.sqrt(DPY)
if (sig - rv) <= 0:
skip = True
if not skip and (cfg["gate_ivr"] > 0 or cfg["crash_skip"] < 1.0) and i >= 60:
ivr = float((dvf[:i] < dvf[i]).mean())
if cfg["gate_ivr"] > 0 and ivr < cfg["gate_ivr"]:
skip = True
if cfg["crash_skip"] < 1.0 and ivr > cfg["crash_skip"]:
skip = True
if not skip and qv is not None and np.isfinite(qv[i]) and qv[i] > gate_q:
skip = True
gated_idx.append(idx[i + tn])
if skip:
rets[idx[i + tn]] = 0.0
i += tn
continue
Ks = _strike_from_delta(S0, T, sig, cfg["short_delta"])
Kl = _strike_from_delta(S0, T, sig, cfg["long_delta"])
net_prem = (_bs_put(S0, Ks, T, sig) - _bs_put(S0, Kl, T, sig)) * cfg["f"]
S1 = px[i + tn]
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
rets[idx[i + tn]] = (net_prem - payoff - cfg["fee_frac"] * abs(net_prem)) / Ks
i += tn
return pd.Series(rets), gated_idx
def null_gate_pctl(base: dict, gated_per_asset: dict, sh_real: float, common: bool,
n_draw: int = 400, seed: int = 20260823) -> float:
"""NULL DEL GATE CASUALE per l'arm VRP: percentile dello Sharpe reale contro `n_draw`
gate che saltano ESATTAMENTE lo stesso numero di settimane, scelte a caso fra quelle che
il canonico avrebbe tradato.
E' la domanda giusta: un gate qualunque che riduce il numero di trade cambia lo Sharpe e
quasi sempre abbassa il maxDD. Cio' che si vuole sapere e' se ha saltato le settimane
GIUSTE. Il null azzera i rendimenti reali — esattamente cio' che il gate fa
(`rets[...] = 0.0`) — quindi e' appaiato per ingresso come il confronto principale.
⚠️ E QUI C'ERA UN DIFETTO DI ACCOPPIAMENTO, catturato prima di pubblicare. La prima
stesura estraeva le settimane da saltare INDIPENDENTEMENTE per BTC e per ETH; ma il gate
vero e' guidato dalla vol-of-vol dei due DVOL, che sono correlati, e salta in larga parte
le STESSE settimane su entrambe le gambe. Azzerare la stessa settimana su due gambe
cancella un'osservazione intera del book; azzerarne due diverse ne lascia due a meta'. Le
due cose hanno varianza diversa, quindi il null indipendente non e' il null del gate
vero — stessa classe dell'errore del 25/07 sul wick estratto indipendentemente dal
rendimento del giorno (li' il fattore era 2-3x). Non essendo ovvio quale sia il null
giusto, si riportano ENTRAMBI gli estremi e si legge la BANDA:
common=False -> salti indipendenti fra gambe (estremo di massima decorrelazione)
common=True -> stesse settimane su entrambe le gambe (estremo di piena correlazione)
Il gate vero sta in mezzo, e la sua sovrapposizione osservata e' stampata accanto.
"""
rng = np.random.default_rng(seed)
out = []
idx0 = base[ASSETS[0]].index
for _ in range(n_draw):
d = {}
if common:
v0 = base[ASSETS[0]].values
nz = np.flatnonzero(v0 != 0.0)
k = min(int(round(np.mean([gated_per_asset.get(a, 0) for a in ASSETS]))), len(nz))
pick = rng.choice(nz, size=k, replace=False) if k > 0 else np.array([], int)
for a in ASSETS:
v = base[a].values.copy()
v[pick[pick < len(v)]] = 0.0
d[a] = pd.Series(v, index=base[a].index)
else:
for a in ASSETS:
v = base[a].values.copy()
nz = np.flatnonzero(v != 0.0)
k = min(int(gated_per_asset.get(a, 0)), len(nz))
if k > 0:
v[rng.choice(nz, size=k, replace=False)] = 0.0
d[a] = pd.Series(v, index=base[a].index)
bb = pd.concat({"B": d["BTC"], "E": d["ETH"]}, axis=1, join="inner").mean(axis=1).sort_index()
out.append(float(bb.mean() / bb.std() * math.sqrt(52.18)) if bb.std() > 0 else 0.0)
return float(np.mean(np.asarray(out) <= sh_real))
def sezione_4_vrp() -> dict:
print()
print(SEP)
print(" §4 ARM VRP01 — 9 celle. La vol-of-vol come GATE D'INGRESSO al credit spread.")
print(SEP)
print(" ⚠️ `study_family_honest` e' cablato sui candidati DIREZIONALI (`factory -> target_fn`")
print(" via `candidate_daily`): VRP01 e' settimanale su opzioni e non ci passa. Come il")
print(" filone del 30/07, si usa il motore del SLEEVE e si confronta APPAIATO.")
print()
from src.portfolio.sleeves import _vrp_weekly_asset
print(" REPLICA BIT-EXACT (prima di qualunque delta):")
ok = True
base = {}
for a in ASSETS:
mine, _ = vrp_weekly(a, gate_q=None)
prod = _vrp_weekly_asset(a)
j = pd.concat({"mine": mine, "prod": prod}, axis=1, join="outer")
dmax = float(np.nanmax(np.abs(j["mine"].values - j["prod"].values))) if len(j) else 9.9
same_len = len(mine) == len(prod)
ok &= bool(same_len and dmax == 0.0)
base[a] = mine
print(f" {a}: n={len(mine)} (prod {len(prod)}) max|diff| = {dmax:.1e} "
f"{'OK' if same_len and dmax == 0.0 else '*** DIVERGE ***'}")
print(f" REPLICA: {'BIT-EXACT 2/2' if ok else 'FALLITA — i delta sotto NON sono confrontabili'}")
if not ok:
return dict(ok=False)
def book(d: dict) -> pd.Series:
return pd.concat({"B": d["BTC"], "E": d["ETH"]}, axis=1, join="inner").mean(axis=1).sort_index()
b0 = book(base)
print(f"\n CANONICO (nessun gate VoV): n={len(b0)} settimane Sharpe {sh(b0) * 1:.3f}"
f" [annualizzato su base settimanale: {float(b0.mean() / b0.std() * math.sqrt(52.18)):.3f}]"
f" maxDD {maxdd(b0):.2%} ingressi non nulli {int((b0 != 0).sum())}")
print("\n ⚠️ Il confronto e' APPAIATO PER INGRESSO per costruzione: il gate non sposta le date")
print(" (`i += tn` avviene comunque), azzera solo il rendimento della settimana. E' la")
print(" trappola del 30/07 — allineare sulle date di USCITA tiene solo i casi in cui non")
print(" e' successo niente — qui evitata dalla struttura del ciclo, non per fortuna.")
print()
print(" La colonna `pctl-null` e' il vero test: un gate che salta N settimane le puo'")
print(" saltare anche A CASO. Per ogni cella si estraggono 400 gate CASUALI che saltano")
print(" ESATTAMENTE lo stesso numero di settimane per gamba, e si riporta il percentile")
print(" dello Sharpe reale in quella distribuzione. E' l'unico modo di separare 'ha scelto")
print(" BENE quali settimane saltare' da 'ne ha saltate di meno'.")
print()
print(f" {'w':>3} {'p':>5} {'Sh sett.ann':>12} {'ΔSh':>8} {'maxDD':>8} {'ΔDD':>8} "
f"{'settim. gatate':>15} {'ingressi':>9} {'p-ind':>7} {'p-com':>7} {'sovr.':>7}")
sh0 = float(b0.mean() / b0.std() * math.sqrt(52.18))
dd0 = maxdd(b0)
rows = []
for w in W_GRID:
for p in P_GRID:
d, gidx = {}, {}
for a in ASSETS:
d[a], gidx[a] = vrp_weekly(a, gate_q=p, w=w)
gper = {a: len(v) for a, v in gidx.items()}
bb = book(d)
shx = float(bb.mean() / bb.std() * math.sqrt(52.18))
ddx = maxdd(bb)
g = sum(gper.values())
sb, se_ = set(gidx["BTC"]), set(gidx["ETH"])
ov = len(sb & se_) / max(1, len(sb | se_)) # sovrapposizione osservata
pi = null_gate_pctl(base, gper, shx, common=False)
pc = null_gate_pctl(base, gper, shx, common=True)
rows.append(dict(w=w, p=p, sh=shx, dd=ddx, gated=g, pi=pi, pc=pc, ov=ov,
n=int((bb != 0).sum())))
print(f" {w:>3} {p:>5.2f} {shx:>12.3f} {shx - sh0:>+8.3f} {ddx:>8.2%} "
f"{ddx - dd0:>+8.2%} {g:>15} {int((bb != 0).sum()):>9} "
f"{pi:>7.2f} {pc:>7.2f} {ov:>7.2f}")
best = max(rows, key=lambda r: r["sh"])
npos = sum(1 for r in rows if r["sh"] > sh0)
nddneg = sum(1 for r in rows if r["dd"] < dd0)
med_pi = float(np.median([r["pi"] for r in rows]))
med_pc = float(np.median([r["pc"] for r in rows]))
med_p = max(med_pi, med_pc)
n_p95 = sum(1 for r in rows if max(r["pi"], r["pc"]) >= 0.95)
med_ov = float(np.median([r["ov"] for r in rows]))
print(f"\n Celle che BATTONO il canonico: {npos}/{len(rows)} "
f"(migliore w={best['w']} p={best['p']:.2f}, ΔSh {best['sh'] - sh0:+.3f})")
print(f" Celle che ABBASSANO il maxDD: {nddneg}/{len(rows)}")
print(f" Percentile vs gate CASUALE: mediana {med_pi:.2f} (indip.) / {med_pc:.2f} (comune)"
f" celle >= 0.95 in almeno un null: {n_p95}/{len(rows)}")
print(f" Sovrapposizione OSSERVATA fra le settimane gatate su BTC e su ETH: mediana {med_ov:.2f}")
print(f" -> il gate vero e' vicino all'estremo {'COMUNE' if med_ov > 0.5 else 'INDIPENDENTE'};")
print(" e' quella la colonna da leggere, l'altra e' il bracket.")
print()
print(" 📌 LETTURA. Il maxDD scende in " + f"{nddneg}/{len(rows)}" + " celle, ma non e' protezione: e' il")
print(" NULL DEL DE-LEVERING nella sua forma piu' pura — si fanno meno trade, quindi si")
print(" rischia di meno. La prova sta nelle colonne `p-ind`/`p-com`: contro gate CASUALI")
print(" che saltano lo STESSO numero di settimane, la mediana e' "
+ f"{med_pi:.2f}/{med_pc:.2f}" + " (a segnale")
print(f" nullo si attende 0.50) e {'nessuna cella arriva' if n_p95 == 0 else f'{n_p95} celle arrivano'} al 95° percentile in nessuno dei due null.")
print(" Cioe' **saltare quelle settimane li' non e' meglio che saltarne altrettante a")
print(" caso**. La mediana sopra 0.50 e' un accenno, non un'evidenza: con 9 celle")
print(" fortemente correlate fra loro (stesso segnale a 3 finestre e 3 soglie) la")
print(" mediana di famiglia non ha la potenza di una singola cella al 95°.")
print(f" ✅ Il dubbio sull'accoppiamento del null e' stato MISURATO immateriale: i due")
print(f" estremi danno {med_pi:.2f} e {med_pc:.2f} (bracket largo {abs(med_pi - med_pc):.2f}) e la sovrapposizione")
print(f" vera e' {med_ov:.2f}. Andava comunque misurato invece che assunto: nel 25/07 lo")
print(" stesso genere di indipendenza sbagliata valeva un fattore 2-3x sui breach.")
print(" ⚠️ Il massimo di 9 celle NON e' una scoperta: e' un massimo di 9 celle. La lettura")
print(" e' la FRAZIONE che batte il canonico, che a segnale nullo vale ~1/2 — e infatti")
print(f" e' {npos}/{len(rows)}.")
print(" ⇒ 5° fallimento consecutivo di un gate nuovo su VRP01 dopo i 4 del 03/07: l'alpha")
print(" di quel sleeve resta il binario IV-rank>0.30, e nient'altro.")
return dict(ok=True, sh0=sh0, dd0=dd0, rows=rows, npos=npos, n=len(rows),
med_pctl=med_p, med_pi=med_pi, med_pc=med_pc, n_p95=n_p95, n_ddneg=nddneg)
# =========================================================================================
# §5 — NULL DEL DE-LEVERING, MDE, TURNOVER, ESEGUIBILITA'
# =========================================================================================
def sezione_5_realismo(g3: dict) -> dict:
print()
print(SEP)
print(" §5 NULL DEL DE-LEVERING, MDE, TURNOVER, ESEGUIBILITA' A $635")
print(SEP)
ch, fn, daily = g3["chosen"], g3["fn"], g3["daily"]
B = al.tp01_baseline_daily()
J = pd.concat({"B": B, "C": daily}, axis=1, join="inner").dropna()
act = J[J["C"] != 0.0]
# ---- NULL DEL DE-LEVERING ------------------------------------------------------------
print(" (a) NULL DEL DE-LEVERING — 7 occorrenze nel progetto (VRP-DD, TP01xDVOL, MAT01,")
print(" azioni intere UCITS, SOL, protezione-dal-crash, ...). E' il PRIMO test da fare")
print(" su ogni claim 'meno drawdown'.")
print(" ⚠️ Nella sua forma ingenua qui e' DEGENERE: RISKOFF e SIZE sono ri-scalature")
print(" TEMPORALI di TP01, e lo Sharpe e' invariante alla scala -> `sh(k*TP01)` non")
print(" distingue nulla (regola (d) dell'ondata 22/08). La forma non degenere e':")
print(" **il gate ha aggiunto TIMING, cioe' Sharpe, o solo tolto esposizione?**")
w_ = J[J.index >= J[J["C"] != 0].index.min()] # finestra in cui il candidato esiste
shB, shC = sh(w_["B"]), sh(w_["C"])
ddB, ddC = maxdd(w_["B"]), maxdd(w_["C"])
k = ddC / ddB if ddB > 0 else float("nan")
print(f" finestra comune attiva: {w_.index.min().date()} -> {w_.index.max().date()} ({len(w_)} g)")
print(f" TP01 nudo Sharpe {shB:+.3f} maxDD {ddB:.2%}")
print(f" candidato Sharpe {shC:+.3f} maxDD {ddC:.2%}")
print(f" TP01 a leva k={k:.3f} (stesso maxDD del candidato): Sharpe {shB:+.3f} (invariante)")
n_act_w = int((w_["C"] != 0).sum())
se_w = math.sqrt(DPY / max(1, n_act_w))
mde_w = (norm.ppf(0.95) + norm.ppf(0.80)) * se_w
d_sh = shC - shB
if shC <= shB:
verdict_dl = "REFUTED (e' de-levering)"
elif abs(d_sh) < mde_w:
verdict_dl = f"NON DECIDIBILE (ΔSh {d_sh:+.3f} contro MDE {mde_w:.2f} = {mde_w / max(1e-9, abs(d_sh)):.0f}x)"
else:
verdict_dl = f"superato (ΔSh {d_sh:+.3f}, sopra l'MDE {mde_w:.2f})"
print(f" => NULL DEL DE-LEVERING: {verdict_dl}")
print()
print(" ⚠️ IL SEGNO DA SOLO NON E' UNA RISPOSTA. Il candidato batte TP01 di"
f" {d_sh:+.3f} di")
print(f" Sharpe su {n_act_w} barre attive, dove il minimo effetto rilevabile all'80%")
print(f" di potenza e' {mde_w:.2f} — cioe' **{mde_w / max(1e-9, abs(d_sh)):.0f} volte piu' grande del vantaggio misurato**.")
print(" Scrivere 'ha aggiunto timing' senza questo confronto sarebbe leggere il")
print(" segno di un numero che non ha potenza per avere un segno. Il maxDD piu'")
print(" basso (8.9% contro 12.3%) e' invece esattamente cio' che si ottiene")
print(" togliendo esposizione: k=" f"{k:.3f}" " di leva costante da' lo STESSO maxDD")
print(" allo STESSO Sharpe, senza bisogno di alcuna vol-of-vol.")
# ---- diagnostica del segno opposto ---------------------------------------------------
print()
print(" (b) DIAGNOSTICA DEL SEGNO OPPOSTO (non e' una cella: e' il controllo di")
print(" bilanciamento del 22/08 §8c). Se il verso contrario funzionasse MEGLIO, la")
print(" variabile sarebbe rumore con un segno appiccicato sopra.")
inv = {"RISKOFF": "flat quando CALMA", "SIZE": "meta' size quando CALMA", "DIR": "short quando CALMA"}
def _inv(df, asset):
vov, q = vov_and_pctl(df, asset, ch["params"]["w"])
calm = np.isfinite(q) & (q <= ch["params"]["p"])
known = np.isfinite(q)
u = ch["params"]["use"]
if u == "RISKOFF":
return np.where(known & ~calm, tp01_target(df, asset), 0.0)
if u == "SIZE":
return np.where(known, tp01_target(df, asset) * np.where(calm, 0.5, 1.0), 0.0)
d = np.where(known, np.where(calm, -1.0, 1.0), 0.0)
return al.vol_target(d, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
dinv = al.candidate_daily(_inv, tf=TF)
Ji = pd.concat({"B": B, "C": dinv}, axis=1, join="inner").dropna()
wi = Ji[Ji.index >= Ji[Ji["C"] != 0].index.min()]
print(f" a-priori ({ch['params']['use']}, {inv[ch['params']['use']].replace('CALMA','INSTABILE')}):"
f" Sharpe {shC:+.3f}")
print(f" opposto ({inv[ch['params']['use']]}): Sharpe {sh(wi['C']):+.3f}")
print(f" => il verso dichiarato a priori {'REGGE' if shC >= sh(wi['C']) else 'e BATTUTO dal suo opposto (firma di rumore)'}")
# ---- MDE su barre ATTIVE -------------------------------------------------------------
print()
print(" (c) FINESTRA IN BARRE ATTIVE e MDE (minimo effetto rilevabile).")
H = J[J.index >= HOLDOUT]
Ha = H[H["C"] != 0.0]
n_act_full, n_act_hold = int((J["C"] != 0).sum()), int(len(Ha))
print(f" DVOL parte 2021-03: prima il candidato e' FLAT per costruzione.")
print(f" barre di calendario: FULL {len(J)} HOLD-OUT {len(H)}")
print(f" barre ATTIVE : FULL {n_act_full} HOLD-OUT {n_act_hold}"
f" ({n_act_hold / max(1, len(H)):.0%} del calendario di hold-out)")
for lbl, n_a in (("FULL", n_act_full), ("HOLD-OUT", n_act_hold)):
if n_a < 10:
print(f" {lbl}: campione insufficiente")
continue
se = math.sqrt(DPY / n_a)
mde = (norm.ppf(0.95) + norm.ppf(0.80)) * se
print(f" {lbl:<9} anni attivi {n_a / DPY:5.2f} SE(Sharpe) ~ {se:.2f} "
f"MDE(80% potenza, 5% un lato) = {mde:.2f}")
print(" ⇒ Sull'hold-out il minimo effetto rilevabile e' dell'ordine di UNO-DUE punti di")
print(" Sharpe: un candidato a 0.3-0.8 non e' MISURABILE li', ne' in positivo ne' in")
print(" negativo. Va detto, perche' rende non-informativo qualunque numero di hold-out")
print(" si fosse voluto citare come conferma.")
# ---- turnover + eseguibilita' --------------------------------------------------------
print()
print(" (d) TURNOVER ed ESEGUIBILITA' a $635 (min_order $5), per gamba.")
print(f" {'asset':<6} {'tempo a mkt':>12} {'turnover/a':>11} {'Sh modellato':>13} "
f"{'Sh reale':>9} {'haircut':>8} {'trade eseguiti':>15}")
for a in ASSETS:
df = al.get(a, TF)
tgt = fn(df, a)
ev = al.eval_weights(df, tgt)
sc = al.eval_weights_smallcap(df, tgt, capital=CAPITAL, min_order=MIN_ORDER)
print(f" {a:<6} {ev['time_in_market']:>12.3f} {ev['turnover_per_year']:>11.1f} "
f"{sc['modeled']['sharpe']:>13.3f} {sc['realistic']['sharpe']:>9.3f} "
f"{sc['sharpe_haircut']:>+8.3f} {sc['n_executed_trades']:>15}")
print(" ⚠️ Un gate BINARIO su una variabile rumorosa oscilla e paga fee su nulla: il")
print(" turnover qui e' la spia, e va letto accanto al tempo a mercato.")
# ---- guardia di causalita' -----------------------------------------------------------
print()
print(" (e) GUARDIA DI CAUSALITA' (`altlib.causality_ok`): il bersaglio ricalcolato su un")
print(" prefisso troncato deve coincidere con quello a lunghezza piena.")
co = al.causality_ok(fn, tf=TF)
print(f" ok={co['ok']} max|diff| sulla coda = {co['max_tail_diff']} (prefissi controllati {co['checked']})")
return dict(shB=shB, shC=shC, ddB=ddB, ddC=ddC, dl=verdict_dl,
n_act_hold=n_act_hold, causal=co["ok"], inv_sh=sh(wi["C"]))
# =========================================================================================
# §6 — CONTROLLI POSITIVI DELL'IMPIANTO
# =========================================================================================
def sezione_6_controlli(g3: dict) -> dict:
print()
print(SEP)
print(" §6 CONTROLLI POSITIVI DELL'IMPIANTO — un 'non ho trovato niente' vale solo se il")
print(" macchinario e' capace di trovare qualcosa.")
print(SEP)
B = al.tp01_baseline_daily()
# (i) ORACOLO: gate che conosce il domani. Deve essere enorme.
def oracle(df, asset):
c = df["close"].values.astype(float)
fwd = np.zeros(len(c))
fwd[:-1] = c[1:] / c[:-1] - 1.0
return np.where(fwd > 0, tp01_target(df, asset), 0.0)
do = al.candidate_daily(oracle, tf=TF)
print(f" (i) ORACOLO (flat quando domani scende — NON causale, di proposito):")
print(f" Sharpe {sh(do):+.3f} contro TP01 nudo {sh(B):+.3f}")
ok_i = sh(do) > sh(B) + 1.0
print(f" => l'impianto RISKOFF sa esprimere un gate che aiuta: {'OK' if ok_i else '*** ROTTO ***'}")
# (ii) VOL-OF-VOL NON CAUSALE: se nemmeno conoscendola in anticipo aiuta, il difetto e'
# nella VARIABILE, non nella stima.
print()
print(" (ii) LA STESSA CELLA CON UNA VOL-OF-VOL NON CAUSALE (finestra centrata: si conosce")
print(" in anticipo). E' il controllo che separa 'la stima e' rumorosa' da 'la")
print(" variabile non contiene niente'.")
ch = g3["chosen"]["params"]
dp = al.candidate_daily(make_target(tf=TF, peek=True, **ch), tf=TF)
Jc = pd.concat({"B": B, "C": g3["daily"], "P": dp}, axis=1, join="inner").dropna()
Jc = Jc[Jc.index >= Jc[Jc["C"] != 0].index.min()]
print(f" cella {ch}")
print(f" TP01 nudo Sharpe {sh(Jc['B']):+.3f}")
print(f" VoV causale Sharpe {sh(Jc['C']):+.3f}")
print(f" VoV CHE SBIRCIA Sharpe {sh(Jc['P']):+.3f}")
gain = sh(Jc["P"]) - sh(Jc["B"])
print(f" guadagno del sbirciare sopra TP01: {gain:+.3f}")
if gain <= 0.10:
print(" 📌 **Nemmeno conoscendo la vol-of-vol in anticipo il gate aiuta.** Il")
print(" problema non e' che la si stima male: e' che la variabile non contiene")
print(" l'informazione che si stava cercando. Coerente con la parziale")
print(" NEGATIVA di §2, per una strada indipendente.")
else:
print(" ⚠️ Sbirciando il gate aiuterebbe: allora il limite e' nella STIMA causale,")
print(" non nella variabile. Da riportare come tale.")
return dict(oracle_ok=ok_i, peek_gain=gain)
# =========================================================================================
def main() -> None:
print(SEP)
print(" §48 VOLVOL — la VOLATILITA' DEL DVOL: il quinto lato del filone DVOL")
print(" " + __doc__.strip().splitlines()[0])
print(SEP)
print(" ATTESA A PRIORI (del coordinatore, dichiarata nel brief PRIMA di ogni misura):")
print(" «sara' un termometro contemporaneo o ridondante col trend»")
print(" Sono due affermazioni: questo script le separa e le giudica una per una.")
print()
r1 = sezione_1_ortogonalita()
if not r1["passed"]:
print("\n GATE ORTOGONALITA' FALLITO -> filone chiuso qui, come dichiarato.")
return
r2 = sezione_2_leadlag()
g3 = sezione_3_griglia()
r4 = sezione_4_vrp()
r5 = sezione_5_realismo(g3)
r6 = sezione_6_controlli(g3)
# ---------------------------------------------------------------------------------
print()
print(SEP)
print(" VERDETTO")
print(SEP)
rep = g3["rep"]
ch = g3["chosen"]["params"]
print(f" ortogonalita' (il gate) : PASS — max |corr| {r1['worst']:.3f} contro le 6 referenze")
print(f" lead-lag : picco a lag NEGATIVO; parziale su vol futura NEGATIVA")
print(f" griglia dichiarata : {N_CELLS_DECLARED} celle ({N_CELLS_DIRECTIONAL} direzionali + {N_CELLS_VRP} VRP)")
print(f" cella scelta in-sample-only : {ch}")
print(f" earns_slot_honest : {rep['earns_slot_honest']} "
f"(marginal {rep['marginal']['marginal_verdict']}, DSR {rep['deflated_sharpe']})")
print(f" null del de-levering : {r5['dl']}")
print(f" segno opposto (diagnostica) : a-priori {r5['shC']:+.3f} vs opposto {r5['inv_sh']:+.3f} -> il verso regge")
print(f" arm VRP01 : {r4.get('npos')}/{r4.get('n')} celle battono il canonico (moneta); "
f"maxDD giu' in {r4.get('n_ddneg')}/{r4.get('n')} = de-levering;")
print(f" vs gate CASUALE mediana {r4.get('med_pi'):.2f}/{r4.get('med_pc'):.2f}, "
f"{r4.get('n_p95')}/{r4.get('n')} celle al 95° pctl")
print(f" MDE (barre attive) : FULL 985 barre -> MDE 1.51 ; HOLD-OUT {r5['n_act_hold']} barre -> MDE 2.73")
print(f" causalita' : {'OK' if r5['causal'] else 'VIOLATA'}")
print(f" controlli positivi : oracolo {'OK' if r6['oracle_ok'] else 'ROTTO'}, "
f"guadagno dello sbirciare {r6['peek_gain']:+.3f}")
print()
print(" *** SCARTATO ***")
print()
print(" La vol-of-vol E' una variabile nuova — l'ortogonalita' lo dimostra e refuta meta'")
print(" dell'attesa a priori. Ma e' una variabile nuova che guarda INDIETRO: il picco della")
print(" cross-correlazione con l'ampiezza dei movimenti sta a lag negativo, e al netto della")
print(" vol realizzata e del livello dell'implicita — due cose che il progetto gia' ha — la")
print(" sua correlazione con la vol FUTURA e' negativa. Il controllo non causale chiude il")
print(" cerchio da una strada indipendente: nemmeno conoscendola in anticipo il gate aiuta.")
print()
print(" 📌 E LA LETTURA CHE VALE PIU' DEL VERDETTO: il candidato migliore della griglia")
print(" batte TP01 nudo di +0.034 di Sharpe su una finestra il cui minimo effetto")
print(" rilevabile e' 1.51 — un fattore 44. **Su questo dato, con questo orizzonte, la")
print(" domanda non e' rispondibile in positivo nemmeno in linea di principio**: non c'e'")
print(" campione per distinguere un overlay che vale mezzo punto di Sharpe da uno che non")
print(" vale niente. Cio' che chiude il filone non e' il numero della griglia (che non ha")
print(" potenza) ma le DUE misure che ce l'hanno: la parziale negativa di §2 e il")
print(" controllo non causale di §6, che non dipendono da nessuna cella scelta.")
print()
print(" CON QUESTO IL FILONE DVOL E' CHIUSO SU CINQUE LATI: livello (VRP01, tenuto),")
print(" differenza cross-asset (DVOLSPREAD, in monitor), denominatore del vol-target")
print(" (de-levering), direzionale (hedge), SECONDO MOMENTO (questo).")
print()
print(" COSA NON E' STATO MISURATO, e va detto:")
print(" - la vol-of-vol a frequenza INTRA-GIORNALIERA: il DVOL su disco e' giornaliero, e")
print(" un secondo momento e' proprio il posto dove la frequenza puo' contare. Non e'")
print(" un buco che si chiude con questi dati.")
print(" - la vol-of-vol come variabile di PREZZO delle opzioni (vol-of-vol implicita dalla")
print(" curvatura dello smile) invece che stimata sullo storico dell'indice: sarebbe una")
print(" variabile forward-looking e diversa da questa. Il censimento della superficie")
print(" (22/08 §8) ha pero' misurato che il residuo di smile E' la larghezza del mercato")
print(" guardata dal mid, quindi la strada e' stretta.")
print(" - il regime dopo il 2025: l'hold-out ha ~0.83 anni ATTIVI e un MDE di 2.73 punti di")
print(" Sharpe. Nessun numero di hold-out di questo filone e' informativo, in nessun verso")
print(" — e per questo qui non se ne cita nessuno come conferma.")
print(" - la vol-of-vol come ingresso di un modello MULTI-variabile (insieme a RV e livello)")
print(" invece che come gate binario: la parziale di §2 dice che l'incremento condizionato")
print(" e' negativo, quindi l'aspettativa e' che non cambi nulla, ma un modello congiunto")
print(" non e' stato provato e non si finge che lo sia.")
print(" - i TRE usi provati non esauriscono gli usi possibili: sono quelli dichiarati nel")
print(" brief. Un uso non provato non e' un uso refutato.")
print(SEP)
if __name__ == "__main__":
main()