cblib causale: spot +1h e DVOL +1 giorno (debito §5.18); §11 rimisurato 0,714 → 0,712, verdetti invariati
- cblib.causale(s, cadenza): spot_series e dvol_series escono causali (asof = ultima chiusura NOTA); il DVOL giornaliero era una seconda serie con lo stesso difetto (fino a 24h avanti), verificato contro l'API pubblica a 1h. Regolamento ST alle 08:00 invece delle 09:00. - §11 riprodotto al millesimo su worktree HEAD (0,714 [0,690-0,779], n=19, flag --al) e rimisurato: 0,712 [0,664-0,732]; solo spot 0,721, solo DVOL 0,693, DVOL orario di controllo 0,717. Mediana onesta BTC 1,45 → 2,12, fee 1,51x/1,87x. r0730: 0,73 → 0,71. §75: ETH pre 0,76, BTC durante 0,89 (orario 0,83). skew panel ≤0,005. vrp_f_watch: f canonico 0,732 → 0,706, differenza +0,097 [+0,042, +0,140]. - r0909 senza doppio shift, contesto sul DVOL del giorno; r0822 regime su date di calendario; r0901 join riallineato (bit-exact). Aperti con costo: DVOL orario (−0,04/+0,06 nel crollo), spot 5m (≤25 min). - test: +3 causalita' in test_cb_chain_vrp, anti-doppio-shift in test_r0909; suite 1011/1011. - revisione fable: 16 segnalazioni, tutte applicate (fra cui un «−1,8% in un'ora» che era +3,9%). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bjj5vPEBoAJrB23P6RjKzs
This commit is contained in:
@@ -13,6 +13,10 @@ Regola che vale per ogni misura fatta con questo modulo: il f della sola gamba c
|
||||
dello spread. Il modello sbaglia soprattutto sull'ala che si COMPRA (piu' OTM, IV piu' alta per
|
||||
skew, prezzata dal modello a vol ATM) -> misurare una gamba sola da' la risposta sbagliata con
|
||||
segno rassicurante.
|
||||
|
||||
Seconda regola (09/09, debito §5.18): spot e DVOL escono da qui gia' CAUSALI — `asof(ts)` da'
|
||||
l'ultima chiusura nota a ts. Chi costruisce una serie di prezzi altrove usi `causale()`, non
|
||||
l'indice del feed com'e' (etichettato all'apertura: D6).
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -65,23 +69,48 @@ def puts(asset: str, df: pd.DataFrame | None = None) -> pd.DataFrame:
|
||||
return d[(d["asset"] == asset) & (d["option_type"] == "P")]
|
||||
|
||||
|
||||
def causale(s: pd.Series, cadenza: str) -> pd.Series:
|
||||
"""Rietichetta una serie di CHIUSURE dall'APERTURA della barra all'istante in cui la chiusura
|
||||
e' nota (apertura + cadenza), cosi' `s.asof(ts)` restituisce l'ultima chiusura CONOSCIUTA a ts
|
||||
e mai una futura. I valori non si toccano, solo le etichette.
|
||||
|
||||
🚨 Perche' esiste (debito §5.18, 09/09): i feed del progetto sono etichettati all'apertura —
|
||||
la barra 1h `20:00` chiude col 5m delle 20:55 (verificato: chiusura 1h a T == chiusura 5m a
|
||||
T+55 nel 100% delle barre) e la riga DVOL del giorno D e' la chiusura delle 23:00 di D
|
||||
(verificato contro la risoluzione 1h dell'API pubblica). Senza questo spostamento `asof(ts)`
|
||||
dava lo spot di UN'ORA dopo e il DVOL di FINO A 24 ORE dopo l'ingresso (D6, quinta occorrenza)."""
|
||||
out = s.copy()
|
||||
out.index = out.index + pd.Timedelta(cadenza)
|
||||
return out
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def spot_series(asset: str) -> pd.Series:
|
||||
"""Chiusure 1h del feed certificato, etichettate all'istante in cui sono NOTE (apertura + 1h).
|
||||
|
||||
`spot_series(a).asof(ts)` = ultima chiusura oraria conosciuta a `ts`. Conseguenza sul
|
||||
regolamento: `asof(exp)` alle 08:00 e' la chiusura della barra 07:00, cioe' il prezzo delle
|
||||
08:00 — prima era il prezzo delle 09:00, un'ora DOPO la scadenza."""
|
||||
from scripts.analysis.research_lab import load_tf
|
||||
px = load_tf(asset, "1h")
|
||||
s = pd.Series(px["close"].values.astype(float),
|
||||
index=pd.to_datetime(px["timestamp"], unit="ms", utc=True)).sort_index()
|
||||
s.index = pd.DatetimeIndex(s.index).as_unit("ns") # le quote bite hanno microsecondi
|
||||
return s
|
||||
return causale(s, "1h")
|
||||
|
||||
|
||||
@lru_cache(maxsize=8)
|
||||
def dvol_series(asset: str) -> pd.Series:
|
||||
"""DVOL giornaliero (chiusura), etichettato all'istante in cui e' NOTO (giorno + 1D).
|
||||
|
||||
`dvol_series(a).asof(ts)` = ultima chiusura giornaliera del DVOL conosciuta a `ts`: dentro il
|
||||
giorno D e' la chiusura di D−1, quindi vecchia fino a 23 ore. E' il prezzo della causalita'
|
||||
con un feed giornaliero; l'alternativa (DVOL orario dall'API pubblica) non e' nel feed."""
|
||||
d = pd.read_parquet(RAW / f"dvol_{asset.lower()}.parquet")
|
||||
s = pd.Series(d["close"].values.astype(float),
|
||||
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True)).sort_index()
|
||||
s.index = pd.DatetimeIndex(s.index).as_unit("ns")
|
||||
return s
|
||||
return causale(s, "1D")
|
||||
|
||||
|
||||
# ------------------------------------------------------------------ prezzo
|
||||
|
||||
@@ -310,11 +310,15 @@ def boot_ci(x: np.ndarray, n: int = 4000, seed: int = 822) -> tuple[float, float
|
||||
def main() -> None:
|
||||
ap = argparse.ArgumentParser()
|
||||
ap.add_argument("--no-live", action="store_true", help="non interrogare il venue")
|
||||
ap.add_argument("--al", default=None,
|
||||
help="data di taglio UTC (riproduzione): esclude le scadenze >= questa data; default adesso")
|
||||
args = ap.parse_args()
|
||||
|
||||
print(__doc__.split("\n\n")[0])
|
||||
puts = load_puts()
|
||||
today = pd.Timestamp.now(tz="UTC")
|
||||
today = pd.Timestamp(args.al, tz="UTC") if args.al else pd.Timestamp.now(tz="UTC")
|
||||
print(f"\n taglio dei trade: scadenze < {today:%Y-%m-%d %H:%M}Z"
|
||||
+ (" (--al: riproduzione)" if args.al else " (adesso)"))
|
||||
venue = venue_read(not args.no_live)
|
||||
|
||||
# ------------------------------------------------------------ §1 REGIME
|
||||
@@ -323,7 +327,7 @@ def main() -> None:
|
||||
"misurato 4 volte). Se nel campione quel gate non scatta mai, il campione non contiene\n"
|
||||
"la strategia — contiene la sua astensione.\n")
|
||||
for a in ASSETS:
|
||||
V = CB.dvol_series(a)
|
||||
V = CB.causale(CB.dvol_series(a), "-1D") # calendario: il DVOL DEL giorno D (la serie di cblib e' causale: sta a D+1)
|
||||
win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())]
|
||||
hist = V[V.index < win.index[0]].to_numpy(float)
|
||||
print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / "
|
||||
@@ -372,7 +376,7 @@ def main() -> None:
|
||||
print(" Regola del 19/06: «rivalutare quando la catena cattura un crash». Un criterio senza\n"
|
||||
" numero non e' un criterio, quindi eccolo (IV-rank espandente causale, come nel sleeve):")
|
||||
for a in ASSETS:
|
||||
V = CB.dvol_series(a)
|
||||
V = CB.causale(CB.dvol_series(a), "-1D") # idem: date di calendario, non etichette causali
|
||||
vals = V.to_numpy(float)
|
||||
ivr = np.full(len(vals), np.nan)
|
||||
for i in range(1, len(vals)):
|
||||
@@ -753,13 +757,17 @@ def main() -> None:
|
||||
|
||||
# ------------------------------------------------------------ §7 VERDETTO
|
||||
hr("§7 VERDETTO")
|
||||
print(""" SCARTATO — su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti
|
||||
print(""" (numeri della stesura del 22/08, RIMISURATI il 09/09 con spot e DVOL causali — debito §5.18:
|
||||
erano 0.714 [0.690, 0.779] · 32.58 / 1.45 / -0.35 · 3.90 -> 2.41 · -1.35% -> +11.22% · +25% / +44%.
|
||||
Il verdetto non cambia: le quattro ragioni non dipendono da un'ora di spot.)
|
||||
|
||||
SCARTATO — su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti
|
||||
l'uno dall'altro: ciascuno da solo basterebbe.
|
||||
|
||||
1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30
|
||||
che E' l'alpha di VRP01 — il sleeve sarebbe stato FLAT per l'intero campione. La DVOL
|
||||
mediana sta al 7 (BTC) / 11 (ETH) percentile della storia 2021+, e il sottostante e'
|
||||
salito del +25% / +44%: vol implicita bassa E rialzo forte = il regime MIGLIORE
|
||||
salito del +21% / +40%: vol implicita bassa E rialzo forte = il regime MIGLIORE
|
||||
possibile per vendere put, non uno neutro. Le settimane sono 10 per asset, non 16
|
||||
(prima del 2026-06-09 l'archivio seguiva solo la finestra 13-20 DTE: quella 4-10 DTE
|
||||
non esiste). Esito: 10/10 vincenti su BTC, e la lente giusta non e' lo Sharpe ma la
|
||||
@@ -770,13 +778,13 @@ def main() -> None:
|
||||
regime di vol bassa persistente, non davanti a un muro.
|
||||
|
||||
2. IL TITOLO NON SOPRAVVIVE ALL'ORA D'INGRESSO. E' un max-of-k mai dichiarato, misurato
|
||||
qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 32.58
|
||||
sta all'89 percentile, la MEDIANA ONESTA e' 1.45 e la banda tocca il NEGATIVO (-0.35).
|
||||
Su ETH 3.90 -> 2.41. Il numero da citare per BTC e' 1.45, non 32.58.
|
||||
qui per la prima volta su questa struttura: su 9 ancore lo Sharpe canonico BTC 29.61
|
||||
sta all'89 percentile, la MEDIANA ONESTA e' 2.12 e la banda tocca il NEGATIVO (-0.19).
|
||||
Su ETH 7.64 -> 3.35. Il numero da citare per BTC e' 2.12, non 29.61.
|
||||
⚠️ E il meccanismo non e' quello che avevo scritto prima di guardarlo. Non e' solo la
|
||||
vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -1.35% a
|
||||
+11.22% del rischio — CAMBIA SEGNO — e la deviazione standard di 11x (ETH: media 2.9x,
|
||||
sd 2.4x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano
|
||||
vol campionaria a ballare: fra le ancore la MEDIA settimanale BTC va da -0.66% a
|
||||
+11.44% del rischio — CAMBIA SEGNO — e la deviazione standard di 9.2x (ETH: media 2.1x,
|
||||
sd 3.2x). Spostare l'ora d'ingresso cambia lo snapshot e quindi quali strike combaciano
|
||||
col delta bersaglio: non e' lo stesso trade un'ora dopo, e' UN'ALTRA STRUTTURA. A 10
|
||||
settimane questo parametro di disturbo domina il segnale che si vorrebbe misurare.
|
||||
|
||||
@@ -793,7 +801,7 @@ def main() -> None:
|
||||
4. LO SPREAD E' IL COSTO DOMINANTE, E SULLA FAMIGLIA ESEGUIBILE E' PEGGIO. Attraversare
|
||||
il bid/ask costa ~10% del credito sugli INVERSE; nella regione di strike che serve lo
|
||||
spread relativo mediano e' 24-37% sugli inverse e 29-59% sulle USDC, con open interest
|
||||
~0. Il f del credito netto si replica indipendentemente a 0.714 (IC95 [0.690, 0.779],
|
||||
~0. Il f del credito netto si replica indipendentemente a 0.712 (IC95 [0.664, 0.732],
|
||||
0/19 osservazioni >= 1.0) — conferma dello 0.73 del 30/07 su un campione piu' lungo e
|
||||
con un percorso diverso.
|
||||
|
||||
|
||||
@@ -167,7 +167,8 @@ def calibra_catena() -> dict:
|
||||
skew[nm] = (np.array(xs), np.array(ys))
|
||||
|
||||
# struttura a termine: IV ATM a DTE t contro il DVOL 30g dello stesso istante
|
||||
dv = cblib.dvol_series("BTC")
|
||||
dv = cblib.causale(cblib.dvol_series("BTC"), "-1D") # join per GIORNO: il DVOL del giorno D — la serie di
|
||||
# cblib e' causale dal 09/09 sera (la chiusura di D sta a D+1); senza questo il join accosterebbe l'IV di D al DVOL di D−1
|
||||
a = q[q["ad"].between(0.45, 0.55)].copy()
|
||||
a["giorno"] = a["ts"].dt.floor("D")
|
||||
dvd = dv.copy()
|
||||
|
||||
@@ -33,8 +33,10 @@ Un episodio e' un episodio: da' la TAGLIA di f nello stress, non la sua distribu
|
||||
|
||||
DUE CORREZIONI DELLA REVISIONE (fable, 09/09), applicate:
|
||||
* il feed 1h e' etichettato all'APERTURA (la barra 20:00 chiude alle 21:00): `S.asof(ts)`
|
||||
guardava un'ora AVANTI. Qui lo spot e' `spot_causale` = chiusura dell'ultima barra chiusa a ts.
|
||||
⚠️ `cblib.spot_series` ha lo stesso difetto (e quindi il f 0,714 di §11): registrato, non toccato.
|
||||
guardava un'ora AVANTI. Corretto la mattina del 09/09 qui con uno `spot_causale` locale;
|
||||
la sera dello stesso giorno la riparazione e' entrata in `cblib` (`causale`, debito §5.18) e
|
||||
copre anche il DVOL giornaliero (fino a 24 ore avanti): questo script usa le serie di `cblib`
|
||||
COME SONO, senza ri-spostarle.
|
||||
* il costo di chiusura `(ask corta − bid lunga)·spot` puo' superare la LARGHEZZA dello spread
|
||||
(una quota da 158% di spread relativo a T→0): uno spread a rischio definito non costa mai piu'
|
||||
della larghezza per chiudere (si lascia regolare). Il MTM e' tagliato alla larghezza e le ore
|
||||
@@ -76,13 +78,6 @@ def leg_fee(prem_base: float, spot: float) -> float:
|
||||
return min(FEE_UNDERLYING * spot, FEE_PREMIUM_CAP * prem_base * spot)
|
||||
|
||||
|
||||
def spot_causale(S: pd.Series) -> pd.Series:
|
||||
"""Il feed 1h e' etichettato all'apertura: la chiusura della barra T e' nota a T+1h. Spostando
|
||||
l'indice di +1h, `asof(ts)` restituisce l'ultima chiusura CONOSCIUTA a ts, mai una futura."""
|
||||
out = S.copy(); out.index = out.index + pd.Timedelta(hours=1)
|
||||
return out
|
||||
|
||||
|
||||
def carica() -> pd.DataFrame:
|
||||
a = pd.read_parquet(ARCHIVIO)
|
||||
a["ts"] = pd.to_datetime(a["ts"], utc=True)
|
||||
@@ -213,7 +208,7 @@ def misura() -> dict:
|
||||
out = {"trades": {}, "contesto": {}, "sleeve": {}}
|
||||
for asset in ASSETS:
|
||||
seg = a[a["asset"] == asset]
|
||||
S, V = spot_causale(CB.spot_series(asset)), CB.dvol_series(asset)
|
||||
S, V = CB.spot_series(asset), CB.dvol_series(asset) # gia' causali (cblib, dal 09/09 sera)
|
||||
Q = Quote(seg, S, V)
|
||||
rows = []
|
||||
for ts in sorted(seg["ts"].unique()):
|
||||
@@ -227,17 +222,21 @@ def misura() -> dict:
|
||||
# contesto: DVOL, RV5 e RV1 sul crollo
|
||||
px = S[(S.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (S.index <= EXP)]
|
||||
pd_ = px.resample("1D").last().dropna() # chiusura del giorno = ultima chiusura nota entro le 24:00
|
||||
hist_all = V[V.index < CROLLO[0]]
|
||||
# CONTESTO per giorno: la serie di cblib e' causale (la chiusura del giorno D sta a D+1);
|
||||
# qui si vuole "il DVOL DEL giorno D" accanto al prezzo del giorno D, quindi si toglie lo
|
||||
# spostamento SOLO per la tabella e per il picco del crollo. `dvol_pre` e `ivr_pre` restano
|
||||
# sulla serie causale: sono cio' che si sapeva alle 00:00 del primo giorno di crollo.
|
||||
dvg = CB.causale(V, "-1D")
|
||||
hist_all = dvg[dvg.index < CROLLO[0]]
|
||||
lr = np.log(pd_).diff()
|
||||
rv5 = lr.rolling(5).std() * np.sqrt(365.25) * 100
|
||||
dv = V[(V.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (V.index <= EXP)]
|
||||
hist = V[V.index < CROLLO[0]]
|
||||
dv = dvg[(dvg.index >= pd.Timestamp("2026-05-20", tz="UTC")) & (dvg.index <= EXP)]
|
||||
out["contesto"][asset] = dict(
|
||||
spot=pd_, ret=lr, rv5=rv5, dvol=dv,
|
||||
dvol_max_crollo=float(dv[(dv.index >= CROLLO[0]) & (dv.index <= CROLLO[1])].max()),
|
||||
dvol_pre=float(V.asof(CROLLO[0])),
|
||||
pctl_picco=float((hist_all < V[(V.index >= CROLLO[0]) & (V.index <= CROLLO[1])].max()).mean()),
|
||||
ivr_pre=float((hist < V.asof(CROLLO[0])).mean()),
|
||||
pctl_picco=float((hist_all < dv[(dv.index >= CROLLO[0]) & (dv.index <= CROLLO[1])].max()).mean()),
|
||||
ivr_pre=float((hist_all < V.asof(CROLLO[0])).mean()),
|
||||
rv5_max=float(rv5[(rv5.index >= CROLLO[0]) & (rv5.index <= CROLLO[1] + pd.Timedelta(days=3))].max()),
|
||||
caduta=float(pd_[CROLLO[0]:CROLLO[1]].min() / pd_[:CROLLO[0]].iloc[-1] - 1.0),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user