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 |
|---|---|---|---|
| 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
@@ -22,3 +22,26 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
una superficie frastagliata. **Nessun cambio a TP01.**
- 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.
### 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(" 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(" 4. Ampiezza effettiva: 6-8 episodi di regime, n_eff 24-53 ore su 2.130. Il regime")
print(" coincide col mese (maggio LONG, giugno SHORT, luglio LONG): lo spread di vol e' UN")
print(" confronto fra tre mesi, non 2.136 osservazioni.")
print(" 4. Ampiezza effettiva: 73 (BTC) / 59 (ETH) episodi di regime, ma SEI e OTTO di essi")
print(" coprono il 78-86% del tempo; n_eff 53 e 24 ore su 2.130. Il regime coincide col")
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(" 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")
+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.)
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%.
La pendenza lineare e' nulla (|t|<1.3), il |ritorno| futuro e' piatto fra i decili (la
'cascata' non c'e'), e la scomposizione per mese mette quasi tutto in giugno.
La pendenza lineare e' nulla (|t|<1.3 in 4 specificazioni su 4); il |ritorno| futuro e'
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,
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
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.
+14 -4
View File
@@ -630,6 +630,7 @@ def deep_dive(results) -> None:
B = A.tp01_baseline_daily()
C = r["C"]
best = None
rows_ctl = []
for p in MOM_CONTROL_GRID:
fnm = mk_mom(tf="1d", **p)
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()
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),
insample=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_tp01=round(float(JB["B"].corr(JB["M"])), 3),
uplift_w25_full=round(up, 3))
print(f" MOM {str(p):<28} Sh {row['sharpe']:+.3f} hold {row['hold']:+.3f} "
f"corr->{r['code']} {row['corr_to_acc']:+.3f} corr->TP01 {row['corr_to_tp01']:+.3f} "
f"uplift {row['uplift_w25_full']:+.3f}")
print(f" MOM {str(p):<28} Sh {row['sharpe']:+.3f} IS {row['insample']:+.3f} "
f"hold {row['hold']:+.3f} corr->{r['code']} {row['corr_to_acc']:+.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"]:
best = row
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; "
"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 = [
+97 -36
View File
@@ -156,28 +156,62 @@ def sezione0(chain: pd.DataFrame) -> None:
# ===========================================================================================
# SEZIONE 1 — RICOSTRUZIONE DELLA CURVA + CERTIFICAZIONE
# ===========================================================================================
def _atm_iv(g: pd.DataFrame) -> float:
"""ATM IV di UNO snapshot+scadenza: interpolazione sul DELTA, mai estrapolazione."""
vals = []
for typ, tgt in (("C", 0.5), ("P", -0.5)):
s = g[g.option_type == typ]
if len(s) >= 2:
d = s["delta"].to_numpy()
if d.min() <= tgt <= d.max():
o = np.argsort(d)
vals.append(float(np.interp(tgt, d[o], s["iv"].to_numpy()[o])))
return float(np.mean(vals)) if vals else np.nan
def _interp_at(gid: np.ndarray, x: np.ndarray, y: np.ndarray, ngrp: int,
target: float) -> np.ndarray:
"""Interpolazione lineare di y(x) a `target`, per ogni gruppo, VETTORIALE e senza
estrapolazione (NaN se il gruppo non racchiude il target).
`gid` dev'essere ordinato crescente e (gid, x) lessicograficamente ordinati.
Equivalente a np.interp gruppo-per-gruppo: qui serve solo perche' i gruppi sono ~30.000
e un ciclo python li paga a caro prezzo su una VPS a 2 core condivisa.
"""
n_tot = np.bincount(gid, minlength=ngrp)
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:
"""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"
if f.exists():
return pd.read_parquet(f)
q = chain[chain.quoted & (chain.dte > DTE_MIN)]
out = (q.groupby(["asset", "hr", "exp"])
.apply(lambda g: pd.Series({"iv": _atm_iv(g), "dte": g["dte"].median()}),
include_groups=False)
.reset_index().dropna(subset=["iv"]))
q = chain[chain.quoted & (chain.dte > DTE_MIN)].copy()
keys = ["asset", "hr", "exp"]
q["gid"] = q.groupby(keys, sort=True).ngroup()
ngrp = int(q["gid"].max()) + 1
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)
out.to_parquet(f)
return out
@@ -266,7 +300,7 @@ def px_hourly(a: str) -> pd.Series:
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:
"""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."""
@@ -400,6 +434,51 @@ def build_atm_cost(chain: pd.DataFrame) -> pd.DataFrame:
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:
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")
@@ -412,25 +491,7 @@ def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) ->
roll_day = {}
for H in (24, 168):
for a in ASSETS:
r = atm_exp[atm_exp.asset == a]
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 = _roll_table(atm_exp[atm_exp.asset == a], H)
d = d[(d.dte >= 4) & (d.dte <= 45)]
if len(d) < 50:
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,
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
return out
@@ -413,8 +414,7 @@ def main() -> None:
for o in offs:
sz = {}
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(base_a, **sp))
sz[a] = sizes_from_L(EX[o][a], leverage_series(EX[o][a]["base_daily"], **sp))
SLE[nm][o] = leg_daily(EX[o], sz)
BK[nm][o] = book(TP, SLE[nm][o])
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}")
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})")
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]
dsr, sr0 = A.deflated_sharpe(fu_med[best], trials, BK[best][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"{imp.get('loss_frac', 0)*100:.1f}% Calmar {imp.get('calmar', 0):.1f}")
cols = {"TP01": TP, "SKH01": SLE["BASE"][can]}
for nm in [best_sleeve, list(BVT)[0]]:
m = vol(SLE[nm][can]) / vol(SLE["BASE"][can])
wp = {"TP01": W_TP, "SKH01": W_SKH * m}
for nm in [best, best_sleeve] + list(BVT)[:2]:
m_s = vol(SLE[nm][can]) / vol(SLE["BASE"][can])
wp = {"TP01": W_TP * MTP[nm][can], "SKH01": W_SKH * m_s}
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))
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(" '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(f" fatto in {time.time()-t0:.0f}s")
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:
sizes = SZ[nm](ex)
elif nm in VTL:
base_a = equity_daily(ex, size_flat(ex))
sizes = sizes_from_L(ex, leverage_series(base_a, **VTL[nm]))
sizes = sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm]))
else: # BOOKVT
L = leverage_series(book(TP, SLE["BASE"][can]), BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"])
sizes = sizes_from_L(ex, L)
+20 -5
View File
@@ -325,9 +325,16 @@ def main() -> None:
for a in ASSETS:
V = CB.dvol_series(a)
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())
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%}")
hist = V[V.index < win.index[0]].to_numpy(float)
print(f" {a}: DVOL nel campione min {win.min().1f} / mediana {win.median().1f} / "
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
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}$ "
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"
" Quale dei due valga dipende dal regime di margine del conto, che NON e' leggibile\n"
" da dati pubblici: si legge sul conto. E' un limite di questa misura, non un dettaglio.")
" Quale dei due valga dipende dal regime di margine del conto (standard vs portfolio),\n"
" 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
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"{trk:>12.2f}{'' if trk < 0.8 else ' '}")
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(" 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.")
@@ -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'))}, "
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*'))}")
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)