diff --git a/docs/research/RESULTS-0822.md b/docs/research/RESULTS-0822.md index 450eb89..36bd8aa 100644 --- a/docs/research/RESULTS-0822.md +++ b/docs/research/RESULTS-0822.md @@ -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 | | 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 @@ -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 **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%.** + +### 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.** diff --git a/scripts/research/r0822_flow_squeeze.py b/scripts/research/r0822_flow_squeeze.py index 89e092f..f587a6e 100644 --- a/scripts/research/r0822_flow_squeeze.py +++ b/scripts/research/r0822_flow_squeeze.py @@ -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] print(g.round(3).to_string()) 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("\n--- CONTROLLO: e' solo 'il prezzo si e' appena mosso'? ---") diff --git a/scripts/research/r0822_oi_pin.py b/scripts/research/r0822_oi_pin.py index 47d73e6..7b46f3c 100644 --- a/scripts/research/r0822_oi_pin.py +++ b/scripts/research/r0822_oi_pin.py @@ -453,9 +453,10 @@ def main() -> None: # ---------------------------------------------------------------- 4. NULL LOCATION-MATCHED 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(" placebo: rand=strike uniforme (== permutazione dell'OI, equivalenza esatta) · adj=strike") - P(" adiacente al max-OI · jit=strike arbitrario a distanza 0.5-1.5x (analogo Fib±jitter) ·") - P(" mid=centro della griglia quotata (zero info OI) · mr7=media 7g dello spot (mean-reversion).") + P(" placebo PER REGOLA: adj_r = strike adiacente a K_r · jit_r = strike arbitrario a") + P(" distanza 0.5-1.5x |K_r - S| (analogo del Fib±jitter del 02/07).") + 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}" f" {'Δ vs rand':>12} {'Δ vs mid':>12} {'Δ vs mr7':>12}") 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(" sizing): se un placebo regge quanto il livello 'speciale', l'OI non aggiunge nulla.") 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 bn, bd in bands.items(): 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"])) N = pd.DataFrame(nulls) 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}")) P(f" celle placebo valutate: {len(N)} (contano come trial: la griglia dichiarata sale)") diff --git a/scripts/research/r0822_term_structure.py b/scripts/research/r0822_term_structure.py index b85f3c3..d051aa9 100644 --- a/scripts/research/r0822_term_structure.py +++ b/scripts/research/r0822_term_structure.py @@ -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?") 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["hr"] = pd.to_datetime(cf["hr"], utc=True).dt.as_unit("ns") 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") 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") - print(f"\n Un calendar spread e' DUE gambe: a $635 una sola struttura ETH impegna il") - print(f" ~15-30% del conto e rende centesimi al giorno. BTC e' fuori dal lotto minimo.") - print(f" Il bersaglio dichiarato e' 50 EUR/giorno: qui si parla di due ordini di grandezza sotto.") + print("\n Un calendar spread e' DUE gambe. Cio' che la tabella mostra e' il PREMIO di UNA") + print(" gamba al lotto minimo: BTC 26% del conto, ETH 10%. Il premio non e' tutto il") + 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)) @@ -671,17 +679,31 @@ def sezione4(curve: pd.DataFrame) -> None: hold = tgt.shift(1).fillna(0.0) net = (hold * d["r"] - A.FEE_SIDE * hold.diff().abs().fillna(0.0)).dropna() 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 - for k in np.arange(0.05, 1.01, 0.05): + for k in ks[::-1]: b = k * d["r"].reindex(net.index) 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 - print(f" {a}: candidato Sharpe {A._sh(net):+.2f} maxDD {dd_c:.2%} | " - + (f"buy&hold de-leverato k={best[0]:.2f} -> stesso DD ({best[2]:.2%}) con Sharpe " - f"{best[1]:+.2f} => {'IL CANDIDATO NON SOPRAVVIVE' if best[1] >= A._sh(net) else 'candidato sopra il null'}" - if best else "nessun k<=1 raggiunge quel DD")) - TRIALS.append((f"Q3 {a}: 20 valori di k nel null de-levering", 20)) + sh_c = A._sh(net) + if best is None: + print(f" {a}: candidato Sharpe {sh_c:+.2f} maxDD {dd_c:.2%} | nessun k in " + f"[0.05,3.00] raggiunge quel DD") + 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: print(f" {k:20s} = {rep[k]}") 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)) 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(" 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(" La pendenza della term structure e', su questa lente, la stessa variabile con la") - print(" gamba realizzata sostituita da una implicita. Il filone non e' nuovo: e' quello.") + print(" earns_slot=False**. Qui, con la variante a SOGLIA BINARIA invece che a z-score,") + print(" il verdetto misurato e' NEUTRAL con uplift NEGATIVO — un verdetto diverso, la") + 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: hr("r0822_term_structure — TERM STRUCTURE DELLA VOL IMPLICITA BTC/ETH") - print(__doc__.split("METODO.")[0].strip()[:0] or "", end="") chain = load_quoted_chain() sezione0(chain) atm_exp = build_atm_by_expiry(chain) diff --git a/scripts/research/r0822_vol_size.py b/scripts/research/r0822_vol_size.py index 357274b..add3258 100644 --- a/scripts/research/r0822_vol_size.py +++ b/scripts/research/r0822_vol_size.py @@ -303,6 +303,20 @@ def sizes_from_L(ex: dict, L: pd.Series) -> np.ndarray: # --------------------------------------------------------------------------- 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: 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("-" * 112) 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"] RES = {} 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}" 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"{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(" 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.") @@ -687,24 +708,10 @@ def main() -> None: 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 or nm in ACT: - 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)) + Lb = (leverage_series(BK["BASE"][can], BVT[nm]["tv"], BVT[nm]["w"], BVT[nm]["cap"]) + if nm in BVT else None) + szs = np.concatenate([sizes_of(nm, EX[can][a], SZ, VTL, ACT, Lb) for a in ASSETS]) + dys = pd.DatetimeIndex(np.concatenate([EX[can][a]["day_ent"].values for a in ASSETS])) 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") @@ -715,6 +722,33 @@ def main() -> None: 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.") + # 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(f" fatto in {time.time()-t0:.0f}s") 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 pos = np.zeros(len(ts1)) L1h = np.ones(len(ts1)) - if nm is None: - 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 + if nm is not None and nm in BVT: # BOOKVT: leva anche sulla gamba TP01 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)) 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"]] for k in range(len(t_in)): j0 = int(np.searchsorted(ts1, t_in[k], side="left")) diff --git a/scripts/research/r0822_vrp_real_quotes.py b/scripts/research/r0822_vrp_real_quotes.py index f00bba1..dd18101 100644 --- a/scripts/research/r0822_vrp_real_quotes.py +++ b/scripts/research/r0822_vrp_real_quotes.py @@ -382,13 +382,19 @@ def main() -> None: thr = float(np.quantile(vals[:-1], 0.30)) last_open = ser[ser > 0.30] 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 — " + " ".join(f"{y}:{v:.0%}" for y, v in per_year.items())) 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"{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" - " 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.") 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"positiva in {band['frac_positive']:.0%} di {band['n_anchors']} ancore · " 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(" 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) rep = A.marginal_vs_tp01(dly) act = int((dly != 0).sum()) - print(f" verdetto={rep.get('verdict')} corr={rep.get('corr'):.3f} " - f"(barre ATTIVE {act}/{len(dly)} = {act / len(dly):.0%})") + def _f(k): + 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' " f"rumore, si riporta per non ometterlo.") except Exception as exc: # noqa: BLE001 @@ -719,36 +749,75 @@ def main() -> None: # ------------------------------------------------------------ §7 VERDETTO hr("§7 VERDETTO") - print(""" SCARTATO — su questi dati la domanda non e' decidibile, e i tre motivi sono - indipendenti l'uno dall'altro: + print(""" SCARTATO — su questi dati la domanda non e' decidibile. Quattro motivi, indipendenti + l'uno dall'altro: ciascuno da solo basterebbe. 1. IL CAMPIONE NON CONTIENE LA STRATEGIA. 0 settimane su 19 passano il gate IV-rank>0.30 - che E' l'alpha di VRP01. Il campione e' vol implicita al 24-28 percentile E un rialzo - forte del sottostante: il regime migliore possibile per vendere put, non uno neutro. - Le 10 settimane per asset (non 16: prima del 2026-06-09 la finestra 4-10 DTE non - esiste) danno 10/10 vincenti su BTC. La lente giusta non e' lo Sharpe, e' la regola - del tre: 0 perdite su 10 lascia un tasso vero fino al 30%, su un payoff che perde - ~7x il credito. + che E' l'alpha di VRP01 — il sleeve sarebbe stato FLAT per l'intero campione. La DVOL + mediana sta al 7 (BTC) / 11 (ETH) percentile della storia 2021+, e il sottostante e' + salito del +25% / +44%: vol implicita bassa E rialzo forte = il regime MIGLIORE + possibile per vendere put, non uno neutro. Le settimane sono 10 per asset, non 16 + (prima del 2026-06-09 l'archivio seguiva solo la finestra 13-20 DTE: quella 4-10 DTE + non esiste). Esito: 10/10 vincenti su BTC, e la lente giusta non e' lo Sharpe ma la + 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 - sono le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC. - La famiglia USDC-lineare esiste per BTC ed ETH ed ha un lotto minimo 10x piu' piccolo - (BTC 0.01 invece di 0.1) — quindi il muro «BTC fuori a $3.000» del 30/07, congelato - in un test, e' misurato sulla famiglia sbagliata. Ma di quella famiglia abbiamo ZERO - ore di storia e l'open interest e' ~0. + 2. IL TITOLO E' QUASI TUTTO FORTUNA D'ANCORA. L'ora d'ingresso e' un max-of-k mai + dichiarato, ed e' la prima volta che si misura su questa struttura: su 9 ancore lo + Sharpe canonico BTC 32.58 sta all'89 percentile e la MEDIANA ONESTA e' 1.45, con la + banda che tocca il NEGATIVO (-0.35). Fortuna +31.13. Su ETH 3.90 -> 2.41. + Il numero da citare per BTC e' 1.45, non 32.58 — e a quel punto non c'e' piu' niente + di straordinario da spiegare. - 3. LO SPREAD E' IL COSTO DOMINANTE E NON E' MODELLABILE DA UN MID. Attraversare il - bid/ask costa da solo una quota grossa del credito, e l'ala che si COMPRA e' la gamba - dove il libro e' piu' largo — lo stesso meccanismo che il 30/07 misurava come f=0.73. + 3. LA FAMIGLIA CHE HA STORIA NON E' QUELLA CHE SI PUO' ESEGUIRE. Le quote raccolte sono + le opzioni INVERSE (margine e regolamento in BTC/ETH); il conto e' in USDC e il book + 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 - in cui lavora. La regola «niente short-vol da MODELLO in deploy» NON diventa «short-vol da - QUOTE»: le quote ora ci sono, ma sono quote della famiglia sbagliata, in un regime solo. + 4. LO SPREAD E' IL COSTO DOMINANTE, E SULLA FAMIGLIA ESEGUIBILE E' PEGGIO. Attraversare + il bid/ask costa ~10% del credito sugli INVERSE; nella regione di strike che serve lo + spread relativo mediano e' 24-37% sugli inverse e 29-59% sulle USDC, con open interest + ~0. Il f del credito netto si replica indipendentemente a 0.714 (IC95 [0.690, 0.779], + 0/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): - far raccogliere a `collect_chain.py` anche `currency=USDC` (famiglie BTC_USDC/ETH_USDC). - Costa ~2 chiamate in piu' per giro; senza, fra sei mesi ci ritroveremo con un anno di - storia della famiglia che non possiamo tradare e zero di quella che potremmo.""") + CIO' CHE NON SI CONCLUDE: che la struttura non funzioni. Non e' stata osservata nel regime + in cui lavora. E un risultato a favore c'e', va detto: su BTC il null «e' solo crypto + travestito» e' REFUTATO (beta +0.019, t=0.18) — il PnL non viene dal rialzo. Non basta, + 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__":