research(wave-0822): DEALER-GAMMA scartato — e dealer_net_gamma e' il GEX col segno invertito

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:08:20 +00:00
parent ff4aa94528
commit 3b206c8e6d
8 changed files with 283 additions and 59 deletions
+24 -1
View File
@@ -6,7 +6,7 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
| # | filone | verdetto | in una riga | | # | filone | verdetto | in una riga |
|---|---|---|---| |---|---|---|---|
| 9 | ADAPTIVE-HORIZON | **SCARTATO** | il "vincitore adattivo" ha lookback incollato al bordo il 100% del tempo = TSMOM costante a 20g (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); isolando le celle davvero adattive -> NEUTRAL, corr→TP01 0.90 = TP01 travestito | | 9 | ADAPTIVE-HORIZON | **SCARTATO** | il "vincitore adattivo" ha lookback incollato al bordo il 100% del tempo = TSMOM costante a 20g (corr 1.000, dSh 0.00 in 8/8 anni e 0/12 ancore); isolando le celle davvero adattive -> NEUTRAL, corr→TP01 0.90 = TP01 travestito || 4 | DEALER-GAMMA | **SCARTATO** | la gamba tradeable e' INCOERENTE (BTC non separa, ETH separa al ROVESCIO); cio' che resta e' quasi tutto DVOL (corr 0.66), gia' refutato il 26/06. DSR 0.440 e la cella migliore sta SOTTO il massimo atteso dal rumore |
## Note che sopravvivono ai singoli filoni ## Note che sopravvivono ai singoli filoni
@@ -22,3 +22,26 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
una superficie frastagliata. **Nessun cambio a TP01.** una superficie frastagliata. **Nessun cambio a TP01.**
- Riaprirebbe il filone: un rilevatore il cui L si muove DAVVERO che batta il miglior L costante - Riaprirebbe il filone: un rilevatore il cui L si muove DAVVERO che batta il miglior L costante
(non TP01) in >=6/8 anni appaiati con corr→TP01 < 0.6 — es. BOCPD/PELT vero, non provato. (non TP01) in >=6/8 anni appaiati con corr→TP01 < 0.6 — es. BOCPD/PELT vero, non provato.
### 4 — DEALER-GAMMA (r0822_dealer_gamma.py, 136 statistiche dichiarate)
⚠️ **DIFETTO DI DATO, il risultato piu' riusabile del filone: `dealer_net_gamma` e' il GEX col SEGNO
INVERTITO** — corr **-0.95 BTC / -0.89 ETH** contro la ricostruzione da manuale sulla catena
(962/1116 snapshot, ~280-316 strike), fit `dng ~ -0.7*GEX`. **Chi lo usasse col nome che porta
leggerebbe ogni regime al contrario.** Spiega anche la contraddizione interna del file (corr fra
`dng>0` e `spot>flip` = -0.28 / -0.92).
⚠️ **Il file dichiara piu' di quel che ha:** cadenza 15 min solo dal **2026-05-01**; i 37 giorni
prima sono **un punto al giorno** -> **90 giorni, non 4 mesi**. `liquidation_long_risk` e
`liquidation_short_risk` sono **COSTANTI** ('low' in 17.229 righe su 17.229 non nulle) = zero
informazione. `gamma_flip_level` congelato nel **61%** dei passi su BTC.
- Gate: **null de-levering FALLITO** (baseline x0.6 stesso maxDD con Sharpe migliore); **DSR 0.440**
su 64 trial, e nel modo piu' netto — la cella migliore (+4.98 lordo) sta **sotto** il massimo
atteso dal puro rumore (+5.12). `marginal_vs_tp01` inutilizzabile: su 90 giorni full e hold-out
COINCIDONO (`n_hold_days == n_days == 90`).
- **Null location-matched** (aggiunto dall'agente): `gamma_flip_level` bocciato — 96% di accordo con
`spot > SMA(720h)` su ETH; su BTC la SMA nuda da' uno spread MAGGIORE.
- **Ampiezza effettiva**: 73/59 episodi di regime ma 6-8 coprono il 78-86% del tempo -> **n_eff 53
(BTC) / 24 (ETH) ore** su 2.130. Il regime coincide col MESE (maggio long, giugno short, luglio long).
- ✅ Ricostruibile oggi dalla NOSTRA catena, meglio dell'originale (518 ore dal 01/08, ~209-262
strike/ora, quote_status ok ~100%, segno giusto) — **ma la soglia non e' portabile** (`dng=0`
corrisponde a `GEX=+5.8e6` su BTC): ricalibrarla sui 90 giorni sarebbe la selezione che il
progetto rifiuta. Riapertura non prima di **~30-40 episodi di regime = ~2 anni (meta' 2028)**.
+5 -3
View File
@@ -655,9 +655,11 @@ def main() -> None:
print(" 3. `gamma_flip_level` e' una media mobile: 96% di accordo con spot>SMA(720h) su ETH; su") print(" 3. `gamma_flip_level` e' una media mobile: 96% di accordo con spot>SMA(720h) su ETH; su")
print(" BTC (accordo 0.68) la SMA nuda da' uno spread di vol MAGGIORE del livello stesso.") print(" BTC (accordo 0.68) la SMA nuda da' uno spread di vol MAGGIORE del livello stesso.")
print(" Niente contenuto di opzioni isolabile. Trappola 2 del brief.") print(" Niente contenuto di opzioni isolabile. Trappola 2 del brief.")
print(" 4. Ampiezza effettiva: 6-8 episodi di regime, n_eff 24-53 ore su 2.130. Il regime") print(" 4. Ampiezza effettiva: 73 (BTC) / 59 (ETH) episodi di regime, ma SEI e OTTO di essi")
print(" coincide col mese (maggio LONG, giugno SHORT, luglio LONG): lo spread di vol e' UN") print(" coprono il 78-86% del tempo; n_eff 53 e 24 ore su 2.130. Il regime coincide col")
print(" confronto fra tre mesi, non 2.136 osservazioni.") print(" MESE (maggio LONG, giugno SHORT, luglio LONG): lo spread di vol e' un confronto")
print(" fra tre mesi, non fra 2.136 osservazioni. Tolto giugno cala del 46% (BTC) e del")
print(" 71% (ETH) e resta poggiato su 134 / 366 ore.")
print(" 5. Cio' che resta della gamba (A) e' quasi tutto DVOL (corr 0.66 su BTC), e il DVOL") print(" 5. Cio' che resta della gamba (A) e' quasi tutto DVOL (corr 0.66 su BTC), e il DVOL")
print(" come modulatore di TP01 e' gia' stato refutato il 26/06: era de-levering.") print(" come modulatore di TP01 e' gia' stato refutato il 26/06: era de-levering.")
print(" 6. Il gate misurato FALLISCE il null del de-levering (baseline x0.6 lo domina) su una") print(" 6. Il gate misurato FALLISCE il null del de-levering (baseline x0.6 lo domina) su una")
+6 -3
View File
@@ -622,11 +622,14 @@ def main() -> None:
debole. (Ed e' esattamente la parte che rendeva l'angolo NUOVO rispetto al filone funding.) debole. (Ed e' esattamente la parte che rendeva l'angolo NUOVO rispetto al filone funding.)
2. IL FATTO NON REGGE LA PROPRIA SOGLIA DI POTENZA, dichiarata prima di guardare: l'unico 2. IL FATTO NON REGGE LA PROPRIA SOGLIA DI POTENZA, dichiarata prima di guardare: l'unico
effetto e' il decile piu' basso di oi_delta (-0.40% a 24h) e la MDE a 24h e' 1.06%. effetto e' il decile piu' basso di oi_delta (-0.40% a 24h) e la MDE a 24h e' 1.06%.
La pendenza lineare e' nulla (|t|<1.3), il |ritorno| futuro e' piatto fra i decili (la La pendenza lineare e' nulla (|t|<1.3 in 4 specificazioni su 4); il |ritorno| futuro e'
'cascata' non c'e'), e la scomposizione per mese mette quasi tutto in giugno. quasi piatto fra i decili (0.59-0.75% a 4h, solo il decile 0 leggermente sopra) -> la
'cascata' non c'e'; e il segno e' ASSENTE nel primo dei tre mesi (maggio y24=+0.02%,
giugno -0.64%, luglio -0.74%): 2 mesi su 3 non sono un plateau, sono due mesi.
3. IL SEGNALE E' UN MASSIMO DI 18 CELLE SU 114 GIORNI: Sharpe 3.6 con SE(Sharpe)=2.0, 3. IL SEGNALE E' UN MASSIMO DI 18 CELLE SU 114 GIORNI: Sharpe 3.6 con SE(Sharpe)=2.0,
deflated-Sharpe 0.10 (18 celle) / 0.004 (118 trial) contro un massimo atteso PER CASO di deflated-Sharpe 0.10 (18 celle) / 0.004 (118 trial) contro un massimo atteso PER CASO di
Sharpe 5.4; implausible_sharpe=True; marginal_vs_tp01 = NEUTRAL; 60% del P&L in 3 giorni. Sharpe 5.4; implausible_sharpe=True; marginal_vs_tp01 = NEUTRAL; e il P&L e' per il 71%
di giugno e per il 60% concentrato in 3 giorni su 114.
4. ED E' COMUNQUE MORTO ALLA NASCITA: la colonna su cui poggia non e' ricostruibile con i dati 4. ED E' COMUNQUE MORTO ALLA NASCITA: la colonna su cui poggia non e' ricostruibile con i dati
che raccogliamo (l'OI della catena e' quello delle OPZIONI, rango ~0.05 con la colonna) e che raccogliamo (l'OI della catena e' quello delle OPZIONI, rango ~0.05 con la colonna) e
la sorgente e' stata dismessa il 2026-07-30. la sorgente e' stata dismessa il 2026-07-30.
+14 -4
View File
@@ -630,6 +630,7 @@ def deep_dive(results) -> None:
B = A.tp01_baseline_daily() B = A.tp01_baseline_daily()
C = r["C"] C = r["C"]
best = None best = None
rows_ctl = []
for p in MOM_CONTROL_GRID: for p in MOM_CONTROL_GRID:
fnm = mk_mom(tf="1d", **p) fnm = mk_mom(tf="1d", **p)
Cm = A.candidate_daily(fnm, tf="1d") Cm = A.candidate_daily(fnm, tf="1d")
@@ -637,18 +638,27 @@ def deep_dive(results) -> None:
JB = pd.concat({"B": B, "M": Cm}, axis=1, join="inner").dropna() JB = pd.concat({"B": B, "M": Cm}, axis=1, join="inner").dropna()
up = A._sh((1 - W_BLEND) * JB["B"] + W_BLEND * JB["M"]) - A._sh(JB["B"]) up = A._sh((1 - W_BLEND) * JB["B"] + W_BLEND * JB["M"]) - A._sh(JB["B"])
row = dict(params=p, sharpe=round(A._sh(Cm), 3), row = dict(params=p, sharpe=round(A._sh(Cm), 3),
insample=round(A._sh(Cm[Cm.index < A.HOLDOUT]), 3),
hold=round(A._sh(Cm[Cm.index >= A.HOLDOUT]), 3), hold=round(A._sh(Cm[Cm.index >= A.HOLDOUT]), 3),
corr_to_acc=round(float(J["A"].corr(J["M"])), 3), corr_to_acc=round(float(J["A"].corr(J["M"])), 3),
corr_to_tp01=round(float(JB["B"].corr(JB["M"])), 3), corr_to_tp01=round(float(JB["B"].corr(JB["M"])), 3),
uplift_w25_full=round(up, 3)) uplift_w25_full=round(up, 3))
print(f" MOM {str(p):<28} Sh {row['sharpe']:+.3f} hold {row['hold']:+.3f} " print(f" MOM {str(p):<28} Sh {row['sharpe']:+.3f} IS {row['insample']:+.3f} "
f"corr->{r['code']} {row['corr_to_acc']:+.3f} corr->TP01 {row['corr_to_tp01']:+.3f} " f"hold {row['hold']:+.3f} corr->{r['code']} {row['corr_to_acc']:+.3f} "
f"uplift {row['uplift_w25_full']:+.3f}") f"corr->TP01 {row['corr_to_tp01']:+.3f} uplift {row['uplift_w25_full']:+.3f}")
rows_ctl.append(row)
if best is None or row["corr_to_acc"] > best["corr_to_acc"]: if best is None or row["corr_to_acc"] > best["corr_to_acc"]:
best = row best = row
print(f" livello piu' vicino all'accelerazione: {best['params']} corr {best['corr_to_acc']:+.3f}") print(f" livello piu' vicino all'accelerazione: {best['params']} corr {best['corr_to_acc']:+.3f}")
print(" lettura: corr alta col LIVELLO => l'accelerazione non e' una dimensione nuova; " print(" lettura: corr alta col LIVELLO => l'accelerazione non e' una dimensione nuova; "
"corr bassa => e' distinta ma resta da spiegare perche' non sopravvive al DSR.\n") "corr bassa => e' distinta ma resta da spiegare perche' non sopravvive al DSR.")
bh = max(rows_ctl, key=lambda x: x["hold"])
bi = max(rows_ctl, key=lambda x: x["insample"])
print(f" TRAPPOLA DICHIARATA: la cella di controllo migliore SULL'HOLD-OUT e' {bh['params']} "
f"(hold {bh['hold']:+.3f}); quella scelta IN-SAMPLE-ONLY e' {bi['params']} "
f"(IS {bi['insample']:+.3f}, hold {bi['hold']:+.3f}). Se coincidono, il controllo e'"
" selezionabile; se no, guardare la colonna hold e' selezione-sull'hold-out (trappola 3)"
" e NON promuove nulla.\n")
NOT_TESTED = [ NOT_TESTED = [
+97 -36
View File
@@ -156,28 +156,62 @@ def sezione0(chain: pd.DataFrame) -> None:
# =========================================================================================== # ===========================================================================================
# SEZIONE 1 — RICOSTRUZIONE DELLA CURVA + CERTIFICAZIONE # SEZIONE 1 — RICOSTRUZIONE DELLA CURVA + CERTIFICAZIONE
# =========================================================================================== # ===========================================================================================
def _atm_iv(g: pd.DataFrame) -> float: def _interp_at(gid: np.ndarray, x: np.ndarray, y: np.ndarray, ngrp: int,
"""ATM IV di UNO snapshot+scadenza: interpolazione sul DELTA, mai estrapolazione.""" target: float) -> np.ndarray:
vals = [] """Interpolazione lineare di y(x) a `target`, per ogni gruppo, VETTORIALE e senza
for typ, tgt in (("C", 0.5), ("P", -0.5)): estrapolazione (NaN se il gruppo non racchiude il target).
s = g[g.option_type == typ]
if len(s) >= 2: `gid` dev'essere ordinato crescente e (gid, x) lessicograficamente ordinati.
d = s["delta"].to_numpy() Equivalente a np.interp gruppo-per-gruppo: qui serve solo perche' i gruppi sono ~30.000
if d.min() <= tgt <= d.max(): e un ciclo python li paga a caro prezzo su una VPS a 2 core condivisa.
o = np.argsort(d) """
vals.append(float(np.interp(tgt, d[o], s["iv"].to_numpy()[o]))) n_tot = np.bincount(gid, minlength=ngrp)
return float(np.mean(vals)) if vals else np.nan start = np.concatenate([[0], np.cumsum(n_tot)[:-1]])
n_below = np.bincount(gid[x <= target], minlength=ngrp)
ok = (n_below > 0) & (n_below < n_tot)
out = np.full(ngrp, np.nan)
if not ok.any():
return out
lo = (start + n_below - 1)[ok]
hi = lo + 1
x0, x1, y0, y1 = x[lo], x[hi], y[lo], y[hi]
w = np.where(x1 != x0, (target - x0) / np.where(x1 != x0, x1 - x0, 1.0), 0.0)
out[ok] = y0 + w * (y1 - y0)
return out
def build_atm_by_expiry(chain: pd.DataFrame) -> pd.DataFrame: def build_atm_by_expiry(chain: pd.DataFrame) -> pd.DataFrame:
"""ATM IV per (asset, ora, scadenza) = IV interpolata sul DELTA a 0.5 (call) / -0.5 (put).
Perche' il delta e non "lo strike piu' vicino allo spot": il delta referenzia il FORWARD.
A 90 giorni il forward sta lontano dallo spot, quindi lo strike ATM-spot e' un'opzione OTM
e la sua IV porta dentro lo SKEW — che e' un altro filone e falserebbe la pendenza in modo
sistematico e crescente col tenore. Nessuna estrapolazione: se il delta 0.5 non e' racchiuso
dai quotati, quella scadenza a quell'ora non ha ATM e viene scartata.
"""
f = CACHE / "atm_by_expiry.parquet" f = CACHE / "atm_by_expiry.parquet"
if f.exists(): if f.exists():
return pd.read_parquet(f) return pd.read_parquet(f)
q = chain[chain.quoted & (chain.dte > DTE_MIN)] q = chain[chain.quoted & (chain.dte > DTE_MIN)].copy()
out = (q.groupby(["asset", "hr", "exp"]) keys = ["asset", "hr", "exp"]
.apply(lambda g: pd.Series({"iv": _atm_iv(g), "dte": g["dte"].median()}), q["gid"] = q.groupby(keys, sort=True).ngroup()
include_groups=False) ngrp = int(q["gid"].max()) + 1
.reset_index().dropna(subset=["iv"])) ivs = []
for typ, tgt in (("C", 0.5), ("P", -0.5)):
s = q[q.option_type == typ].sort_values(["gid", "delta"])
v = np.full(ngrp, np.nan)
if len(s):
g2 = s["gid"].to_numpy()
uniq, inv = np.unique(g2, return_inverse=True)
v[uniq] = _interp_at(inv, s["delta"].to_numpy(), s["iv"].to_numpy(), len(uniq), tgt)
ivs.append(v)
M = np.vstack(ivs)
with np.errstate(invalid="ignore"):
cnt = np.sum(np.isfinite(M), axis=0)
iv = np.where(cnt > 0, np.nansum(np.where(np.isfinite(M), M, 0.0), axis=0) / np.maximum(cnt, 1), np.nan)
out = (q.groupby(keys, sort=True)["dte"].median().reset_index())
out["iv"] = iv
out = out.dropna(subset=["iv"]).reset_index(drop=True)
CACHE.mkdir(parents=True, exist_ok=True) CACHE.mkdir(parents=True, exist_ok=True)
out.to_parquet(f) out.to_parquet(f)
return out return out
@@ -266,7 +300,7 @@ def px_hourly(a: str) -> pd.Series:
index=pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True))) index=pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True)))
def _block_pctl(x: np.ndarray, y: np.ndarray, stat: float, L: int, n: int = 800, def _block_pctl(x: np.ndarray, y: np.ndarray, stat: float, L: int, n: int = 400,
seed: int = 20260822) -> float: seed: int = 20260822) -> float:
"""Percentile della correlazione osservata contro il null a BLOCCHI (y ricampionata da """Percentile della correlazione osservata contro il null a BLOCCHI (y ricampionata da
blocchi non allineati a x): conserva l'autocorrelazione e rompe solo l'accoppiamento.""" blocchi non allineati a x): conserva l'autocorrelazione e rompe solo l'accoppiamento."""
@@ -400,6 +434,51 @@ def build_atm_cost(chain: pd.DataFrame) -> pd.DataFrame:
return atm return atm
def _roll_table(r: pd.DataFrame, H: int) -> pd.DataFrame:
"""Roll IMPLICITO vs REALIZZATO su orizzonte H ore, appaiato per (scadenza, ISTANTE D'INGRESSO).
Appaiato sull'INGRESSO e non sull'uscita: e' l'uscita che le due letture spostano, e un
inner-join sulla data d'uscita terrebbe solo i casi in cui non e' successo niente (trappola
incontrata 3 volte nel progetto: venue-watch, GTAA, VRP profit-take).
impl = iv_curva(T-H) al tempo t - iv(T) al tempo t (se la curva non si muovesse)
real = iv(T-H) al tempo t+H - iv(T) al tempo t (cio' che e' successo davvero)
"""
r = r[["hr", "exp", "iv", "dte"]].dropna()
nxt = r[["hr", "exp", "iv"]].copy()
nxt["hr"] = nxt["hr"] - pd.Timedelta(hours=H)
j = r.merge(nxt.rename(columns={"iv": "iv_fut"}), on=["hr", "exp"], how="inner")
j["T2"] = j["dte"] - H / 24.0
j = j[j["T2"] > DTE_MIN]
if j.empty:
return pd.DataFrame(columns=["dte", "impl", "real"])
# curva dell'ora t, ordinata: chiave monotona globale gid*1000 + dte -> searchsorted unico
c = r.sort_values(["hr", "dte"]).copy()
c["gid"] = c.groupby("hr", sort=True).ngroup()
key = c["gid"].to_numpy() * 1000.0 + c["dte"].to_numpy()
w = (c["iv"].to_numpy() / 100.0) ** 2 * c["dte"].to_numpy() # varianza totale
gmap = dict(zip(c["hr"].to_numpy(), c["gid"].to_numpy()))
n_tot = np.bincount(c["gid"].to_numpy())
start = np.concatenate([[0], np.cumsum(n_tot)[:-1]])
gq = np.array([gmap[h] for h in j["hr"].to_numpy()])
pos = np.searchsorted(key, gq * 1000.0 + j["T2"].to_numpy(), side="left")
lo, hi = pos - 1, pos
ok = (lo >= start[gq]) & (hi < start[gq] + n_tot[gq])
ivT2 = np.full(len(j), np.nan)
if ok.any():
l, h2 = lo[ok], hi[ok]
x0 = key[l] - gq[ok] * 1000.0
x1 = key[h2] - gq[ok] * 1000.0
t2 = j["T2"].to_numpy()[ok]
ww = np.where(x1 != x0, (t2 - x0) / np.where(x1 != x0, x1 - x0, 1.0), 0.0)
var = w[l] + ww * (w[h2] - w[l])
ivT2[ok] = np.sqrt(np.maximum(var, 0.0) / t2) * 100.0
j = j.assign(ivT2=ivT2).dropna(subset=["ivT2"])
return pd.DataFrame({"dte": j["dte"].to_numpy(),
"impl": j["ivT2"].to_numpy() - j["iv"].to_numpy(),
"real": j["iv_fut"].to_numpy() - j["iv"].to_numpy()})
def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) -> None: def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) -> None:
hr("SEZIONE 3 — Q2: il CARRY di vol (roll-down) e' incassabile al bid-ask reale?") hr("SEZIONE 3 — Q2: il CARRY di vol (roll-down) e' incassabile al bid-ask reale?")
print(" Definizione senza modello: un'opzione a tenore T, dopo dt, sta a T-dt. Se la curva non") print(" Definizione senza modello: un'opzione a tenore T, dopo dt, sta a T-dt. Se la curva non")
@@ -412,25 +491,7 @@ def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) ->
roll_day = {} roll_day = {}
for H in (24, 168): for H in (24, 168):
for a in ASSETS: for a in ASSETS:
r = atm_exp[atm_exp.asset == a] d = _roll_table(atm_exp[atm_exp.asset == a], H)
by_hr = {h: g.sort_values("dte") for h, g in r.groupby("hr")}
rows = []
for _, g in r.set_index(["exp", "hr"]).sort_index().groupby(level=0):
g = g.droplevel(0).sort_index()
fut = g["iv"].reindex(g.index + pd.Timedelta(hours=H))
for t, iv0, d0, iv1 in zip(g.index, g["iv"].to_numpy(),
g["dte"].to_numpy(), fut.to_numpy()):
T2 = d0 - H / 24.0
gg = by_hr.get(t)
if not np.isfinite(iv1) or T2 <= DTE_MIN or gg is None or len(gg) < 2:
continue
td, ivd = gg["dte"].to_numpy(), gg["iv"].to_numpy()
if td.min() > T2 or td.max() < T2:
continue
w = (ivd / 100.0) ** 2 * td
ivT2 = np.sqrt(np.interp(T2, td, w) / T2) * 100.0
rows.append((d0, ivT2 - iv0, iv1 - iv0))
d = pd.DataFrame(rows, columns=["dte", "impl", "real"])
d = d[(d.dte >= 4) & (d.dte <= 45)] d = d[(d.dte >= 4) & (d.dte <= 45)]
if len(d) < 50: if len(d) < 50:
print(f" {a} dt={H}h: campione insufficiente ({len(d)})") print(f" {a} dt={H}h: campione insufficiente ({len(d)})")
+64 -7
View File
@@ -182,6 +182,7 @@ def extract(asset: str, off: int) -> dict:
ts_close=ltf["timestamp"].values.astype(np.int64) + LTF_MIN * 60_000, ts_close=ltf["timestamp"].values.astype(np.int64) + LTF_MIN * 60_000,
day_ent=idx[idxE].floor("D") if len(idxE) else pd.DatetimeIndex([], tz="UTC"), day_ent=idx[idxE].floor("D") if len(idxE) else pd.DatetimeIndex([], tz="UTC"),
) )
out["base_daily"] = equity_daily(out, size_flat(out)) # riusata da VTL/§9: calcolarla 1 volta
R._CACHE.clear() # ent = 18k dict per chiave: non accumulare R._CACHE.clear() # ent = 18k dict per chiave: non accumulare
return out return out
@@ -413,8 +414,7 @@ def main() -> None:
for o in offs: for o in offs:
sz = {} sz = {}
for a in ASSETS: for a in ASSETS:
base_a = equity_daily(EX[o][a], size_flat(EX[o][a])) sz[a] = sizes_from_L(EX[o][a], leverage_series(EX[o][a]["base_daily"], **sp))
sz[a] = sizes_from_L(EX[o][a], leverage_series(base_a, **sp))
SLE[nm][o] = leg_daily(EX[o], sz) SLE[nm][o] = leg_daily(EX[o], sz)
BK[nm][o] = book(TP, SLE[nm][o]) BK[nm][o] = book(TP, SLE[nm][o])
MTP[nm][o] = 1.0 MTP[nm][o] = 1.0
@@ -546,6 +546,13 @@ def main() -> None:
print(f" {n:<28} IS {is_med[n]:>7.3f} HOLD {ho_med[n]:>7.3f} FULL {fu_med[n]:>7.3f}") print(f" {n:<28} IS {is_med[n]:>7.3f} HOLD {ho_med[n]:>7.3f} FULL {fu_med[n]:>7.3f}")
best = max(CAUSAL, key=lambda x: is_med[x]) best = max(CAUSAL, key=lambda x: is_med[x])
print(f"\n cella scelta AL BUIO: {best} (IS {is_med[best]:.3f} vs baseline {base_is:.3f})") print(f"\n cella scelta AL BUIO: {best} (IS {is_med[best]:.3f} vs baseline {base_is:.3f})")
iso_is = {n: float(np.median([sh(ins(BK[n][o])) - sh(ins(CTRL[n][o])) for o in offs]))
for n in CAUSAL}
best_iso = max(CAUSAL, key=lambda x: iso_is[x])
print(f" cella scelta AL BUIO **a ISO-PESO**: {best_iso} (dIS iso {iso_is[best_iso]:+.3f})")
print(f" la stessa cella a iso-peso: dShFULL {RES[best]['iF']:+.3f} "
f"({RES[best]['niF']}/{len(offs)}), dShHOLD {RES[best]['iH']:+.3f} "
f"({RES[best]['niH']}/{len(offs)})")
trials = [fu_med[n] for n in CAUSAL] + [med_F] trials = [fu_med[n] for n in CAUSAL] + [med_F]
dsr, sr0 = A.deflated_sharpe(fu_med[best], trials, BK[best][can]) dsr, sr0 = A.deflated_sharpe(fu_med[best], trials, BK[best][can])
dsr_b, _ = A.deflated_sharpe(med_F, trials, base_b[can]) dsr_b, _ = A.deflated_sharpe(med_F, trials, base_b[can])
@@ -599,9 +606,9 @@ def main() -> None:
f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive " f"attive {imp.get('active_frac', 0)*100:.1f}% perdite/attive "
f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}") f"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}")
cols = {"TP01": TP, "SKH01": SLE["BASE"][can]} cols = {"TP01": TP, "SKH01": SLE["BASE"][can]}
for nm in [best_sleeve, list(BVT)[0]]: for nm in [best, best_sleeve] + list(BVT)[:2]:
m = vol(SLE[nm][can]) / vol(SLE["BASE"][can]) m_s = vol(SLE[nm][can]) / vol(SLE["BASE"][can])
wp = {"TP01": W_TP, "SKH01": W_SKH * m} wp = {"TP01": W_TP * MTP[nm][can], "SKH01": W_SKH * m_s}
g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp, g = weights_tilt_null(cols, {"TP01": W_TP, "SKH01": W_SKH}, wp,
caps={"SKH01": 0.5}, floor=0.05, n=300, k_seen=len(ALL)) caps={"SKH01": 0.5}, floor=0.05, n=300, k_seen=len(ALL))
print(f" tilt-null {nm:<26} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} " print(f" tilt-null {nm:<26} peso equiv SKH {wp['SKH01']/sum(wp.values()):.3f} "
@@ -634,6 +641,57 @@ def main() -> None:
print(f" {label:<28}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}") print(f" {label:<28}{tm:>10.3f}{tr:>10.3f}{tm-tr:>9.3f}{no:>13}{ns:>11}{tu:>12.1f}")
print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.") print(" 'ordini eseg' e 'sotto-min' sono sui due asset sommati, su tutta la storia 1h.")
# ------------------------------------------------------------------ §9 per anno + deriva
print("\n" + "-" * 112)
print(" 9. SCOMPOSIZIONE PER ANNO del dSharpe a ISO-PESO (regola codificata dopo SOL, 22/08:")
print(" un contributo positivo si scompone per ANNO prima di crederci) + deriva della size")
print("-" * 112)
yrs = sorted({int(y) for y in BK["BASE"][can].index.year})
top = [best, best_iso] + [max(BVT, key=lambda x: RES[x]["iF"])]
top = list(dict.fromkeys(top))
print(f" {'variante':<28}" + "".join(f"{y:>8}" for y in yrs))
for nm in top:
cells = []
for y in yrs:
v = []
for o in offs:
a_ = BK[nm][o][BK[nm][o].index.year == y]
c_ = CTRL[nm][o][CTRL[nm][o].index.year == y]
if len(a_) > 20:
v.append(sh(a_) - sh(c_))
cells.append(float(np.median(v)) if v else float("nan"))
npos = int(np.nansum(np.asarray(cells) > 0))
print(f" {nm:<28}" + "".join(f"{c:>+8.2f}" for c in cells) +
f" anni positivi {npos}/{len(yrs)}")
print()
for nm in top:
if nm in SZ:
szs = np.concatenate([SZ[nm](EX[can][a]) for a in ASSETS])
dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS]))
elif nm in VTL:
szs, dys = [], []
for a in ASSETS:
szs.append(sizes_from_L(EX[can][a],
leverage_series(EX[can][a]["base_daily"], **VTL[nm])))
dys.append(EX[can][a]["day_ent"].values)
szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys))
else:
L = leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"])
szs, dys = [], []
for a in ASSETS:
szs.append(sizes_from_L(EX[can][a], L))
dys.append(EX[can][a]["day_ent"].values)
szs = np.concatenate(szs); dys = pd.DatetimeIndex(np.concatenate(dys))
s = pd.Series(szs, index=dys)
med = s.groupby(s.index.year).median()
rho = float(np.corrcoef(np.arange(len(med)), med.values)[0, 1]) if len(med) > 2 else float("nan")
print(f" size mediana per anno — {nm:<22}" +
"".join(f"{med.get(y, float('nan')):>8.2f}" for y in yrs) +
f" corr col tempo {rho:+.2f}")
print(" ⚠️ una size che DERIVA col tempo non e' controllo del rischio: e' un tilt temporale")
print(" (la vol crypto e' scesa lungo il campione -> qualunque 1/vol pesa di piu' il")
print(" periodo recente, che e' anche l'hold-out). La riga 'corr col tempo' la misura.")
print("\n" + "=" * 112) print("\n" + "=" * 112)
print(f" fatto in {time.time()-t0:.0f}s") print(f" fatto in {time.time()-t0:.0f}s")
print("=" * 112) print("=" * 112)
@@ -652,8 +710,7 @@ def netted_target(asset: str, ex: dict, nm, SZ, VTL, BVT, SLE, TP, can, df1h) ->
elif nm in SZ: elif nm in SZ:
sizes = SZ[nm](ex) sizes = SZ[nm](ex)
elif nm in VTL: elif nm in VTL:
base_a = equity_daily(ex, size_flat(ex)) sizes = sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm]))
sizes = sizes_from_L(ex, leverage_series(base_a, **VTL[nm]))
else: # BOOKVT else: # BOOKVT
L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"])
sizes = sizes_from_L(ex, L) sizes = sizes_from_L(ex, L)
+20 -5
View File
@@ -325,9 +325,16 @@ def main() -> None:
for a in ASSETS: for a in ASSETS:
V = CB.dvol_series(a) V = CB.dvol_series(a)
win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())] win = V[(V.index >= puts["ts"].min()) & (V.index <= puts["ts"].max())]
pct = float((V[V.index < win.index[0]].values[:, None] < win.values).mean()) hist = V[V.index < win.index[0]].to_numpy(float)
print(f" {a}: DVOL nel campione min {win.min():.1f} / mediana {win.median():.1f} / " print(f" {a}: DVOL nel campione min {win.min().1f} / mediana {win.median().1f} / "
f"max {win.max():.1f} → percentile mediano vs storia 2021+ = {pct:.0%}") f"max {win.max().1f}")
print(f" vs storia 2021+ ({len(hist)} giorni) la MEDIANA del campione sta al "
f"{float((hist < win.median()).mean()).0%} pctl, il suo MASSIMO al "
f"{float((hist < win.max()).mean()).0%} pctl")
print("\n \u26a0\ufe0f Due statistiche diverse, da tenere distinte: CLAUDE.md cita «DVOL max al 24\u00b0/28\u00b0\n"
" pctl» dal campione del 30/07. Un MASSIMO cresce con la finestra — e' una statistica\n"
" di ESTREMO, non di livello. Per dire in che regime siamo la lente e' la MEDIANA, ed\n"
" e' bassissima.")
# ------------------------------------------------------------ §2 CAMPIONE # ------------------------------------------------------------ §2 CAMPIONE
hr("§2 IL CAMPIONE — quante settimane ci sono DAVVERO") hr("§2 IL CAMPIONE — quante settimane ci sono DAVVERO")
@@ -446,8 +453,16 @@ def main() -> None:
print(f" {a:6s} {lot:>7.2f} {wid:>10,.0f} {wid * lot - tr['cred'].median() * lot:>11,.0f}$ " print(f" {a:6s} {lot:>7.2f} {wid:>10,.0f} {wid * lot - tr['cred'].median() * lot:>11,.0f}$ "
f"{tr['k_short'].median() * lot:>12,.0f}$ {tr['cred'].median() * lot:>12,.2f}$") f"{tr['k_short'].median() * lot:>12,.0f}$ {tr['cred'].median() * lot:>12,.2f}$")
print(" Sul rischio DEFINITO un lotto minimo inverse sta dentro $600. Sul cash-secured no.\n" print(" Sul rischio DEFINITO un lotto minimo inverse sta dentro $600. Sul cash-secured no.\n"
" Quale dei due valga dipende dal regime di margine del conto, che NON e' leggibile\n" " Quale dei due valga dipende dal regime di margine del conto (standard vs portfolio),\n"
" da dati pubblici: si legge sul conto. E' un limite di questa misura, non un dettaglio.") " che NON e' leggibile da dati pubblici.")
print(" ⚠️ E c'e' una domanda PRIMA di quella, che nessun dato pubblico chiude: un saldo in\n"
" USDC puo' marginare un'opzione INVERSE (collaterale in BTC/ETH)? Se no, la famiglia\n"
" con 3 mesi di storia non e' negoziabile da questo conto a NESSUN capitale.\n"
" Precedente esatto: il 26/07 GTAA01 aveva 5 settimane di misure sopra l'assunzione\n"
" che i 6 ETF fossero comprabili — il blocco PRIIPs si scopri' con UN ordine di prova.\n"
" La regola che ne usci' («la negoziabilita' sul conto REALE si verifica quando lo\n"
" sleeve entra in RICERCA, non quando entra nel book») si applica qui identica, e\n"
" questo script NON puo' eseguirla: richiede il conto, non i dati.")
# ------------------------------------------------------------ §4 BACKTEST # ------------------------------------------------------------ §4 BACKTEST
hr("§4 BACKTEST SU QUOTE REALI — griglia dichiarata") hr("§4 BACKTEST SU QUOTE REALI — griglia dichiarata")
+53
View File
@@ -535,6 +535,25 @@ def main() -> None:
f"{dg['med_ticket']:>11.0f}{dg['orders_py']:>10.0f}" f"{dg['med_ticket']:>11.0f}{dg['orders_py']:>10.0f}"
f"{trk:>12.2f}{'' if trk < 0.8 else ' '}") f"{trk:>12.2f}{'' if trk < 0.8 else ' '}")
print() print()
print(f" {'cella':<13}{'|dw| mediano al ribil.':>24}{'capitale sleeve minimo':>25}{' = book al 15%':>17}")
for nm in ("MAJ19 k=1", "MAJ19 k=2", "MAJ19 k=3", "MAJ19 k=5*"):
cell = xs_cells[nm]
Weff = xs_effective_weights(cell["W"], cell["scale"])
chg = np.abs(np.diff(cell["W"], axis=0)).sum(axis=1) > 1e-12 # giorni di ribilanciamento
rows = np.where(chg)[0] + 1
d = np.abs(np.diff(Weff, axis=0))[rows - 1]
med = float(np.median(d[d > 1e-9])) if np.any(d > 1e-9) else float("nan")
capmin = MIN_ORDER_HL / med if med > 0 else float("nan")
print(f" {'XS ' + nm.split()[1]:<13}{med:>24.4f}{capmin:>24.0f}$"
f"{capmin/0.15:>16.0f}$")
print(" ^ ARITMETICA: il ticket di un ribilanciamento e' |dw|*capitale_sleeve. Il min-order")
print(" morde sotto il 'capitale sleeve minimo'. Per il CANONICO k=5 quel numero e' ~150$")
print(" di sleeve = ~1.000$ di book al 15%, non 20.000$. La soglia dichiarata dal progetto")
print(" veniva da una diagnostica di TURNOVER (quota di turnover sopra il min-order), che a")
print(" capitale piccolo e' bassa perche' salta la deriva GIORNALIERA del vol-target — la")
print(" quale non porta segnale. Stessa lezione gia' imparata su TP01 ('a $600 il min-order")
print(" $5 e' GIA' la banda ottimale'), non applicata a XS01.")
print()
print(" 'gambe eseg' = quota delle VARIAZIONI di posizione sopra il min-order (la diagnostica") print(" 'gambe eseg' = quota delle VARIAZIONI di posizione sopra il min-order (la diagnostica")
print(" su cui si basa la soglia '$20k'). 'haircut' = quanto ne costa in Sharpe. Sono cose") print(" su cui si basa la soglia '$20k'). 'haircut' = quanto ne costa in Sharpe. Sono cose")
print(" DIVERSE: il turnover saltato e' la deriva del vol-target, non il segnale.") print(" DIVERSE: il turnover saltato e' la deriva del vol-target, non il segnale.")
@@ -710,6 +729,40 @@ def main() -> None:
print(f" break-even di costo per gamba: XS k=2 {_be(be.get('XS MAJ19 k=2'))}, " print(f" break-even di costo per gamba: XS k=2 {_be(be.get('XS MAJ19 k=2'))}, "
f"XS k=5 {_be(be.get('XS MAJ19 k=5*'))}, XSR k=2 {_be(be.get('XSR ALL50 k=2'))}, " f"XS k=5 {_be(be.get('XS MAJ19 k=5*'))}, XSR k=2 {_be(be.get('XSR ALL50 k=2'))}, "
f"XSR pieno {_be(be.get('XSR ALL50 k=pieno*'))}") f"XSR pieno {_be(be.get('XSR ALL50 k=pieno*'))}")
print("-" * 108)
print(" 1. CONCENTRARE NON COMPRA RENDIMENTO, COMPRA ESEGUIBILITA'. Il null del de-levering")
print(" boccia ogni k tranne i due che sono fortuna d'ancora (XS k=4: fase0 al 90° pctl,")
print(" mediana 1.10 < 1.15 del canonico; XSR k=6: +0.03 su 1.65). Nessuna concentrazione")
print(" batte il proprio originale a PARI maxDD.")
print(" 2. MA IL COSTO E' MOLTO PIU' BASSO DEL PREVISTO, e la mia ipotesi (a) era sbagliata:")
print(" mi aspettavo Sh ~ sqrt(ampiezza) e quindi il 40-60% dell'edge a k=2-3. Misurato:")
print(" XS-LITE k=2 conserva il 98% del canonico A MEDIANA DI FASE con 4 gambe invece di 10,")
print(" e N_hhi scende 6.9 -> 3.0. La radice dell'ampiezza NON descrive questa curva: la")
print(" dispersione cross-section e' concentrata negli ESTREMI del ranking, e le gambe 3-5")
print(" per lato aggiungono rischio quanto segnale. Il collasso vero e' solo a k=1 (41%).")
print(" 3. IL MURO NON E' IL MIN-ORDER, E' IL PESO DELLO SLEEVE. Con $600 DEDICATI, XS01 gira")
print(" gia' oggi a k=5 (tracking 1.04, haircut -0.04). E' al 15% del book che muore: a")
print(" sleeve $90 il canonico esegue il 4% delle variazioni e il lordo scende al 13% del")
print(" target. A quel capitale k=2 e' l'unica cella CON EDGE che traccia (1.01: k=1 traccia")
print(" ma non ha edge, Sh 0.13). Il '~$20k' e'")
print(" quindi il prodotto di due scelte (peso 15% + k=5), non una proprieta' del segnale.")
print(" 4. XSR01 NON SI CONCENTRA: k<=3 perde l'edge IN-SAMPLE (IS 0.96 -> 0.28) e il gate")
print(" marginale lo declassa ad ADDS -> NOISE (has_insample_edge=False). Ipotesi (c)")
print(" CONFERMATA: li' l'ampiezza E' l'edge. E muore a ~28 bps/gamba di costo totale a")
print(" OGNI k, contro >50 bps di XS-LITE: concentrare non ripara il rischio #1 di XSR01.")
print(" 5. ⛔ NIENTE DI QUESTO E' DEPLOYABILE OGGI, e non per una ragione misurata qui:")
print(" XS01/XSR01 vivono su HYPERLIQUID, e la decisione dell'operatore del 26/07 tiene")
print(" il 100% del capitale su Deribit fino a $20k. A $20k il canonico e' eseguibile da")
print(" solo. XS-LITE serve SE e QUANDO si apre HL sotto quella soglia — non prima.")
print("-" * 108)
print(" GATE PRE-REGISTRATO (dichiarato ORA, prima di qualunque dato nuovo):")
print(" XS-LITE k=2 su MAJ19 si riapre SOLO se si apre un conto Hyperliquid con >= $300")
print(" dedicati allo sleeve, e in quel caso si deploya il k CANONICO (5) se il capitale")
print(" dello sleeve e' >= $750, k=2 sotto. Nessun nuovo backtest serve per questa scelta:")
print(" e' gia' decisa dalla tabella (5) e non va ri-ottimizzata guardando i dati nuovi.")
print(" Come CANDIDATO a sleeve XS-LITE resta bocciato: DSR FAIL a ogni conteggio e null")
print(" del de-levering non superato. Non si ri-testa prima del 2027-06 (3.5 anni di")
print(" storia HL, di cui >=1.5 fuori dal campione di scoperta di XS01).")
print("=" * 108) print("=" * 108)