Files
PythagorasGoal/scripts/research/r0822_monitor_audit.py
T

702 lines
39 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.35)
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: che i tre monitor giornalieri su feed crypto (xsr, statarb,
dvolspread) avessero la stessa patologia identica; che `paper_prevday`, essendo su griglia 1h con
24 barre al giorno, ne avesse una MOLTO piu' mite (una barra su 24 troncata); e che
`paper_combo`, che vive sul calendario di borsa, fosse o rotto come gli altri o sano per caso.
Solo la terza aspettativa e' stata una sorpresa.
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)")
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 = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep]
c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs]))
print("\n pannello: `build_joint('1d')` -> `altlib.get(...,'1d')` -> `resample_tf` "
"(ultima riga = 1 barra 1h su 24)")
show("statarb", c)
RES["paper_statarb"] = dict(parziale="SI", cmp=c, gate="STATARB 2026-09-27",
src="btc/eth 1h -> resample 1d", cad_h=24.0)
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 = [(float(r["net_modeled"]), rep[int(r["ts"])]) for r in rec if int(r["ts"]) in rep]
c = compare(np.array([p[0] for p in pairs]), np.array([p[1] for p in pairs]))
print("\n pannello: `_panel` -> `ortholib.aligned` -> `altlib.get(...,'1d')` -> `resample_tf`")
show("dvolspread", c)
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=c, gate="DVOLSPREAD 2026-10-24 / 2027-01-24",
src="btc/eth 1h -> resample 1d + dvol", cad_h=24.0)
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])
c = compare(A, B)
print("\n pannello: `build_bars` -> `harness.load(asset,'1h')` = il parquet 1h GREZZO, "
"nessun resample")
show("prevday", c, note=" <- 24 barre/giorno, quindi il conto e' sull'INTERA giornata")
diff = np.abs(A - B) > 1e-6
hh = pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True).hour
print(f"\n DOVE cade la divergenza: {int(diff.sum())} barre su {len(A)} "
f"({diff.mean() * 100:.1f}%)")
vc = pd.Series(hh[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()))
giorni = len(set(pd.to_datetime([p[0] for p in pairs], unit="ms", utc=True).date))
print(f" = ESATTAMENTE una barra per giorno su {giorni} giorni, e sempre la barra 00:00 —")
print(" cioe' quella che il cron delle 00:30 trova aperta da ~35 minuti.")
print(f"\n ⚠ LA MISURA CHE CONTA E' L'ALTRA META': {int((~diff).sum())} barre su {len(A)} "
"coincidono BIT A BIT con")
print(" il replay fatto oggi. Non e' un dettaglio contabile: e' la prova indipendente che il")
print(" feed BTC/ETH certificato NON riscrive le barre gia' chiuse (62 giorni di riscritture")
print(" notturne). Senza questa prova la ricostruzione della sezione E sarebbe un'ipotesi.")
RES["paper_prevday"] = dict(parziale="SI", cmp=c, gate="nessuno (lead in monitor)",
src="btc/eth 1h grezzo", cad_h=1.0)
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 = [], []
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])
c = compare(np.array(A), np.array(B))
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)")
show("portfolio", c)
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.")
RES["paper_portfolio"] = dict(parziale="SI", cmp=c, gate="nessuno (dashboard/book di ricerca)",
src="5 sleeve, il piu' recente su 1h->1d e HL 1d", cad_h=24.0)
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 = [], []
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])
c = compare(np.array(A), np.array(B))
print("\n serie: `combo_daily()` = TP01 compoundato sulla griglia dei GIORNI DI BORSA + GTAA")
show("combo", c)
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=c, gate="nessuno (paper cross-venue)",
src="TP01 1h->1d su griglia IB", cad_h=24.0)
# ============================================================================================
# 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.35"),
"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(f"\n {'monitor':<19}{'parz.':>6}{'ident./tot':>13}{'corr':>9}{'min/g':>9}"
f"{'ricostr.':>10} gate dipendente")
for k, v in RES.items():
c = v["cmp"]
ratio = "{}/{}".format(c["ident"], c["n"])
print(f" {k:<19}{v['parziale']:>6}{ratio:>13}{c['corr']:>+9.3f}"
f"{c['minuti']:>9,.0f}{'SI':>10} {GATE_INFO[k][0]}")
print("\n 'ricostr.' = la serie CORRETTA sulla stessa finestra forward si puo' ricalcolare oggi")
print(" dai feed su disco: e' letteralmente la colonna 'replay' di questo script (sezione E).")
# ============================================================================================
# 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}")
print(f" -> MANCA {len(miss)} monitor rotti su {sum(1 for v in RES.values() if v['parziale'] == 'SI')}"
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(f"\n {'monitor / gate':<34}{'barre oggi':>12}{'data gate':>13}{'se AZZERI':>12}"
f"{'ritardo':>10}")
for mon, nome, gate, ser in righe:
n = len(read_jsonl(ROOT / "data" / mon / ser))
# con una barra al giorno, tornare a n barre richiede n giorni dal reset
rip = oggi + pd.Timedelta(days=n)
rit = (rip - gate).days
print(f" {f'{mon} / {nome}':<34}{n:>12}{str(gate.date()):>13}"
f"{str(rip.date()):>12}{rit:>+9}g")
print("\n Cosi' letta, la riparazione sposta i tre gate di 1-2 mesi. MA la colonna giusta e'")
print(" un'altra, e cambia la decisione:")
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**: `paper_prevday` ha
1426 barre su 1488 identiche BIT A BIT al replay dopo 62 giorni di riscritture
notturne del parquet BTC/ETH (sezione B4). Per Hyperliquid la stessa cosa e' misurata
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, e va scritta nel diario: i libri REAL saltano i
ribilanci sotto il min-order, e quella decisione dipende dal capitale corrente, quindi il
percorso REAL rigenerato non e' il percorso REAL registrato — non e' "lo stesso numero
calcolato meglio", e' un secondo percorso deterministico. Per XSR01 conta: il gate del 23/10
legge net_REAL-$5000 per l'haircut.""")
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_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()