monitor: advance() consuma solo barre CHIUSE — serie rigenerate, guardia PREMATURO, 4 debiti chiusi

S5.1 RIPARATO E RIGENERATO. Filtro condiviso src/live/paper_guard.py (barra
open-labeled chiusa = ts + cadenza <= adesso) importato da tutti e 6 i monitor;
serie rigenerate dallo stesso start_ts con scripts/live/paper_regen.py (evidenza
in *.pre_regen_20260826.*): statarb +1,95 -> -1,61 (il ribaltamento del gate
27/09 previsto dall'audit), dvolspread -14,73 -> -4,41, xsr -4,98 -> -2,72,
prevday invariato. Nessuna data di gate si sposta. Guardia cablata in
monitor_health: stato PREMATURO (ultima barra che chiude dopo l'mtime, grazia
5 min, open_labeled=False per collect_chain) — sul dato vivo segnala i 5 rotti
e tace sui 2 sani; dopo la rigenerazione 7/7 OK. paper_portfolio non rigenerato
(GTAA su ADJUSTED_LAST: replay != serie registrata, P12), tolta la coda non
chiusa. D6 pagata di nuovo nel fix: asi8 in pandas 3 e' in us, non ns —
blindata con test su tre risoluzioni.

S5.12 ESTESO: conftest devia anche trades.db (wrapper su connect: il default
e' catturato alla definizione) e docs/journal/; book_executions.jsonl
sorvegliato con impronta inizio/fine suite.

S5.5 FATTO: test_leva_massima cancellato con nota (misurava frac*n_asset:
con una chiave di scala avrebbe continuato a passare smettendo di controllare).

S5.9 INDAGATO E RIPARATO (r0826_skh_band_drift): il dato regge (taglio 02/07
riproduce l'audit 1,6376, in-sample identico su ogni taglio); la deriva era la
finestra hold-out — e la sola settimana 15-22/08 vale +0,35 di Sharpe hold-out.
Il test ora taglia il feed al 02/07 e verifica la riproduzione stretta.

Suite: 751 passati, 0 falliti.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01B9UyJLHzR7EJzxR3iQ3RN1
This commit is contained in:
Adriano Dal Pastro
2026-08-26 10:12:23 +00:00
parent a51844875b
commit d597fc64e8
20 changed files with 872 additions and 54 deletions
+32 -6
View File
@@ -41,13 +41,14 @@ from __future__ import annotations
import csv
import json
from dataclasses import dataclass
from datetime import datetime, timezone
from datetime import datetime, timedelta, timezone
from pathlib import Path
import numpy as np
MIN_COVERAGE = 0.80 # sotto: la serie ha buchi -> non ci si decide sopra un gate
MIN_BARS_PER_JUDGE = 2 # con una barra sola non esiste ne' coda ne' copertura
GRAZIA_PREMATURO_MIN = 5.0 # tolleranza fra chiusura della barra e mtime del file (clock/IO)
@dataclass(frozen=True)
@@ -60,6 +61,11 @@ class MonitorSpec:
calendar: str = "crypto" # crypto (365g) | equity (giorni di borsa)
max_age_h: float = 48.0 # oltre: FERMO
gate: str | None = None
# True = i timestamp sono APERTURE di barra (convenzione resample_tf/ccxt/HL) -> vale il
# controllo PREMATURO. False = timbri di esecuzione (es. collect_chain scrive l'ora del
# giro): il timbro coincide con la scrittura per costruzione e il controllo mentirebbe
# SEMPRE (P14: una guardia piu' stretta del contratto insegna a ignorare la sezione).
open_labeled: bool = True
MONITORS: tuple[MonitorSpec, ...] = (
@@ -78,7 +84,8 @@ MONITORS: tuple[MonitorSpec, ...] = (
# fissa ma una serie IRRECUPERABILE — un'ora non raccolta non si recupera da nessuna fonte,
# quindi la soglia d'eta' e' stretta (3h = due giri persi).
MonitorSpec("collect_chain", "chain_collect", "runs.jsonl", 1.0, max_age_h=3.0,
gate="serie irrecuperabile — la catena opzioni non si ricostruisce a posteriori"),
gate="serie irrecuperabile — la catena opzioni non si ricostruisce a posteriori",
open_labeled=False),
)
@@ -130,14 +137,30 @@ def age_hours(last: datetime, now: datetime, calendar: str) -> float:
return raw
def assess(spec: MonitorSpec, stamps: list[datetime], now: datetime) -> dict:
def assess(spec: MonitorSpec, stamps: list[datetime], now: datetime,
mtime: datetime | None = None) -> dict:
"""Verdetto su un monitor. Gli stati NON sono due: 'assente' e 'troppo giovane per un
giudizio' non sono 'OK', e non devono poter passare per tali."""
giudizio' non sono 'OK', e non devono poter passare per tali.
`mtime` = quando il file di serie e' stato scritto l'ultima volta. Serve al controllo
PREMATURO (r0822_monitor_audit): se l'ultima barra registrata CHIUDE dopo che il file e'
stato scritto, il monitor ha consumato una barra parziale — e' il difetto che ha fatto
registrare 4/41/2 minuti di mercato al giorno a 3 monitor con gate. Freschezza e copertura
NON possono vederlo: una serie fresca, completa e SBAGLIATA le passa entrambe."""
base = dict(name=spec.name, gate=spec.gate, n_bars=len(stamps),
age_h=None, coverage=None, expected=None)
if not stamps:
return {**base, "status": "ASSENTE",
"why": "nessuna serie leggibile: il monitor non ha mai scritto, o lo stato e' perso"}
if mtime is not None and spec.open_labeled:
chiude = stamps[-1] + timedelta(hours=spec.cadence_h)
ritardo_min = (chiude - mtime).total_seconds() / 60.0
if ritardo_min > GRAZIA_PREMATURO_MIN:
return {**base, "age_h": age_hours(stamps[-1], now, spec.calendar),
"status": "PREMATURO",
"why": (f"l'ultima barra chiude alle {chiude:%Y-%m-%d %H:%M}Z ma il file e' "
f"stato scritto {ritardo_min/60:.1f}h PRIMA: registrata una barra "
"parziale — la serie va rigenerata, non solo riparata")}
age = age_hours(stamps[-1], now, spec.calendar)
if len(stamps) < MIN_BARS_PER_JUDGE:
return {**base, "age_h": age, "status": "NUOVO",
@@ -159,8 +182,11 @@ def check_all(root: Path, now: datetime | None = None,
now = now or datetime.now(timezone.utc)
out = []
for spec in monitors:
stamps = read_stamps(root / "data" / spec.directory / spec.series)
out.append(assess(spec, stamps, now))
f = root / "data" / spec.directory / spec.series
stamps = read_stamps(f)
mtime = (datetime.fromtimestamp(f.stat().st_mtime, tz=timezone.utc)
if f.exists() else None)
out.append(assess(spec, stamps, now, mtime=mtime))
return out
+64
View File
@@ -0,0 +1,64 @@
"""paper_guard.py — solo barre CHIUSE nei forward-monitor.
IL DIFETTO CHE QUESTO MODULO CHIUDE (misurato in r0822_monitor_audit, §5.1 di CLAUDE.md):
`fetch_hyperliquid`/`resample_tf` scrivono anche la barra del giorno IN CORSO; `advance()` dei
monitor consumava le barre con `ts > last_ts` — ultima inclusa — e portava `last_ts` sopra la
barra parziale. Risultato: `paper_statarb` registrava ~4 minuti di mercato al giorno,
`paper_dvolspread` ~2, `paper_xsr` ~41, e le 23 ore e mezza restanti di OGNI giorno non
entravano in nessun rendimento registrato. Il gate STATARB del 27/09 si ribaltava su 2 criteri
su 3 (Sharpe +1,96 registrato contro 2,02 ricostruito).
LA REGOLA: una barra open-labeled (convenzione di TUTTO il progetto: `resample_tf` usa
`label='left'`, Hyperliquid e ccxt/Deribit timbrano all'apertura) e' CHIUSA quando
`ts + cadenza <= adesso`. L'orologio si LEGGE al momento della chiamata, mai cablato.
D2: "una barra presente non e' una giornata presente" — questo modulo e' quella regola
trasformata in codice condiviso, cosi' il prossimo monitor non puo' ricommettere il difetto
dimenticandosene: importa `nuove_chiuse` e basta.
"""
from __future__ import annotations
import numpy as np
import pandas as pd
MS_1H = 3_600_000
MS_1D = 86_400_000
def _now_ms(now_ms: int | None) -> int:
if now_ms is not None:
return int(now_ms)
# stessa forma indipendente-dalla-risoluzione di `indice_chiuso` (D6)
epoch = pd.Timestamp("1970-01-01", tz="UTC")
return int((pd.Timestamp.now(tz="UTC") - epoch) // pd.Timedelta(milliseconds=1))
def chiusa(ts_ms: int, cadenza_ms: int, now_ms: int | None = None) -> bool:
"""True se la barra open-labeled `ts_ms` e' interamente nel passato."""
return int(ts_ms) + int(cadenza_ms) <= _now_ms(now_ms)
def nuove_chiuse(ts, last_ts: int, cadenza_ms: int, now_ms: int | None = None) -> list[int]:
"""Indici delle barre DOPO `last_ts` e gia' CHIUSE — il rimpiazzo della riga
`new = [i for i in range(len(ts)) if ts[i] > last_ts]` che ha rotto 4 monitor su 6."""
now = _now_ms(now_ms)
ts = np.asarray(ts, dtype="int64")
return [int(i) for i in np.where((ts > int(last_ts)) & (ts + int(cadenza_ms) <= now))[0]]
def indice_chiuso(index: pd.DatetimeIndex, cadenza_ms: int,
now_ms: int | None = None) -> np.ndarray:
"""Maschera 'barra chiusa' per un DatetimeIndex (i monitor a serie pandas: portfolio/combo).
⚠️ NIENTE `.asi8`/`.view("int64")`: la risoluzione dell'indice non e' garantita (pandas 3
usa `us` di default, non `ns`) e un intero letto nella scala sbagliata passa SENZA eccezioni
— e' la lezione D6, gia' pagata, e ci si e' ricascati alla prima stesura di QUESTA funzione
(il test di fumo dava "chiusa" la barra del giorno in corso). La sottrazione dall'epoca con
`// Timedelta` e' indipendente dalla risoluzione: e' la stessa forma di `resample_tf`."""
now = _now_ms(now_ms)
ix = pd.DatetimeIndex(index)
if ix.tz is None:
ix = ix.tz_localize("UTC")
epoch = pd.Timestamp("1970-01-01", tz="UTC")
ts = ((ix - epoch) // pd.Timedelta(milliseconds=1)).to_numpy()
return (ts + int(cadenza_ms)) <= now