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:
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user