research(wave-0822): GATE-RECON — il difetto ribalta STATARB (+1,96 -> -2,02), la premessa della rigenerazione e' confermata per via diretta

This commit is contained in:
Adriano Dal Pastro
2026-08-22 21:54:59 +00:00
parent bcffcc01af
commit dd4391b55b
2 changed files with 789 additions and 0 deletions
+719
View File
@@ -0,0 +1,719 @@
"""GATE-RECON — cosa direbbero i tre gate pre-registrati se le serie forward fossero giuste.
IL FATTO DA CUI NASCE (filone MONITOR-AUDIT, 22/08, gia' misurato e NON riparato per scelta).
`fetch_hyperliquid` e `resample_tf` scrivono la barra del giorno IN CORSO; `advance()` la consuma
e porta `last_ts` su di essa -> le ore restanti non entrano in nessun rendimento. Quattro monitor
su sei registrano una FRAZIONE del giorno (2, 4, 19 e 41 minuti su 1440). Tre decisioni
pre-registrate leggono serie costruite cosi':
STATARB 2026-09-27 r0724_statarb_deploy_gate (Sh>=0.5, maxDD<10%, haircut<0.5pp)
XSR01 2026-10-23 r0725_xsr_deploy_gate (Sh>=1.0 E haircut$5000<=40%)
DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread)
QUESTO SCRIPT NON RIPARA E NON DECIDE. La riparazione e' una decisione dell'operatore e i gate
hanno una data: anticiparli sarebbe selezione-sul-forward, il bias che questo progetto ha gia'
pagato due volte. La consegna e' un DELTA, non un verdetto:
"alla data di oggi, con la serie riparata il criterio direbbe Y; con la serie com'e', Z"
e la distanza fra Y e Z e' la misura del danno del difetto — l'unica cosa che serve per decidere
SE e QUANDO riparare. Sola lettura su tutto cio' che e' produzione: i moduli di `scripts/live/`
si importano senza chiamarne `main()`, e `_append` viene sostituito con un raccoglitore in memoria
PRIMA di qualunque chiamata ad `advance()` -> nessun file di `data/` viene aperto in scrittura.
METODO. La serie RIPARATA non e' una reimplementazione: e' il codice di produzione rieseguito
dall'inception sui dati di oggi, TOLTA l'ultima barra (l'unica ancora aperta). Tutto il resto del
pannello e' fatto di barre chiuse, quindi il replay E' la serie che il monitor avrebbe registrato
se avesse aspettato la chiusura. Questo poggia su due condizioni, entrambe verificate qui:
(P1) il feed non riscrive le barre gia' chiuse -> sezione 0;
(P2) il pannello che leggo ora e' quello che il cron ha letto stanotte, e troncare l'ULTIMA
barra non cambia nessuna barra precedente -> sezione 1 (tre prove d'identita').
Senza (P2) starei ricostruendo una strategia diversa invece della stessa strategia riparata, ed e'
il modo piu' facile di sbagliare questo lavoro.
COSA MI ASPETTAVO PRIMA DI MISURARE (registrato qui perche' sia falsificabile):
* la premessa (P1) vera ma verificabile solo di traverso -> **sbagliato, ed e' la sorpresa
utile**: `data/_feed_backup/*.prebuild.bak` conserva il vintage PRE-riscrittura di stanotte,
quindi la premessa si verifica DIRETTAMENTE su ~914.000 barre, senza passare da una strategia;
* i tre gate tutti ribaltati -> **sbagliato, e la ragione conta**: solo STATARB si ribalta.
XSR01 e DVOLSPREAD dicono "ritiro/kill" su ENTRAMBE le lenti, ma con numeri che differiscono
di 3-7x e che a 28 barre non sono letture di gate su nessuna delle due;
* il veto d'integrita' di DVOLSPREAD (l'unico dei tre che ha una guardia) capace di accorgersene
-> **confermato che NON se ne accorge**, e per una ragione strutturale: conta se il DVOL C'ERA,
non quanto dura la barra su cui c'era.
nice -n 19 timeout 900 uv run python scripts/research/r0822c_gate_recon.py
"""
from __future__ import annotations
import importlib
import importlib.util
import json
import sys
from datetime import date
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path(__file__).resolve().parents[2]
RAW = ROOT / "data" / "raw"
BAK = ROOT / "data" / "_feed_backup"
for _p in (ROOT, ROOT / "scripts" / "research", ROOT / "scripts" / "research" / "alt",
ROOT / "scripts" / "research" / "ortho"):
sys.path.insert(0, str(_p))
ANN = np.sqrt(365.0)
TODAY = date.today()
H = "=" * 100
# Incidente del feed 2026-07-09 -> 2026-07-15 (`rebuild_history` abortito 7 giri, EPERM sul backup,
# gia' riparato): col feed fermo i monitor non trovavano barre nuove e al ripristino ne hanno
# recuperate in blocco, GIA' CHIUSE. Sono le uniche barre corrette di `paper_statarb` e vanno
# escluse da ogni stima del difetto, o gli si attribuisce un'attenuazione che viene da altro.
FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC"))
# --------------------------------------------------------------------------------------------
# infrastruttura di sola lettura (stessa di r0822_monitor_audit: nessuna reimplementazione)
# --------------------------------------------------------------------------------------------
_MODS: dict[str, object] = {}
def live(name: str):
"""Esegue un modulo di `scripts/live/` per averne le DEFINIZIONI. Mai `main()`."""
if name in _MODS:
return _MODS[name]
spec = importlib.util.spec_from_file_location(f"_ro_{name}", ROOT / "scripts" / "live" / f"{name}.py")
m = importlib.util.module_from_spec(spec)
spec.loader.exec_module(m)
_MODS[name] = m
return m
class Sink:
"""Sostituisce `_append`: raccoglie in memoria invece di scrivere. E' l'unica ragione per cui
si puo' rieseguire il codice di produzione TALE E QUALE restando in sola lettura."""
def __init__(self):
self.rows: list[tuple[str, dict]] = []
def __call__(self, path, rec):
self.rows.append((Path(path).name, dict(rec)))
def of(self, fname: str) -> list[dict]:
return [r for n, r in self.rows if n == fname]
def read_jsonl(p: Path) -> list[dict]:
return [json.loads(x) for x in p.read_text().splitlines() if x.strip()] if p.exists() else []
def mets(a) -> dict:
"""Sharpe (365), maxDD e ritorno cumulato di una serie di rendimenti netti per barra."""
a = np.asarray(a, float)
a = a[np.isfinite(a)]
if len(a) < 2 or a.std() == 0:
return dict(n=len(a), sh=float("nan"), dd=float("nan"), tot=float("nan"),
mu=float("nan"), sd=float("nan"))
eq = np.cumprod(1.0 + a)
pk = np.maximum.accumulate(eq)
return dict(n=len(a), sh=float(a.mean() / a.std() * ANN),
dd=float(np.max((pk - eq) / pk)), tot=float(eq[-1] - 1.0),
mu=float(a.mean() * 365.0), sd=float(a.std() * ANN))
def se_sharpe(n: int) -> float:
"""Errore standard grezzo di uno Sharpe annualizzato su n barre giornaliere (~sqrt(365/n))."""
return float(np.sqrt(365.0 / max(n, 1)))
# ============================================================================================
# 0 — LA PREMESSA: rigenerare non perde la finestra SOLO se le barre chiuse non vengono riscritte
# ============================================================================================
def sezione_0() -> dict:
print(H)
print(" 0 — PREMESSA: il feed riscrive le barre gia' CHIUSE?")
print(H)
print("""
Perche' e' la premessa di tutto: la raccomandazione del filone MONITOR-AUDIT e' "riparare
`advance()` + RIGENERARE", cioe' ricalcolare la finestra forward dall'inception sui dati di
oggi. Se il feed riscrivesse le barre chiuse, quella rigenerazione produrrebbe una serie
DIVERSA da quella che si sarebbe registrata giorno per giorno -> la finestra andrebbe persa e
la conclusione cambierebbe. E' stata verificata due volte di traverso (via strategie); qui si
verifica una terza volta e in modo DIRETTO.""")
out = {}
print("\n 0a. VERIFICA DIRETTA sul dato grezzo — vintage PRE-riscrittura contro oggi")
print(" `rebuild_history` copia il parquet in data/_feed_backup/*.prebuild.bak PRIMA di")
print(" sovrascriverlo: sul disco c'e' il file com'era ieri sera. Nessuna strategia in mezzo.")
tot_ch, tot_div = 0, 0
for a in ("btc", "eth"):
for tf in ("1h", "5m"):
new_p, old_p = RAW / f"{a}_{tf}.parquet", BAK / f"{a}_{tf}.parquet.prebuild.bak"
if not (new_p.exists() and old_p.exists()):
print(f" {a}_{tf}: vintage assente -> NON MISURATO")
continue
new = pd.read_parquet(new_p)
old = pd.read_parquet(old_p)
m = old.merge(new, on="timestamp", suffixes=("_o", "_n"))
cols = ["open", "high", "low", "close", "volume"]
diff = np.zeros(len(m), bool)
for c in cols:
diff |= (m[f"{c}_o"].values != m[f"{c}_n"].values)
last_old = int(old["timestamp"].max())
chiuse = m["timestamp"].values < last_old # nel vecchio file l'ultima riga era APERTA
nch, nd = int(chiuse.sum()), int((diff & chiuse).sum())
tot_ch += nch
tot_div += nd
print(f" {a}_{tf}: {nch:,} barre gia' chiuse nel vintage -> DIVERSE OGGI: {nd}"
f" (righe cambiate in tutto: {int(diff.sum())})")
if diff.sum() == 1:
i = int(np.where(diff)[0][0])
ts = pd.Timestamp(int(m["timestamp"].iloc[i]), unit="ms", tz="UTC")
print(f" l'unica riga cambiata e' {ts} = l'ULTIMA del vintage, che era "
f"aperta: close {m['close_o'].iloc[i]:g} -> {m['close_n'].iloc[i]:g}, "
f"volume {m['volume_o'].iloc[i]:,.0f} -> {m['volume_n'].iloc[i]:,.0f}")
out["chiuse"], out["div"] = tot_ch, tot_div
print(f"\n TOTALE: {tot_div} barre chiuse cambiate su {tot_ch:,} "
f"-> la riscrittura notturna e' un NO-OP sulle barre chiuse.")
print(" ⚠ questo e' UN ciclo di riscrittura (ieri->oggi). La stessa riga mostra anche il")
print(" DIFETTO: l'ultima riga del vintage era una barra parziale, ed e' esattamente")
print(" quella che i monitor consumano.")
print("\n 0b. VERIFICA su 62 CICLI — tasso di divergenza per ETA' della barra (paper_prevday)")
print(" Statistica diversa da quella del 22/08 (che contava le identiche): se il feed")
print(" riscrivesse le barre chiuse, il tasso CRESCEREBBE con l'eta' (piu' riscritture")
print(" subite). Se il difetto e' solo la barra parziale al momento della registrazione,")
print(" il tasso e' PIATTO nell'eta' e vale ~1/24.")
pp = live("paper_prevday")
st = json.loads((ROOT / "data" / "paper_prevday" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_prevday" / "returns.jsonl")
dfs = pp.build_bars()
S = st["start_ts"]
pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS}
sink = Sink()
pp._append = sink
st0 = dict(start_ts=S, last_ts=S, n_bars=0, pos_modeled=pos, pos_real=dict(pos),
cap_modeled=pp.MODELED_CAPITAL, cap_real=pp.REAL_CAPITAL,
peak_modeled=pp.MODELED_CAPITAL, peak_real=pp.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
pp.advance(st0, dfs)
rep = {int(r["ts"]): float(r["net_modeled"]) for r in sink.of("returns.jsonl")}
pairs = [(int(r["ts"]), float(r["net_modeled"]), rep[int(r["ts"])])
for r in rec if int(r["ts"]) in rep]
T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True))
A = np.array([p[1] for p in pairs])
B = np.array([p[2] for p in pairs])
dv = np.abs(A - B) > 1e-6
now = pd.Timestamp.now(tz="UTC")
age = (now - T).days
d = pd.DataFrame(dict(age=age, div=dv))
d["b"] = pd.cut(d.age, [-1, 7, 14, 21, 30, 45, 60, 999],
labels=["0-7g", "8-14g", "15-21g", "22-30g", "31-45g", "46-60g", ">60g"])
g = d.groupby("b", observed=True).agg(n=("div", "size"), div=("div", "sum"))
g["tasso"] = g["div"] / g["n"]
print(f" {len(A)} barre orarie appaiate, {int(dv.sum())} divergenti "
f"({dv.mean() * 100:.2f}%; identiche {len(A) - int(dv.sum())}/{len(A)})")
print(" " + " ".join(f"{ix}:{r.tasso * 100:4.1f}%" for ix, r in g.iterrows()))
hh = pd.Series(T.hour[dv]).value_counts().sort_index()
print(" ora UTC delle divergenti: " + ", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in hh.items())
+ " <- il cron gira alle 00:30")
out["prevday_rate_flat"] = (float(g["tasso"].min()), float(g["tasso"].max()))
print("\n 0c. LIMITE DICHIARATO — cosa NON e' verificato direttamente")
print(" Il vintage prebuild esiste solo per BTC/ETH (lo scrive `rebuild_history`).")
print(" `hl_*_1d.parquet` (che alimenta XSR01) e `dvol_*.parquet` NON hanno vintage su")
print(" disco: per loro l'evidenza resta indiretta (le barre che i monitor hanno letto")
print(" gia' chiuse coincidono al bit dopo settimane di riscritture — sezione 1).")
print("\n -> PREMESSA CONFERMATA sul feed BTC/ETH, per via diretta e su 62 cicli.")
print(" Rigenerare NON perde la finestra forward; nessuna data di gate si sposta.")
return out
# ============================================================================================
# 1 — RICOSTRUZIONE DELLE TRE SERIE + PROVE D'IDENTITA'
# ============================================================================================
def _ident_pannello(nome: str, salvato, calcolato) -> float:
d = float(np.max(np.abs(np.asarray(salvato, float) - np.asarray(calcolato, float))))
print(f" I1 pannello == quello del cron di stanotte ({nome}): max|d| = {d:g}"
f" {'OK' if d == 0.0 else '*** DIVERSO ***'}")
return d
def ric_statarb() -> dict:
print("\n 1a. paper_statarb — gate STATARB 2026-09-27")
ps = live("paper_statarb")
st = json.loads((ROOT / "data" / "paper_statarb" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_statarb" / "returns.jsonl")
j = ps.build_joint("1d")
ts, dt, pos, sr = ps._signal(j)
_ident_pannello("pos_modeled", [st["pos_modeled"]], [pos[-1]])
i0 = int(np.where(ts == st["start_ts"])[0][0])
def replay(frame):
sink = Sink()
ps._append = sink
t2, _, p2, _ = ps._signal(frame)
k = int(np.where(t2 == st["start_ts"])[0][0])
s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0,
pos_modeled=float(p2[k]), pos_real=float(p2[k]),
cap_modeled=ps.MODELED_CAPITAL, cap_real=ps.REAL_CAPITAL,
peak_modeled=ps.MODELED_CAPITAL, peak_real=ps.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_trades=0)
ps.advance(s0, frame)
return sink.of("returns.jsonl")
full = replay(j)
trunc = replay(j.iloc[:-1].reset_index(drop=True))
a = np.array([float(r["net_modeled"]) for r in full[:-1]])
b = np.array([float(r["net_modeled"]) for r in trunc])
print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, "
f"max|d| = {float(np.max(np.abs(a - b))):g} (l'ultima barra non retroagisce)")
return dict(mod=ps, rec=rec, rep=full, panel=j, extra=dict(pos=pos, ts=ts))
def ric_xsr() -> dict:
print("\n 1b. paper_xsr — gate XSR01 2026-10-23")
px = live("paper_xsr")
st = json.loads((ROOT / "data" / "paper_xsr" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_xsr" / "returns.jsonl")
sink = Sink()
px._append = sink
ts, dt, W, R, rb, syms = px.build_panel()
_ident_pannello("w[modeled]", st["modeled"]["w"], W[-1])
s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"],
modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])),
reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS})
px.advance(s0)
full = sink.of("returns.jsonl")
# I3: troncare l'INPUT (l'ultima riga di OGNI parquet HL) e ricostruire da capo
orig = px.load_hl
def load_trunc(sym):
return orig(sym).iloc[:-1]
px.load_hl = load_trunc
sink2 = Sink()
px._append = sink2
try:
ts2, _, W2, _, _, syms2 = px.build_panel()
s2 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, syms=st["syms"],
modeled=px._book(px.MODELED_CAPITAL, len(st["syms"])),
reals={n: px._book(c, len(st["syms"])) for c, n in px.REAL_BOOKS})
px.advance(s2)
tr = sink2.of("returns.jsonl")
a = np.array([float(r["net_modeled"]) for r in full[:-1]])
b = np.array([float(r["net_modeled"]) for r in tr])
dmax = float(np.max(np.abs(a - b))) if len(a) == len(b) else float("nan")
print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, max|d| = {dmax:g}")
finally:
px.load_hl = orig
return dict(mod=px, rec=rec, rep=full)
def ric_dvolspread() -> dict:
print("\n 1c. paper_dvolspread — kill 2026-10-24 / decisione 2027-01-24")
pv = live("paper_dvolspread")
st = json.loads((ROOT / "data" / "paper_dvolspread" / "state.json").read_text())
rec = read_jsonl(ROOT / "data" / "paper_dvolspread" / "returns.jsonl")
P = pv._panel()
_ident_pannello("wb_modeled", [st["wb_modeled"]], [P["wb"][-1]])
i0 = int(np.where(P["ts"] == st["start_ts"])[0][0])
def replay(Q):
sink = Sink()
pv._append = sink
k = int(np.where(Q["ts"] == st["start_ts"])[0][0])
s0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0, n_active=0,
n_flat_signal=0, n_flat_nodata=0,
wb_modeled=float(Q["wb"][k]), wb_real=float(Q["wb"][k]),
cap_modeled=pv.MODELED_CAPITAL, cap_real=pv.REAL_CAPITAL,
peak_modeled=pv.MODELED_CAPITAL, peak_real=pv.REAL_CAPITAL,
dd_modeled=0.0, dd_real=0.0, n_flips=0, frozen=pv.FROZEN)
out = pv.advance(s0, Q)
return sink.of("returns.jsonl"), out
full, st_full = replay(P)
Q = {k: (v[:-1] if hasattr(v, "__len__") else v) for k, v in P.items()}
trunc, st_tr = replay(Q)
a = np.array([float(r["net_modeled"]) for r in full[:-1]])
b = np.array([float(r["net_modeled"]) for r in trunc])
print(f" I3 troncare l'INPUT == affettare l'OUTPUT: n {len(a)}/{len(b)}, "
f"max|d| = {float(np.max(np.abs(a - b))):g}")
return dict(mod=pv, rec=rec, rep=full, st_rep=st_tr, st_rec=st)
def sezione_1() -> dict:
print("\n" + H)
print(" 1 — RICOSTRUZIONE + PROVE D'IDENTITA' (senza queste, il numero e' un'altra strategia)")
print(H)
print("""
Tre prove, ognuna chiude un modo diverso di sbagliare:
I1 il pannello che leggo ORA e' quello che il cron ha letto stanotte (i parquet hanno mtime
00:30-00:35 e nulla li riscrive fino al prossimo giro): si verifica contro lo `state.json`
del monitor, che e' stato scritto DA LUI alle 00:35.
I2 sulle barre che il monitor ha letto GIA' CHIUSE la ricostruzione coincide AL BIT: e' la
parte non affetta dal difetto, ed e' l'unico posto dove i due numeri devono coincidere.
I3 troncare l'ULTIMA barra dell'INPUT da' lo stesso risultato che affettare l'output: nessuna
barra precedente dipende dall'ultima (se dipendesse, la 'riparazione' sarebbe un altro
segnale, non lo stesso).""")
S = dict(statarb=ric_statarb(), xsr=ric_xsr(), dvol=ric_dvolspread())
print("\n I2 — barre che il monitor ha letto CHIUSE, e coincidenza al bit:")
for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")):
rec, rep = S[k]["rec"], S[k]["rep"]
d = {int(r["ts"]): float(r["net_modeled"]) for r in rep}
pr = [(int(r["ts"]), float(r["net_modeled"]), d[int(r["ts"])]) for r in rec if int(r["ts"]) in d]
A = np.array([p[1] for p in pr])
B = np.array([p[2] for p in pr])
T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pr], unit="ms", utc=True))
eq = np.abs(A - B) <= 1e-6
S[k]["pairs"] = (T, A, B)
gg = sorted({str(x.date()) for x in T[eq]})
note = f" ({gg[0]} -> {gg[-1]})" if gg else ""
print(f" {lab:<18} {int(eq.sum())}/{len(A)} identiche{note}")
if k == "statarb":
print(" quelle 6 sono le barre recuperate dopo l'incidente del feed 09-15/07:")
print(" registrate il 16/07 e riprodotte oggi al bit dopo ~37 riscritture")
print(" notturne -> conferma indipendente della premessa della sezione 0.")
if k == "dvol":
print(" *** ZERO barre non affette: per questo monitor la prova I2 NON ESISTE.")
print(" Restano I1 (pannello identico, max|d|=0), I3 (troncamento innocuo) e il")
print(" fatto strutturale che il replay chiama `advance()` di produzione senza")
print(" una riga riscritta. Va detto: e' una prova piu' debole delle altre due.")
return S
# ============================================================================================
# 2 — I TRE GATE, ALLA LETTERA
# ============================================================================================
def _riga(lab, m, extra=""):
print(f" {lab:<22} n={m['n']:>3} Sharpe {m['sh']:+7.2f} maxDD {m['dd'] * 100:6.2f}% "
f"ritorno {m['tot'] * 100:+6.2f}% vol {m['sd'] * 100:5.2f}%{extra}")
def gate_dvolspread(S: dict) -> tuple[str, str]:
print("\n" + H)
print(" 2a — DVOLSPREAD (kill 2026-10-24, decisione 2027-01-24) — PER PRIMO: e' l'unico dei")
print(" tre che ha gia' un VETO D'INTEGRITA', e la domanda e' se il difetto lo fa scattare")
print(H)
pv, rec, rep = S["dvol"]["mod"], S["dvol"]["rec"], S["dvol"]["rep"]
A = np.array([float(r["net_modeled"]) for r in rec])
B = np.array([float(r["net_modeled"]) for r in rep])[:-1] # RIPARATA = solo barre chiuse
mA, mB = mets(A), mets(B)
print(f"\n criterio kill (pre-registrato 26/07): Sharpe forward MODELED < {pv.KILL_SHARPE:+.2f}"
f" -> RITIRO")
_riga("serie ATTUALE", mA)
_riga("serie RIPARATA", mB)
vA = "KILL" if mA["sh"] < pv.KILL_SHARPE else "vivo"
vB = "KILL" if mB["sh"] < pv.KILL_SHARPE else "vivo"
print(f" -> criterio kill: serie attuale {vA} · serie riparata {vB} "
f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}")
print(f" ⚠ ma a n={mA['n']} barre SE(Sharpe) ~ {se_sharpe(mA['n']):.2f}: nessuna delle due")
print(" e' una lettura di gate, e la data del kill e' fra "
f"{(date(2026, 10, 24) - TODAY).days} giorni.")
print("\n IL VETO D'INTEGRITA' (`n_active + n_flat_signal >= 80% delle barre`):")
stA, stB = S["dvol"]["st_rec"], S["dvol"]["st_rep"]
fA = (stA["n_active"] + stA["n_flat_signal"]) / max(stA["n_bars"], 1)
fB = (stB["n_active"] + stB["n_flat_signal"]) / max(stB["n_bars"], 1)
print(f" serie ATTUALE : attive {stA['n_active']} · flat-da-segnale {stA['n_flat_signal']}"
f" · flat-SENZA-DATO {stA['n_flat_nodata']} -> valide {fA:.0%} "
f"(soglia {pv.MIN_ACTIVE_FRAC:.0%}) -> {'VETO' if fA < pv.MIN_ACTIVE_FRAC else 'PASSA'}")
print(f" serie RIPARATA: attive {stB['n_active']} · flat-da-segnale {stB['n_flat_signal']}"
f" · flat-SENZA-DATO {stB['n_flat_nodata']} -> valide {fB:.0%} -> "
f"{'VETO' if fB < pv.MIN_ACTIVE_FRAC else 'PASSA'}")
print("""
📌 IL RISULTATO E' SUL VETO, NON SUL MONITOR. Una serie che misura DUE MINUTI di mercato al
giorno passa al 100% un veto d'integrita' progettato apposta per non far decidere su dati
mancanti. Non e' un difetto di taratura: e' che il veto e' costruito sulla domanda giusta
per un altro guasto. Conta *se il DVOL c'era* (e c'era, 28/28) e non puo' vedere *quanto
dura la barra su cui c'era* — sono due assi ortogonali. La stessa soglia 0.80 applicata alla
quota di barre CHIUSE leggerebbe 0/28 = 0% e vieterebbe la decisione.
E' la stessa forma gia' pagata dal progetto tre volte: 'una riga presente non e' un dato
presente'. Qui la variante nuova e': *una barra presente non e' una GIORNATA presente*.""")
print("\n criteri (b) marginale ADDS / (c) deflated-Sharpe / (d) weights_tilt_null: NON GIRATI,")
print(" e il motivo e' una misura, non pigrizia: si valutano sul campione ESTESO (in-sample")
print(f" attivo 1949 barre + forward). Oggi il forward pesa {mA['n']}/{1949 + mA['n']} = "
f"{100 * mA['n'] / (1949 + mA['n']):.1f}% del campione; alla decisione del 24/01/2027 ne")
print(" pesera' ~8%. -> il difetto NON puo' ribaltare (c): puo' ribaltare solo (a), che e'")
print(" calcolato sulla sola finestra forward ed e' interamente determinato da lui.")
return vA, vB
def gate_statarb(S: dict) -> tuple[str, str]:
print("\n" + H)
print(" 2b — STATARB (decisione 2026-09-27) — il gate su cui il difetto MORDE")
print(H)
rec, rep = S["statarb"]["rec"], S["statarb"]["rep"]
gate = importlib.import_module("r0724_statarb_deploy_gate")
A = np.array([float(r["net_modeled"]) for r in rec])
Ar = np.array([float(r["net_real"]) for r in rec])
B = np.array([float(r["net_modeled"]) for r in rep])[:-1]
Br = np.array([float(r["net_real"]) for r in rep])[:-1]
mA, mB = mets(A), mets(B)
hc = lambda m, r: abs(float((np.prod(1 + m) - np.prod(1 + r)) * 100))
hA, hB = hc(A, Ar), hc(B, Br)
print(f"\n soglie pre-registrate 24/07: Sharpe >= {gate.SH_DEPLOY} · maxDD < {gate.DD_MAX:.0%}"
f" · fill-haircut < {gate.HAIRCUT_MAX_PP} pp")
_riga("serie ATTUALE", mA, f" haircut {hA:.3f} pp")
_riga("serie RIPARATA", mB, f" haircut {hB:.3f} pp")
def verdetto(sh, dd, h):
if sh >= gate.SH_DEPLOY and dd < gate.DD_MAX and h < gate.HAIRCUT_MAX_PP:
return "CANDIDATO AL DEPLOY"
if sh >= 0:
return "ESTENSIONE (unica, al 2026-12-26)"
return "RITIRO"
vA, vB = verdetto(mA["sh"], mA["dd"], hA), verdetto(mB["sh"], mB["dd"], hB)
for lab, sh, dd, h, v in (("ATTUALE", mA["sh"], mA["dd"], hA, vA),
("RIPARATA", mB["sh"], mB["dd"], hB, vB)):
print(f" {lab:<9} Sharpe>={gate.SH_DEPLOY}: {'SI' if sh >= gate.SH_DEPLOY else 'NO':<3}"
f" | maxDD<{gate.DD_MAX:.0%}: {'SI' if dd < gate.DD_MAX else 'NO':<3}"
f" | haircut<{gate.HAIRCUT_MAX_PP}pp: {'SI' if h < gate.HAIRCUT_MAX_PP else 'NO':<3}"
f" -> {v}")
print(f"\n -> DUE CRITERI SU TRE SI RIBALTANO. Sharpe {mA['sh']:+.2f} -> {mB['sh']:+.2f} "
f"(attraversa 0 e la soglia 0.5); maxDD {mA['dd']:.1%} -> {mB['dd']:.1%} (attraversa la")
print(" guardia del 10%). Il terzo (haircut) non si ribalta e non poteva: e' un")
print(" rapporto fra due libri che subiscono lo STESSO troncamento.")
print("\n DIAGNOSTICA PRE-REGISTRATA IL 25/07 (statica sempre-short a pari vol-target):")
bA = gate._bench_static_short(rec)
bB = gate._bench_static_short(rep[:-1])
if bA and bB:
print(f" benchmark sulla finestra ATTUALE : {bA[0]:+.2f} su {bA[1]} barre")
print(f" benchmark sulla finestra RIPARATA: {bB[0]:+.2f} su {bB[1]} barre")
print(f" STATARB - benchmark: attuale {mA['sh'] - bA[0]:+.2f} "
f"riparata {mB['sh'] - bB[0]:+.2f}")
print("""
📌 ASIMMETRIA CHE VALE PIU' DEI NUMERI: il benchmark NON e' affetto dal difetto — e'
ricalcolato dal pannello, non letto dalla serie registrata (varia di %+.2f fra le due
finestre, contro %+.2f della strategia). Alla data del gate l'operatore metterebbe quindi
a confronto un benchmark GIUSTO con un numero di strategia SBAGLIATO, e la diagnostica
sembrerebbe piu' favorevole di quanto sia: +%.2f invece di +%.2f di margine.
Il segno del margine sopravvive comunque (STATARB batte la statica in entrambe le lenti):
cio' che il difetto ribalta e' il gate, non questa diagnostica.""" %
(bB[0] - bA[0], mB["sh"] - mA["sh"], mA["sh"] - bA[0], mB["sh"] - bB[0]))
else:
print(" benchmark NON calcolabile -> non girato.")
return vA, vB
def gate_xsr(S: dict) -> tuple[str, str]:
print("\n" + H)
print(" 2c — XSR01 (decisione 2026-10-23)")
print(H)
rec, rep = S["xsr"]["rec"], S["xsr"]["rep"]
gate = importlib.import_module("r0725_xsr_deploy_gate")
k5 = "net_REAL-$5000"
A = np.array([float(r["net_modeled"]) for r in rec])
A5 = np.array([float(r[k5]) for r in rec])
B = np.array([float(r["net_modeled"]) for r in rep])[:-1]
B5 = np.array([float(r[k5]) for r in rep])[:-1]
mA, mB = mets(A), mets(B)
def haircut(m, r5):
tm = float(np.prod(1 + m) - 1)
t5 = float(np.prod(1 + r5) - 1)
return (tm - t5) / abs(tm) if tm != 0 else float("nan")
hA, hB = haircut(A, A5), haircut(B, B5)
print(f"\n soglie pre-registrate 25/07: Sharpe >= {gate.SH_DEPLOY} E haircut$5000 <= "
f"{gate.HAIRCUT_MAX:.0%} · ritiro sotto Sharpe {gate.SH_RETIRE}")
print(f" ⚠ se l'haircut sfonda -> RITIRO a prescindere dallo Sharpe (regola esplicita)")
_riga("serie ATTUALE", mA, f" haircut$5000 {hA * 100:+.1f}%")
_riga("serie RIPARATA", mB, f" haircut$5000 {hB * 100:+.1f}%")
def verdetto(sh, h):
if np.isfinite(h) and h > gate.HAIRCUT_MAX:
return "RITIRO (haircut)"
if sh >= gate.SH_DEPLOY:
return "CANDIDATO SLEEVE"
if sh >= gate.SH_RETIRE:
return "ESTENSIONE (unica, al 2027-01-21)"
return "RITIRO"
vA, vB = verdetto(mA["sh"], hA), verdetto(mB["sh"], hB)
print(f" ATTUALE -> {vA}\n RIPARATA -> {vB} "
f"{'STESSO VERDETTO' if vA == vB else '*** VERDETTO RIBALTATO ***'}")
print(f"\n -> il verdetto NON si ribalta, ma per un caso: entrambe stanno sotto "
f"{gate.SH_RETIRE}. Il")
print(f" numero pero' cambia di {abs(mA['sh'] / mB['sh']):.1f}x ({mA['sh']:+.2f} contro "
f"{mB['sh']:+.2f}), e con SE ~ {se_sharpe(mA['n']):.1f} nessuno dei due decide niente.")
print(" L'haircut e' l'unico criterio robusto al difetto (rapporto fra libri troncati")
print(" allo stesso modo) ed e' lontanissimo dalla guardia in entrambe le lenti.")
return vA, vB
# ============================================================================================
# 3 — IL MECCANISMO: perche' il troncamento non e' rumore
# ============================================================================================
def sezione_3(S: dict) -> None:
print("\n" + H)
print(" 3 — PERCHE' IL DIFETTO NON E' RUMORE: comprime la VOL piu' della MEDIA")
print(H)
print("""
Un troncamento a k minuti su 1440 scala la media del rendimento circa come k e la deviazione
standard come sqrt(k) (random walk). Quindi il rapporto media/sd — cioe' lo Sharpe — viene
MOLTIPLICATO per ~sqrt(k/1440)... in valore assoluto no: il numeratore e' la media del pezzo
troncato, che non ha lo stesso SEGNO della media della giornata intera. Risultato: la serie
registrata produce letture di Sharpe piu' ESTREME e in una direzione che non e' quella vera.
Si vede in tabella: la vol registrata e' una frazione di quella vera, ma il |Sharpe| non e' piu'
piccolo — in due casi su tre e' molto piu' GRANDE.""")
print(f"\n {'monitor':<20} {'vol REG':>9} {'vol RIP':>9} {'min/giorno':>11} "
f"{'Sh REG':>8} {'Sh RIP':>8} nota")
for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")):
T, A, B = S[k]["pairs"]
Bc = B[:-1] if len(B) == len(A) else B
# ⚠ l'INTERA finestra dell'incidente del feed (09-16/07) va esclusa dalla stima dei
# minuti/giorno, non le sole barre risultate identiche: durante il blocco il monitor non
# trovava barre nuove e al ripristino ne ha recuperate 7 in un colpo, alcune solo
# PARZIALMENTE riallineate. Escludendo le sole identiche la stima esce 4x piu' alta
# (16,7 invece di 3,9 min/giorno su statarb) — e' la stessa maschera del 22/08.
keep = ~((T >= FREEZE[0]) & (T <= FREEZE[1]))
mA, mB = mets(A), mets(Bc)
kA, kB = mets(A[keep]), mets(B[:len(A)][keep])
minuti = (kA["sd"] / kB["sd"]) ** 2 * 1440.0
nota = ("SEGNO OPPOSTO" if np.sign(mA["sh"]) != np.sign(mB["sh"])
else f"|Sh| x{abs(mA['sh']) / abs(mB['sh']):.1f}")
print(f" {lab:<20} {mA['sd'] * 100:8.2f}% {mB['sd'] * 100:8.2f}% {minuti:10.1f} "
f"{mA['sh']:+8.2f} {mB['sh']:+8.2f} {nota}"
+ (f" ({int((~keep).sum())} barre dell'incidente escluse)" if (~keep).any() else ""))
print("""
Il caso peggiore NON e' quello col |Sharpe| piu' gonfiato: e' `paper_statarb`, dove il
|Sharpe| e' quasi identico (1,96 contro 2,02) e cambia il SEGNO. Un controllo che cercasse
"numeri troppo belli" lo lascerebbe passare senza una parola: e' proprio il monitor che
alimenta il gate piu' vicino.
-> la firma da cercare non e' un |Sharpe| alto ma una VOL FORWARD molto sotto quella del
backtest: 7,9% contro ~31%, 0,5% contro ~2,8%, 0,8% contro ~19%.
⚠ i "min/giorno" sono una LETTURA del rapporto di varianza (assume random walk
sull'intervallo troncato), non un cronometro, e sono sensibili alla maschera: su statarb
3,9 escludendo l'incidente del feed, 16,7 escludendo le sole barre identiche, 92
includendo tutto. Si cita la prima, che e' quella del 22/08; l'ordine di grandezza e'
quello, la cifra no.
✅ Replica indipendente del filone MONITOR-AUDIT: 3,9 / 41,2 / 2,5 contro i 4 / 41 / 2
pubblicati stamattina, per un percorso scritto separatamente.""")
print("\n `implausible_sharpe` puntato sulle serie forward (mai fatto — nota del 22/08).")
print(" ⚠ il gate chiede n>=30: due serie forward su tre NON sono giudicabili, e questo")
print(" e' esso stesso un risultato — il gate esiste dal 26/07 ma non puo' sorvegliare")
print(" una finestra forward giovane, che e' esattamente quando serve.")
import altlib as al
for k, lab in (("statarb", "paper_statarb"), ("xsr", "paper_xsr"), ("dvol", "paper_dvolspread")):
T, A, B = S[k]["pairs"]
for nome, v in (("REGISTRATA", A), ("RIPARATA", B[:-1] if len(B) == len(A) else B)):
idx = T[:len(v)]
try:
r = al.implausible_sharpe(pd.Series(v, index=idx))
corto = any("troppo corto" in x for x in r["reasons"])
flag = ("SEGNALA" if r["implausible"]
else "n/giudic." if corto else "ok")
print(f" {lab:<18} {nome:<11} {flag:<10} {'; '.join(r['reasons'])[:66]}")
except Exception as e: # noqa: BLE001
print(f" {lab:<18} {nome:<11} non girato ({type(e).__name__})")
# ============================================================================================
# 4 — QUANTO COSTA ASPETTARE
# ============================================================================================
GATES = (
("STATARB", "statarb", date(2026, 9, 27)),
("XSR01", "xsr", date(2026, 10, 23)),
("DVOLSPREAD", "dvol", date(2026, 10, 24)),
)
def sezione_4(S: dict) -> None:
print("\n" + H)
print(" 4 — QUANTO COSTA ASPETTARE (l'unico argomento quantitativo su QUANDO riparare)")
print(H)
print("""
Due scenari, e la differenza fra loro e' tutto il contenuto della sezione:
RIPARA+RIGENERA : si corregge `advance()` e si RICALCOLA la finestra dall'inception.
Possibile perche' le barre chiuse non vengono riscritte (sezione 0).
RIPARA IN AVANTI : si corregge `advance()` e basta. Le barre gia' scritte restano sbagliate.""")
print(f"\n {'gate':<12} {'data':<12} {'barre al gate':>14} {'oggi':>6} "
f"{'corrette se si ripara OGGI':>28} {'se si ripara alla vigilia':>27}")
for nome, key, gd in GATES:
rec = S[key]["rec"]
first = pd.Timestamp(rec[0]["dt"][:10]).date()
n_gate = (gd - first).days + 1
n_oggi = len(rec)
print(f" {nome:<12} {str(gd):<12} {n_gate:>14} {n_oggi:>6} "
f"{f'{n_gate - n_oggi} ({(n_gate - n_oggi) / n_gate:.0%})':>28} "
f"{f'1 ({1 / n_gate:.0%})':>27}")
print("\n Con RIGENERAZIONE la colonna giusta e' un'altra: 100% a QUALUNQUE data di")
print(" riparazione. Il costo dell'attesa e' ZERO barre — cio' che si paga aspettando non")
print(" e' dato perso ma il RISCHIO che la riparazione non arrivi prima del gate.")
print(" Senza rigenerazione, invece, riparare oggi lascia comunque sbagliata la maggioranza")
print(" della finestra di STATARB, e riparare alla vigilia non serve a niente.")
print("\n SENSIBILITA' — quanto deve essere contaminata la finestra perche' il verdetto")
print(" cambi. Misurata sui dati di OGGI, non estrapolata: si costruisce la serie mista")
print(" `registrate[:k] + riparate[k:]` (= riparazione in avanti dal giorno k) e si legge")
print(" il criterio. k=0 e' la rigenerazione completa, k=n e' la serie di oggi.")
gate = importlib.import_module("r0724_statarb_deploy_gate")
rec, rep = S["statarb"]["rec"], S["statarb"]["rep"]
A = np.array([float(r["net_modeled"]) for r in rec])[:-1]
B = np.array([float(r["net_modeled"]) for r in rep])[:-1]
n = len(A)
print(f"\n STATARB — {n} barre chiuse; soglia deploy {gate.SH_DEPLOY}, ritiro sotto 0, "
f"guardia maxDD {gate.DD_MAX:.0%}")
print(f" {'corrette (coda)':>16} {'contaminate':>12} {'Sharpe':>8} {'maxDD':>8} verdetto")
verd = []
for k in range(0, n + 1):
m = mets(np.concatenate([A[:k], B[k:]]))
v = ("CANDIDATO" if (m["sh"] >= gate.SH_DEPLOY and m["dd"] < gate.DD_MAX)
else "ESTENSIONE" if m["sh"] >= 0 else "RITIRO")
verd.append((k, m, v))
# minimo numero di barre CORRETTE in coda che basta a leggere gia' il verdetto riparato
v_rip = verd[0][2]
min_ok = min((n - k for k, m, v in verd if v == v_rip), default=n)
for k, m, v in verd:
if k in (0, n // 4, n // 2, (3 * n) // 4, n - min_ok, n - min_ok + 1, n):
print(f" {n - k:>16} {f'{k} ({k / n:.0%})':>12} {m['sh']:+8.2f} "
f"{m['dd'] * 100:7.2f}% {v}")
n_res = 90 - len(S["statarb"]["rec"]) # barre che restano da qui al gate del 27/09
print(f"""
📌 LA RIPARAZIONE PARZIALE NON E' PROPORZIONALE. Su questo campione bastano
**{min_ok} barre corrette in coda su {n}** ({min_ok / n:.0%}) perche' il criterio legga gia' il
verdetto della serie riparata. Il meccanismo e' quello della sezione 3: una barra troncata a
~4 minuti su 1440 porta ~1/370 della varianza E ~1/370 del rendimento di una barra vera
(entrambi scalano con la frazione di giornata), quindi pesa quasi nulla sia nella media sia
nella deviazione della miscela. La maggioranza NUMERICA delle barre non comanda.
Detto meglio, ed e' la formulazione onesta: riparare in avanti senza rigenerare non
CONTAMINA la finestra, la ACCORCIA — il criterio finisce per leggere la strategia sulle sole
barre corrette. Riparando oggi, al 27/09 STATARB avrebbe {n_res} barre vere su 90: una
finestra piu' corta ma VERA, con SE(Sharpe) ~{se_sharpe(n_res):.1f} invece di ~{se_sharpe(90):.1f}.
Riparare alla vigilia (1 barra) no: li' il numero resta quello di oggi.
⚠ Il {min_ok}/{n} NON e' una costante — dipende da quali barre capitano in coda, e cambia col
campione: si legge come "poche", non come una soglia. E rigenerare resta strettamente
migliore, per due cose che l'accorciamento non da': un maxDD leggibile (la miscela ha un
percorso di equity che non e' mai esistito) e una finestra rileggibile con altri criteri.""")
# ============================================================================================
def main() -> None:
print(H)
print(" GATE-RECON — cosa direbbero i tre gate pre-registrati con le serie riparate")
print(f" {TODAY} SOLA LETTURA su produzione (nessun file di data/ aperto in scrittura)")
print(H)
sezione_0()
S = sezione_1()
v_dv = gate_dvolspread(S)
v_st = gate_statarb(S)
v_xs = gate_xsr(S)
sezione_3(S)
sezione_4(S)
print("\n" + H)
print(" RIEPILOGO — il DELTA, non la decisione (i gate hanno una data e non si anticipano)")
print(H)
print(f" STATARB 27/09 — serie riparata: {v_st[1]:<36} · serie attuale: {v_st[0]}")
print(f" XSR01 23/10 — serie riparata: {v_xs[1]:<36} · serie attuale: {v_xs[0]}")
print(f" DVOLSPREAD 24/10 — serie riparata: {v_dv[1]:<36} · serie attuale: {v_dv[0]}")
print("\n Nessuno di questi e' il verdetto del gate: sono i criteri letti OGGI, su una finestra")
print(" ancora incompleta. Servono a una sola decisione, che e' dell'operatore: riparare")
print(" `advance()` e RIGENERARE prima del 27/09, oppure sapere che quel giorno il criterio")
print(" verra' letto su una serie che misura 4 minuti di mercato al giorno.")
if __name__ == "__main__":
main()