research(wave-0822): ORTHO-SCREEN 7/7 scartato (e uno screen largo non puo' passare il proprio DSR); XS-LITE falsifica il muro dei 20k di XS01

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:12:15 +00:00
parent 14c1a75a7e
commit bd5b634209
6 changed files with 256 additions and 79 deletions
+49
View File
@@ -8,6 +8,8 @@ null de-levering superato + eseguibilita' al capitale dichiarato.
|---|---|---|---| |---|---|---|---|
| 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 | | 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 |
| 2 | GROWTH-POLICY | **LEAD** (condizione, non data) | il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log; il gradino eseguibile 1.00x->1.25-1.50x vale 14.7a -> 12.9-11.6a al muro. Ma un solo giorno -10% all'anno porta k* da ~10x a **2x**: non si decide su un conto vero con la lente close-only | | 2 | GROWTH-POLICY | **LEAD** (condizione, non data) | il libro gira al **7% di Kelly** e raccoglie il **15%** della crescita massima in log; il gradino eseguibile 1.00x->1.25-1.50x vale 14.7a -> 12.9-11.6a al muro. Ma un solo giorno -10% all'anno porta k* da ~10x a **2x**: non si decide su un conto vero con la lente close-only |
| 12 | ORTHO-SCREEN | **SCARTATO 7/7** | nessuna famiglia arriva a ADDS+DSR; e il motivo e' **aritmetico**: su 168 trial il massimo atteso dal rumore e' Sharpe **1.572**, SOPRA il soffitto direzionale misurato (~1.3) -> uno screen largo su BTC/ETH direzionale **non puo'** passare un DSR, per costruzione |
| 3 | XS-LITE | **SCARTATO** (come sleeve) + **1 soglia pubblicata falsificata** | concentrare XS01 non crea uno sleeve nuovo (corr 0.81-0.96 col canonico, DSR FAIL, de-levering non superato) — ma il muro *"XS01 serve ~$20k"* e' misurato su una diagnostica di TURNOVER: il ribilancio vero smette di passare sotto **~$109 di sleeve (~$730 di book), 27x piu' in basso** |
## Note che sopravvivono ai singoli filoni ## Note che sopravvivono ai singoli filoni
@@ -69,3 +71,50 @@ vedono la variabile**. E' per questo che la leva e' l'unica leva del libro mai e
- 📌 **Sottoprodotto da verificare (tocca un'etichetta del libro live):** a `target_vol=20%` la vol - 📌 **Sottoprodotto da verificare (tocca un'etichetta del libro live):** a `target_vol=20%` la vol
**realizzata** di TP01 e' **12,14%** — il target vale sulla posizione quando c'e', e TP01 e' **realizzata** di TP01 e' **12,14%** — il target vale sulla posizione quando c'e', e TP01 e'
long-flat. **L'etichetta sovrastima il rischio preso di ~40%.** long-flat. **L'etichetta sovrastima il rischio preso di ~40%.**
### 12 — ORTHO-SCREEN (r0822_ortho_screen.py, 174 trial: 7 famiglie x 24 + 6 controlli)
📌 **Il risultato piu' riusabile dell'ondata, ed e' metodologico:** il deflated-Sharpe va calcolato
**di screen** (168 trial) e non solo di famiglia (24). Su quel pool il massimo atteso dal puro rumore
e' **Sharpe 1.572**, cioe' **sopra il soffitto direzionale misurato del progetto (~1.3)**.
**Conseguenza: una ricerca a rete larga su un singolo stream direzionale BTC/ETH non puo' passare il
proprio gate — non e' sfortuna, e' aritmetica.** Le ondate future o dichiarano famiglie molto piu'
piccole in anticipo, o cambiano meccanismo (non-direzionale / cross-sectional).
📌 **Per la prima volta nel progetto l'eseguibilita' a $600 non e' il vincolo binding di niente**
(haircut 0.00-0.01 su 7/7): muore tutto molto prima, sull'edge.
- Famiglie: RSKEW (skew realizzata), TACC (accelerazione del trend), VPX (volume-prezzo), XDISP
(dispersione dei 51 alt come timer), XCORR (corr BTC-ETH), XTAIL (dopo shock 3σ), VRAT
(variance-ratio). Miglior DSR di famiglia 0.633, di screen **0.034**. `robust_oos` False 7/7.
- **TACC** e' l'unica ADDS (corr→TP01 0.09) ma il suo uplift hold-out **vive tutto nel 2026** (2025
da solo 0.314) = finestra fortunata. E la domanda della famiglia ha risposta: **e' il LIVELLO,
non l'accelerazione** — la cella scelta si riduce al ritorno a 5 giorni, corr **+0.784** col
momentum di livello.
-**Tre falsificazioni che chiudono spazio:** (1) la mean-reversion **non torna in vita** sotto due
conditioner mai provati (volume, shock 3σ): la selezione in-sample sceglie il ramo di
CONTINUAZIONE in entrambi i casi; (2) la dispersione dei 51 alt come timer di mercato e'
**negativa** (0.899 sulla sola finestra attiva, non e' un artefatto di calendario); (3) RSKEW e'
il ritratto del fitting — **miglior in-sample dello screen (1.281) e peggior hold-out (0.494)**,
col segno OPPOSTO all'a-priori teorico dichiarato.
- ⚠️ **6ª occorrenza del null del de-levering** (VRAT, maxDD 4.4% -> `k*TP01` fa meglio).
- ⚠️ Due candidati hanno l'ancora canonica come **PEGGIORE** delle 8 (TACC, VRAT): la fortuna
d'ancora non ha un verso fisso, e guardare solo il canonico a volte **inventa un danno**.
### 3 — XS-LITE (r0822_xs_lite.py, 26 celle + 120 valutazioni d'ancora)
✅ Repliche **bit-exact** prima di ogni delta: XS01 vs `sleeves._xsec_returns` e XSR01 vs
`basket_from_positions(demean=True)`, entrambe `max|diff| = 0.0`.
📌 **Falsificazione di un numero pubblicato:** il muro *"XS01 serve ~$20k"* non e' un fatto di
Sharpe ma di **turnover** — e a capitale piccolo il min-order salta la **deriva del vol-target**
(|Δw| giornaliero 0.001) che **non porta segnale**, non il ribilancio (|Δw| 0.091-0.157). Capitale
minimo perche' il ribilancio passi: **$109 (k=5) / $86 (k=3) / $64 (k=2)** di sleeve. Stessa lezione
gia' imparata su TP01 nel 2026 ("a $600 il min-order e' gia' la banda ottimale") e **mai applicata a
XS01**. ⚠️ L'agente ha usato **min-order $10 (Hyperliquid)**, non $5 (Deribit) — un audit dedicato
sta verificando i minimi veri del venue.
- Concentrare **non** ripara il rischio #1 di XSR01: XS-LITE regge oltre **50 bps/gamba**, XSR-LITE
muore a **~28 bps a ogni k, pieno incluso**.
- La radice dell'ampiezza **non descrive** XS01: da 10 a 4 gambe si perde il **2%** di Sharpe
mediano-di-fase mentre N_hhi passa 6.9 -> 3.0 (il segnale sta negli **estremi** del ranking; le
gambe 3-5 per lato aggiungono rischio quanto segnale). Il collasso arriva solo a k=1 (41%).
- ⚠️ **Da inseguire — tocca un gate pre-registrato: lo Sharpe 1.82 di XSR01 NON si riproduce oggi.**
Sulla finestra identica a quella di scoperta la lente "paniere" da' **1.75** e la lente "libro" di
`paper_xsr` da' **2.23**; 1.82 non e' nessuna delle due. Spiegazione piu' probabile: `data/raw` e'
gitignored e il cron riscrive i parquet HL ogni notte -> **stesso codice, dati diversi** (identico
allo scoperto del 07/08 su GTAA/TLT). **La decisione del 23/10 poggia su quel numero.**
+1 -1
View File
@@ -254,7 +254,7 @@ def sezione2(snap: pd.DataFrame) -> pd.DataFrame:
g["p_x24"] = [bboot_p(d[d.odec == k].x24.values, block=48) for k in g.index] g["p_x24"] = [bboot_p(d[d.odec == k].x24.values, block=48) for k in g.index]
print(g.round(3).to_string()) print(g.round(3).to_string())
print(" av4/av24 = |ret| futuro medio: e' la gamba 'squeeze/cascata' dell'ipotesi") print(" av4/av24 = |ret| futuro medio: e' la gamba 'squeeze/cascata' dell'ipotesi")
print(" (piu' vol dopo un estremo di OI). E' QUASI PIATTA (0.59-0.75%% a 4h, solo il decile 0") print(" (piu' vol dopo un estremo di OI). E' QUASI PIATTA (0.59-0.75% a 4h, solo il decile 0")
print(" leggermente sopra): la 'cascata' prevedibile non c'e'.") print(" leggermente sopra): la 'cascata' prevedibile non c'e'.")
print("\n--- CONTROLLO: e' solo 'il prezzo si e' appena mosso'? ---") print("\n--- CONTROLLO: e' solo 'il prezzo si e' appena mosso'? ---")
+6 -5
View File
@@ -453,9 +453,10 @@ def main() -> None:
# ---------------------------------------------------------------- 4. NULL LOCATION-MATCHED # ---------------------------------------------------------------- 4. NULL LOCATION-MATCHED
P("\n[4] NULL LOCATION-MATCHED (regola 02/07: e' la POSIZIONE o e' l'OI?)") P("\n[4] NULL LOCATION-MATCHED (regola 02/07: e' la POSIZIONE o e' l'OI?)")
P(" Confronto APPAIATO per evento: tw(livello speciale) - tw(placebo alla stessa distanza).") P(" Confronto APPAIATO per evento: tw(livello speciale) - tw(placebo alla stessa distanza).")
P(" placebo: rand=strike uniforme (== permutazione dell'OI, equivalenza esatta) · adj=strike") P(" placebo PER REGOLA: adj_r = strike adiacente a K_r · jit_r = strike arbitrario a")
P(" adiacente al max-OI · jit=strike arbitrario a distanza 0.5-1.5x (analogo Fib±jitter) ·") P(" distanza 0.5-1.5x |K_r - S| (analogo del Fib±jitter del 02/07).")
P(" mid=centro della griglia quotata (zero info OI) · mr7=media 7g dello spot (mean-reversion).") P(" placebo GLOBALI: rand = strike uniforme (== permutazione dell'OI, equivalenza esatta) ·")
P(" mid = centro della griglia quotata (zero info OI) · mr7 = media 7g dello spot.")
P(f" {'hte':>4} {'rule':>5} {'Δ vs adj_r':>12} {'Δ vs jit_r':>12}" P(f" {'hte':>4} {'rule':>5} {'Δ vs adj_r':>12} {'Δ vs jit_r':>12}"
f" {'Δ vs rand':>12} {'Δ vs mid':>12} {'Δ vs mr7':>12}") f" {'Δ vs rand':>12} {'Δ vs mid':>12} {'Δ vs mr7':>12}")
npos = ntot = 0 npos = ntot = 0
@@ -518,7 +519,7 @@ def main() -> None:
P("\n [6-bis] LA STESSA MACCHINA SUI LIVELLI PLACEBO (stesse finestre, stessa fee, stesso") P("\n [6-bis] LA STESSA MACCHINA SUI LIVELLI PLACEBO (stesse finestre, stessa fee, stesso")
P(" sizing): se un placebo regge quanto il livello 'speciale', l'OI non aggiunge nulla.") P(" sizing): se un placebo regge quanto il livello 'speciale', l'OI non aggiunge nulla.")
nulls = [] nulls = []
for rl in ("mp", "oi", "mid", "mr7", "rand", "jit", "maj", "short", "long"): for rl in ("mp", "oi", "mid", "mr7", "rand", "jit_mp", "maj", "short", "long"):
for hte in (3, 6, 12, 24): for hte in (3, 6, 12, 24):
for bn, bd in bands.items(): for bn, bd in bands.items():
d, _i = strat_daily(ev, rl, hte, bd, "sign", "all", FEE_SIDE_REAL) d, _i = strat_daily(ev, rl, hte, bd, "sign", "all", FEE_SIDE_REAL)
@@ -527,7 +528,7 @@ def main() -> None:
cagr=mm["cagr"], dd=mm["dd"])) cagr=mm["cagr"], dd=mm["dd"]))
N = pd.DataFrame(nulls) N = pd.DataFrame(nulls)
piv = N.pivot_table(index=["hte", "band"], columns="rule", values="sharpe") piv = N.pivot_table(index=["hte", "band"], columns="rule", values="sharpe")
P(piv.reindex(columns=["mp", "oi", "mid", "mr7", "rand", "jit", "maj", "short", "long"]) P(piv.reindex(columns=["mp", "oi", "mid", "mr7", "rand", "jit_mp", "maj", "short", "long"])
.to_string(float_format=lambda v: f"{v:6.2f}")) .to_string(float_format=lambda v: f"{v:6.2f}"))
P(f" celle placebo valutate: {len(N)} (contano come trial: la griglia dichiarata sale)") P(f" celle placebo valutate: {len(N)} (contano come trial: la griglia dichiarata sale)")
+46 -16
View File
@@ -360,7 +360,10 @@ def sezione2(curve: pd.DataFrame) -> dict:
print("\n (c) IL CONFOUND DI QUESTO FILONE — la pendenza e' informazione NUOVA o RV riciclata?") print("\n (c) IL CONFOUND DI QUESTO FILONE — la pendenza e' informazione NUOVA o RV riciclata?")
rvp = (r1.rolling(24).std() * np.sqrt(24 * 365) * 100).reindex(idx) rvp = (r1.rolling(24).std() * np.sqrt(24 * 365) * 100).reindex(idx)
rvf = (r1.shift(-24).rolling(24).std().shift(-23) * np.sqrt(24 * 365) * 100).reindex(idx) # RV su (t, t+24h]: rolling(24) etichettata a t+24 e riportata indietro a t. (Una prima
# stesura aveva uno shift di troppo e misurava la finestra (+24h, +47h): corretto in
# sessione confrontando con una seconda implementazione a serie rovesciata.)
rvf = (r1.rolling(24).std().shift(-24) * np.sqrt(24 * 365) * 100).reindex(idx)
cf = curve[curve.asset == a].copy() cf = curve[curve.asset == a].copy()
cf["hr"] = pd.to_datetime(cf["hr"], utc=True).dt.as_unit("ns") cf["hr"] = pd.to_datetime(cf["hr"], utc=True).dt.as_unit("ns")
ivf = cf.set_index("hr")[f"iv_{FRONT}"].reindex(idx) ivf = cf.set_index("hr")[f"iv_{FRONT}"].reindex(idx)
@@ -563,9 +566,14 @@ def sezione3(chain: pd.DataFrame, atm_exp: pd.DataFrame, curve: pd.DataFrame) ->
f"({prem/CAPITALE:.0%} del conto), vega ${vega:.2f}/pt-vol, fee ${fee:.2f}/gamba") f"({prem/CAPITALE:.0%} del conto), vega ${vega:.2f}/pt-vol, fee ${fee:.2f}/gamba")
print(f" carry lordo ${carry_g:+.3f}/giorno per struttura; attrito per giro " print(f" carry lordo ${carry_g:+.3f}/giorno per struttura; attrito per giro "
f"${friz:.2f} => {friz/carry_g:.0f} giorni per andare in pari") f"${friz:.2f} => {friz/carry_g:.0f} giorni per andare in pari")
print(f"\n Un calendar spread e' DUE gambe: a $635 una sola struttura ETH impegna il") print("\n Un calendar spread e' DUE gambe. Cio' che la tabella mostra e' il PREMIO di UNA")
print(f" ~15-30% del conto e rende centesimi al giorno. BTC e' fuori dal lotto minimo.") print(" gamba al lotto minimo: BTC 26% del conto, ETH 10%. Il premio non e' tutto il")
print(f" Il bersaglio dichiarato e' 50 EUR/giorno: qui si parla di due ordini di grandezza sotto.") print(" fabbisogno — la gamba CORTA vuole margine, e la misura del 2026-07-30 su una")
print(" struttura BTC a rischio definito dava $6.210 di collaterale per lotto. Quindi:")
print(" BTC resta fuori per il margine, ETH entra ma una sola struttura impegna il")
print(" 10-25% del conto per un carry LORDO di centesimi al giorno.")
print(" Il bersaglio dichiarato e' 50 EUR/giorno: qui siamo 2-3 ordini di grandezza sotto,")
print(" e il conto non regge la SECONDA struttura che servirebbe per diversificare.")
TRIALS.append(("Q2: 2 asset x eseguibilita' al lotto minimo", 2)) TRIALS.append(("Q2: 2 asset x eseguibilita' al lotto minimo", 2))
@@ -671,17 +679,31 @@ def sezione4(curve: pd.DataFrame) -> None:
hold = tgt.shift(1).fillna(0.0) hold = tgt.shift(1).fillna(0.0)
net = (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna() net = (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna()
dd_c = A._dd_ret(net) dd_c = A._dd_ret(net)
# ⚠️ il k giusto e' il PIU' GRANDE che sta sotto il DD del candidato, non il primo che
# trovi salendo da 0.05: partendo dal basso si confronta il candidato con un baseline
# quasi spento, che ha un DD ridicolo e uno Sharpe gonfiato dal rapporto. (Errore
# commesso e corretto in sessione: dava "candidato sopra il null" con k=0.05.)
ks = np.arange(0.05, 3.01, 0.05)
best = None best = None
for k in np.arange(0.05, 1.01, 0.05): for k in ks[::-1]:
b = k * d["r"].reindex(net.index) b = k * d["r"].reindex(net.index)
if A._dd_ret(b) <= dd_c: if A._dd_ret(b) <= dd_c:
best = (k, A._sh(b), A._dd_ret(b)) best = (float(k), A._sh(b), A._dd_ret(b))
break break
print(f" {a}: candidato Sharpe {A._sh(net):+.2f} maxDD {dd_c:.2%} | " sh_c = A._sh(net)
+ (f"buy&hold de-leverato k={best[0]:.2f} -> stesso DD ({best[2]:.2%}) con Sharpe " if best is None:
f"{best[1]:+.2f} => {'IL CANDIDATO NON SOPRAVVIVE' if best[1] >= A._sh(net) else 'candidato sopra il null'}" print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | nessun k in "
if best else "nessun k<=1 raggiunge quel DD")) f"[0.05,3.00] raggiunge quel DD")
TRIALS.append((f"Q3 {a}: 20 valori di k nel null de-levering", 20)) else:
verdict = ("IL CANDIDATO NON SOPRAVVIVE (il de-levering fa meglio)"
if best[1] >= sh_c else "candidato sopra il null di de-levering")
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)
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))
# =========================================================================================== # ===========================================================================================
@@ -745,15 +767,24 @@ def sezione5(curve: pd.DataFrame) -> None:
if k in rep: if k in rep:
print(f" {k:20s} = {rep[k]}") print(f" {k:20s} = {rep[k]}")
imp = A.implausible_sharpe(era) imp = A.implausible_sharpe(era)
print(f" implausible_sharpe: flagged={imp.get('flagged')} ({imp.get('reasons')})") print(f" implausible_sharpe (proxy, era DVOL): implausible={imp['implausible']} "
f"{imp['reasons']}")
print(f" earns_slot = (assoluto non-FAIL) AND (marginale ADDS): qui il marginale e' "
f"{rep.get('marginal_verdict')} -> **earns_slot = False**.")
TRIALS.append(("Sez.5: 2 varianti proxy (long-only, L/S) x 1 soglia", 2)) TRIALS.append(("Sez.5: 2 varianti proxy (long-only, L/S) x 1 soglia", 2))
sub("(c) il precedente che questo replica") sub("(c) il precedente che questo replica")
print(" `dvol_directional.py` (2026-06-29) testo' VRP-Z = z-score causale di (IV-RV) come") print(" `dvol_directional.py` (2026-06-29) testo' VRP-Z = z-score causale di (IV-RV) come")
print(" segnale DIREZIONALE su 5 anni, con la griglia completa: verdetto **HEDGE,") print(" segnale DIREZIONALE su 5 anni, con la griglia completa: verdetto **HEDGE,")
print(" earns_slot=False** — paga solo quando TP01 e' debole, quindi non e' alpha.") print(" earns_slot=False**. Qui, con la variante a SOGLIA BINARIA invece che a z-score,")
print(" La pendenza della term structure e', su questa lente, la stessa variabile con la") print(" il verdetto misurato e' NEUTRAL con uplift NEGATIVO — un verdetto diverso, la")
print(" gamba realizzata sostituita da una implicita. Il filone non e' nuovo: e' quello.") print(" stessa conclusione (earns_slot=False in entrambi). La differenza sta nella")
print(" variante, non nell'esito, e va riportata cosi': non sto dichiarando una replica")
print(" che non ho fatto.")
print(" Il punto strutturale resta: la pendenza e' il premio di vol con la gamba")
print(" REALIZZATA sostituita da una IMPLICITA (SEZIONE 2c: corr(pendenza, RV passata)")
print(" = -0.50/-0.57). Non e' una variabile nuova per questo progetto: e' quella, con")
print(" un rumore di misura piu' basso e 5 anni di storia in meno.")
# =========================================================================================== # ===========================================================================================
@@ -778,7 +809,6 @@ def sezione6() -> None:
def main() -> None: def main() -> None:
hr("r0822_term_structure — TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH") hr("r0822_term_structure — TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH")
print(__doc__.split("METODO.")[0].strip()[:0] or "", end="")
chain = load_quoted_chain() chain = load_quoted_chain()
sezione0(chain) sezione0(chain)
atm_exp = build_atm_by_expiry(chain) atm_exp = build_atm_by_expiry(chain)
+58 -30
View File
@@ -303,6 +303,20 @@ def sizes_from_L(ex: dict, L: pd.Series) -> np.ndarray:
# --------------------------------------------------------------------------- libro # --------------------------------------------------------------------------- libro
def sizes_of(nm, ex: dict, SZ: dict, VTL: dict, ACT: dict, Lbook: pd.Series | None = None):
"""Vettore di size per-trade di una cella qualsiasi. Un solo posto in cui e' definita:
§8 (eseguibilita'), §9 (deriva) e §10 (null di permutazione) devono usare LA STESSA."""
if nm is None or nm == "BASE":
return size_flat(ex)
if nm in SZ:
return SZ[nm](ex)
if nm in VTL:
return sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm]))
if nm in ACT:
return sizes_from_L(ex, activity_L(ex["base_daily"], **ACT[nm]))
return sizes_from_L(ex, Lbook)
def book(tp: pd.Series, skh: pd.Series) -> pd.Series: def book(tp: pd.Series, skh: pd.Series) -> pd.Series:
return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH}) return combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": W_TP, "SKH01": W_SKH})
@@ -498,7 +512,8 @@ def main() -> None:
print(" de-levering (k<1 sul BASELINE che pareggia il maxDD della variante)") print(" de-levering (k<1 sul BASELINE che pareggia il maxDD della variante)")
print("-" * 112) print("-" * 112)
print(f" {'variante':<28}{'wSKH eff':>9}{'dShFULL':>9}{'pos/n':>8}{'dShHOLD':>9}{'pos/n':>8}" print(f" {'variante':<28}{'wSKH eff':>9}{'dShFULL':>9}{'pos/n':>8}{'dShHOLD':>9}{'pos/n':>8}"
f"{'| ISO dF':>10}{'pos/n':>8}{'ISO dH':>9}{'pos/n':>8}{'dDDpp':>8}{'k_iso':>8}") f"{'| ISO dF':>10}{'pos/n':>8}{'ISO dH':>9}{'pos/n':>8}{'dDDpp':>8}{'k_iso':>8}"
f"{'null de-lev':>14}")
base_b = BK["BASE"] base_b = BK["BASE"]
RES = {} RES = {}
for nm in ALL: for nm in ALL:
@@ -525,7 +540,13 @@ def main() -> None:
print(f" {nm:<28}{we:>9.3f}{dF['median_paired']:>+9.3f}{dF['n_positive']:>5}/{len(offs):<3}" print(f" {nm:<28}{we:>9.3f}{dF['median_paired']:>+9.3f}{dF['n_positive']:>5}/{len(offs):<3}"
f"{dH['median_paired']:>+9.3f}{dH['n_positive']:>5}/{len(offs):<3}" f"{dH['median_paired']:>+9.3f}{dH['n_positive']:>5}/{len(offs):<3}"
f"{iF['median_paired']:>+10.3f}{iF['n_positive']:>5}/{len(offs):<3}" f"{iF['median_paired']:>+10.3f}{iF['n_positive']:>5}/{len(offs):<3}"
f"{iH['median_paired']:>+9.3f}{iH['n_positive']:>5}/{len(offs):<3}{dd:>+8.2f}{ks:>8}") f"{iH['median_paired']:>+9.3f}{iH['n_positive']:>5}/{len(offs):<3}{dd:>+8.2f}{ks:>8}"
f"{vd:>14}")
print(" ⚠️ NOTA sul null del de-levering: `sh(k*base)` e' IDENTICO a `sh(base)` perche' lo")
print(" Sharpe e' invariante a una costante -> in questa forma il null COINCIDE col confronto")
print(" degli Sharpe, ed e' per questo che qui lo Sharpe e' la metrica che decide e il DD no.")
print(" 'k_iso' resta come diagnostica: quanta leva dovrebbe cedere il baseline per pareggiare")
print(" il maxDD della variante. 'DE-LEVERING' = la variante NON batte quel baseline ridotto.")
print(" wSKH eff = peso EFFETTIVO di SKH nel libro (i nominali restano 75/25: la size lo") print(" wSKH eff = peso EFFETTIVO di SKH nel libro (i nominali restano 75/25: la size lo")
print(" muove). Colonne 'ISO' = confronto vs il baseline RI-SCALATO a quello stesso peso:") print(" muove). Colonne 'ISO' = confronto vs il baseline RI-SCALATO a quello stesso peso:")
print(" e' li' che si legge la FORMA. Le colonne non-ISO contengono anche il cambio di peso.") print(" e' li' che si legge la FORMA. Le colonne non-ISO contengono anche il cambio di peso.")
@@ -687,24 +708,10 @@ def main() -> None:
f" anni positivi {npos}/{len(yrs)}") f" anni positivi {npos}/{len(yrs)}")
print() print()
for nm in top: for nm in top:
if nm in SZ: Lb = (leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"])
szs = np.concatenate([SZ[nm](EX[can][a]) for a in ASSETS]) if nm in BVT else None)
dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS])) szs = np.concatenate([sizes_of(nm, EX[can][a], SZ, VTL, ACT, Lb) for a in ASSETS])
elif nm in VTL or nm in ACT: dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS]))
mk = ((lambda b: leverage_series(b, **VTL[nm])) if nm in VTL
else (lambda b: activity_L(b, **ACT[nm])))
szs, dys = [], []
for a in ASSETS:
szs.append(sizes_from_L(EX[can][a], mk(EX[can][a]["base_daily"])))
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) s = pd.Series(szs, index=dys)
med = s.groupby(s.index.year).median() 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") rho = float(np.corrcoef(np.arange(len(med)), med.values)[0, 1]) if len(med) > 2 else float("nan")
@@ -715,6 +722,33 @@ def main() -> None:
print(" (la vol crypto e' scesa lungo il campione -> qualunque 1/vol pesa di piu' il") 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(" periodo recente, che e' anche l'hold-out). La riga 'corr col tempo' la misura.")
# noqa
print("\n" + "-" * 112)
print(" 10. NULL DI PERMUTAZIONE DELLA SIZE — la stessa MULTINSIEME di size, riassegnata a")
print(" caso ai trade. Se il vantaggio a iso-peso sopravvive al null, conta QUALE trade")
print(" riceve quale size (informazione); se no, e' solo la DISTRIBUZIONE delle size.")
print("-" * 112)
rng = np.random.default_rng(20260822)
a10 = list(offs[:: max(1, len(offs) // 3)])[:3]
NDRAW = 150
for nm in dict.fromkeys([best_iso, best_sleeve]):
real = float(np.median([sh(BK[nm][o]) - sh(CTRL[nm][o]) for o in a10]))
nul = []
for _ in range(NDRAW):
v = []
for o in a10:
szp = {a: rng.permutation(sizes_of(nm, EX[o][a], SZ, VTL, ACT)) for a in ASSETS}
lg = leg_daily(EX[o], szp)
m_s = vol(lg) / vol(SLE["BASE"][o]) if vol(SLE["BASE"][o]) > 0 else 1.0
v.append(sh(book(TP, lg)) - sh(book(TP, SLE["BASE"][o] * m_s)))
nul.append(float(np.median(v)))
nul = np.asarray(nul)
pc = float((nul < real).mean()) * 100
print(f" {nm:<26} reale {real:+.3f} null mediana {np.median(nul):+.3f} "
f"p90 {np.percentile(nul, 90):+.3f} max {nul.max():+.3f} "
f"-> percentile {pc:.1f} (p ~ {(1 - pc / 100):.3f})")
print(f" {NDRAW} permutazioni x {len(a10)} ancore {a10}; seme fisso 20260822.")
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)
@@ -728,19 +762,13 @@ def netted_target(asset: str, ex: dict, nm, SZ, VTL, ACT, BVT, SLE, TP, can, df1
ts1 = df1h["timestamp"].values.astype(np.int64) + 3_600_000 ts1 = df1h["timestamp"].values.astype(np.int64) + 3_600_000
pos = np.zeros(len(ts1)) pos = np.zeros(len(ts1))
L1h = np.ones(len(ts1)) L1h = np.ones(len(ts1))
if nm is None: if nm is not None and nm in BVT: # BOOKVT: leva anche sulla gamba TP01
sizes = size_flat(ex)
elif nm in SZ:
sizes = SZ[nm](ex)
elif nm in VTL:
sizes = sizes_from_L(ex, leverage_series(ex["base_daily"], **VTL[nm]))
elif nm in ACT:
sizes = sizes_from_L(ex, activity_L(ex["base_daily"], **ACT[nm]))
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_of(nm, ex, SZ, VTL, ACT, L)
idx1h = pd.DatetimeIndex(pd.to_datetime(df1h["timestamp"].values, unit="ms", utc=True)) idx1h = pd.DatetimeIndex(pd.to_datetime(df1h["timestamp"].values, unit="ms", utc=True))
L1h = L.reindex(idx1h.floor("D")).ffill().fillna(1.0).values L1h = L.reindex(idx1h.floor("D")).ffill().fillna(1.0).values
else:
sizes = sizes_of(nm, ex, SZ, VTL, ACT)
t_in, t_out = ex["ts_close"][ex["i_ent"]], ex["ts_close"][ex["i_ex"]] t_in, t_out = ex["ts_close"][ex["i_ent"]], ex["ts_close"][ex["i_ex"]]
for k in range(len(t_in)): for k in range(len(t_in)):
j0 = int(np.searchsorted(ts1, t_in[k], side="left")) j0 = int(np.searchsorted(ts1, t_in[k], side="left"))
+96 -27
View File
@@ -382,13 +382,19 @@ def main() -> None:
thr = float(np.quantile(vals[:-1], 0.30)) thr = float(np.quantile(vals[:-1], 0.30))
last_open = ser[ser > 0.30] last_open = ser[ser > 0.30]
since = (V.index[-1] - last_open.index[-1]).days if len(last_open) else None since = (V.index[-1] - last_open.index[-1]).days if len(last_open) else None
inwin = ser[(ser.index >= puts["ts"].min()) & (ser.index <= puts["ts"].max())]
near = inwin.max() if len(inwin) else float("nan")
print(f" {a}: quota di giorni col gate APERTO per anno — " print(f" {a}: quota di giorni col gate APERTO per anno — "
+ " ".join(f"{y}:{v:.0%}" for y, v in per_year.items())) + " ".join(f"{y}:{v:.0%}" for y, v in per_year.items()))
print(f" DVOL oggi {vals[-1]:.1f} · serve DVOL > {thr:.1f} " print(f" DVOL oggi {vals[-1]:.1f} · serve DVOL > {thr:.1f} "
f"(+{thr / vals[-1] - 1:.0%}) per riaprire il gate · ultimo giorno aperto " f"(+{thr / vals[-1] - 1:.0%}) per riaprire il gate · ultimo giorno aperto "
f"{last_open.index[-1]:%Y-%m-%d} ({since} giorni fa)") f"{last_open.index[-1]:%Y-%m-%d} ({since} giorni fa)")
print(f" quanto vicino c'e' andato DENTRO la finestra: IV-rank max "
f"{near:.4f} il {inwin.idxmax():%Y-%m-%d}"
f"{'mancato per ' + format(0.30 - near, '.4f') if near <= 0.30 else 'aperto'}")
print(" → il campione non e' 'sfortunato': cade dentro un regime di vol bassa lungo e\n" print(" → il campione non e' 'sfortunato': cade dentro un regime di vol bassa lungo e\n"
" persistente. La misura giusta si potra' rifare quando quel numero sara' superato,\n" " persistente. Ma il margine NON e' comodo: il gate ha sfiorato l'apertura, e\n"
" bastano ~+10% di DVOL per riaprirlo. La misura giusta si potra' rifare quando quel numero sara' superato,\n"
" e da allora servira' comunque un anno di settimane per avere un campione.") " e da allora servira' comunque un anno di settimane per avere un campione.")
ret = {a: (canon[a]["ST"].iloc[-1] / canon[a]["spot"].iloc[0] - 1.0) for a in ASSETS} ret = {a: (canon[a]["ST"].iloc[-1] / canon[a]["spot"].iloc[0] - 1.0) for a in ASSETS}
@@ -668,6 +674,23 @@ def main() -> None:
f"MEDIANA ONESTA {band['median']:.2f} · banda [{band['lo']:.2f}, {band['hi']:.2f}] · " f"MEDIANA ONESTA {band['median']:.2f} · banda [{band['lo']:.2f}, {band['hi']:.2f}] · "
f"positiva in {band['frac_positive']:.0%} di {band['n_anchors']} ancore · " f"positiva in {band['frac_positive']:.0%} di {band['n_anchors']} ancore · "
f"fortuna {band['luck']:+.2f}") f"fortuna {band['luck']:+.2f}")
# DOVE vive l'instabilita': nel numeratore (il guadagno) o nel denominatore (la vol)?
det = []
for o in offsets:
w = by_off(o)
if len(w) > 2 and w.std(ddof=1) > 0:
det.append((o, float(w.mean()), float(w.std(ddof=1)), len(w)))
if det:
mm = np.array([d[1] for d in det])
ss = np.array([d[2] for d in det])
print(f" media settimanale fra le ancore: [{mm.min():+.2%}, {mm.max():+.2%}] "
f"(rapporto {mm.max() / max(mm.min(), 1e-9):.1f}x) · "
f"deviazione standard: [{ss.min():.2%}, {ss.max():.2%}] "
f"(rapporto {ss.max() / max(ss.min(), 1e-9):.1f}x)")
print(f" → l'ancora NON cambia quanto si guadagna, cambia il DENOMINATORE: con\n"
f" {det[0][3]} settimane e nessuna perdita la vol campionaria e' quasi\n"
f" arbitraria, e lo Sharpe con lei. Non e' che le 08:00 siano un'ora\n"
f" magica — e' che questa statistica non porta informazione.")
print("\n (d) null del de-levering / beta — 'e' reddito da volatilita' o crypto travestito?'") print("\n (d) null del de-levering / beta — 'e' reddito da volatilita' o crypto travestito?'")
print(" Lo spread ha delta positivo (≈ +0.18 all'ingresso): in un campione che sale del") print(" Lo spread ha delta positivo (≈ +0.18 all'ingresso): in un campione che sale del")
@@ -697,8 +720,15 @@ def main() -> None:
dly = comb.resample("1D").sum().fillna(0.0) dly = comb.resample("1D").sum().fillna(0.0)
rep = A.marginal_vs_tp01(dly) rep = A.marginal_vs_tp01(dly)
act = int((dly != 0).sum()) act = int((dly != 0).sum())
print(f" verdetto={rep.get('verdict')} corr={rep.get('corr'):.3f} " def _f(k):
f"(barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%})") v = rep.get(k)
return f"{v:.3f}" if isinstance(v, (int, float)) and v is not None else str(v)
print(f" verdetto={rep.get('verdict')} corr={_f('corr')} "
f"insample_edge={rep.get('has_insample_edge')} robust_oos={rep.get('robust_oos')} "
f"is_hedge={rep.get('is_hedge')}")
print(f" barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%} — un `None` qui non e'\n"
f" un bug: le sotto-misure che richiedono un hold-out non hanno dati per\n"
f" esistere, e la funzione lo dichiara invece di inventare un numero.")
print(f" ⚠️ {len(comb)} settimane su un hold-out che non esiste: il verdetto e' " print(f" ⚠️ {len(comb)} settimane su un hold-out che non esiste: il verdetto e' "
f"rumore, si riporta per non ometterlo.") f"rumore, si riporta per non ometterlo.")
except Exception as exc: # noqa: BLE001 except Exception as exc: # noqa: BLE001
@@ -719,36 +749,75 @@ def main() -> None:
# ------------------------------------------------------------ §7 VERDETTO # ------------------------------------------------------------ §7 VERDETTO
hr("§7 VERDETTO") hr("§7 VERDETTO")
print(""" SCARTATO — su questi dati la domanda non e' decidibile, e i tre motivi sono print(""" SCARTATO — su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti
indipendenti l'uno dall'altro: 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 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 campione e' vol implicita al 24-28 percentile E un rialzo che E' l'alpha di VRP01 il sleeve sarebbe stato FLAT per l'intero campione. La DVOL
forte del sottostante: il regime migliore possibile per vendere put, non uno neutro. mediana sta al 7 (BTC) / 11 (ETH) percentile della storia 2021+, e il sottostante e'
Le 10 settimane per asset (non 16: prima del 2026-06-09 la finestra 4-10 DTE non salito del +25% / +44%: vol implicita bassa E rialzo forte = il regime MIGLIORE
esiste) danno 10/10 vincenti su BTC. La lente giusta non e' lo Sharpe, e' la regola possibile per vendere put, non uno neutro. Le settimane sono 10 per asset, non 16
del tre: 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che perde (prima del 2026-06-09 l'archivio seguiva solo la finestra 13-20 DTE: quella 4-10 DTE
~7x il credito. non esiste). Esito: 10/10 vincenti su BTC, e la lente giusta non e' lo Sharpe ma la
regola del tre 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che
perde ~7x il credito.
Sfumatura che va detta: il gate non e' lontano. Ha mancato l'apertura per 0.0002 di
IV-rank il 2026-06-07, e bastano ~+10% di DVOL per riaprirlo. Il campione e' dentro un
regime di vol bassa persistente, non davanti a un muro.
2. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte 2. IL TITOLO E' QUASI TUTTO FORTUNA D'ANCORA. L'ora d'ingresso e' un max-of-k mai
sono le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC. dichiarato, ed e' la prima volta che si misura su questa struttura: su 9 ancore lo
La famiglia USDC-lineare esiste per BTC ed ETH ed ha un lotto minimo 10x piu' piccolo Sharpe canonico BTC 32.58 sta all'89 percentile e la MEDIANA ONESTA e' 1.45, con la
(BTC 0.01 invece di 0.1) quindi il muro «BTC fuori a $3.000» del 30/07, congelato banda che tocca il NEGATIVO (-0.35). Fortuna +31.13. Su ETH 3.90 -> 2.41.
in un test, e' misurato sulla famiglia sbagliata. Ma di quella famiglia abbiamo ZERO Il numero da citare per BTC e' 1.45, non 32.58 — e a quel punto non c'e' piu' niente
ore di storia e l'open interest e' ~0. di straordinario da spiegare.
3. LO SPREAD E' IL COSTO DOMINANTE E NON E' MODELLABILE DA UN MID. Attraversare il 3. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte sono
bid/ask costa da solo una quota grossa del credito, e l'ala che si COMPRA e' la gamba le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC e il book
dove il libro e' piu' largo lo stesso meccanismo che il 30/07 misurava come f=0.73. esegue sui perpetual lineari USDC. Esiste una famiglia USDC-lineare per BTC ed ETH col
lotto minimo 10x piu' piccolo (BTC 0.01 = $773 di nozionale invece di 0.1 = $7.728),
quindi il muro «BTC fuori a $3.000» del 30/07 congelato in
tests/test_vrp_profit_take.py e' misurato sulla famiglia che il conto non margina.
E' la 3a occorrenza della stessa forma dopo fee_watch (21/08) e la taratura di
venue_watch: un numero puntato su una configurazione diversa da quella che gira.
Ma di quella famiglia abbiamo ZERO ore di storia.
Cio' che NON si conclude: che la struttura non funzioni. Non e' stata osservata nel regime 4. LO SPREAD E' IL COSTO DOMINANTE, E SULLA FAMIGLIA ESEGUIBILE E' PEGGIO. Attraversare
in cui lavora. La regola «niente short-vol da MODELLO in deploy» NON diventa «short-vol da il bid/ask costa ~10% del credito sugli INVERSE; nella regione di strike che serve lo
QUOTE»: le quote ora ci sono, ma sono quote della famiglia sbagliata, in un regime solo. 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/19 osservazioni >= 1.0) conferma dello 0.73 del 30/07 su un campione piu' lungo e
con un percorso diverso.
AZIONE RACCOMANDATA (non eseguita tocca la produzione, e la decisione e' dell'operatore): CIO' CHE NON SI CONCLUDE: che la struttura non funzioni. Non e' stata osservata nel regime
far raccogliere a `collect_chain.py` anche `currency=USDC` (famiglie BTC_USDC/ETH_USDC). in cui lavora. E un risultato a favore c'e', va detto: su BTC il null «e' solo crypto
Costa ~2 chiamate in piu' per giro; senza, fra sei mesi ci ritroveremo con un anno di travestito» e' REFUTATO (beta +0.019, t=0.18) — il PnL non viene dal rialzo. Non basta,
storia della famiglia che non possiamo tradare e zero di quella che potremmo.""") perche' il punto 2 toglie il titolo comunque.
LA REGOLA NON CAMBIA. «Niente short-vol da MODELLO in deploy» non diventa «short-vol da
QUOTE»: le quote adesso ci sono, ma sono quote della famiglia sbagliata, in un regime solo,
e con l'ancora che spiega il risultato. La sostituzione della regola resta NON DIMOSTRATA.
DUE AZIONI, NESSUNA ESEGUITA (toccano la produzione o il conto: decide l'operatore).
(A) DATO far raccogliere a `collect_chain.py` anche `currency=USDC`. Costo MISURATO col
filtro OI>=100 gia' in vigore: ETH_USDC porterebbe 119 strumenti per giro (+21% di
chiamate, da ~570 a ~690), BTC_USDC ne porterebbe 5 su 686 (0.7%) = nulla di
utilizzabile. Cioe': l'unica struttura USDC-regolata con un libro vero e' su ETH,
lotto minimo 0.1 ETH = $242. Senza questa raccolta, fra sei mesi avremo un anno di
storia della famiglia che non possiamo marginare e zero di quella che potremmo che
e' esattamente la posizione di oggi, un'ondata dopo l'altra.
(B) CONTO una domanda che nessun dato pubblico chiude e che viene PRIMA di ogni
backtest: un saldo in USDC puo' marginare un'opzione INVERSE? Se no, la famiglia con
3 mesi di storia non e' negoziabile da questo conto a nessun capitale, e il punto 3
smette di essere un dettaglio di lotto per diventare un muro. Si risolve come si
risolse il PRIIPs il 26/07: guardando il conto, non i dati.
SE E QUANDO RIAPRIRLO. Non «quando ci sara' un crash» (criterio senza numero) ma: DVOL BTC
> 48.3 o ETH > 62.8 in modo persistente, PIU' ~un anno di settimane raccolte in quel
regime sulla famiglia eseguibile. Prima di allora ogni misura su questa struttura
descrivera' la sua astensione.""")
if __name__ == "__main__": if __name__ == "__main__":