775 lines
45 KiB
Python
775 lines
45 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 = [(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 = freeze_evidence()
|
|
print(f" Prova indipendente dal log del cron: `rebuild_history` e' abortito in "
|
|
f"{len(ev)} giri\n ({', '.join(sorted(ev)[:8])}{'...' if len(ev) > 8 else ''}) — "
|
|
"EPERM sul backup, difetto gia' riparato\n nel sorgente. Col feed fermo il monitor "
|
|
"non trovava barre nuove; al ripristino ne ha\n recuperate 7 in un colpo solo, di "
|
|
"cui 6 GIA' CHIUSE -> coincidono col replay 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)")
|
|
|
|
|
|
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)")
|
|
|
|
|
|
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]) + ") = giorni in cui TUTTI e cinque")
|
|
print(" gli sleeve erano flat (TP01 risk-off, GTAA a borsa chiusa, 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* (equity IB ri-aggiustata)")
|
|
|
|
|
|
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* (equity IB ri-aggiustata)")
|
|
|
|
|
|
# ============================================================================================
|
|
# 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("\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}{'parz.':>6}{'ident./tot':>13}{'corr':>9}{'min/g':>9}"
|
|
f"{'ricostruibile':>25} 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}{v['ric']:>25} {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(" ricostruibile ma NON al bit, perche' una gamba viene dal feed equity IB che e'")
|
|
print(" ri-aggiustato all'indietro (`ADJUSTED_LAST`).")
|
|
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.")
|
|
|
|
|
|
# ============================================================================================
|
|
# 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()
|