d597fc64e8
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
229 lines
10 KiB
Python
229 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)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|