research(wave-0822): VRP-QUOTE-VERE scartato — e collect_chain raccoglie la famiglia di contratti che il conto non puo' marginare

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:19:07 +00:00
parent f4b14f305b
commit 75606d0f44
9 changed files with 1800 additions and 37 deletions
+116 -2
View File
@@ -5,7 +5,11 @@ DOMANDA. Tre, dichiarate prima di misurare:
Q2 il CARRY di vol (roll-down lungo la curva) e' INCASSABILE al bid-ask reale?
Q3 vale come GATE DI RISCHIO sopra TP01/SKH01?
IPOTESI A PRIORI (registrate prima di guardare i numeri — servono a poterle smentire):
IPOTESI A PRIORI (⚠️ da dove vengono: NON da una busta sigillata. Vengono dai precedenti gia'
DOCUMENTATI del progetto — i 4 gate di de-risk risultati ridondanti col trend, il verdetto HEDGE
su VRP-Z del 29/06, l'aritmetica fee/carry gia' fatta su VRP01 e gamma-scalping. Sono state
scritte prima di far girare QUESTO script, ma dopo una fase esplorativa sui dati: si dichiara,
perche' "l'avevo previsto" vale solo se si dice quanto costava prevederlo):
Q1 contemporanea. Il front reagisce allo spot: un tuffo alza la vol a 7g piu' di quella a 30g.
Se e' cosi', la pendenza e' un TERMOMETRO, non un segnale.
Q2 no. Il roll-down fra 7g e 30g vale frazioni di punto-vol al giorno; il bid-ask ATM di
@@ -77,6 +81,10 @@ CAPITALE = 635.0 # il conto vero
# Conteggio dei trial, AL RIALZO (sezione 6). Ogni voce e' una configurazione VALUTATA.
TRIALS: list[tuple[str, int]] = []
# I pochi numeri su cui poggia il verdetto, raccolti dalle sezioni che li misurano: il verdetto
# NON li riscrive a mano (un numero ricopiato in una conclusione e' un numero che smette di
# aggiornarsi quando i dati crescono).
FACTS: dict = {}
def hr(t: str = "") -> None:
@@ -333,8 +341,11 @@ def sezione2(curve: pd.DataFrame) -> dict:
idx = j.index
r1 = lp.diff()
sub(f"{a} — n ore = {len(j)} ({idx.min()} -> {idx.max()})")
FACTS[f"back_sev_{a}"] = float((j.slope < -5).mean())
FACTS[f"back_{a}"] = float((j.slope < 0).mean())
print(f" pendenza iv_{BACK}-iv_{FRONT}: media {j.slope.mean():+.2f} pt-vol, "
f"sd {j.slope.std():.2f}, backwardation nel {100*(j.slope<0).mean():.1f}% delle ore")
f"sd {j.slope.std():.2f}, backwardation nel {100*(j.slope<0).mean():.1f}% delle ore, "
f"SEVERA (< -5 pt-vol) nel {100*(j.slope<-5).mean():.1f}%")
print("\n (a) CROSS-CORRELAZIONE fra la VARIAZIONE oraria di pendenza e il ritorno orario.")
print(" lag<0 = ritorno PRIMA della variazione (il prezzo guida) | lag>0 = DOPO (la")
@@ -374,6 +385,7 @@ def sezione2(curve: pd.DataFrame) -> dict:
print(f" corr(iv_{FRONT}, RV 24h passata) {float(ivf.corr(rvp)):+.3f} "
f"corr(iv_{FRONT}, RV futura) {float(ivf.corr(rvf)):+.3f}")
res[(a, "conf_past")], res[(a, "conf_fwd")] = c_pp, c_pf
FACTS[f"conf_past_{a}"], FACTS[f"conf_fwd_{a}"] = c_pp, c_pf
print("\n (d) La correlazione col futuro SOPRAVVIVE se si controlla per il PASSATO?")
print(" (regressione del ritorno futuro su pendenza + ritorno passato + RV passata;")
@@ -412,6 +424,8 @@ def sezione2(curve: pd.DataFrame) -> dict:
p = _block_pctl(x, y, c, L=max(3 * h, 7))
c7 = float(np.corrcoef(x[:-7], y[:-7])[0, 1]) if len(x) > 20 else np.nan
c7b = float(np.corrcoef(x[7:], y[7:])[0, 1]) if len(x) > 20 else np.nan
if h == 5:
FACTS[f"c5_{a}"], FACTS[f"c5_no7_{a}"] = c, c7
print(f" h={h}g n={m.sum():3d} corr={c:+.3f} pctl vs null a blocchi={p:.3f}"
f" | senza ultima settimana {c7:+.3f} | senza prima settimana {c7b:+.3f}")
print(" ^ le due colonne di destra sono il test di RISOLUZIONE: se togliere 7")
@@ -548,6 +562,7 @@ def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) ->
f"un round-trip ATM 10-20g costa {sp:.3f} pt-vol")
print(f" => servono {sp/rd:.1f} GIORNI di carry per pagare UN solo giro, "
f"e nel frattempo si porta il vega.")
FACTS[f"giorni_carry_{a}"] = sp / rd
TRIALS.append(("Q2: 5 secchi di tenore x 2 asset (costo)", 10))
sub("(d) e a $635 la domanda non e' nemmeno questa: il LOTTO MINIMO")
@@ -610,6 +625,8 @@ def sezione4(curve: pd.DataFrame) -> None:
sub("(a) il fatto che decide da solo la sezione")
W = pd.concat(pos, axis=1).loc[TS_START:]
flat = (W.abs() < 0.05).mean()
FACTS["tp01_flat"] = float(flat.mean())
FACTS["n_giorni"] = int(len(W))
print(f" Nella finestra {TS_START.date()} -> oggi ({len(W)} giorni), TP01 e' FLAT nel "
f"{flat['BTC']:.1%} (BTC) / {flat['ETH']:.1%} (ETH) dei giorni.")
print(f" Esposizione media: BTC {W['BTC'].mean():.3f}, ETH {W['ETH'].mean():.3f} "
@@ -665,6 +682,8 @@ def sezione4(curve: pd.DataFrame) -> None:
print(f" buy&hold sulla stessa finestra/ancore: mediana {bh['median']:+.2f} "
f"[{bh['lo']:+.2f}, {bh['hi']:+.2f}] <-- il vero termine di paragone in un toro")
dlt = A.anchor_luck_delta(cand, base, list(range(24)))
FACTS[f"vs_bh_{a}"] = dlt["median_paired"]
FACTS[f"vs_bh_n_{a}"] = (dlt["n_positive"], dlt["n_anchors"])
print(f" mediana delle DIFFERENZE APPAIATE candidato-B&H: {dlt['median_paired']:+.2f} "
f"(positiva in {dlt['n_positive']}/{dlt['n_anchors']} ancore)")
TRIALS.append((f"Q3 {a}: 24 ancore x 2 varianti", 48))
@@ -697,10 +716,12 @@ def sezione4(curve: pd.DataFrame) -> None:
else:
verdict = ("IL CANDIDATO NON SOPRAVVIVE (il de-levering fa meglio)"
if best[1] >= sh_c else "candidato sopra il null di de-levering")
FACTS[f"delev_{a}"] = bool(best[1] >= sh_c)
print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | buy&hold a leva "
f"k={best[0]:.2f} -> stesso o minor DD ({best[2]:.2%}) con Sharpe {best[1]:+.2f}"
f" => {verdict}")
imp = A.implausible_sharpe(net)
FACTS[f"implaus_{a}"] = bool(imp["implausible"])
print(f" implausible_sharpe sul candidato di finestra: implausible="
f"{imp['implausible']} {imp['reasons']}")
TRIALS.append((f"Q3 {a}: 60 valori di k nel null de-levering", 60))
@@ -760,6 +781,9 @@ def sezione5(curve: pd.DataFrame) -> None:
print(f" era DVOL (2021-10 -> oggi, {len(era)} giorni): Sharpe {A._sh(era):+.3f} "
f"maxDD {A._dd_ret(era):.2%}")
rep = A.marginal_vs_tp01(cd)
FACTS["proxy_marginal"] = rep.get("marginal_verdict")
FACTS["proxy_sharpe"] = A._sh(era)
FACTS["proxy_uplift"] = rep["blends"]["w25"]["uplift_full"]
print(f" marginal_vs_tp01 -> {rep.get('marginal_verdict')} | corr a TP01 "
f"{rep.get('corr_full')} | uplift w25 full {rep['blends']['w25']['uplift_full']:+.3f} "
f"hold {rep['blends']['w25']['uplift_hold']}")
@@ -807,6 +831,95 @@ def sezione6() -> None:
print(" su cui questo filone ricade (VRP-Z: earns_slot=False).")
# ===========================================================================================
# SEZIONE 7 — VERDETTO
# ===========================================================================================
def sezione7() -> None:
F = FACTS
hr("SEZIONE 7 — VERDETTO")
print(f"""
Q1 LA PENDENZA E' UN TERMOMETRO, NON UN SEGNALE — e la finestra non ha risoluzione.
· La cross-correlazione oraria ha il picco a lag -1h/0h (il prezzo si muove PRIMA); da
+1h in poi e' rumore (|corr| <= 0.05 su ogni lag fino a 24h).
· corr(pendenza, RV 24h PASSATA) = {F.get('conf_past_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_past_ETH', float('nan')):+.2f} (ETH)
corr(pendenza, RV 24h FUTURA) = {F.get('conf_fwd_BTC', float('nan')):+.2f} (BTC) / {F.get('conf_fwd_ETH', float('nan')):+.2f} (ETH)
La pendenza SA qualcosa della vol futura, ma sa DI PIU' di quella passata: e' un premio
di vol con la gamba realizzata sostituita da una implicita, non una variabile nuova.
· L'unica correlazione col ritorno futuro che sembra qualcosa (h=5g: {F.get('c5_BTC', float('nan')):+.2f} BTC /
{F.get('c5_ETH', float('nan')):+.2f} ETH) NON SOPRAVVIVE alla rimozione dell'ULTIMA SETTIMANA:
diventa {F.get('c5_no7_BTC', float('nan')):+.2f} / {F.get('c5_no7_ETH', float('nan')):+.2f}. Sette giorni tolti da un campione di 70 ne
ribaltano il SEGNO — ed e' il test che conta piu' del percentile contro il null.
E nella regressione controllata per il ritorno passato il t di Newey-West sta sotto 1.0
a ogni orizzonte. Non e' "debole": e' sotto la risoluzione del campione.
Q2 IL CARRY C'E' MA NON PAGA IL BID-ASK, e non e' nemmeno incassato.
· Roll-down da corto: ~0.07 (BTC) / ~0.13 (ETH) punti-vol al GIORNO.
Round-trip ATM 10-20g: 1.26 (BTC) / 1.77 (ETH) punti-vol.
=> {F.get('giorni_carry_BTC', float('nan')):.0f} giorni (BTC) / {F.get('giorni_carry_ETH', float('nan')):.0f} giorni (ETH) di carry per pagare UN giro.
· E il roll NON viene incassato: beta(realizzato ~ implicito) e' NEGATIVO (~-1 a una
settimana), cioe' la forma della curva torna indietro piu' di quanto la curva rotoli.
La differenza realizzato-implicito e' dentro il rumore (|t| <= 0.9 a blocchi).
· A $635 il vincolo binding e' prima di tutto il LOTTO MINIMO: una gamba ATM costa il 26%
(BTC) / 10% (ETH) del conto in premio, la gamba corta vuole margine, e il carry lordo
di una struttura ETH e' ~$0.19/giorno. Il bersaglio dichiarato e' 50 EUR/giorno.
Q3 GATE DI RISCHIO: RIDONDANTE COL TREND — 5a occorrenza, e NON MISURABILE su questa finestra.
· TP01 e' FLAT nel {F.get('tp01_flat', float('nan')):.0%} dei giorni della finestra: non c'e' rischio da togliere.
Un gate che spegne il 25% dei giorni trova TP01 gia' flat nel 79-90% di quei giorni, e
corr(pendenza, esposizione TP01) e' NEGATIVA (-0.29 BTC / -0.52 ETH): la backwardation
arriva quando il trend e' gia' uscito. Firma identica ai 4 precedenti.
· Il candidato DIREZIONALE fa Sharpe ~3 su 24/24 ancore — e non vuol dire niente: il
buy&hold sulla stessa finestra fa altrettanto. Mediana delle DIFFERENZE APPAIATE contro
buy&hold: {F.get('vs_bh_BTC', float('nan')):+.2f} BTC (positiva in {F.get('vs_bh_n_BTC', (0, 0))[0]}/{F.get('vs_bh_n_BTC', (0, 0))[1]} ancore) e {F.get('vs_bh_ETH', float('nan')):+.2f} ETH ({F.get('vs_bh_n_ETH', (0, 0))[0]}/{F.get('vs_bh_n_ETH', (0, 0))[1]}).
· Null del de-levering: ETH {'NON superato' if F.get('delev_ETH') else 'superato'}, BTC {'NON superato' if F.get('delev_BTC') else 'superato'}.
· `implausible_sharpe` segnala ENTRAMBI i candidati di finestra (implaus BTC={F.get('implaus_BTC')},
ETH={F.get('implaus_ETH')}): Calmar 26-52 su 74 giorni di toro = il modo di perdita non e' nel
campione. Il gate qui funziona da controllo positivo del CAMPIONE, non della strategia.
TEST LUNGO (proxy DVOL-RV7, 5 anni): Sharpe {F.get('proxy_sharpe', float('nan')):+.2f}, marginale vs TP01 =
{F.get('proxy_marginal')}, uplift w25 {F.get('proxy_uplift', float('nan')):+.3f} (NEGATIVO), robust_oos=False
=> earns_slot = False. Il proxy e' sfocato (spiega il 21-48% della varianza della pendenza
vera), quindi il suo negativo e' evidenza PIU' DEBOLE di un negativo diretto — ma va nella
stessa direzione del verdetto HEDGE gia' dato a VRP-Z il 2026-06-29.
======================================================================================
VERDETTO: SCARTATO come sleeve e come gate. LEAD residuo SOLO sulla term structure come
PREVISORE DI VOL (non di ritorni), con gate pre-registrato qui sotto.
======================================================================================
Perche' non "LEAD" sul lato direzionale: un LEAD e' una cosa che vale la pena SORVEGLIARE, e
sorvegliare costa (la lezione DVOLSPREAD: 35 giorni di limbo). Qui il lato direzionale non ha
bisogno di altri dati per essere giudicato: e' gia' misurato su 5 anni via proxy (earns_slot
False) e la variabile e' quella di VRP-Z. Aprire un monitor sarebbe pagare due volte lo stesso
filone. Il lato del CARRY e' chiuso dall'aritmetica, che non migliora col tempo: il bid-ask
ATM di Deribit dovrebbe stringersi di 10-20x perche' il roll-down lo paghi.
GATE PRE-REGISTRATO (l'unico pezzo che il tempo puo' cambiare):
· COSA: la pendenza come previsore della RV a 24h (misurata: corr {F.get('conf_fwd_BTC', float('nan')):+.2f} / {F.get('conf_fwd_ETH', float('nan')):+.2f}).
Non serve a prendere posizione sul prezzo; servirebbe come DENOMINATORE del
vol-target, cioe' esattamente cio' che `tp01_dvol_overlay` (26/06) provo' col DVOL
a un tenore solo e trovo' essere puro de-levering. La curva a piu' tenori e' l'unica
variante di quell'esperimento mai fatta.
· QUANDO: 2027-02-01. A quella data la catena avra' ~8 mesi di curva (vs 2,4 oggi) e la
finestra conterra' almeno un regime di vol diverso da questo toro.
· SOGLIE (dichiarate ORA, non allora): si riapre SOLO se, sulla finestra estesa,
(a) corr(pendenza, RV futura 24h) resta <= -0.30 su ENTRAMBI gli asset, E
(b) la pendenza batte il DVOL a tenore singolo come previsore di RV
(R2 incrementale > 0 in una regressione che contiene gia' il DVOL: se non
aggiunge nulla al DVOL, la curva non serve e basta il tenore che abbiamo dal 2021), E
(c) il null del de-levering e' superato sul vol-target risultante.
Se (b) fallisce il filone si CHIUDE definitivamente: vorra' dire che la term
structure non porta informazione oltre il livello, e il livello ce l'abbiamo gia'.
· COSA MI SMENTIREBBE OGGI: una finestra che contenga un vero crash. Tutte e tre le risposte
qui sono misurate su 74 giorni di TORO con TP01 flat il {F.get('tp01_flat', float('nan')):.0%} del tempo. La backwardation
severa (pendenza < -5 pt-vol) copre il {F.get('back_sev_BTC', float('nan')):.1%} delle ore BTC e il {F.get('back_sev_ETH', float('nan')):.1%} delle ore
ETH, e mai dentro un drawdown vero del libro: se in un crash
la pendenza anticipasse i ritorni di ore invece di seguirli, Q1 cambierebbe risposta. Cio'
che NON cambierebbe e' Q2 (aritmetica del bid-ask) ne' l'eseguibilita' a $635.
""")
def main() -> None:
hr("r0822_term_structure — TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH")
chain = load_quoted_chain()
@@ -820,6 +933,7 @@ def main() -> None:
sezione4(curve)
sezione5(curve)
sezione6()
sezione7()
hr("FINE")