"""paper_regen.py — RIGENERA le serie dei forward-monitor rotti da `advance()` (§5.1). PERCHE' RIGENERARE E NON AZZERARE. L'audit r0822_monitor_audit ha provato due volte, in modo indipendente, che il replay sul pannello di oggi E' la serie corretta sulla stessa finestra: (a) `paper_prevday` ha 1427/1488 barre identiche al bit dopo 62 notti di riscrittura del feed; (b) le 6 barre chiuse di `paper_statarb` recuperate dopo il guasto EPERM coincidono al bit. Quindi la finestra forward NON va persa: si rigenera dallo STESSO `start_ts` pre-registrato, con la config congelata e il codice di produzione riparato — e nessuna data di gate si sposta. COSA FA, per ciascun monitor (statarb · dvolspread · xsr · prevday): 1. archivia i file correnti come *.pre_regen_.* (NON li cancella: sono l'evidenza del difetto, e stanno nel perimetro di backup); 2. ricostruisce lo stato iniziale all'ORIGINALE `start_ts` (stessa costruzione dell'audit); 3. riesegue `advance()` di produzione — che ora consuma SOLO barre chiuse (paper_guard); 4. salva lo stato e stampa il verdetto a runtime (N11): barre, Sharpe vecchio vs nuovo, e la prova che l'ultima barra registrata e' chiusa. `paper_portfolio` e `paper_combo` NON si rigenerano qui, e la scelta e' dichiarata: la gamba GTAA legge `ADJUSTED_LAST` di IB ri-aggiustato all'indietro, quindi il replay NON sarebbe la serie che un monitor sano avrebbe registrato (P12: meglio una storia corrotta e dichiarata di una riscritta e spacciata per registrata). Sono dashboard senza gate; dal fix in poi scrivono solo barre chiuse. uv run python scripts/live/paper_regen.py # tutti e 4 uv run python scripts/live/paper_regen.py --solo paper_statarb """ from __future__ import annotations import importlib.util import json import shutil 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] sys.path.insert(0, str(ROOT)) ANN = float(np.sqrt(365.0)) SUFFISSO = datetime.now(timezone.utc).strftime("pre_regen_%Y%m%d") def live(name: str): spec = importlib.util.spec_from_file_location(f"_regen_{name}", ROOT / "scripts" / "live" / f"{name}.py") m = importlib.util.module_from_spec(spec) spec.loader.exec_module(m) return m def sharpe_jsonl(path: Path, campo: str = "net_modeled") -> tuple[float, int]: if not path.exists(): return float("nan"), 0 r = [] for ln in path.read_text().splitlines(): ln = ln.strip() if ln: r.append(float(json.loads(ln)[campo])) r = np.asarray(r) if len(r) < 2 or r.std() == 0: return float("nan"), len(r) return float(r.mean() / r.std() * ANN), len(r) def archivia(d: Path, nomi: tuple[str, ...]) -> None: for n in nomi: f = d / n if f.exists(): dest = d / f"{f.stem}.{SUFFISSO}{f.suffix}" if dest.exists(): raise SystemExit(f" {dest} esiste gia': rigenerazione doppia nello stesso " "giorno? Verificare a mano prima di riprovare.") shutil.move(str(f), str(dest)) print(f" archiviato {f.name} -> {dest.name}") def verdetto(nome: str, d: Path, mod, st: dict, sh_prima: float, n_prima: int, cadenza_ms: int) -> None: from src.live.paper_guard import chiusa sh_dopo, n_dopo = sharpe_jsonl(d / "returns.jsonl") ult = st["last_ts"] if "last_ts" in st else None ok_chiusa = chiusa(int(ult), cadenza_ms) if ult else False print(f" barre: {n_prima} registrate (rotte) -> {n_dopo} rigenerate (chiuse)") print(f" Sharpe ann. net_modeled: {sh_prima:+.2f} (serie rotta) -> {sh_dopo:+.2f} " f"(serie vera)") print(f" ultima barra {pd.Timestamp(int(ult), unit='ms', tz='UTC').date() if ult else '?'} " f"chiusa: {'SI' if ok_chiusa else 'NO — QUALCOSA NON VA'}") if not ok_chiusa: raise SystemExit(f" {nome}: la rigenerazione ha consumato una barra NON chiusa") def regen_statarb() -> None: print("\n== paper_statarb (gate 2026-09-27) " + "=" * 50) d = ROOT / "data" / "paper_statarb" ps = live("paper_statarb") st_old = json.loads((d / "state.json").read_text()) sh0, n0 = sharpe_jsonl(d / "returns.jsonl") j = ps.build_joint("1d") ts, _, pos, _ = ps._signal(j) i0 = int(np.where(ts == st_old["start_ts"])[0][0]) archivia(d, ("returns.jsonl", "trades.jsonl", "state.json")) st = dict(start_ts=st_old["start_ts"], last_ts=st_old["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) st = ps.advance(st, j) ps._state_io(st) verdetto("paper_statarb", d, ps, st, sh0, n0, 86_400_000) def regen_dvolspread() -> None: print("\n== paper_dvolspread (kill 2026-10-24 / decisione 2027-01-24) " + "=" * 24) d = ROOT / "data" / "paper_dvolspread" pv = live("paper_dvolspread") st_old = json.loads((d / "state.json").read_text()) sh0, n0 = sharpe_jsonl(d / "returns.jsonl") P = pv._panel() i0 = int(np.where(P["ts"] == st_old["start_ts"])[0][0]) archivia(d, ("returns.jsonl", "state.json")) st = dict(start_ts=st_old["start_ts"], last_ts=st_old["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) st = pv.advance(st, P) pv._state_io(st) verdetto("paper_dvolspread", d, pv, st, sh0, n0, 86_400_000) def regen_xsr() -> None: print("\n== paper_xsr (gate 2026-10-23) " + "=" * 54) d = ROOT / "data" / "paper_xsr" px = live("paper_xsr") st_old = json.loads((d / "state.json").read_text()) sh0, n0 = sharpe_jsonl(d / "returns.jsonl") archivia(d, ("returns.jsonl", "state.json")) st = dict(start_ts=st_old["start_ts"], last_ts=st_old["start_ts"], n_bars=0, syms=st_old["syms"], modeled=px._book(px.MODELED_CAPITAL, len(st_old["syms"])), reals={n: px._book(c, len(st_old["syms"])) for c, n in px.REAL_BOOKS}) st = px.advance(st) px._state_io(st) verdetto("paper_xsr", d, px, st, sh0, n0, 86_400_000) def regen_prevday() -> None: print("\n== paper_prevday (griglia ORARIA, nessun gate a data fissa) " + "=" * 24) d = ROOT / "data" / "paper_prevday" pp = live("paper_prevday") st_old = json.loads((d / "state.json").read_text()) sh0, n0 = sharpe_jsonl(d / "returns.jsonl") dfs = pp.build_bars() S = st_old["start_ts"] pos = {a: pp.pb.current_target(dfs[a][dfs[a]["timestamp"] <= S]) for a in pp.ASSETS} archivia(d, ("returns.jsonl", "trades.jsonl", "state.json")) st = 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) st = pp.advance(st, dfs) pp._state_io(st) verdetto("paper_prevday", d, pp, st, sh0, n0, 3_600_000) def trim_portfolio() -> None: """`paper_portfolio` NON si rigenera (la gamba GTAA legge ADJUSTED_LAST ri-aggiustato: il replay non sarebbe la serie registrata — P12). Ma la sua CODA contiene la riga del giorno in corso, scritta dal codice vecchio: senza toglierla la guardia PREMATURO resterebbe accesa per sempre su un difetto gia' riparato (P14). Il taglio e' onesto: la storia registrata resta com'e' (troncata e DICHIARATA in memoria), va via solo la riga che dice di essere un giorno e non lo e' — la versione chiusa la riscrive il prossimo giro del cron col codice riparato.""" from src.live.paper_guard import MS_1D, indice_chiuso print("\n== paper_portfolio (dashboard — TRIM della coda non chiusa, non rigenerazione) ==") d = ROOT / "data" / "paper_portfolio" righe = (d / "equity.csv").read_text().strip().splitlines() testa, dati = righe[0], righe[1:] date = pd.DatetimeIndex([r.split(",")[0] for r in dati]) m = indice_chiuso(date, MS_1D) tolte = [r for r, k in zip(dati, m) if not k] if not tolte: print(" coda gia' pulita: niente da fare") return archivia(d, ("equity.csv", "state.json")) dati_ok = [r for r, k in zip(dati, m) if k] (d / "equity.csv").write_text("\n".join([testa] + dati_ok) + "\n") eq = [float(r.split(",")[1]) for r in dati_ok] peak, dd = 0.0, 0.0 for e in eq: peak = max(peak, e) dd = max(dd, (peak - e) / peak if peak > 0 else 0.0) st_old = json.loads((d / f"state.{SUFFISSO}.json").read_text()) st = dict(st_old, last=dati_ok[-1].split(",")[0], equity=eq[-1], peak=peak, max_dd=dd, n_days=st_old["n_days"] - len(tolte)) (d / "state.json").write_text(json.dumps(st, indent=2)) for r in tolte: print(f" tolta riga non chiusa: {r}") print(f" ultima riga ora: {dati_ok[-1]} (peak/max_dd ricalcolati dalla serie)") REGEN = dict(paper_statarb=regen_statarb, paper_dvolspread=regen_dvolspread, paper_xsr=regen_xsr, paper_prevday=regen_prevday, paper_portfolio=trim_portfolio) def main() -> None: print("=" * 90) print(" RIGENERAZIONE forward-monitor — stesso start_ts, config congelata, solo barre chiuse") print("=" * 90) solo = None if "--solo" in sys.argv: solo = sys.argv[sys.argv.index("--solo") + 1] if solo not in REGEN: raise SystemExit(f"monitor sconosciuto: {solo} (validi: {', '.join(REGEN)})") for nome, fn in REGEN.items(): if solo and nome != solo: continue fn() print("\n fatto. I file *.%s.* sono l'evidenza del difetto e restano nel backup." % SUFFISSO) if __name__ == "__main__": main()