Files
PythagorasGoal/scripts/research/r0822_monitor_audit.py
T

896 lines
53 KiB
Python

"""MONITOR-AUDIT — quanti dei sei forward-monitor registrano la giornata che credono di registrare?
PERCHE'. Il filone XSR-REPRO (22/08) ha trovato che `paper_xsr.advance()` consuma la barra 1d
del giorno IN CORSO — `fetch_hyperliquid` gira nel cron delle 00:30 con END = oggi e scrive una
barra parziale — e poi porta `last_ts` su quella barra: le 23 ore e mezza restanti di ogni giorno
non entrano in nessun rendimento registrato. Misura: 1/28 barre identiche al replay, corr -0,045,
vol registrata 0,46% contro 2,74% ricalcolata.
Il difetto non e' di XSR01: e' della FORMA `new = [i for i in range(len(ts)) if ts[i] > last_ts]`
applicata a un pannello la cui ultima riga e' provvisoria. Quella forma e' copiata in sei file.
Tre decisioni PRE-REGISTRATE si leggono su queste serie:
STATARB 2026-09-27 (r0724_statarb_deploy_gate: Sharpe di net_modeled >= 0.5)
XSR01 2026-10-23 (r0725_xsr_deploy_gate: Sharpe >= 1.0 E haircut $5.000 <= 40%)
DVOLSPREAD 2026-10-24 kill / 2027-01-24 decisione (soglie dentro paper_dvolspread)
e `monitor_health` dichiara **OK** tutti e sei, perche' misura freschezza e buchi: una serie
fresca, completa e SBAGLIATA passa ogni controllo di freschezza.
QUESTO SCRIPT E' UN AUDIT, NON UNA RIPARAZIONE. E' in SOLA LETTURA su tutto cio' che e'
produzione: importa i moduli di `scripts/live/` senza chiamarne `main()`, e prima di rieseguirne
`advance()` sostituisce `_append` con una funzione che raccoglie in memoria — nessun file di
`data/` viene aperto in scrittura. La riparazione e' una decisione dell'operatore e ha un costo
(azzerare una finestra forward): la sezione E lo quantifica.
METODO — per ogni monitor tre domande, ognuna con una PROVA e non con la lettura di un docstring
(il progetto ha gia' trovato QUATTRO docstring di produzione che dichiaravano una causalita' che
il codice non aveva: `_skyhook_positions`, `resample_5m`, `current_target` di TP01, e
`prevday_breakout.current_target` qui sotto):
A. DA DOVE VIENE L'ULTIMA BARRA. Si misura la COMPLETEZZA dell'ultima riga di ogni sorgente
con un contatore indipendente dal prezzo: quante barre 5m stanno dentro l'ultima barra 1h,
quante barre 1h dentro l'ultimo giorno, quanto volume ha l'ultima barra HL rispetto alla
mediana, quanti giorni di ritardo ha l'ultima barra IB.
B. IL DIFETTO MORDE? Replay della strategia CONGELATA sui dati di oggi dall'inception, contro
cio' che il monitor ha registrato giorno per giorno, appaiato per timestamp. Non "legge una
barra parziale" ma **quanta parte della giornata finisce nella serie registrata**: barre
identiche, correlazione, rapporto di varianza -> minuti al giorno effettivamente misurati.
C. QUALE GATE NE DIPENDE e se il criterio resta leggibile.
Poi:
D. LA GUARDIA CHE MANCA a `monitor_health`, con soglia dichiarata e controllo positivo nei due
versi (un rilevatore tarato per non segnalare e' indistinguibile da uno rotto).
E. IL COSTO DELLA RIPARAZIONE per gate: giorni di finestra persi, data in cui il contatore
nuovo tornerebbe alla stessa numerosita', e se esiste una RICOSTRUZIONE a posteriori.
COSA MI ASPETTAVO PRIMA DI MISURARE, e cosa e' andato diversamente:
* i tre monitor giornalieri su feed crypto (xsr, statarb, dvolspread) con la STESSA patologia,
intorno ai ~35 minuti al giorno di XSR01 -> **meta' giusto**: la patologia e' la stessa ma la
TAGLIA no (2, 4 e 41 min/giorno). Un libro dollar-neutral perde molto piu' di un libro
direzionale a parita' di troncamento, perche' su 35 minuti le due gambe si muovono insieme;
* `paper_prevday` MOLTO piu' mite (1 barra su 24) -> **confermato** (1.438 min/giorno su 1.440),
con un dettaglio non previsto: il difetto si propaga alla barra successiva quando il bersaglio
calcolato sulla barra parziale differisce;
* `paper_combo` rotto o sano per caso -> **sano, e per caso**: aspetta la chiusura di una BORSA;
* NON previsto: le uniche barre corrette di `paper_statarb` esistono perche' il feed si ROMPE
per 7 giorni a luglio e il monitor le recupera CHIUSE. Sono anche la prova che serviva per
dire che la finestra e' ricostruibile.
nice -n 19 timeout 900 uv run python scripts/research/r0822_monitor_audit.py
"""
from __future__ import annotations
import csv
import importlib.util
import json
import sys
from datetime import datetime, timezone
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))
ANN = np.sqrt(365.0)
NOW = datetime.now(timezone.utc)
# --------------------------------------------------------------------------------------------
# Caricamento SOLA LETTURA di un modulo di produzione: si esegue il modulo (definizioni), mai
# `main()`. `_append` viene sostituito subito dopo, prima di qualunque chiamata ad `advance`.
# --------------------------------------------------------------------------------------------
_MODS: dict[str, object] = {}
def live(name: str):
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` dei monitor: raccoglie in memoria invece di scrivere su disco.
E' l'unica ragione per cui questo audit puo' rieseguire il codice di produzione TALE E QUALE
(nessuna reimplementazione -> nessun drift fra audit e monitor) 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]:
if not p.exists():
return []
return [json.loads(x) for x in p.read_text().splitlines() if x.strip()]
def read_equity_csv(p: Path, col: int = 1) -> tuple[list[pd.Timestamp], np.ndarray]:
"""(date, equity) da un equity.csv dei paper_portfolio / paper_combo."""
dts, eq = [], []
with p.open() as fh:
for row in csv.reader(fh):
if not row or row[0] == "date":
continue
try:
v = float(row[col])
except (ValueError, IndexError):
continue
dts.append(pd.Timestamp(row[0]))
eq.append(v)
return dts, np.asarray(eq, float)
# --------------------------------------------------------------------------------------------
# Statistica del confronto registrato-vs-replay. La domanda non e' "coincide?" ma "quanta parte
# della giornata e' finita nella serie": in radice di tempo il rapporto di VARIANZA fra le due
# serie e' la frazione di giornata misurata (ipotesi: random walk sull'intervallo troncato).
# --------------------------------------------------------------------------------------------
def compare(rec: np.ndarray, rep: np.ndarray, mask: np.ndarray | None = None) -> dict:
ok = np.isfinite(rec) & np.isfinite(rep)
if mask is not None:
ok &= mask
a, b = rec[ok], rep[ok]
n = len(a)
if n < 3:
return dict(n=n, ident=0, corr=float("nan"), vr=float("nan"), minuti=float("nan"),
sd_rec=float("nan"), sd_rep=float("nan"), sh_rec=float("nan"), sh_rep=float("nan"))
ident = int((np.abs(a - b) <= 1e-6).sum())
corr = float(np.corrcoef(a, b)[0, 1]) if a.std() > 0 and b.std() > 0 else float("nan")
vr = float((a.std() / b.std()) ** 2) if b.std() > 0 else float("nan")
return dict(n=n, ident=ident, corr=corr, vr=vr, minuti=vr * 1440.0,
sd_rec=float(a.std() * ANN * 100), sd_rep=float(b.std() * ANN * 100),
sh_rec=float(a.mean() / a.std() * ANN) if a.std() > 0 else float("nan"),
sh_rep=float(b.mean() / b.std() * ANN) if b.std() > 0 else float("nan"))
def show(T: pd.DatetimeIndex, rec: np.ndarray, rep: np.ndarray, note: str = "") -> dict:
"""Stampa il confronto e ritorna la statistica sulla finestra PULITA (senza l'incidente)."""
c = compare(rec, rep)
fuori = ~((T >= FREEZE[0]) & (T <= FREEZE[1]))
cf = compare(rec, rep, mask=fuori)
print(f" barre appaiate {c['n']} identiche entro 1e-6: {c['ident']}/{c['n']} "
f"corr {c['corr']:+.4f}")
print(f" vol annualizzata REGISTRATA {c['sd_rec']:6.2f}% RICALCOLATA {c['sd_rep']:6.2f}%"
f" rapporto di varianza {c['vr']:.4f}")
print(f" -> il monitor misura ~{c['minuti']:,.0f} minuti di mercato al giorno (su 1440){note}")
if cf["n"] != c["n"]:
print(f" ESCLUSO l'incidente del feed 09-15/07 ({c['n'] - cf['n']} barre recuperate a "
f"posteriori, quindi CHIUSE):")
print(f" barre {cf['n']} identiche {cf['ident']}/{cf['n']} corr {cf['corr']:+.4f}"
f" vol reg. {cf['sd_rec']:.2f}% vs ric. {cf['sd_rep']:.2f}%"
f" -> ~{cf['minuti']:,.0f} min/giorno")
print(f" Sharpe della finestra: REGISTRATO {c['sh_rec']:+.2f} RICALCOLATO {c['sh_rep']:+.2f}"
f" [SE ~{np.sqrt(365.0 / max(c['n'], 1)):.2f}: NON e' una lettura di gate]")
return dict(all=c, clean=cf)
RES: dict[str, dict] = {} # riepilogo per la tabella finale
# --------------------------------------------------------------------------------------------
# L'INCIDENTE DEL FEED (2026-07-09 -> 2026-07-15). Non e' un'assunzione: e' documentato nel
# sorgente di produzione (`rebuild_history.py`, commento "incidente 2026-07-15: feed fermo 7g" —
# `shutil.copy2` del backup falliva con EPERM e ABORTIVA il rebuild) ed e' verificabile nel log
# del cron. Conta qui per una ragione precisa: durante il congelamento i monitor NON trovavano
# barre nuove, e al ripristino hanno recuperato in un colpo solo barre gia' CHIUSE — cioe' le
# uniche barre corrette delle loro serie. Vanno escluse da ogni stima del difetto, o si finisce
# per attribuire al difetto un'attenuazione che viene da un guasto diverso.
FREEZE = (pd.Timestamp("2026-07-09", tz="UTC"), pd.Timestamp("2026-07-16", tz="UTC"))
CRONLOG = ROOT / "logs" / "cron_daily.log"
def freeze_evidence() -> dict[str, int]:
"""Giorni in cui `rebuild_history` e' ABORTITO nel cron (letto dal log, non assunto)."""
out: dict[str, int] = {}
day = None
lines = CRONLOG.read_text(errors="replace").splitlines() if CRONLOG.exists() else []
for i, ln in enumerate(lines):
if ln.startswith("===== 2026-") and "cron_daily" in ln:
day = ln.split()[1][:10]
if day and "rebuild_history.py" in ln and "Traceback" in "".join(lines[max(0, i - 3):i]):
out[day] = out.get(day, 0) + 1
return out
# ============================================================================================
# A — PROVENIENZA: quanto e' completa l'ultima riga di ogni sorgente
# ============================================================================================
def sezione_A() -> dict:
print("=" * 102)
print(" A — DA DOVE VIENE L'ULTIMA BARRA (completezza misurata sul disco, non dedotta)")
print("=" * 102)
src = {}
print("\n A1. BTC/ETH — il feed 1h certificato (scritto da rebuild_history nel cron delle 00:30)")
for a in ("btc", "eth"):
d1 = pd.read_parquet(RAW / f"{a}_1h.parquet", columns=["timestamp", "volume"])
t1 = pd.to_datetime(d1["timestamp"], unit="ms", utc=True)
d5 = pd.read_parquet(RAW / f"{a}_5m.parquet", columns=["timestamp"])
t5 = pd.to_datetime(d5["timestamp"], unit="ms", utc=True)
last_h = t1.iloc[-1]
n5 = int(((t5 >= last_h) & (t5 < last_h + pd.Timedelta("1h"))).sum())
n1d = int((t1 >= last_h.normalize()).sum())
src[a] = dict(last=last_h, n5=n5, n1d=n1d, mtime=datetime.fromtimestamp(
(RAW / f"{a}_1h.parquet").stat().st_mtime, tz=timezone.utc))
print(f" {a.upper()} ultima barra 1h {last_h} barre 5m dentro: {n5}/12 "
f"(~{n5 * 5} minuti su 60)")
print(f" barre 1h dentro l'ultimo giorno UTC: {n1d}/24 -> la barra 1d che ne "
f"esce dal resample copre ~{n5 * 5 + (n1d - 1) * 60} minuti su 1440")
print(f" parquet scritto alle {src[a]['mtime']:%H:%M:%S} UTC")
print(" `resample_tf` (src/strategies/trend_portfolio.py:195) NON scarta la barra in corso —")
print(" lo dichiara il suo stesso docstring — e `altlib.get(asset,'1d')` ci passa dentro.")
print("\n A2. Hyperliquid 1d (scritto da fetch_hyperliquid nello stesso cron, END = oggi)")
pq = RAW / "hl_btc_1d.parquet"
d = pd.read_parquet(pq)
v = d["volume"].astype(float).values
tsl = pd.Timestamp(int(d["timestamp"].iloc[-1]), unit="ms", tz="UTC")
mt = pd.Timestamp(pq.stat().st_mtime, unit="s", tz="UTC")
q = float(v[-1] / np.median(v[-40:-1]))
orolog = (mt - tsl).total_seconds() / 60.0
src["hl"] = dict(last=tsl, frac=q, min_orologio=orolog)
print(f" hl_btc_1d ultima barra {tsl} parquet scritto alle {mt:%H:%M:%S} UTC")
print(f" misura da OROLOGIO (diretta): la barra era aperta da {orolog:.0f} minuti su 1440 "
f"quando e' stata scritta")
print(f" misura da VOLUME (proxy): {v[-1]:,.0f} = {q * 100:.1f}% del volume mediano di "
f"un giorno pieno -> {q * 1440:,.0f} minuti")
print(" ⚠ i due stimatori NON coincidono (fattore ~2) e la discrepanza va detta invece che")
print(" mediata: il volume dei primi 30 minuti UTC non e' 1/48 di quello giornaliero")
print(" (stagionalita' intragiornaliera). Il numero da usare e' quello da OROLOGIO — e'")
print(" una misura diretta del tempo trascorso; il volume e' solo un controllo di segno.")
print("\n A3. ETF/IB 1d (fetch_ib_equities, stesso cron ~00:35 UTC) e DVOL")
for sym in ("spy", "tlt"):
p = RAW / f"eq_{sym}_1d.parquet"
if not p.exists():
continue
e = pd.read_parquet(p)
last = pd.Timestamp(int(e["timestamp"].iloc[-1]), unit="ms", tz="UTC")
lag = (pd.Timestamp(NOW).normalize() - last.normalize()).days
src[f"eq_{sym}"] = dict(last=last, lag=lag)
print(f" eq_{sym}_1d ultima barra {last.date()} = {lag} giorni fa "
f"(la borsa USA chiude alle 20:00 UTC: alle 00:35 l'ultima barra e' COMPLETA)")
for a in ("btc", "eth"):
p = RAW / f"dvol_{a}.parquet"
if p.exists():
dv = pd.read_parquet(p)
tl = pd.Timestamp(int(dv["timestamp"].iloc[-1]), unit="ms", tz="UTC")
print(f" dvol_{a} ultima barra {tl}")
print("\n LETTURA: tre sorgenti su quattro consegnano un'ultima riga PROVVISORIA (crypto 1h,")
print(" crypto 1d, HL 1d); la quarta (IB) e' l'unica che consegna solo barre chiuse, e non per")
print(" merito del nostro codice ma perche' la borsa e' chiusa quando il cron gira.")
return src
# ============================================================================================
# B — IL DIFETTO MORDE? replay contro registrato, monitor per monitor
# ============================================================================================
def b_xsr():
print("\n" + "=" * 102)
print(" B1 — paper_xsr (CONTROLLO: difetto gia' stabilito da r0822_xsr_repro, qui replicato)")
print("=" * 102)
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()
st0 = 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(st0)
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))
print("\n pannello: `build_panel` -> `load_hl` -> parquet HL 1d (ultima riga = giorno in corso)")
print(f" inception {T[0].date()} = DOPO l'incidente del feed: la finestra e' gia' pulita.")
out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs]))
RES["paper_xsr"] = dict(parziale="SI", cmp=out["clean"], gate="XSR01 2026-10-23",
src="hl_*_1d.parquet", cad_h=24.0, ric="SI bit-exact",
rec=rec, rep=sink.of("returns.jsonl"))
def b_statarb():
print("\n" + "=" * 102)
print(" B2 — paper_statarb (gate PRE-REGISTRATO 2026-09-27)")
print("=" * 102)
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)
i0 = int(np.where(ts == st["start_ts"])[0][0])
sink = Sink(); ps._append = sink
st0 = dict(start_ts=st["start_ts"], last_ts=st["start_ts"], n_bars=0,
pos_modeled=float(pos[i0]), pos_real=float(pos[i0]),
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(st0, j)
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])
print("\n pannello: `build_joint('1d')` -> `altlib.get(...,'1d')` -> `resample_tf` "
"(ultima riga = 1 barra 1h su 24)")
out = show(T, A, B)
ident = np.abs(A - B) <= 1e-6
if ident.any():
gg = sorted({str(x.date()) for x in T[ident]})
print(f"\n 📌 LE UNICHE BARRE CORRETTE DELLA SERIE VENGONO DA UN GUASTO: le {int(ident.sum())} "
f"identiche\n sono CONSECUTIVE ({gg[0]} -> {gg[-1]}) e cadono dentro l'incidente "
"del feed.")
ev = sorted(freeze_evidence())
print(f" Prova indipendente dal log del cron: `rebuild_history` e' abortito in "
f"{len(ev)} giri consecutivi")
print(f" ({ev[0]} -> {ev[-1]}), EPERM sul backup — difetto gia' riparato nel sorgente.")
print(" Col feed fermo il monitor non trovava barre nuove; al ripristino ne ha")
print(" recuperate 7 in un colpo solo, di cui 6 GIA' CHIUSE -> coincidono al bit.")
print(" Due conseguenze: (a) il 'min/giorno' globale e' GONFIATO da quelle barre — la")
print(" riga 'ESCLUSO l'incidente' e' la stima onesta; (b) quelle 6 barre sono la prova")
print(" che, su barre chiuse, il codice di produzione riproduce il replay ESATTAMENTE.")
RES["paper_statarb"] = dict(parziale="SI", cmp=out["clean"], gate="STATARB 2026-09-27",
src="btc/eth 1h -> resample 1d", cad_h=24.0, ric="SI bit-exact",
rec=rec, rep=sink.of("returns.jsonl"))
def b_dvolspread():
print("\n" + "=" * 102)
print(" B3 — paper_dvolspread (kill PRE-REGISTRATO 2026-10-24, decisione 2027-01-24)")
print("=" * 102)
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()
i0 = int(np.where(P["ts"] == st["start_ts"])[0][0])
sink = Sink(); pv._append = sink
st0 = 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(P["wb"][i0]), wb_real=float(P["wb"][i0]),
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)
pv.advance(st0, P)
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))
print("\n pannello: `_panel` -> `ortholib.aligned` -> `altlib.get(...,'1d')` -> `resample_tf`")
print(f" inception {T[0].date()} = dopo l'incidente del feed: finestra gia' pulita, "
"nessuna barra recuperata.")
out = show(T, np.array([p[1] for p in pairs]), np.array([p[2] for p in pairs]))
print("\n ⚠ E' IL PEGGIORE DEI SEI, e non e' un caso: il libro e' uno SPREAD BTC-ETH e su 35")
print(" minuti le due gambe si muovono quasi insieme -> la parte idiosincratica, che e' cio'")
print(" che questo libro tenta di catturare, e' quasi tutta ancora da venire quando la barra")
print(" viene letta. Un libro dollar-neutral perde piu' di un libro direzionale a parita' di")
print(" troncamento — il numero di sopra non e' 35/1440 ma un ordine di grandezza sotto.")
print("\n ⚠ La contabilita' a 3 stati (ATTIVE / flat-da-segnale / flat-SENZA-DATO) e il veto")
print(" d'integrita' all'80% funzionano e restano validi: contano se il DVOL c'era. Non")
print(" possono vedere QUANTO dura la barra su cui il DVOL c'era — sono ortogonali a questo")
print(" difetto, ed e' il motivo per cui l'unico monitor con una guardia dedicata e'")
print(" compromesso quanto quelli senza.")
RES["paper_dvolspread"] = dict(parziale="SI", cmp=out["clean"],
gate="DVOLSPREAD 2026-10-24 / 2027-01-24",
src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0,
ric="SI bit-exact", rec=rec, rep=sink.of("returns.jsonl"))
def b_prevday():
print("\n" + "=" * 102)
print(" B4 — paper_prevday (griglia ORARIA: la patologia puo' essere diversa — si misura)")
print("=" * 102)
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]
A = np.array([p[1] for p in pairs]); B = np.array([p[2] for p in pairs])
T = pd.DatetimeIndex(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True))
print("\n pannello: `build_bars` -> `harness.load(asset,'1h')` = il parquet 1h GREZZO, "
"nessun resample")
out = show(T, A, B, note=" <- 24 barre/giorno, quindi il conto e' sull'INTERA giornata")
diff = np.abs(A - B) > 1e-6
print(f"\n DOVE cade la divergenza: {int(diff.sum())} barre su {len(A)} "
f"({diff.mean() * 100:.1f}%), |scarto| mediano "
f"{np.median(np.abs(A - B)[diff]):.2e} su |ritorno| mediano {np.median(np.abs(B)):.2e}")
vc = pd.Series(T.hour[diff]).value_counts().sort_index()
print(" ora UTC delle barre divergenti: " +
", ".join(f"{int(h):02d}:00 x{int(n)}" for h, n in vc.items()))
g = pd.DataFrame(dict(day=T.date, d=diff)).groupby("day")["d"].sum()
z, uno, due = (g == 0), (g == 1), (g == 2)
print(f" per giorno: {int(uno.sum())} giorni con 1 barra divergente, {int(due.sum())} con 2, "
f"{int(z.sum())} con 0 (totale {len(g)} giorni)")
print(f" i {int(due.sum())} giorni con DUE sono sempre 00:00 + 01:00 = il difetto si PROPAGA "
"una barra avanti,")
print(" perche' il bersaglio calcolato sulla barra parziale e' la posizione TENUTA in quella")
print(" dopo. (Il docstring di `prevday_breakout.current_target` dice 'ultima barra chiusa':")
print(" quinto docstring di produzione che dichiara una causalita' che il codice non ha.)")
zg = [str(x) for x in g[z].index]
print(f" i {int(z.sum())} giorni con ZERO divergenze: {', '.join(zg)}")
print(" = inception + oggi + i 7 giorni dell'incidente del feed (barre recuperate CHIUSE).")
print(f"\n ⚠ LA META' CHE CONTA DI PIU': {int((~diff).sum())} barre su {len(A)} coincidono BIT "
"A BIT col replay")
print(" fatto oggi, dopo 62 notti di riscrittura del parquet. E' la PROVA che il feed BTC/ETH")
print(" certificato non tocca le barre gia' chiuse — senza la quale la ricostruzione della")
print(" sezione E sarebbe un'ipotesi invece di un fatto.")
print(" 📌 E il verdetto operativo e' l'opposto degli altri quattro: su griglia oraria il difetto")
print(" c'e' ma vale ~0.1% della varianza. `paper_prevday` NON e' da rigenerare.")
RES["paper_prevday"] = dict(parziale="SI", cmp=out["clean"], gate="nessuno (lead in monitor)",
src="btc/eth 1h grezzo", cad_h=1.0, ric="SI bit-exact")
def b_portfolio():
print("\n" + "=" * 102)
print(" B5 — paper_portfolio (book di RICERCA a 5 sleeve: TP01+XS01+VRP01+SKH01+GTAA01)")
print("=" * 102)
ppf = live("paper_portfolio")
dts, eq = read_equity_csv(ROOT / "data" / "paper_portfolio" / "equity.csv")
_, r = ppf.portfolio_daily()
ridx = {pd.Timestamp(k): float(v) for k, v in r.items()}
A, B, D = [], [], []
for i in range(1, len(dts)):
d = dts[i]
if d in ridx:
A.append(eq[i] / eq[i - 1] - 1.0)
B.append(ridx[d])
D.append(d)
T = pd.DatetimeIndex(D)
print("\n serie: `StrategyPortfolio(active_sleeves()).combined_daily()`; i rendimenti "
"registrati si\n ricavano da equity.csv (il monitor salva l'equity, non i ritorni)")
A = np.array(A); B = np.array(B)
out = show(T, A, B)
ident = np.abs(A - B) <= 1e-6
zeri = ident & (np.abs(B) <= 1e-12)
if ident.any():
print(f"\n ⚠ TRAPPOLA DA NON PRENDERE: delle {int(ident.sum())} barre 'identiche', "
f"{int(zeri.sum())} sono ZERO contro ZERO")
print(" (" + ", ".join(str(x.date()) for x in T[zeri]) + ")")
print(" = giorni in cui TUTTI e cinque gli sleeve erano flat insieme (TP01 risk-off,")
print(" GTAA a borsa chiusa nel weekend, VRP fuori scadenza).")
print(" Contarle come 'coincidono' sarebbe la trappola gia' codificata dal progetto —")
print(" barre ATTIVE, non barre di calendario (26/07, il gate che segnalo' VRP01 perche'")
print(" contava il 94% di zeri). Su barre attive le coincidenze qui sono "
f"{int((ident & ~zeri).sum())}.")
print("\n ⚠ ULTIMA RIGA ESCLUSA DAL GIUDIZIO? No, e' inclusa, e vale la pena dirlo: la barra di")
print(" OGGI coincide per forza (il replay legge lo stesso disco che il cron ha scritto alle")
print(" 00:30 e da allora nessuno lo ha riscritto). Un audit che girasse SOLO sull'ultima")
print(" barra concluderebbe 'tutto a posto'. La potenza sta nelle barre di IERI e prima.")
print(" ⚠ Ricostruzione NON bit-exact per questo monitor: uno dei cinque sleeve e' GTAA01, che")
print(" legge `eq_*_1d` da IB con `ADJUSTED_LAST` — ri-aggiustato all'indietro a ogni stacco")
print(" di dividendo (difetto gia' incontrato il 07/08 su TLT). La finestra si ricostruisce,")
print(" ma la parte equity non tornera' identica al centesimo, e va detto.")
RES["paper_portfolio"] = dict(parziale="SI", cmp=out["clean"],
gate="nessuno (dashboard/book di ricerca)",
src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0,
ric="SI non al bit")
def b_combo():
print("\n" + "=" * 102)
print(" B6 — paper_combo (calendario di BORSA: TP01 Deribit + GTAA IB)")
print("=" * 102)
pc = live("paper_combo")
dts, eq = read_equity_csv(ROOT / "data" / "paper_combo" / "equity.csv", col=1)
naked, _ = pc.both_daily()
ridx = {pd.Timestamp(k): float(v) for k, v in naked.items()}
A, B, D = [], [], []
for i in range(1, len(dts)):
d = dts[i]
if d in ridx:
A.append(eq[i] / eq[i - 1] - 1.0)
B.append(ridx[d])
D.append(d)
A = np.array(A); B = np.array(B); T = pd.DatetimeIndex(D)
print("\n serie: `combo_daily()` = TP01 compoundato sulla griglia dei GIORNI DI BORSA + GTAA")
out = show(T, A, B)
dif = np.abs(A - B)
print(f"\n Le barre non identiche NON sono il difetto della barra parziale: |scarto| mediano "
f"{np.median(dif):.2e}\n contro |ritorno| mediano {np.median(np.abs(B)):.2e} "
f"(= {np.median(dif) / np.median(np.abs(B)) * 100:.0f}% del ritorno tipico, massimo "
f"{dif.max():.2e}) e si concentrano\n nella finestra dell'incidente del feed + nei giorni "
"in cui IB ha ri-aggiustato `ADJUSTED_LAST`.")
tp_last = pd.Timestamp(list(ridx.keys())[-1])
print(f"\n PERCHE' QUESTO E' DIVERSO. La griglia e' `eq.index` (i giorni di borsa di IB),")
print(f" non il calendario crypto: l'ultima riga della serie e' {tp_last.date()}, mentre il")
print(f" feed crypto su disco arriva a oggi. Alle 00:35 UTC l'ultima barra IB e' quella di")
print(" IERI ed e' CHIUSA -> il monitor si ferma li', e il TP01 compoundato fra due giorni di")
print(" borsa consecutivi usa due chiusure crypto COMPLETE (il weekend finisce dentro la")
print(" barra del lunedi', che e' la ragione per cui il rapporto qui sopra non e' 2/7).")
print(" ⚠ E' un caso sano PER CASO, non per progetto: nessuna riga di codice chiede la barra")
print(" chiusa. Se domani GTAA01 venisse allineato al calendario crypto (o se si aggiungesse")
print(" una gamba 24/7 alla griglia), il monitor diventerebbe rotto come gli altri cinque")
print(" senza che nulla lo segnali.")
RES["paper_combo"] = dict(parziale="NO", cmp=out["clean"], gate="nessuno (paper cross-venue)",
src="TP01 1h->1d su griglia IB", cad_h=24.0,
ric="SI non al bit")
# ============================================================================================
# C — TABELLA DI SINTESI
# ============================================================================================
GATE_INFO = {
"paper_xsr": ("XSR01 2026-10-23", "Sharpe>=1.0 E haircut $5.000<=40%"),
"paper_statarb": ("STATARB 2026-09-27", "Sharpe di net_modeled >= 0.5"),
"paper_dvolspread": ("DVOLSPREAD 24/10 kill", "Sharpe<-0.50 -> ritiro; 27/01 decisione piena"),
"paper_prevday": ("-", "lead senza data"),
"paper_portfolio": ("-", "dashboard"),
"paper_combo": ("-", "paper cross-venue"),
}
def sezione_C():
print("\n" + "=" * 102)
print(" C — TABELLA: una riga per monitor")
print("=" * 102)
print("\n Le colonne sono calcolate ESCLUDENDO l'incidente del feed 09-15/07 (le barre")
print(" recuperate a posteriori sono chiuse e attenuerebbero il difetto per la ragione")
print(" sbagliata). 'min/g' = minuti di mercato al giorno effettivamente misurati, su 1440.")
print(f"\n {'monitor':<19}{'barra parz.':>13}{'ident./tot':>13}{'corr':>9}{'min/g':>8}"
f"{'ricostruibile':>17} gate dipendente")
for k, v in RES.items():
c = v["cmp"]
ratio = "{}/{}".format(c["ident"], c["n"])
print(f" {k:<19}{v['parziale']:>13}{ratio:>13}{c['corr']:>+9.3f}"
f"{c['minuti']:>8,.0f}{v['ric']:>17} {GATE_INFO[k][0]}")
print("\n 'ricostruibile' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare")
print(" oggi dai feed su disco: e' letteralmente la colonna 'replay' di questo script.")
print(" 'non al bit' = ricostruibile, ma una gamba viene dal feed equity IB, che e'")
print(" ri-aggiustato all'indietro (`ADJUSTED_LAST`) -> i centesimi non torneranno uguali.")
print("\n LETTURA IN UNA RIGA: quattro monitor su sei registrano fra 2 e 41 minuti di mercato")
print(" al giorno spacciandoli per 24 ore; i due che non lo fanno sono quello a griglia oraria")
print(" (dove il difetto vale una barra su 24) e quello che aspetta la chiusura di una BORSA.")
def sezione_C2():
"""Il criterio di ciascun gate PRE-REGISTRATO resta leggibile? Le soglie NON sono ridichiarate
qui: si IMPORTANO dagli script di gate e dal monitor (regola del progetto — un sorvegliante
deriva il proprio bersaglio dal codice sorvegliato, mai lo ridichiara: 21/08, `fee_watch`)."""
print("\n" + "=" * 102)
print(" C2 — IL CRITERIO DEI TRE GATE PRE-REGISTRATI E' ANCORA LEGGIBILE?")
print("=" * 102)
import r0724_statarb_deploy_gate as GS # noqa: E402 (solo costanti: ha il main-guard)
import r0725_xsr_deploy_gate as GX # noqa: E402
pv = live("paper_dvolspread")
def metriche(rows: list[dict], key_m="net_modeled", key_r=None):
rm = np.array([float(r[key_m]) for r in rows])
eq = np.cumprod(1 + rm)
pk = np.maximum.accumulate(eq)
out = dict(sh=float(rm.mean() / rm.std() * np.sqrt(365)) if rm.std() > 0 else 0.0,
dd=float(np.max((pk - eq) / pk)), tot=float(np.prod(1 + rm) - 1))
if key_r:
rr = np.array([float(r.get(key_r, np.nan)) for r in rows])
rr = rr[np.isfinite(rr)]
out["tot_r"] = float(np.prod(1 + rr) - 1) if len(rr) else float("nan")
return out
def coppia(name):
rec = RES[name]["rec"]
rep = {int(r["ts"]): r for r in RES[name]["rep"]}
keep = [r for r in rec if int(r["ts"]) in rep]
return keep, [rep[int(r["ts"])] for r in keep]
print(f"\n {'gate':<26}{'metrica primaria':<26}{'soglia':>10}{'REGISTR.':>11}"
f"{'RICOSTR.':>11} verdetto")
# --- STATARB -------------------------------------------------------------------------
a, b = coppia("paper_statarb")
ma, mb = metriche(a, key_r="net_real"), metriche(b, key_r="net_real")
flip = (ma["sh"] >= GS.SH_DEPLOY) != (mb["sh"] >= GS.SH_DEPLOY)
print(f" {'STATARB ' + str(GS.DECISION):<26}{'Sharpe net_modeled':<26}"
f"{'>= ' + str(GS.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} "
f"{'*** SI RIBALTA ***' if flip else 'stesso'}")
print(f" {'':<26}{'maxDD':<26}{'< ' + f'{GS.DD_MAX:.0%}':>10}{ma['dd']:>10.1%}"
f"{mb['dd']:>11.1%} "
f"{'*** SI RIBALTA ***' if (ma['dd'] < GS.DD_MAX) != (mb['dd'] < GS.DD_MAX) else 'stesso'}")
hp_a = abs((1 + ma["tot"]) - (1 + ma["tot_r"])) * 100
hp_b = abs((1 + mb["tot"]) - (1 + mb["tot_r"])) * 100
print(f" {'':<26}{'fill-haircut cum (pp)':<26}{'< ' + str(GS.HAIRCUT_MAX_PP):>10}"
f"{hp_a:>11.2f}{hp_b:>11.2f} "
f"{'*** SI RIBALTA ***' if (hp_a < GS.HAIRCUT_MAX_PP) != (hp_b < GS.HAIRCUT_MAX_PP) else 'stesso'}")
# --- XSR01 ---------------------------------------------------------------------------
a, b = coppia("paper_xsr")
K5 = "net_REAL-$5000"
ma, mb = metriche(a, key_r=K5), metriche(b, key_r=K5)
hc_a = (ma["tot"] - ma["tot_r"]) / abs(ma["tot"]) if ma["tot"] else float("nan")
hc_b = (mb["tot"] - mb["tot_r"]) / abs(mb["tot"]) if mb["tot"] else float("nan")
fl1 = (ma["sh"] >= GX.SH_DEPLOY) != (mb["sh"] >= GX.SH_DEPLOY)
fl2 = (hc_a <= GX.HAIRCUT_MAX) != (hc_b <= GX.HAIRCUT_MAX)
print(f" {'XSR01 ' + str(GX.DECISION):<26}{'Sharpe net_modeled':<26}"
f"{'>= ' + str(GX.SH_DEPLOY):>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} "
f"{'*** SI RIBALTA ***' if fl1 else 'stesso'}")
print(f" {'':<26}{'haircut $5.000':<26}{'<= ' + f'{GX.HAIRCUT_MAX:.0%}':>10}"
f"{hc_a:>10.1%}{hc_b:>11.1%} {'*** SI RIBALTA ***' if fl2 else 'stesso'}")
# --- DVOLSPREAD ----------------------------------------------------------------------
a, b = coppia("paper_dvolspread")
ma, mb = metriche(a), metriche(b)
fl = (ma["sh"] < pv.KILL_SHARPE) != (mb["sh"] < pv.KILL_SHARPE)
print(f" {'DVOLSPREAD ' + pv.KILL_DATE:<26}{'Sharpe fwd (kill se sotto)':<26}"
f"{'> ' + f'{pv.KILL_SHARPE:+.2f}':>10}{ma['sh']:>+11.2f}{mb['sh']:>+11.2f} "
f"{'*** SI RIBALTA ***' if fl else 'stesso (KILL in entrambe)'}")
print("""
⚠ QUESTA NON E' UNA LETTURA DEI GATE, e non deve diventarlo: le tre date sono nel futuro
(anticiparle sarebbe selezione-sul-forward, contro la regola del progetto) e con 28-54 barre
l'errore standard di uno Sharpe annualizzato e' 2,6-3,6. Serve a una cosa sola, e la mostra:
il difetto NON e' rumore centrato — sposta la metrica primaria abbastanza da attraversare la
soglia. Su STATARB la serie registrata dice +1,96 (sopra la soglia di deploy 0,5) e quella
ricostruita dice -2,02: **stesso codice, stessa finestra, verdetto opposto**.
⚠ E c'e' un secondo segnale, gratis: uno Sharpe registrato di -15,8 su 28 barre e' IMPOSSIBILE
(|Sharpe| oltre 4 SE). Il gate `implausible_sharpe` esiste gia' in `altlib` dal 26/07 ma
nessuno lo punta sulle serie FORWARD — solo sui candidati. Un numero fuori scala e' un
rilevatore di guasti a costo zero, e in questo caso era li' da leggere da settimane.""")
# ============================================================================================
# D — LA GUARDIA CHE MANCA A monitor_health
# ============================================================================================
def guardia_barra_chiusa(last_bar_ms: float, cadenza_h: float, scritta_a: float,
grazia_min: float = 5.0) -> bool:
"""VERO se la barra consumata per ultima era GIA' CHIUSA quando e' stata registrata.
`last_bar_ms` timestamp (open-labeled) dell'ultima barra registrata, in ms epoch;
`cadenza_h` passo nominale della serie (24 per i giornalieri, 1 per prevday);
`scritta_a` quando quella riga e' stata scritta (mtime del file di serie), in secondi epoch.
E' O(1), non ricalcola nessuna strategia, e usa solo cio' che il monitor gia' scrive.
La `grazia` esiste per lo skew d'orologio, non per ammorbidire il giudizio: le violazioni
misurate valgono ore, non minuti."""
chiusura = last_bar_ms / 1000.0 + cadenza_h * 3600.0
return chiusura <= scritta_a + grazia_min * 60.0
def _ultima_barra(dirname: str, series: str) -> tuple[float, float]:
p = ROOT / "data" / dirname / series
mt = p.stat().st_mtime
if series.endswith(".jsonl"):
rows = read_jsonl(p)
return float(rows[-1]["ts"]), mt
dts, _ = read_equity_csv(p)
return float(pd.Timestamp(dts[-1]).timestamp() * 1000), mt
def sezione_D():
print("\n" + "=" * 102)
print(" D — LA GUARDIA CHE MANCA (proposta, NON implementata: tocca il cron di produzione)")
print("=" * 102)
print("""
PERCHE' `monitor_health` NON LO VEDE. Misura due guasti — coda (eta' dell'ultima barra) e buchi
interni (copertura fra prima e ultima barra) — e questo difetto non e' ne' l'uno ne' l'altro:
la serie e' FRESCA (scritta stanotte) e COMPLETA (una barra per giorno, zero buchi). Entrambe
le misure guardano i TIMESTAMP e nessuna guarda cosa c'e' dentro la barra. Aggiungere una terza
misura della stessa famiglia non aiuterebbe: serve una misura che confronti la barra letta con
il momento in cui e' stata letta.
DUE CANDIDATI, e non sono equivalenti.""")
# ---- candidato 1: vol registrata vs vol ricalcolata --------------------------------------
print("\n D1. CANDIDATO 'RAPPORTO DI VOLATILITA'' (vol registrata / vol ricalcolata).")
print(f" {'monitor':<19}{'vol reg.':>10}{'vol ric.':>10}{'rapporto':>10} verdetto a "
"soglia [0.70, 1.40]")
lo, hi = 0.70, 1.40
miss = []
for k, v in RES.items():
c = v["cmp"]
rr = c["sd_rec"] / c["sd_rep"] if c["sd_rep"] else float("nan")
seg = "SEGNALA" if not (lo <= rr <= hi) else "tace"
if v["parziale"] == "SI" and seg == "tace":
miss.append(k)
print(f" {k:<19}{c['sd_rec']:>9.2f}%{c['sd_rep']:>9.2f}%{rr:>10.3f} {seg}")
rotti = sum(1 for v in RES.values() if v["parziale"] == "SI")
print(f" -> segnala {rotti - len(miss)} dei {rotti} monitor che leggono una barra parziale,")
print(f" NON segnala il sano (`paper_combo`, 0.987), e MANCA {len(miss)}: "
f"{', '.join(miss) if miss else '-'}")
print(" Motivo strutturale, non taratura: su una griglia oraria il difetto tocca 1 barra")
print(" su 24 e sposta la vol dell'1-2%, cioe' DENTRO il rumore di stima di una vol su")
print(" poche settimane. Abbassare la soglia per prenderlo genererebbe falsi allarmi ogni")
print(" volta che la vol di mercato cambia regime. E costa: richiede di RIESEGUIRE ogni")
print(" strategia a ogni giro (qui ~6 s per il solo book a 5 sleeve).")
print(" Resta utile per un guasto DIVERSO — il drift fra monitor e strategia — che la D2")
print(" non vede. Va aggiunta DOPO, non al posto della D2.")
# ---- candidato 2: barra chiusa ------------------------------------------------------------
print("\n D2. CANDIDATO 'BARRA CHIUSA' — RACCOMANDATO.")
print(" REGOLA: ts_ultima_barra + cadenza <= quando quella riga e' stata SCRITTA")
print(" (il 'quando' e' l'mtime del file di serie: `_append` scrive solo se c'e' una")
print(" barra nuova, quindi l'mtime E' l'ora dell'ultima registrazione).")
print(" Soglia: grazia 5 minuti per lo skew d'orologio. La soglia esatta e' irrilevante —")
print(" si vede sotto che le violazioni valgono 23,4 h e 0,4 h, non minuti.")
specs = [("paper_xsr", "returns.jsonl", 24.0), ("paper_statarb", "returns.jsonl", 24.0),
("paper_dvolspread", "returns.jsonl", 24.0), ("paper_prevday", "returns.jsonl", 1.0),
("paper_portfolio", "equity.csv", 24.0), ("paper_combo", "equity.csv", 24.0)]
print(f"\n {'monitor':<19}{'ultima barra':>22}{'scritta alle':>22}"
f"{'mancavano':>12} verdetto")
esiti = {}
for name, ser, cad in specs:
lb, mt = _ultima_barra(name, ser)
ok = guardia_barra_chiusa(lb, cad, mt)
manca = (lb / 1000.0 + cad * 3600.0 - mt) / 3600.0
esiti[name] = ok
print(f" {name:<19}"
f"{str(pd.Timestamp(lb, unit='ms', tz='UTC'))[:19]:>22}"
f"{str(pd.Timestamp(mt, unit='s', tz='UTC'))[:19]:>22}"
f"{max(manca, 0):>11.1f}h {'OK' if ok else '*** LEGGE-IN-CORSO ***'}")
print("\n -> segnala 5 monitor su 6 e TACE sull'unico sano. Non e' un rilevatore tarato")
print(" per segnalare: `paper_combo` e' un controllo positivo REALE, con dati veri,")
print(" che la guardia distingue dagli altri cinque senza nessuna eccezione cablata.")
# ---- controllo positivo sintetico, nei due versi ------------------------------------------
print("\n D3. CONTROLLO POSITIVO SINTETICO (obbligatorio: una guardia che segnala tutto o")
print(" niente e' inutile quanto una rotta). Casi costruiti, nei DUE versi:")
base = pd.Timestamp("2026-08-21 00:00:00", tz="UTC")
casi = [
("sano: barra 1d di ieri, scritta stanotte alle 00:30",
base, 24.0, pd.Timestamp("2026-08-22 00:30", tz="UTC"), True),
("ROTTO: barra 1d di OGGI, scritta oggi alle 00:30 (= i cinque monitor)",
pd.Timestamp("2026-08-22 00:00", tz="UTC"), 24.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), False),
("sano: barra 1h delle 23:00, scritta a mezzanotte e mezza",
pd.Timestamp("2026-08-21 23:00", tz="UTC"), 1.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), True),
("ROTTO: barra 1h delle 00:00, scritta alle 00:30 (= paper_prevday)",
pd.Timestamp("2026-08-22 00:00", tz="UTC"), 1.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), False),
("sano: monitor in ritardo di 3 giorni (fermo, ma non legge una barra aperta)",
pd.Timestamp("2026-08-18 00:00", tz="UTC"), 24.0,
pd.Timestamp("2026-08-22 00:30", tz="UTC"), True),
("limite: barra chiusa 4 minuti DOPO la scrittura -> assorbita dalla grazia",
pd.Timestamp("2026-08-21 00:04", tz="UTC"), 24.0,
pd.Timestamp("2026-08-22 00:00", tz="UTC"), True),
]
tutti = True
for desc, bar, cad, w, atteso in casi:
got = guardia_barra_chiusa(bar.timestamp() * 1000, cad, w.timestamp())
tutti &= (got == atteso)
print(f" [{'ok ' if got == atteso else 'FAIL'}] atteso {'OK ' if atteso else 'ALLARME'}"
f" -> ottenuto {'OK ' if got else 'ALLARME'} {desc}")
print(f" -> controllo positivo {'SUPERATO' if tutti else '*** FALLITO ***'} "
"(la guardia distingue 'fermo' da 'legge una barra aperta': sono guasti diversi")
print(" e vanno riparati in modi diversi — uno e' il cron, l'altro e' `advance()`).")
print("\n D4. COSA NON COPRE, dichiarato. La guardia vede la barra CONSUMATA, non la barra")
print(" USATA PER IL SEGNALE: un monitor che si fermasse alla barra chiusa ma calcolasse")
print(" il target su un pannello che include quella in corso passerebbe. Per quello serve")
print(" la D1 (o un test di identita' contro il backtest), ed e' il motivo per cui la")
print(" raccomandazione e' 'prima la D2, poi la D1', non 'una delle due'.")
# ============================================================================================
# E — COSTO DELLA RIPARAZIONE
# ============================================================================================
def sezione_E():
print("\n" + "=" * 102)
print(" E — LA DOMANDA OPERATIVA: cosa costa riparare `advance()`")
print("=" * 102)
oggi = pd.Timestamp(NOW).normalize().tz_localize(None)
righe = [
("paper_statarb", "STATARB", pd.Timestamp("2026-09-27"), "returns.jsonl"),
("paper_xsr", "XSR01", pd.Timestamp("2026-10-23"), "returns.jsonl"),
("paper_dvolspread", "DVOLSPREAD (kill)", pd.Timestamp("2026-10-24"), "returns.jsonl"),
("paper_dvolspread", "DVOLSPREAD (decisione)", pd.Timestamp("2027-01-24"), "returns.jsonl"),
]
print("\n IPOTESI PESSIMISTA — si ripara `advance()` e si fa `--reset` (la finestra riparte da")
print(" zero oggi). Con una barra al giorno, tornare a N barre richiede N giorni:")
print(f"\n {'monitor / gate':<42}{'barre':>7}{'data gate':>13}{'reset -> N barre':>18}"
f"{'ritardo':>10}")
ritardi = []
for mon, nome, gate, ser in righe:
n = len(read_jsonl(ROOT / "data" / mon / ser))
rip = oggi + pd.Timedelta(days=n)
rit = (rip - gate).days
ritardi.append((nome, rit))
print(f" {f'{mon} / {nome}':<42}{n:>7}{str(gate.date()):>13}"
f"{str(rip.date()):>18}{rit:>+9}g")
peggiore = max(ritardi, key=lambda x: x[1])
print(f"\n Anche nell'ipotesi pessimista il danno NON e' uniforme: l'unico gate che slitterebbe")
print(f" e' {peggiore[0]} ({peggiore[1]:+d} giorni) — gli altri hanno finestre cosi' giovani che")
print(" ricostruirebbero la propria numerosita' PRIMA della data di decisione. Ma la colonna")
print(" giusta e' ancora un'altra, e toglie di mezzo anche quel ritardo:")
print("""
✅ LA FINESTRA FORWARD **NON VA PERSA**: e' RICOSTRUIBILE, per tutti e sei i monitor.
Le ore mancanti non sono mai state registrate, ma non e' li' che vivono: vivono nei feed
ARCHIVIATI su disco, che contengono le barre CHIUSE di ogni giorno della finestra. Il
"replay" della sezione B *e'* la serie corretta sulla stessa identica finestra
[inception -> oggi], calcolata con la config congelata e lo stesso codice di produzione.
Ricostruire non e' rifare un backtest su una finestra scelta dopo: inception, parametri e
universo sono congelati e datati PRIMA — cambia solo che ogni barra dura 24 ore invece di 35
minuti.
LE DUE CONDIZIONI, ed e' giusto chiedere che siano verificate e non assunte:
(1) il feed non riscrive le barre gia' chiuse. **MISURATO qui, due volte e in modo
indipendente**: `paper_prevday` ha 1427 barre su 1488 identiche BIT A BIT al replay
dopo 62 notti di riscrittura del parquet BTC/ETH (B4); e le 6 barre che `paper_statarb`
recupero' durante l'incidente del feed coincidono anch'esse al bit (B2) — due percorsi
diversi, stesso verdetto. Per Hyperliquid la misura e' in `r0822_xsr_repro` T2 (Sharpe
2024 e 2025 riprodotti al centesimo su 731 barre).
(2) la config e' congelata e verificabile: `paper_dvolspread` lo controlla da solo
(`frozen` nello stato, esce 1 se diverge); per gli altri il congelamento e' nel
sorgente ed e' in git.
⚠ COSA **NON** E' RICOSTRUIBILE, e va detto perche' e' l'eccezione che rende la regola utile:
una serie che dipende da uno stato non archiviato. Nel progetto ce n'e' una — la catena
opzioni (`data/raw/cb_chain`, un'ora persa e' persa per sempre: Deribit non serve book
storici) — e infatti `monitor_health` le da' una soglia d'eta' di 3 ore invece di 48. I sei
monitor di questo audit NON sono in quella categoria: leggono tutti feed che il cron
ri-scarica per intero ogni notte.
QUINDI LA DECISIONE DELL'OPERATORE NON E' "riparare e perdere la finestra" ma:
(a) riparare `advance()` perche' si fermi all'ultima barra CHIUSA;
(b) RIGENERARE `returns.jsonl` / `equity.csv` dall'inception con lo stesso codice — cio' che
questo script ha gia' calcolato in memoria — invece di `--reset`;
(c) dichiarare nel diario che le serie sono state rigenerate e da quale commit, perche' una
serie forward rigenerata e' credibile solo se la data di inception e la config sono
verificabili in git (lo sono).
Il costo vero e' (c), non i giorni: nessuna delle tre date di gate si sposta.
⚠ UNA COSA CHE LA RIGENERAZIONE CAMBIA DAVVERO, ma di poco — e il "di poco" e' MISURATO, non
assunto. I libri REAL saltano i ribilanci sotto il min-order e quella decisione dipende dal
capitale corrente: il percorso REAL rigenerato non e' "lo stesso numero calcolato meglio", e'
un secondo percorso deterministico. Contava sapere quanto, perche' il gate XSR01 del 23/10
legge `net_REAL-$5000` per l'haircut: misurato in C2, **1,7% registrato contro 1,8%
ricostruito**. Trascurabile a $5.000 (i ticket superano il min-order comunque). Il caveat
resta vero in linea di principio e va scritto nel diario; non e' un ostacolo alla decisione.
COSA FARE, MONITOR PER MONITOR (raccomandazione, non azione — questo script non tocca nulla).
⚠ Le due cose sono SEPARATE: la RIPARAZIONE di `advance()` (fermarsi all'ultima barra chiusa)
ha senso su tutti e sei — su un monitor sano non cambia niente e lo protegge da un cambio di
griglia futuro; la RIGENERAZIONE della serie e' un'altra decisione, e va fatta solo dove il
difetto morde abbastanza da spostare cio' che si leggera':
paper_xsr rigenerare — 41 min/g, e il gate del 23/10 legge questa serie
paper_statarb rigenerare — 4 min/g, gate 27/09: e' il piu' vicino nel tempo
paper_dvolspread rigenerare — 2 min/g, il peggiore dei sei; kill-check il 24/10
paper_prevday LASCIARE — 1.438/1.440 min/g: la rigenerazione cambierebbe lo 0,1%
della varianza e azzererebbe 1.488 barre di storia
paper_portfolio rigenerare — 19 min/g; nessun gate, ma e' la serie che il dashboard
mostra e che finisce nei diari come "il book sta facendo X"
paper_combo LASCIARE — sano; semmai va PROTETTO con un test che rompa se la sua
griglia smette di essere il calendario di borsa""")
def main() -> None:
print("\n" + "#" * 102)
print("# r0822_monitor_audit — SOLA LETTURA su scripts/live, src/live, data, config, cron")
print(f"# ora UTC {NOW:%Y-%m-%d %H:%M:%S} (il cron_daily di oggi ha girato alle 00:30)")
print("#" * 102 + "\n")
sezione_A()
b_xsr()
b_statarb()
b_dvolspread()
b_prevday()
b_portfolio()
b_combo()
sezione_C()
sezione_C2()
sezione_D()
sezione_E()
print("\n" + "=" * 102)
print(" FINE — nessun file di produzione e' stato modificato da questo script.")
print("=" * 102 + "\n")
if __name__ == "__main__":
main()