From f975272ec8ce1da3a8810da6d6d5c522c32494c0 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sat, 22 Aug 2026 17:24:10 +0000 Subject: [PATCH] =?UTF-8?q?research(wave-0822):=20VOL-SIZE=20=E2=80=94=20i?= =?UTF-8?q?l=20p-value=20del=20null=20di=20permutazione=20si=20cita=20col?= =?UTF-8?q?=20segno,=20non=20col=20decimale=20(3=20ancore=20su=2023)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- scripts/research/r0822_basis_calendar.py | 37 +- scripts/research/r0822_hl_exec.py | 67 +++- scripts/research/r0822_skew.py | 37 ++ scripts/research/r0822_xsr_repro.py | 431 +++++++++++++++++++++++ 4 files changed, 550 insertions(+), 22 deletions(-) create mode 100644 scripts/research/r0822_xsr_repro.py diff --git a/scripts/research/r0822_basis_calendar.py b/scripts/research/r0822_basis_calendar.py index 6955f8a..a3138b2 100644 --- a/scripts/research/r0822_basis_calendar.py +++ b/scripts/research/r0822_basis_calendar.py @@ -318,6 +318,14 @@ def certify(asset: str, wide: pd.DataFrame, vwide: pd.DataFrame, meta: pd.DataFr print(f" convergenza a scadenza |ln(F/indice)| ultima ora: " f"mediana {np.median(conv):.1f} bps, max {np.max(conv):.1f} bps (n={len(conv)})") + # (c-bis) scarto PERP vs INDICE: la gamba FvP lo assume piccolo, quindi si misura + pj = perp.reindex(fund.set_index("ts").index).dropna() + if len(pj) > 1000: + ii = fund.set_index("ts")["index"].reindex(pj.index) + pb = (np.log(pj / ii) * 1e4).replace([np.inf, -np.inf], np.nan).dropna() + print(f" perp vs indice: mediana {pb.median():+.1f} bps, p95 |scarto| " + f"{pb.abs().quantile(0.95):.1f} bps (n={len(pb):,}) — la gamba FvP e' sul PERP") + # (d) CROSS-CHECK INDIPENDENTE: il forward implicito nella catena opzioni xchk = cross_check_option_forward(asset, wide, ok) return dict(flat=fb, vol=vb, conv=conv, xchk=xchk) @@ -370,7 +378,8 @@ def cross_check_option_forward(asset: str, wide: pd.DataFrame, meta: pd.DataFram # 2. CURVA — front/back trimestrali, basis annualizzato, slope forward # ========================================================================== def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame, - fund: pd.DataFrame, roll_dte: int) -> pd.DataFrame: + fund: pd.DataFrame, roll_dte: int, + perp: pd.Series | None = None) -> pd.DataFrame: """Per ogni ora: front = trimestrale piu' vicino con dte >= roll_dte, back = il successivo. Ritorna prezzi, tau, basis annualizzati, slope forward e funding.""" ok = meta[meta.status == "ok"].sort_values("exp") @@ -408,6 +417,15 @@ def curve_frame(asset: str, wide: pd.DataFrame, meta: pd.DataFrame, out["tau_front_h"], out["tau_back_h"] = tf, tb out["index"] = F["index"].values out["f1h"] = F["f1h"].values + # gamba perp: il PERP CERTIFICATO, non l'indice (l'indice non si compra). + # fallback all'indice solo se il perp non copre l'ora, e lo si dichiara. + if perp is not None: + pp = perp.reindex(idx) + out["perp"] = pp.where(pp.notna(), out["index"]).values + out["perp_from_index"] = pp.isna().values + else: + out["perp"] = out["index"].values + out["perp_from_index"] = True # basis annualizzato di ciascuna gamba vs INDICE out["c_front"] = np.log(Ff / out["index"]) * HOURS_Y / tf out["c_back"] = np.log(Fb / out["index"]) * HOURS_Y / tb @@ -532,7 +550,7 @@ def run_strategy(cv: pd.DataFrame, ctx: dict, family: str, sig: str, turn = np.abs(dP).sum(axis=1) if family == "FvP": - perp_r = np.nan_to_num(cv["index"].pct_change().values, nan=0.0) + perp_r = np.nan_to_num(cv["perp"].pct_change().values, nan=0.0) f1h = np.nan_to_num(cv["f1h"].values, nan=0.0) wheld = held.sum(axis=1) # una sola gamba datata # long datato => short perp: guadagna -w*perp_r e INCASSA w*funding @@ -609,11 +627,14 @@ def main() -> None: # ---------------- STEP 2: certificazione ---------------- print("\n[2] CERTIFICAZIONE (regola 4: un edge su un book fermo non e' un edge)") - certs = {} + certs, perps = {}, {} for a in ASSETS: - perp = A.get(a, "1h").set_index(pd.DatetimeIndex( - pd.to_datetime(A.get(a, "1h")["datetime"], utc=True)))["close"] - certs[a] = certify(a, panels[a], vwides[a], metas[a], funds[a], perp) + pdf = A.get(a, "1h") + perps[a] = pd.Series( + pdf["close"].astype(float).values, + index=pd.DatetimeIndex(pd.to_datetime(pdf["datetime"], utc=True))).sort_index() + perps[a] = perps[a][~perps[a].index.duplicated()] + certs[a] = certify(a, panels[a], vwides[a], metas[a], funds[a], perps[a]) x = certs[a]["xchk"] if x.get("status") == "ok" and len(x["table"]): t = x["table"] @@ -628,7 +649,7 @@ def main() -> None: print("\n[3] Q1 — LA CURVA E' PREVEDIBILE?") curves = {} for a in ASSETS: - cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7) + cv = curve_frame(a, panels[a], metas[a], funds[a], roll_dte=7, perp=perps[a]) curves[a] = cv describe_curve(a, cv) @@ -665,7 +686,7 @@ def main() -> None: f" le conto lo stesso, al rialzo)") ctxs = {a: asset_ctx(panels[a]) for a in ASSETS} - cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl) + cvcache = {(a, rl): curve_frame(a, panels[a], metas[a], funds[a], rl, perp=perps[a]) for a in ASSETS for rl in ROLLS} pidx = {(a, rl): pair_idx(cvcache[(a, rl)], ctxs[a]) for a in ASSETS for rl in ROLLS} diff --git a/scripts/research/r0822_hl_exec.py b/scripts/research/r0822_hl_exec.py index 7ab27f1..28a2587 100644 --- a/scripts/research/r0822_hl_exec.py +++ b/scripts/research/r0822_hl_exec.py @@ -466,35 +466,54 @@ def main() -> None: print(f"\n[3] XS01 — soglia PUBBLICATA ~$20.000 (origine: stima a occhio, 'rumore arrotondamento')") Wxs, dret, C, cols, dif = xs01_pesi() print(f" prova d'identita' col sleeve ufficiale `_xsec_returns()`: max|diff| = {dif:.3e}") + from src.portfolio.sleeves import XS_CFG, _xsec_returns + off_xs = _xsec_returns() + m_off = metriche(off_xs.values, pd.DatetimeIndex(off_xs.index)) gross_xs = np.abs(Wxs).sum(axis=1) att = gross_xs > 1e-9 dW = np.abs(np.diff(Wxs, axis=0, prepend=np.zeros((1, Wxs.shape[1])))) tk_nz = dW[dW > 1e-9] + # DUE popolazioni di ordini, e vanno separate: il RIBILANCIAMENTO del segnale (ogni H=10 giorni, + # cambia il paniere 5+5) e la DERIVA del vol-target (ogni giorno, micro-aggiustamenti). + H = XS_CFG["H"] + is_reb = np.zeros(len(Wxs), bool); is_reb[::H] = True + reb = dW[is_reb]; reb = reb[reb > 1e-9] + drift = dW[~is_reb]; drift = drift[drift > 1e-9] + print(f" sleeve ufficiale `_xsec_returns()`: Sharpe {m_off['sharpe']:.2f} " + f"maxDD {m_off['maxdd']:.1f}% CAGR {m_off['cagr']:.1f}%") print(f" gambe simultanee: {int((np.abs(Wxs) > 1e-9).sum(axis=1)[att].max())} " f"(k=5 long + 5 short); lordo mediano {np.median(gross_xs[att]):.2f}x il capitale " f"(vol-target, cap 3x)") - print(f" peso mediano mosso per gamba per ribilanciamento: {np.median(tk_nz)*100:.2f}% " - f"del capitale; 10° pctl {np.percentile(tk_nz,10)*100:.2f}%") - print(f"\n ticket per gamba (mediano) e % di gambe ESEGUIBILI, per capitale allocato a XS01:") - print(f" {'cap XS01':>10}{'ticket med':>12}{'ticket p10':>12}" - f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'lotto ko':>10}") + print(f"\n DUE popolazioni di ordini (separarle e' il punto: non hanno la stessa taglia):") + print(f" RIBILANCIAMENTO (ogni {H}g, cambia il paniere): n={len(reb)} " + f"peso mosso mediano {np.median(reb)*100:.2f}% p10 {np.percentile(reb,10)*100:.2f}%") + print(f" DERIVA vol-target (giornaliera) : n={len(drift)} " + f"peso mosso mediano {np.median(drift)*100:.3f}% p10 {np.percentile(drift,10)*100:.3f}%") + print(f" -> {100*len(drift)/max(len(reb)+len(drift),1):.0f}% degli ordini sono DERIVA, " + f"ed e' la deriva che il pavimento taglia.") + print(f"\n ticket per gamba e % di ordini RICHIESTI eseguibili, per capitale allocato a XS01") + print(f" (conto = capitale/0.15, perche' XS01 pesa 15% del book di ricerca):") + print(f" {'cap XS01':>10}{'conto @15%':>12}{'tick reb':>10}{'tick drift':>12}" + f"{'>=$5 [D]':>10}{'>=$10 [C]':>11}{'reb>=$10':>10}") R_xs = dret idx = C.index base = None for cap in CAPITALI: - med = np.median(tk_nz) * cap - p10 = np.percentile(tk_nz, 10) * cap pct5 = 100.0 * float((tk_nz * cap >= MIN_ORDER_ASSUNTO).mean()) pct10 = 100.0 * float((tk_nz * cap >= MIN_ORDER_HL).mean()) - lot = np.array([meta.loc[s, "lot_usd"] if s in meta.index else 0.0 for s in cols]) - lotko = 100.0 * float((tk_nz * cap < np.median(lot)).mean()) - print(f" {cap:>10,.0f}{med:>12.2f}{p10:>12.2f}{pct5:>9.0f}%{pct10:>10.0f}%{lotko:>9.1f}%") + pr10 = 100.0 * float((reb * cap >= MIN_ORDER_HL).mean()) + print(f" {cap:>10,.0f}{cap/0.15:>12,.0f}{np.median(reb)*cap:>10.2f}" + f"{np.median(drift)*cap:>12.2f}{pct5:>9.0f}%{pct10:>10.0f}%{pr10:>9.0f}%") lot_xs = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in cols]) px_xs = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in cols]) fee_xs_mod = 0.001 / 2.0 # [D] cio' che il sleeve modella: 5 bps/lato - print(f"\n haircut di eseguibilita' (Sharpe modellato - Sharpe realistico), fee {fee_xs_mod*1e4:.1f} bps/lato:") + print(f"\n haircut di eseguibilita' (Sharpe modellato - Sharpe realistico), fee {fee_xs_mod*1e4:.1f} bps/lato.") + print(f" NB il 'Sh mod' qui INCLUDE la fee sulla deriva del vol-target, che lo sleeve " + f"ufficiale NON addebita") + print(f" (`turn=|dW|` prima dello scaling): e' il motivo per cui sta sotto il " + f"{m_off['sharpe']:.2f} pubblicato.") print(f" {'cap XS01':>10}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" - f"{'haircut $10':>13}{'esegui%':>9}") + f"{'haircut $10':>13}{'ordini fatti':>14}") for cap in CAPITALI: m0 = sim_libro(Wxs, R_xs, None, cap, None, None, None, fee_xs_mod) m5 = sim_libro(Wxs, R_xs, None, cap, MIN_ORDER_ASSUNTO, None, None, fee_xs_mod) @@ -506,9 +525,29 @@ def main() -> None: # ---------------------------------------------------------------- 4. XSR01 print(f"\n[4] XSR01 — soglia PUBBLICATA ~$5.000, gate 2026-10-23 con haircut a $5.000 <= 40%") - from scripts.live.paper_xsr import build_panel # stessa costruzione del monitor in produzione + from scripts.live.paper_xsr import FEE_LEG, MIN_ORDER, _book, _step, build_panel ts, dtx, Wx, Rx, rbx, sx = build_panel() idxx = pd.DatetimeIndex(dtx) + + # PROVA D'IDENTITA' col monitor IN PRODUZIONE: sim_libro deve riprodurre paper_xsr._step + # barra per barra. Senza questa, i numeri qui sotto sarebbero di un'altra macchina. + for mo in (None, MIN_ORDER): + bk = _book(5000.0, len(sx)) + ref = np.array([_step(bk, Wx[i], Rx[i], float(rbx[i]), mo) for i in range(len(Wx))]) + mine = sim_libro(Wx, Rx, rbx, 5000.0, mo, None, None, FEE_LEG)["net"] + print(f" identita' vs scripts/live/paper_xsr._step (min_order={mo}): " + f"max|diff| = {np.abs(ref - mine).max():.3e}") + assert abs(FEE_LEG - FEE_LEG_MODELLO) < 1e-12, "la config congelata di XSR01 e' cambiata" + assert abs(MIN_ORDER - MIN_ORDER_ASSUNTO) < 1e-12, "il min-order del monitor e' cambiato" + + # RICONCILIAZIONE col numero PUBBLICATO (1.82): la scoperta (r0725_statarb_basket_gate, + # basket_from_positions) addebita "2 gambe per coppia" = 50 alt + 50 BTC. Ma il demeaning + # ANNULLA la gamba BTC (sum dei pesi = 0): non si tradano 50 BTC, se ne trada UNA di taglia + # zero. Il monitor in produzione lo sa (`d_btc = |sum(w_new) - sum(w_held)|` ~ 0) -> il + # pubblicato SOVRASTIMA le fee di ~2x. + net_x2 = sim_libro(Wx, Rx, rbx, 5000.0, None, None, None, 2 * FEE_LEG_MODELLO)["net"] + print(f" riconciliazione col pubblicato 1.82: a fee RADDOPPIATA (= convenzione della " + f"scoperta, 2 gambe/coppia) Sharpe {metriche(net_x2, idxx)['sharpe']:.2f}") lot_x = np.array([float(meta.loc[s, "lot_usd"]) if s in meta.index else 0.0 for s in sx]) px_x = np.array([float(meta.loc[s, "markPx"]) if s in meta.index else np.nan for s in sx]) dWx = np.abs(np.diff(Wx, axis=0, prepend=np.zeros((1, Wx.shape[1])))) @@ -516,7 +555,7 @@ def main() -> None: print(f" gambe {len(sx)} lordo mediano {np.median(np.abs(Wx).sum(axis=1)):.2f}x " f"peso mosso mediano/gamba {np.median(tkx)*100:.3f}% p10 {np.percentile(tkx,10)*100:.3f}%") print(f"\n {'cap':>9}{'ticket med':>12}{'Sh mod':>9}{'Sh $5 [D]':>11}{'Sh $10 [C]':>12}" - f"{'haircut $5':>12}{'haircut $10':>13}{'esegui%':>9}") + f"{'haircut $5':>12}{'haircut $10':>13}{'ordini fatti':>14}") hc10_5000 = np.nan for cap in CAPITALI: m0 = sim_libro(Wx, Rx, rbx, cap, None, None, None, FEE_LEG_MODELLO) diff --git a/scripts/research/r0822_skew.py b/scripts/research/r0822_skew.py index 48648fb..37b2dec 100644 --- a/scripts/research/r0822_skew.py +++ b/scripts/research/r0822_skew.py @@ -643,6 +643,12 @@ def main() -> None: except Exception as ex: print(f" non girato: {ex}") + imp = A.implausible_sharpe(bs) + print(f"\n implausible_sharpe sulla cella migliore: implausible={imp['implausible']} " + f"reasons={imp.get('reasons')}") + print(f" (Sharpe {imp.get('sharpe')}, maxDD {imp.get('maxdd')}, " + f"perdite sulle barre ATTIVE {imp.get('loss_frac')}, attive {imp.get('active_frac')})") + print("\n causalita' (tagli DENTRO la finestra dello skew, non all'80% della storia prezzi):") for a in ASSETS: f_, w_, sg_ = best.split("|") @@ -798,6 +804,37 @@ def main() -> None: P[c] = P[c].round(3) print(P.to_string(index=False)) + # ------------------------------------------------------------ 5. SOMMARIO + rule("5. SOMMARIO — cosa regge, cosa no, e cosa non e' misurabile con 75 giorni") + print(""" + Q1 lo skew NON anticipa lo spot: e' lo spot che muove lo skew. + Il prezzo precede il RR con t da 3.1 a 9.4 su 8 test su 8 e su entrambi gli asset; + in avanti il |t| massimo su 16 test e' 2.35 contro un massimo atteso dal rumore di 2.08, + non replica sul secondo asset, e l'unica cella apparente perde i due terzi togliendo + 4 giorni di rally su 24. La strategia giornaliera migliore su 18 celle fa Sharpe +2.61 + con IC95% [-2.90, +5.86] e DSR 0.163 -> SCARTATO. + + Q2 gate di rischio: NON MISURABILE su questa finestra, e la ragione e' strutturale. + Nei 75 giorni TP01 e' attivo 5 giorni su 75 e il suo maxDD e' 0.14%: non c'e' sinistro + da prevenire. Cio' che si vede: alle soglie z<-1.0 e z<-1.5 il gate spegne giorni in cui + TP01 era GIA' flat nel 100% dei casi -> 5a conferma della ridondanza col trend, ma su un + campione senza potenza. Il null de-levering gira e dice REFUTED per assenza di DD, non + per merito del baseline. + + Q3 il risultato della sessione, e non e' una strategia: e' una CORREZIONE DI PREZZATURA. + Il f=0.73 di VRP01 e' replicato in modo indipendente (0.714 su 22 ingressi, 0.723 su 2.582 + ore) e poi SCOMPOSTO. La causa maggiore NON e' lo skew: + struttura a termine 42% | spread denaro-lettera 30% | skew 25% | fit 2% + Il sleeve prezza un'opzione a 7 giorni con il DVOL a 30 giorni, che nel 90% delle ore sta + ~3 punti di vol SOPRA l'ATM a 7 giorni -> sovrapprezza entrambe le gambe. Il "+0.8pp sulla + gamba corta" pubblicato il 30/07 (qui +0.20pp) e' la CANCELLAZIONE di due errori da ~3pp: + -2.97pp di termine e +2.79pp di skew. Guardando le gambe contro l'ATM a 7 giorni si vede + che il modello sbaglia su ENTRAMBE, non solo sull'ala. + E il pezzo grande CAMBIA SEGNO col regime: f_term 0.83 nel quartile basso di IV-rank -> + 0.93 nel quartile alto, e 1.05 nelle ore in backwardation, mentre f_skew resta piatto + (0.93 -> 0.92). Cioe' l'errore piu' grande si annulla proprio dove il gate IV-rank fa + tradare il sleeve. +""") rule(f"fine — {time.time()-t_start:.0f}s") diff --git a/scripts/research/r0822_xsr_repro.py b/scripts/research/r0822_xsr_repro.py new file mode 100644 index 0000000..c387ae0 --- /dev/null +++ b/scripts/research/r0822_xsr_repro.py @@ -0,0 +1,431 @@ +"""XSR-REPRO — lo Sharpe 1.82 di XSR01 non si riproduce: e' un problema di CODICE, di DATI, o di LENTE? + +PERCHE'. XSR01 ha un gate di deploy PRE-REGISTRATO al 2026-10-23 (`r0725_xsr_deploy_gate.py`): +deploy solo se Sharpe forward >= 1.0 E haircut di eseguibilita' a $5.000 <= 40%. I suoi numeri di +ammissione — Sharpe netta 1.82, lorda 2.70, maxDD -2.6%, vol 2.3%, ret 4.2%/a, lag 1.82/1.19/0.81/ +0.51, Sharpe per anno 1.03/1.98/3.11 — sono in CLAUDE.md, nel diario del 25/07 e CABLATI nel gate +(`IS_SHARPE_NET = 1.82`). Il filone XS-LITE (22/08) ha misurato che sulla finestra identica a quella +di scoperta oggi escono 1.75 (lente "paniere") e 2.23 (lente "libro"): 1.82 non e' nessuna delle due. +Una decisione fra due mesi poggia su un numero che non si riproduce. + +Questo script NON giudica XSR01 e NON anticipa il gate del 23/10 (sarebbe selection-on-forward). +Stabilisce SOLO se il numero su cui si decidera' e' solido, in quest'ordine: + + T1 QUALE LENTE. Tre implementazioni convivono nel repo e danno numeri diversi sugli STESSI dati: + L1 `r0725_statarb_basket_gate.basket_from_positions(demean=True)` — media per riga sulle sole + colonne NON-NaN (divisore variabile). E' la lente che ha girato i gate (marginale, DSR). + L2 `r0725_statarb_demean_skeptic.ret_from_pos` — P.fillna(0) e divisore FISSO 50. + L3 `scripts/live/paper_xsr.build_panel` + `_step` — libro con capitale, pesi Q/50, gamba BTC + esplicita. E' quella che alimenta il gate. + T2 QUALE STATO DEL DATO. `data/raw` e' gitignored e il cron riscrive i parquet HL ogni notte. + Ipotesi da falsificare: "stesso codice, dati diversi" (identico allo scoperto GTAA/TLT 07/08). + Prove indipendenti: (a) il log del cron registra `reali`/`bfill`/`start_reale` per simbolo a + ogni giro -> se il conteggio delle barre PASSATE e' cambiato, il feed e' stato riscritto; + (b) lo Sharpe per ANNO CHIUSO (2024, 2025) e' un'impronta della storia: se la storia e' + stabile deve riprodursi al centesimo attraverso 4 settimane di riscritture. + T3 IL MONITOR FORWARD. `paper_xsr` e' forward-only e append-only, quindi in teoria immune alla + riscrittura. Test diretto: replay del libro MODELED sui dati di OGGI dall'inception, barra per + barra, contro cio' che il monitor ha REGISTRATO giorno per giorno. Se la storia fosse stabile + e la lente giusta, le due serie devono coincidere. + T4 L'HAIRCUT A $5.000, l'altra meta' del gate: dipende dal min-order del venue. Il numero + pubblicato ($14.41 di ticket, soglia 40%) assume min-order $5 (Deribit). Hyperliquid potrebbe + essere $10. Si misura la sensibilita'. + +COSA MI ASPETTAVO PRIMA DI MISURARE: che fosse la lente (tre implementazioni, nessuna nominata nel +diario) e che il dato fosse stabile, perche' il log del cron mostra bar-count che crescono di 1/g. +Solo la prima meta' e' risultata vera. + + nice -n 19 timeout 900 uv run python scripts/research/r0822_xsr_repro.py +""" +from __future__ import annotations + +import json +import re +import sys +from collections import OrderedDict +from pathlib import Path + +import numpy as np +import pandas as pd + +ROOT = Path(__file__).resolve().parents[2] +RAW = ROOT / "data" / "raw" +for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt"): + sys.path.insert(0, str(_p)) + +from r0725_statarb_multi import BASE, MIN_BARS, _dd, _sh, load_hl, signal, universe # noqa: E402 +from r0725_statarb_basket_gate import basket_from_positions, pair_frames # noqa: E402 +from r0725_statarb_demean_skeptic import ret_from_pos # noqa: E402 + +ANN = np.sqrt(365.0) +DISCOVERY = pd.Timestamp("2026-07-25", tz="UTC") # ultima barra vista dalla scoperta +CRONLOG = ROOT / "logs" / "cron_daily.log" +FWD = ROOT / "data" / "paper_xsr" / "returns.jsonl" +FWD_STATE = ROOT / "data" / "paper_xsr" / "state.json" + +# I NUMERI PUBBLICATI (CLAUDE.md + diario 2026-07-25 + docstring di paper_xsr / del gate) +PUB = dict(netta=1.82, lorda=2.70, dd=-2.6, vol=2.3, ret=4.2, + lag=(1.82, 1.19, 0.81, 0.51), anni=(1.03, 1.98, 3.11)) + + +def _cut(df, ts): + return df[df.index <= ts] + + +def _stats(r: pd.Series) -> dict: + return dict(n=len(r), sh=_sh(r.values), dd=_dd(r.values) * 100, + vol=float(r.std() * ANN * 100), ret=float(r.mean() * 365 * 100)) + + +# ------------------------------------------------------------------ pannelli +def panel(partial_last: bool = False, cut: pd.Timestamp | None = None): + """(P, S) come `pair_frames`, ma con due gradi di liberta' espliciti: + `cut` -> ultima barra inclusa (troncamento dell'INPUT, non dell'output); + `partial_last` -> l'ultima barra e' resa PARZIALE (close := open), cioe' com'era il giorno in + cui il cron l'aveva appena scritta. E' l'unico stato del dato che questo + script NON puo' leggere da disco: va ricostruito.""" + def px(sym): + d = pd.read_parquet(RAW / f"hl_{sym.lower()}_1d.parquet") + ix = pd.to_datetime(d["timestamp"], unit="ms", utc=True) + c = pd.Series(d["close"].astype(float).values, index=ix).sort_index() + o = pd.Series(d["open"].astype(float).values, index=ix).sort_index() + c = c[~c.index.duplicated(keep="last")] + o = o[~o.index.duplicated(keep="last")] + if cut is not None: + c, o = c[c.index <= cut], o[o.index <= cut] + if partial_last and len(c): + c.iloc[-1] = o.iloc[-1] + return c + + base = px(BASE) + pc, sc = {}, {} + for sym in universe(): + try: + tgt = px(sym) + except FileNotFoundError: + continue + ix = base.index.intersection(tgt.index) + if len(ix) < MIN_BARS: + continue + p, s = signal(base[ix], tgt[ix]) + pc[sym] = pd.Series(p, index=ix) + sc[sym] = pd.Series(s, index=ix) + P = pd.concat(pc, axis=1, sort=True).sort_index() + S = pd.concat(sc, axis=1, sort=True).sort_index() + return P, S + + +def lens_L1(P, S): + return basket_from_positions(P, S, demean=True) + + +def lens_L2(P, S, fee_leg=0.0005, lag=0): + P0 = P.fillna(0.0) + S0 = S.reindex(P0.index)[P0.columns] + return ret_from_pos(P0, S0, fee_leg=fee_leg, lag=lag) + + +# ------------------------------------------------------------------ T1 +def t1_lenti(): + print("=" * 100) + print(" T1 — QUALE LENTE, E QUALE STATO DEL DATO, PRODUCE 1.82") + print("=" * 100) + P, S = pair_frames() + print(f"\n dato su disco oggi: {P.shape[1]} coppie, {len(P)} barre " + f"({P.index[0].date()} -> {P.index[-1].date()})") + + rows = [] + for tag, (Pi, Si) in [ + ("OGGI pieno", (P, S)), + (f"tronc. {DISCOVERY.date()} (barra completa)", (_cut(P, DISCOVERY), _cut(S, DISCOVERY))), + ]: + rows.append((f"L1 gate {tag}", _stats(lens_L1(Pi, Si)))) + rows.append((f"L2 scett. {tag}", _stats(lens_L2(Pi, Si)))) + + Pp, Sp = panel(partial_last=True, cut=DISCOVERY) + rows.append(("L1 gate STATO DEL 25/07 (ultima barra PARZIALE)", _stats(lens_L1(Pp, Sp)))) + rows.append(("L2 scett. STATO DEL 25/07 (ultima barra PARZIALE)", _stats(lens_L2(Pp, Sp)))) + + print(f"\n {'lente / stato del dato':<52}{'n':>6}{'Sharpe':>9}{'maxDD':>9}{'vol':>8}{'ret/a':>8}") + for nm, s in rows: + print(f" {nm:<52}{s['n']:>6}{s['sh']:>9.4f}{s['dd']:>8.2f}%{s['vol']:>7.2f}%{s['ret']:>7.2f}%") + print(f" {'--- PUBBLICATO 2026-07-25':<52}{'937?':>6}{PUB['netta']:>9.2f}" + f"{PUB['dd']:>8.1f}%{PUB['vol']:>7.1f}%{PUB['ret']:>7.1f}%") + + # confronto completo sugli OTTO numeri pubblicati, per la sola combinazione che li riproduce + print("\n --- verifica completa sullo stato che riproduce: L2 + ultima barra PARZIALE ---") + r = lens_L2(Pp, Sp) + g = lens_L2(Pp, Sp, fee_leg=0.0) + lags = tuple(_sh(lens_L2(Pp, Sp, lag=L).values) for L in (0, 1, 2, 3)) + anni = tuple(_sh(r[r.index.year == y].values) for y in (2024, 2025, 2026)) + st = _stats(r) + ok = 0 + def chk(nome, mio, pub, tol): + nonlocal ok + good = abs(mio - pub) <= tol + ok += good + print(f" {nome:<22} misurato {mio:>8.4f} pubblicato {pub:>7.2f} " + f"{'COINCIDE' if good else 'DIVERGE'}") + chk("Sharpe netta", st["sh"], PUB["netta"], 0.005) + chk("Sharpe lorda", _sh(g.values), PUB["lorda"], 0.005) + chk("maxDD %", st["dd"], PUB["dd"], 0.05) + chk("vol ann %", st["vol"], PUB["vol"], 0.05) + chk("ret ann %", st["ret"], PUB["ret"], 0.05) + for i, L in enumerate((0, 1, 2, 3)): + chk(f"lag +{L}g", lags[i], PUB["lag"][i], 0.006) + for i, y in enumerate((2024, 2025, 2026)): + chk(f"Sharpe {y}", anni[i], PUB["anni"][i], 0.005) + print(f"\n -> {ok}/12 numeri pubblicati riprodotti da L2 sullo stato del dato del 25/07.") + + # la lente che ha girato i GATE non e' quella del titolo + print("\n --- e la lente che ha girato i gate (marginale, deflated-Sharpe) e' L1, non L2 ---") + print(f" L1 sullo stato del 25/07 : Sharpe {_sh(lens_L1(Pp, Sp).values):.4f}") + print(" docstring di r0725_statarb_demean_skeptic.py, riga 5, cita il gate come " + "\"Sharpe 1.79\"") + print(" -> il numero di TITOLO (1.82, L2) e i numeri di GATE (DSR/marginale, L1) non sono") + print(" mai stati la stessa serie.") + + # deflated-Sharpe delle 3 varianti pre-registrate, oggi, sulle due lenti + try: + from altlib import deflated_sharpe + from src.portfolio.sleeves import XS_UNIVERSE + maj = [s for s in XS_UNIVERSE if s != BASE] + Pc, Sc = _cut(P, DISCOVERY), _cut(S, DISCOVERY) + var = {"V1 ALL50": dict(cols=None, demean=False), + "V2 MAJ19": dict(cols=maj, demean=False), + "V3 DEMEAN": dict(cols=None, demean=True)} + ser = {k: basket_from_positions(Pc, Sc, **v) for k, v in var.items()} + srs = [_sh(v.values) for v in ser.values()] + print("\n --- deflated-Sharpe delle 3 varianti pre-registrate, L1, finestra 25/07 ---") + for k, v in ser.items(): + d, nm = deflated_sharpe(_sh(v.values), srs, v.values, dpy=365.0) + print(f" {k:<11} Sharpe {_sh(v.values):>5.2f} DSR {d:>6.3f} " + f"(max atteso dal null {nm:>5.2f}) {'PASS' if d >= 0.95 else 'SOTTO 0.95'}") + print(" (pubblicato per V3: DSR 0.985)") + except Exception as e: # pragma: no cover + print(f" [DSR non calcolabile: {e.__class__.__name__}: {e}]") + return P, S + + +# ------------------------------------------------------------------ T2 +def t2_feed(P, S): + print("\n" + "=" * 100) + print(" T2 — IL FEED HL E' STATO RISCRITTO? (l'ipotesi principale: stesso codice, dati diversi)") + print("=" * 100) + + # (a) il log del cron registra reali/bfill/start_reale a ogni giro: e' un registro storico + runs, cur, inblk, hdr = OrderedDict(), None, False, None + for ln in CRONLOG.read_text(errors="replace").splitlines(): + m = re.match(r"^===== (\S+) cron_daily =====", ln) + if m: + cur, inblk = m.group(1), False + runs[cur] = {} + continue + if "FETCH + CERTIFY Hyperliquid" in ln: + inblk, hdr = True, None + continue + if inblk and cur is not None: + if ln.strip().startswith("sym"): + hdr = ln.split() + continue + f = ln.split() + if hdr and len(f) >= 7 and re.match(r"^[A-Z0-9]+$", f[0]) and f[1].isdigit(): + runs[cur][f[0]] = ((int(f[1]), f[3], f[-1]) if len(hdr) >= 9 + else (int(f[1]), f[2], f[-1])) + elif runs[cur] and (not ln.strip() or ln.startswith("=")): + inblk = False + keys = [k for k, v in runs.items() if v] + a = next((k for k in keys if k.startswith("2026-07-25")), None) + b = keys[-1] if keys else None + print(f"\n (a) registro del cron: {len(keys)} giri con blocco Hyperliquid, " + f"da {keys[0][:10]} a {keys[-1][:10]}") + if a and b: + A, B = runs[a], runs[b] + atteso = (pd.Timestamp(b[:10]) - pd.Timestamp(a[:10])).days + anom = [] + for s in sorted(set(A) | set(B)): + ra, rb = A.get(s), B.get(s) + if ra is None or rb is None: + anom.append((s, "presente in un solo giro", ra, rb)); continue + if rb[0] - ra[0] != atteso or ra[1] != rb[1]: + anom.append((s, f"delta {rb[0]-ra[0]} invece di {atteso}; start {ra[1]}->{rb[1]}", + ra, rb)) + print(f" confronto {a[:10]} vs {b[:10]} su {len(set(A)|set(B))} simboli, " + f"crescita attesa {atteso} barre") + if not anom: + print(" -> TUTTI i simboli crescono di esattamente il numero di giorni e " + "start_reale INVARIATO") + for s, why, ra, rb in anom: + print(f" {s:<8} {why} (verdetto oggi: {rb[2] if rb else '-'})") + print(" NB: i simboli con delta 0 sono quelli con verdetto `scarta` (serie ferma sul") + print(" venue): NON producono un parquet, quindi non entrano nell'universo.") + + # (b) impronta indipendente della storia: lo Sharpe degli ANNI CHIUSI + Pp, Sp = panel(partial_last=True, cut=DISCOVERY) + r_then = lens_L2(Pp, Sp) + r_now = lens_L2(P, S) + print("\n (b) impronta della storia — Sharpe degli anni CHIUSI, che una riscrittura sposterebbe:") + print(f" {'anno':<8}{'stato 25/07':>14}{'stato oggi':>13}{'pubblicato':>13}{'barre':>8}") + for y, pub in zip((2024, 2025, 2026), PUB["anni"]): + s1 = r_then[r_then.index.year == y] + s2 = r_now[r_now.index.year == y] + note = " <- anno IN CORSO: cambia perche' contiene barre nuove" if y == 2026 else "" + print(f" {y:<8}{_sh(s1.values):>14.4f}{_sh(s2.values):>13.4f}{pub:>13.2f}" + f"{len(s2):>8}{note}") + print(" -> 2024 e 2025 (731 barre) si riproducono al centesimo attraverso 4 settimane di") + print(" riscritture notturne: la STORIA CHIUSA e' stabile.") + + # (c) l'unica barra che cambia: l'ultima + d = pd.read_parquet(RAW / "hl_btc_1d.parquet") + vol = d["volume"].astype(float).values + q = float(vol[-1] / np.median(vol[-40:-1])) + print("\n (c) l'unica barra che NON e' definitiva e' l'ULTIMA. `fetch_hyperliquid` gira nel") + print(" cron delle 00:30 con END = oggi, quindi scrive la barra del giorno IN CORSO:") + print(f" hl_btc_1d ultima barra {pd.Timestamp(int(d['timestamp'].iloc[-1]), unit='ms', tz='UTC')}" + f" volume {vol[-1]:,.0f} = {q*100:.1f}% del volume mediano di un giorno pieno") + print(f" (una barra piena di ~24h; questa ne ha ~{q*24*60:.0f} minuti di scambi)") + print("\n VERDETTO T2: il feed VIENE riscritto, ma NON come ipotizzato. Non c'e' riscrittura") + print(" arbitraria della storia: c'e' UNA barra provvisoria per volta — l'ultima — che il giro") + print(" successivo completa. Ogni numero calcolato su dato HL il giorno D contiene una barra") + print(" di ~30-40 minuti travestita da giorno.") + + +# ------------------------------------------------------------------ T3 +def t3_monitor(): + print("\n" + "=" * 100) + print(" T3 — IL MONITOR FORWARD (che alimenta il gate del 23/10) REGISTRA CIO' CHE CREDE?") + print("=" * 100) + if not FWD.exists() or not FWD_STATE.exists(): + print(" [stato di paper_xsr assente: non misurabile]") + return + import importlib.util + spec = importlib.util.spec_from_file_location("paper_xsr_ro", ROOT / "scripts" / "live" / "paper_xsr.py") + px = importlib.util.module_from_spec(spec) + spec.loader.exec_module(px) # sola lettura: non si chiama main() + + rec = [json.loads(x) for x in FWD.read_text().splitlines() if x.strip()] + st = json.loads(FWD_STATE.read_text()) + ts, dt, W, R, rb, syms = px.build_panel() + print(f"\n inception {pd.Timestamp(st['start_ts'], unit='ms', tz='UTC').date()} " + f"barre registrate {len(rec)} universo {len(syms)} " + f"(uguale a quello congelato: {syms == st['syms']})") + + i0 = int(np.where(ts == st["start_ts"])[0][0]) + bk = px._book(px.MODELED_CAPITAL, len(syms)) + ric = {} + for i in range(i0 + 1, len(ts)): + ric[int(ts[i])] = px._step(bk, W[i], R[i], float(rb[i]), None) + + a = np.array([r["net_modeled"] for r in rec if r["ts"] in ric]) + b = np.array([ric[r["ts"]] for r in rec if r["ts"] in ric]) + same = int((np.abs(a - b) <= 1e-6).sum()) + print(f"\n replay del libro MODELED sui dati di OGGI, dall'inception, contro cio' che il") + print(f" monitor ha REGISTRATO giorno per giorno ({len(a)} barre appaiate):") + print(f" barre identiche entro 1e-6 : {same}/{len(a)}") + print(f" correlazione registrato/ricalcolato: {np.corrcoef(a, b)[0, 1]:+.4f}") + print(f" vol annualizzata REGISTRATA : {a.std()*ANN*100:.2f}%") + print(f" vol annualizzata RICALCOLATA : {b.std()*ANN*100:.2f}%") + print(f" (la strategia e' progettata a vol ~{PUB['vol']:.1f}%: la seconda e' quella giusta)") + frac = (a.std() / b.std()) ** 2 + print(f" rapporto di varianza {(a.std()/b.std())**2:.4f} -> in radice di tempo il monitor") + print(f" sta misurando ~{frac*24*60:.0f} minuti di mercato al giorno, non 24 ore.") + print("\n MECCANISMO (leggibile nel codice, non dedotto): `advance()` processa le barre con") + print(" ts > last_ts e poi porta last_ts sull'ULTIMA di esse. Al giro delle 00:30 del giorno D") + print(" l'ultima barra e' quella di D, appena scritta e lunga ~30 minuti: il monitor ne") + print(" registra il rendimento e SPOSTA last_ts oltre. Quando il giro successivo trova la") + print(" barra di D completata, D e' gia' passato. Le 23 ore e mezza restanti di OGNI giorno") + print(" non entrano in nessun rendimento registrato.") + sh_a = float(a.mean() / a.std() * ANN) + sh_b = float(b.mean() / b.std() * ANN) + se = float(ANN / np.sqrt(len(a)) / np.sqrt(365) * np.sqrt(365)) + se = float(np.sqrt(365.0 / len(a))) + print(f"\n Sharpe della finestra forward: REGISTRATO {sh_a:+.2f} RICALCOLATO {sh_b:+.2f}") + print(f" ⚠ NESSUNO DEI DUE E' UNA LETTURA DEL GATE. Con {len(a)} barre l'errore standard di") + print(f" uno Sharpe annualizzato e' ~{se:.2f}: entrambi i numeri sono indistinguibili da 0 e") + print(" dalla soglia 1.0. Il gate e' il 2026-10-23 e questo script non lo anticipa; qui") + print(" servono solo a mostrare che i due strumenti non misurano la stessa grandezza.") + + +# ------------------------------------------------------------------ T4 +def t4_haircut(P, S): + print("\n" + "=" * 100) + print(" T4 — L'ALTRA META' DEL GATE: l'haircut di eseguibilita' a $5.000 e il min-order") + print("=" * 100) + import importlib.util + spec = importlib.util.spec_from_file_location("paper_xsr_ro2", ROOT / "scripts" / "live" / "paper_xsr.py") + px = importlib.util.module_from_spec(spec) + spec.loader.exec_module(px) + ts, dt, W, R, rb, syms = px.build_panel() + n = len(syms) + + def replay(cap0, min_order): + bk = px._book(cap0, n) + out = [] + for i in range(len(ts)): + out.append(px._step(bk, W[i], R[i], float(rb[i]), min_order)) + r = pd.Series(out, index=pd.to_datetime(ts, unit="ms", utc=True)) + tot = float(np.prod(1 + r.values) - 1) + fill = bk["n_fill"] / max(bk["n_fill"] + bk["n_skip"], 1) + return r, tot, fill + + print("\n replay dell'INTERA storia in-sample (non della finestra forward: quella e' del gate)") + print(f" {'libro':<28}{'Sharpe':>9}{'ret tot':>10}{'gambe eseguite':>17}{'haircut vs modeled':>21}") + rm, tot_m, _ = replay(5000.0, None) + print(f" {'MODELED (ribil. continuo)':<28}{_sh(rm.values):>9.2f}{tot_m*100:>9.1f}%" + f"{'100%':>17}{'-':>21}") + for mo in (5.0, 10.0, 25.0): + r5, tot5, fill = replay(5000.0, mo) + hc = (tot_m - tot5) / abs(tot_m) if tot_m else float("nan") + flag = "" if hc <= 0.40 else " <-- SOPRA LA GUARDIA 40%" + print(f" {f'REAL $5.000 min-order ${mo:.0f}':<28}{_sh(r5.values):>9.2f}{tot5*100:>9.1f}%" + f"{fill*100:>16.0f}%{hc*100:>20.1f}%{flag}") + + # ticket medio per gamba + dW = np.abs(np.diff(W, axis=0, prepend=np.zeros((1, n)))) + tick = dW[dW > 0] * 5000.0 + print(f"\n ticket per gamba a $5.000: mediano ${np.median(tick):.2f} medio ${tick.mean():.2f}") + print(f" (pubblicato: $14.41 medio) quota di ordini sotto $5: {(tick < 5).mean()*100:.0f}%" + f" sotto $10: {(tick < 10).mean()*100:.0f}%") + print("\n -> il min-order NON e' un dettaglio: e' la variabile che decide la seconda meta' del") + print(" gate, e il numero pubblicato assume $5. Se Hyperliquid e' a $10, la riga da leggere") + print(" il 23/10 e' la seconda, non la prima.") + + +def main() -> None: + P, S = t1_lenti() + t2_feed(P, S) + t3_monitor() + t4_haircut(P, S) + print("\n" + "=" * 100) + print(" CONTROMISURA PROPOSTA (non implementata: questo script non tocca produzione)") + print("=" * 100) + print(""" + Il difetto strutturale e' che `data/raw` e' gitignored e ogni numero di ammissione basato su + hl_* e' irriproducibile per costruzione. La contromisura minima, nella forma che il progetto usa + gia' altrove (guardia `TRONCATO`/`STORIA-CORTA` di `fetch_ib_equities.certify`): + + 1. IMPRONTA PER FINESTRA CHIUSA. A ogni giro, il certificatore HL scrive in un file versionato + (non in data/raw) `sha256` + conteggio barre + primo/ultimo timestamp per simbolo, calcolati + SOLO sulle barre fino a ieri (finestra chiusa). Un test confronta l'impronta di una finestra + gia' registrata: se cambia, la storia e' stata riscritta e il test rompe. Costa ~50 hash/giorno. + 2. LA BARRA IN CORSO VA ETICHETTATA, NON NASCOSTA. `load_hl` (e chiunque legga hl_*) deve poter + chiedere solo le barre CHIUSE. La forma minima e' un flag esplicito con default sicuro; la + forma che il progetto preferisce e' derivare la regola dal codice sorvegliato invece di + ridichiararla in ogni consumatore. + 3. I MONITOR FORWARD NON DEVONO CONSUMARE LA BARRA IN CORSO. `advance()` deve fermarsi + all'ultima barra CHIUSA; oggi ne consuma una provvisoria e sposta `last_ts` oltre, perdendo + il 98% di ogni giornata. E' un bug di produzione, non una scelta di modellazione. + + RISULTATI PUBBLICATI CHE POGGIANO SU hl_* (tutti da citare con questa riserva): + XS01 (sleeve nel book di ricerca al 15%) — 19 major HL 1d + XSR01 (candidato, gate 23/10) — 50 alt HL 1d + STATARB-MULTI (falsificazione 25/07) — 50 coppie alt/BTC HL + XS-LITE (22/08) — stesse serie + CC01 / breadth alt / dispersione XDISP — stesse serie + Per i BACKTEST l'effetto e' una barra su ~965 (piccolo ma non nullo: qui vale 1.82 -> 1.78). + Per i MONITOR FORWARD l'effetto e' totale, perche' la barra provvisoria e' l'UNICA che leggono. +""") + + +if __name__ == "__main__": + main()