Files
Adriano Dal Pastro de83909db9 un flag sconosciuto non e' l'azione di default: guardia su 18 script, indice USDE orario, pulizia
MISURATO oggi durante la revisione: `trades_db.py --help` non stampava l'uso, cadeva in sync()
e riscriveva meta.ultimo_sync. Nessuno dei 18 script di scripts/live/ usava argparse: un flag
sbagliato era il ramo else. Su journal.py avrebbe scritto pagina e riga di DB, su analista.py
avrebbe speso una chiamata al modello e mandato un Telegram.

- src/live/cli.valida: prima istruzione di ogni __main__, prima di connect()/sync/rete.
  --help -> 0 con l'uso; flag ignoto, valore mancante o posizionale -> 2 con l'elenco dei
  previsti (P4). NIENTE argparse: cambierebbe messaggi, codici d'uscita e --help di script
  che il cron gia' chiama.
- 18 script cablati (i 3 che scrivono + 14 + cc01), flag invariati.
- tests/test_cli_flag.py (30): elenco DERIVATO dalla cartella (P1), valida come prima
  istruzione, uso che documenta i flag, e i flag che il CRON usa davvero restano accettati
  (P15/P16); end-to-end su --help e flag ignoto con trades.db non toccato (M15).
  Verificato a mano: monitor_health --quiet, trades_db --sync --quiet, book_execute dry-run.

Debito §5.15, primo passo: balance_watch (orario) registra `usde_usdc` a ogni campione — None
con la ragione se illeggibile, mai 1,0. Il cablaggio nel bound quando la serie ha storia.

Pulizia dalla revisione: tests/helpers.carica_script al posto della 15a copia del loader
importlib (5 file del libro live); il fill di prova via upsert_fills invece di un INSERT che
lasciava verified NULL; asserzioni non ancorate al padding; movimenti_capitale accetta le
righe gia' lette (una SELECT invece di due ai due lati di una scrittura del cron).

Test 910 verdi (+52). Diario 2026-09-02c.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XDJsH3iDSaBns3ccpPBwiu
2026-09-02 15:49:40 +00:00

239 lines
10 KiB
Python

"""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_<data>.* (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)
USO = """uso: paper_regen.py [--solo V]
paper_regen.py — RIGENERA le serie dei forward-monitor rotti da `advance()` (§5.1).
uv run python scripts/live/paper_regen.py # tutti e 4
uv run python scripts/live/paper_regen.py --solo paper_statarb
Dettaglio nel docstring in testa al file."""
if __name__ == "__main__":
from src.live.cli import valida
valida("paper_regen.py", USO, flag=(), con_valore=("--solo",))
main()