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:
@@ -422,6 +422,78 @@ def vrp_decomposizione(q: pd.DataFrame, cm7: dict) -> pd.DataFrame:
|
||||
return pd.DataFrame(righe)
|
||||
|
||||
|
||||
MARKFIT_OK = (0.85, 1.15) # banda del CONTROLLO, dichiarata prima di guardare i f
|
||||
|
||||
|
||||
def qualita(D: pd.DataFrame) -> pd.Series:
|
||||
"""Filtro dichiarato: si tiene un'osservazione solo se il CONTROLLO passa, cioe' se la IV di
|
||||
Deribit riprezza i suoi stessi mid entro +-15% sul credito, e se il credito conservativo e'
|
||||
positivo. NON e' un filtro sull'esito (il f), e' un filtro sulla variabile di controllo:
|
||||
dove il controllo fallisce non si sta misurando lo skew, si sta misurando una quota rotta."""
|
||||
return (D["f_markfit"].between(*MARKFIT_OK)) & (D["f_spread"] > 0) & D["f_term"].gt(0)
|
||||
|
||||
|
||||
def attribuzione_log(D: pd.DataFrame) -> pd.DataFrame:
|
||||
"""Attribuzione ESATTA e additiva: log(f_tot) = log(f_term)+log(f_skew)+log(f_markfit)+
|
||||
log(f_spread), per costruzione (la catena dei rapporti telescopia). Le MEDIANE dei fattori
|
||||
non si moltiplicano fra loro; i log si sommano. Da qui la QUOTA del difetto per causa."""
|
||||
g = D[qualita(D)]
|
||||
parti = ["f_term", "f_skew", "f_markfit", "f_spread"]
|
||||
L = np.log(g[parti])
|
||||
tot = float(np.log(g["f_tot"]).mean())
|
||||
rows = []
|
||||
for c in parti:
|
||||
m = float(L[c].mean())
|
||||
rows.append(dict(fattore=c, media_log=round(m, 4),
|
||||
fattore_geometrico=round(float(np.exp(m)), 3),
|
||||
quota_del_difetto=round(m / tot, 3) if tot != 0 else np.nan))
|
||||
rows.append(dict(fattore="TOTALE", media_log=round(tot, 4),
|
||||
fattore_geometrico=round(float(np.exp(tot)), 3), quota_del_difetto=1.0))
|
||||
return pd.DataFrame(rows)
|
||||
|
||||
|
||||
def panel_f(q: pd.DataFrame, cm7: dict) -> pd.DataFrame:
|
||||
"""La stessa decomposizione, ma a OGNI ora in cui lo spread e' costruibile (non solo ai 22
|
||||
ingressi settimanali di VRP01). Serve alla RISOLUZIONE: 22 punti non separano due cause da
|
||||
~10% l'una. ⚠ Le ore dentro la stessa scadenza NON sono indipendenti: il campione efficace
|
||||
resta il numero di SCADENZE, che si riporta accanto."""
|
||||
out = []
|
||||
for a in ASSETS:
|
||||
S7 = cm7[a]
|
||||
S, V = CB.spot_series(a), CB.dvol_series(a)
|
||||
p = q[(q["asset"] == a) & (q["option_type"] == "P")
|
||||
& (q["dte"] >= 4.0) & (q["dte"] <= 10.0)]
|
||||
for (hr, exp), g in p.groupby(["hr", "exp"], sort=False):
|
||||
if hr not in S7.index:
|
||||
continue
|
||||
legs = CB.pick_legs(g)
|
||||
if legs is None:
|
||||
continue
|
||||
ts = g["ts"].max()
|
||||
dte = float((exp - ts).total_seconds() / 86400.0)
|
||||
if not (4.0 <= dte <= 10.0):
|
||||
continue
|
||||
spot = float(S.asof(ts)); dvol = float(V.asof(ts)) / 100.0
|
||||
atm7 = float(S7.loc[hr, "atm"]) / 100.0
|
||||
T = dte / 365.25
|
||||
c_mod = CB.bs_put(spot, legs["k_short"], T, dvol) - CB.bs_put(spot, legs["k_long"], T, dvol)
|
||||
c_atm = CB.bs_put(spot, legs["k_short"], T, atm7) - CB.bs_put(spot, legs["k_long"], T, atm7)
|
||||
c_sml = (CB.bs_put(spot, legs["k_short"], T, legs["iv_short"] / 100.0)
|
||||
- CB.bs_put(spot, legs["k_long"], T, legs["iv_long"] / 100.0))
|
||||
c_mid = (legs["mid_short"] - legs["mid_long"]) * spot
|
||||
c_real = (legs["bid_short"] - legs["ask_long"]) * spot
|
||||
if not (c_mod > 0 and c_atm > 0 and c_sml > 0):
|
||||
continue
|
||||
hist = V[V.index < ts]
|
||||
ivr = float((hist < dvol * 100).mean()) if len(hist) else np.nan
|
||||
out.append(dict(asset=a, hr=hr, exp=exp, dte=dte, rr7=float(S7.loc[hr, "rr"]),
|
||||
atm7=float(S7.loc[hr, "atm"]), dvol=dvol * 100, ivrank=ivr,
|
||||
f_term=c_atm / c_mod, f_skew=c_sml / c_atm,
|
||||
f_markfit=c_mid / c_sml, f_spread=c_real / c_mid,
|
||||
f_tot=c_real / c_mod, f_tot_mid=c_mid / c_mod))
|
||||
return pd.DataFrame(out)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
def main() -> None:
|
||||
t_start = time.time()
|
||||
@@ -469,6 +541,14 @@ def main() -> None:
|
||||
print(f"\n {a} — corr fra dRR (variazione oraria dello skew) e il rendimento nella")
|
||||
print(" finestra indicata, ancorata al TIMESTAMP VERO delle quote (ts_max):")
|
||||
print(LL[LL["asset"] == a].to_string(index=False))
|
||||
fw = LL[LL["minuti"] > 0]
|
||||
print(f"\n LAG IN AVANTI: {len(fw)} test ({fw['minuti'].nunique()} orizzonti x {len(ASSETS)} asset).")
|
||||
print(f" |t| massimo osservato = {fw['t_dRR'].abs().max():.2f} su {len(fw)} test;")
|
||||
print(f" |t| atteso come MASSIMO di {len(fw)} test indipendenti sotto il nullo ~ "
|
||||
f"{float(np.abs(np.random.default_rng(0).standard_normal((4000, len(fw)))).max(axis=1).mean()):.2f}.")
|
||||
print(" -> il massimo in avanti e' quello che il rumore produce da solo. LAG INDIETRO:")
|
||||
print(f" |t| minimo = {LL[LL['minuti'] < 0]['t_dRR'].abs().min():.2f} su "
|
||||
f"{int((LL['minuti'] < 0).sum())} test, tutti dello STESSO segno.")
|
||||
print("\n Controllo POSITIVO — la stessa misura con l'ancora ASSUNTA a :30 dell'ora:")
|
||||
FF = pd.concat([leadlag_ancora_sbagliata(cm30[a], a) for a in ASSETS], ignore_index=True)
|
||||
print(FF.to_string(index=False))
|
||||
@@ -550,8 +630,13 @@ def main() -> None:
|
||||
z_all[a] = pd.Series(z.to_numpy(), index=pd.DatetimeIndex(pd.to_datetime(sub["datetime"], utc=True)))
|
||||
Z = pd.concat(z_all, axis=1).mean(axis=1)
|
||||
Z = Z.reindex(Bw.index).ffill()
|
||||
att = int((Bw != 0).sum())
|
||||
print(f" baseline TP01 sulla finestra dello skew: n={len(Bw)} giorni, "
|
||||
f"Sharpe {sh_of(Bw.to_numpy()):+.2f}, maxDD {dd_of(Bw.to_numpy()):.2%}")
|
||||
print(f" ⚠ barre ATTIVE del baseline: {att}/{len(Bw)} ({att/len(Bw):.1%}) — TP01 e' FLAT il resto")
|
||||
print(f" del tempo, e nella finestra il suo maxDD e' {dd_of(Bw.to_numpy()):.2%}: un gate di")
|
||||
print(" de-risk non ha NIENTE da proteggere qui. Il null de-levering girera' e dira'")
|
||||
print(" 'REFUTED', ma il motivo e' che manca il sinistro, non che il gate sia stato battuto.")
|
||||
for thr in (-0.5, -1.0, -1.5):
|
||||
gate = (Z.shift(1) > thr).astype(float) # de-risk quando lo skew si irripidisce (z basso)
|
||||
ov = (Bw * gate).to_numpy()
|
||||
@@ -565,8 +650,10 @@ def main() -> None:
|
||||
f"Sharpe {nul['sh_base_k']:+.2f} vs overlay {nul['sh_overlay']:+.2f} "
|
||||
f"-> {'SUPERATO' if nul['superato'] else 'REFUTED'}")
|
||||
if n_fire:
|
||||
gia_flat = int((gg == 0).sum())
|
||||
print(f" nei giorni de-riskati TP01 faceva in media {gg.mean()*100:+.3f}%/g "
|
||||
f"(gli altri {Bw[gate==1].mean()*100:+.3f}%/g)")
|
||||
f"(gli altri {Bw[gate==1].mean()*100:+.3f}%/g); di quei {n_fire} giorni TP01 era "
|
||||
f"GIA' FLAT in {gia_flat} ({gia_flat/n_fire:.0%}) -> il gate spegne cio' che era gia' spento")
|
||||
print("\n ridondanza col trend — corr fra lo z dello skew e il rendimento di TP01: "
|
||||
f"{float(pd.concat({'z': Z, 'b': Bw}, axis=1).dropna().corr().iloc[0,1]):+.3f}")
|
||||
print(" POTENZA: con 75 giorni un gate che scatta ~10 volte non e' misurabile. Il numero da")
|
||||
@@ -600,11 +687,67 @@ def main() -> None:
|
||||
print(" Se sta a 1, RR e BF di questa sessione sono un fatto di PREZZO, non un artefatto")
|
||||
print(" del fit di Deribit -> vale anche come controllo di confound per Q1/Q2.")
|
||||
|
||||
print("\n ATTRIBUZIONE ESATTA (in log: i fattori si SOMMANO, le mediane no)")
|
||||
Q = qualita(D)
|
||||
print(f" osservazioni che passano il CONTROLLO f_markfit in {MARKFIT_OK} e credito>0: "
|
||||
f"{int(Q.sum())}/{len(D)}")
|
||||
scartate = D[~Q][["asset", "ts", "f_markfit", "f_spread"]].copy()
|
||||
if len(scartate):
|
||||
scartate["ts"] = pd.DatetimeIndex(scartate["ts"]).strftime("%Y-%m-%d")
|
||||
print(" scartate (il controllo fallisce -> non si sta misurando lo skew):")
|
||||
print(scartate.round(3).to_string(index=False))
|
||||
print(attribuzione_log(D).to_string(index=False))
|
||||
print(f" f_tot mediana sul sottoinsieme che passa il controllo: {D[Q]['f_tot'].median():.3f}")
|
||||
|
||||
print("\n PANEL ORARIO — stessa decomposizione a OGNI ora costruibile (risoluzione)")
|
||||
PN = panel_f(q, cm7)
|
||||
QP = qualita(PN)
|
||||
print(f" osservazioni {len(PN)} ({PN.groupby('asset').size().to_dict()}), di cui passano il "
|
||||
f"controllo {int(QP.sum())}; scadenze distinte (campione EFFICACE) "
|
||||
f"{PN.groupby('asset')['exp'].nunique().to_dict()}")
|
||||
G = PN[QP]
|
||||
for a in ASSETS + ("TUTTI",):
|
||||
g = G if a == "TUTTI" else G[G["asset"] == a]
|
||||
if g.empty:
|
||||
continue
|
||||
print(f" {a:6s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | "
|
||||
f"f_skew {g['f_skew'].median():.3f} | f_markfit {g['f_markfit'].median():.3f} | "
|
||||
f"f_spread {g['f_spread'].median():.3f} || f_tot {g['f_tot'].median():.3f}")
|
||||
print(" attribuzione log sul panel:")
|
||||
print(attribuzione_log(PN).to_string(index=False))
|
||||
|
||||
print("\n Il f dipende dal REGIME? (la domanda che decide se 0.73 e' conservativo o ottimista)")
|
||||
D2 = D.dropna(subset=["f_tot_mid", "rr7"])
|
||||
D2 = D[qualita(D)].dropna(subset=["f_tot_mid", "rr7"])
|
||||
for var in ("rr7", "atm7", "ivrank", "bf7"):
|
||||
c = float(D2["f_tot_mid"].corr(D2[var]))
|
||||
print(f" corr(f_tot_mid, {var:7s}) = {c:+.3f} [n={len(D2)}]")
|
||||
cg = float(G["f_tot_mid"].corr(G[var])) if var in G.columns else np.nan
|
||||
print(f" corr(f_tot_mid, {var:7s}) = {c:+.3f} [ingressi n={len(D2)}]"
|
||||
+ (f" {cg:+.3f} [panel n={len(G)}]" if np.isfinite(cg) else ""))
|
||||
print(" (positivo = il credito reale si avvicina al modello quando lo skew e' PIATTO;")
|
||||
print(" e' il verso atteso dal meccanismo, ma n=22 e il panel e' autocorrelato.)")
|
||||
print("\n IL PEZZO PIU' GRANDE (f_term) CAMBIA SEGNO COL REGIME — ed e' questo che decide")
|
||||
print(" se 0.73 e' conservativo o ottimista dove VRP01 tradera' DAVVERO.")
|
||||
G2 = G.copy()
|
||||
G2["contango"] = G2["atm7"] < G2["dvol"]
|
||||
print(f" ore del panel con ATM7 < DVOL30 (contango, il modello SOVRAPPREZZA): "
|
||||
f"{G2['contango'].mean():.1%}")
|
||||
for nome, m in (("contango (ATM7<DVOL)", G2["contango"]), ("backwardation (ATM7>DVOL)", ~G2["contango"])):
|
||||
g = G2[m]
|
||||
if g.empty:
|
||||
continue
|
||||
print(f" {nome:26s} n={len(g):5d} | f_term {g['f_term'].median():.3f} | "
|
||||
f"f_skew {g['f_skew'].median():.3f} | f_tot {g['f_tot'].median():.3f} | "
|
||||
f"IV-rank mediano {g['ivrank'].median():.3f}")
|
||||
qs = pd.qcut(G2["ivrank"], 4, labels=["q1", "q2", "q3", "q4"], duplicates="drop")
|
||||
st = G2.groupby(qs, observed=True).agg(n=("f_term", "size"), ivr=("ivrank", "median"),
|
||||
f_term=("f_term", "median"), f_skew=("f_skew", "median"),
|
||||
f_tot=("f_tot", "median"))
|
||||
print(" stratificato per quartile di IV-rank (DENTRO il campione: max 0.23, il gate e' 0.30):")
|
||||
print(st.round(3).to_string())
|
||||
print(" -> f_term sale con l'IV-rank; f_skew no. L'estrapolazione oltre 0.30 NON e' misurata")
|
||||
print(" e in questo campione non e' misurabile: l'unico episodio ad alta vol e' un RALLY")
|
||||
print(" (2026-08-21, +26% in una settimana), non un crash. Manca il regime che conta.")
|
||||
|
||||
print(f"\n IV-rank agli ingressi: min {D['ivrank'].min():.3f} mediana {D['ivrank'].median():.3f} "
|
||||
f"max {D['ivrank'].max():.3f}")
|
||||
n_gate = int((D["ivrank"] > 0.30).sum())
|
||||
|
||||
Reference in New Issue
Block a user