research(wave-0822): VOL-SIZE — il p-value del null di permutazione si cita col segno, non col decimale (3 ancore su 23)

This commit is contained in:
Adriano Dal Pastro
2026-08-22 17:24:10 +00:00
parent e8bbb92f8a
commit f975272ec8
4 changed files with 550 additions and 22 deletions
+29 -8
View File
@@ -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}
+53 -14
View File
@@ -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)
+37
View File
@@ -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")
+431
View File
@@ -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()