9cb3465374
§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
1013 lines
57 KiB
Python
1013 lines
57 KiB
Python
#!/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 DVOL−RV 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 DVOL−RV (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()
|