"""FILONE 2 (2026-07-03) — ONESTA' DELLA CODA + FRONTIERA FIDATA. Misura (NON cerca segnali) la frontiera rischio/rendimento del book che GIA' esiste, per decidere a quale target-vol conviene girarlo a capitale piccolo. Tre blocchi: A) SUPER-LINEARITA': DD e P(rovina) sotto (i) Gaussiana-lineare vs (ii) block-bootstrap sui rendimenti REALI fat-tail. Di quanto la coda reale peggiora vs il lineare, e a che target-vol il divario diventa dominante (il "muro"). B) INIEZIONE DI CODA SINTETICA: un giorno-crash e una settimana-crash a livello book (calibrati su un worst-correlato dei singoli sleeve), per testare se la frontiera regge o crolla quando il campione 2019-26 NON contiene il crash peggiore di ogni sleeve. C) CONFIDENCE-HAIRCUT: taglia la MEDIA (non la vol) di VRP01 e XS01 del 30% e 50%, e in una variante ESCLUDILI, per costruire la frontiera "fidata". Il GAP full-vs-fidata = margine d'onestita'. Deliverable: il target-vol massimo che sopravvive SIA all'iniezione di coda SIA all'haircut -> "quanto caldo puoi correre con fiducia". SOLA LETTURA su src/ (importa i builder degli sleeve). Output in scratchpad. NON committare. """ from __future__ import annotations import sys, os, math, json, zlib, gc import numpy as np import pandas as pd sys.path.insert(0, '/opt/docker/PythagorasGoal') from src.portfolio.sleeves import (_tp01_returns, _skyhook_returns, _xsec_returns, _vrp_combo_returns, _gtaa_daily_returns) from src.portfolio.portfolio import combine_outer, to_daily, HOLDOUT, DAYS_PER_YEAR OUT = '/tmp/claude-1001/-opt-docker-PythagorasGoal/e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad' os.makedirs(OUT, exist_ok=True) AY = DAYS_PER_YEAR # 365.25 # ---------------------------------------------------------------- helpers def ann_vol(r): r = np.asarray(pd.Series(r).dropna().values, float); return r.std() * math.sqrt(AY) def ann_mean(r): r = np.asarray(pd.Series(r).dropna().values, float); return r.mean() * AY def sharpe(r): r = np.asarray(pd.Series(r).dropna().values, float) return r.mean() / r.std() * math.sqrt(AY) if r.std() > 0 else 0.0 def emp_cagr(r): r = np.asarray(pd.Series(r).dropna().values, float) eq = np.cumprod(1 + r); y = len(r) / AY return eq[-1] ** (1 / y) - 1 if (y > 0 and eq[-1] > 0) else -1.0 def emp_maxdd(r): r = np.asarray(pd.Series(r).dropna().values, float) eq = np.cumprod(1 + r); pk = np.maximum.accumulate(eq) return float(np.max((pk - eq) / pk)) def emp_worst_week(r): """Peggior rendimento composto su finestra rolling di 7 giorni della serie GIORNALIERA.""" s = pd.Series(r).dropna() w = (1 + s).rolling(7).apply(np.prod, raw=True) - 1.0 return float(w.min()) def haircut_mean(s: pd.Series, frac: float) -> pd.Series: """Taglia la MEDIA di una serie del `frac` (0.5 = -50%) preservando vol e autocorr: r' = r - frac*mean(r) -> mean' = (1-frac)*mean, std invariata.""" s = s.dropna() return s - frac * s.mean() # ---------------------------------------------------------------- build sleeves (daily grid) print("costruzione sleeve (griglia giornaliera)...", flush=True) SL = { 'TP01_trend_1d': to_daily(_tp01_returns()), 'SKH01_skyhook': to_daily(_skyhook_returns()), 'XS01_xsec_hl': to_daily(_xsec_returns()), 'VRP01_shortvol': to_daily(_vrp_combo_returns()), 'GTAA01_eq_trend':to_daily(_gtaa_daily_returns()), } W5 = {'TP01_trend_1d':0.33,'XS01_xsec_hl':0.15,'VRP01_shortvol':0.12,'SKH01_skyhook':0.20,'GTAA01_eq_trend':0.20} W2 = {'TP01_trend_1d':0.75,'SKH01_skyhook':0.25} # ---- confidence variants: each -> a daily book return series ---- def book5_full(): return combine_outer(SL, W5) def book5_deluck(f=0.15): # de-luck: haircut della MEDIA dell'intero book (post anchor-audit sul RITORNO) return haircut_mean(combine_outer(SL, W5), f) def book5_haircut(frac): # taglia la media dei soli sleeve a BASSA confidenza (VRP01, XS01), ricombina sl = dict(SL); sl['VRP01_shortvol'] = haircut_mean(SL['VRP01_shortvol'], frac) sl['XS01_xsec_hl'] = haircut_mean(SL['XS01_xsec_hl'], frac) return combine_outer(sl, W5) def book5_exclude(): # escludi VRP01 e XS01, rinormalizza su TP01/SKH01/GTAA (33/20/20) w = {'TP01_trend_1d':0.33,'SKH01_skyhook':0.20,'GTAA01_eq_trend':0.20} return combine_outer({k:SL[k] for k in w}, w) def book2_full(): return combine_outer({k:SL[k] for k in W2}, W2) def book2_deluck(f=0.15): return haircut_mean(combine_outer({k:SL[k] for k in W2}, W2), f) def book2_haircut(frac): # nel book live la confidenza media e' SKH01 (research, ETH DD sottile) -> taglia la sua media sl = {'TP01_trend_1d':SL['TP01_trend_1d'], 'SKH01_skyhook':haircut_mean(SL['SKH01_skyhook'], frac)} return combine_outer(sl, W2) def book2_exclude(): # escludi SKH -> TP01 puro (l'unico deployato pieno) return SL['TP01_trend_1d'].dropna() # ---------------------------------------------------------------- SANITY def line(r, tag): ho = r[r.index >= HOLDOUT] return (f" {tag:14s} n={len(r):5d} FULL sh={sharpe(r):.3f} cagr={emp_cagr(r):+.4f} " f"dd={emp_maxdd(r):.4f} vol={ann_vol(r):.4f} | HOLD sh={sharpe(ho):.3f} " f"cagr={emp_cagr(ho):+.4f} dd={emp_maxdd(ho):.4f} ww={emp_worst_week(r):+.4f}") rep = [] def P(*a): s = " ".join(str(x) for x in a); print(s, flush=True); rep.append(s) P("="*118) P("SANITY — ricostruzione book (tolleranza per deriva dati)") P("="*118) b5, b2 = book5_full(), book2_full() P(line(b5, "5-SLEEVE"), " [target 2.24 / 2.46 / 6.2%]") P(line(b2, "2-SLEEVE"), " [target 1.78 / 1.17 / 9.0%]") for nm, s in SL.items(): P(f" sleeve {nm:16s} sh_full={sharpe(s):+.2f} vol={ann_vol(s):.3f} cagr={emp_cagr(s):+.3f} " f"worst_day={float(pd.Series(s).min()):+.4f} worst_wk={emp_worst_week(s):+.4f} n={len(s)}") # ---------------------------------------------------------------- bootstrap engine def make_paths(r, n_paths, horizon, block, rng): """Block-bootstrap: matrice (n_paths, horizon) di rendimenti UNSCALED campionati da r.""" r = np.asarray(pd.Series(r).dropna().values, float) N = len(r); nb = int(math.ceil(horizon / block)) starts = rng.integers(0, N - block + 1, size=(n_paths, nb)) off = np.arange(block) idx = (starts[:, :, None] + off[None, None, :]).reshape(n_paths, nb * block)[:, :horizon] return r[idx].astype(np.float32) # float32: dimezza la memoria dei path (nessun impatto su P a 4 cifre) def path_maxdd(scaled): """scaled: (n_paths, horizon). Ritorna maxDD per path (clip equity floor a 0 = liquidazione).""" sc = np.clip(scaled, -1.0, None) eq = np.cumprod(1.0 + sc, axis=1) pk = np.maximum.accumulate(eq, axis=1) dd = (pk - eq) / pk return dd.max(axis=1), eq[:, -1] def gauss_maxdd(mu_d, sd_d, n_paths, horizon, rng): x = rng.normal(mu_d, sd_d, size=(n_paths, horizon)).astype(np.float32) return path_maxdd(x) # ---------------------------------------------------------------- config NPATH = 4000 HYR = 5 HOR = int(round(HYR * 365)) # 5 anni ~ 1825 giorni BLOCK = 15 SEED = 20260703 TVOLS = [0.05, 0.06, 0.08, 0.10, 0.125, 0.15, 0.20, 0.25, 0.30] RUIN = 0.50 # rovina = maxDD >= 50% DDLIM = 0.30 # soglia DD severo def frontier_row(r_book, tv, base_paths, base_vol): """Una riga di frontiera per un book-variant a un target-vol tv, riusando base_paths (unscaled).""" lam = tv / base_vol scaled_emp = np.asarray(pd.Series(r_book).dropna().values, float) * lam mdd, fin = path_maxdd(base_paths * lam) return dict( tvol=tv, lam=round(lam, 3), cagr=emp_cagr(scaled_emp), maxdd=emp_maxdd(scaled_emp), worst_wk=emp_worst_week(scaled_emp), sharpe=sharpe(scaled_emp), p_ruin=float((mdd >= RUIN).mean()), p_dd30=float((mdd >= DDLIM).mean()), med_dd=float(np.median(mdd)), p95_dd=float(np.percentile(mdd, 95)), ) # ---------------------------------------------------------------- A) SUPER-LINEARITY (full 5-sleeve) P("\n" + "="*118) P("A) SUPER-LINEARITA' — coda reale (block-bootstrap) vs Gaussiana-lineare [book 5-sleeve FULL, orizzonte 5y]") P("="*118) rng = np.random.default_rng(SEED) r5 = np.asarray(book5_full().dropna().values, float) v5 = r5.std() * math.sqrt(AY); m5 = r5.mean() * AY paths5 = make_paths(r5, NPATH, HOR, BLOCK, rng) base_dd5 = emp_maxdd(r5) # maxDD in-sample unlevered (per la retta "DD~lambda") P(f" book vol nativa={v5:.4f} mean_ann={m5:.4f} maxDD in-sample(unlev)={base_dd5:.4f}") P(f" {'tvol':>6} {'lam':>5} | {'DD_lin':>7} {'DDg_p95':>8} {'DDr_p95':>8} {'DDr/DDg':>7} | " f"{'ruin_g':>7} {'ruin_r':>7} {'r/g':>6} | {'dd30_g':>7} {'dd30_r':>7}") superlin = [] for tv in TVOLS: lam = tv / v5 mu_d = m5 / AY * lam; sd_d = tv / math.sqrt(AY) mdd_r, _ = path_maxdd(paths5 * lam) mdd_g, _ = gauss_maxdd(mu_d, sd_d, NPATH, HOR, rng) dd_lin = base_dd5 * lam ddg95, ddr95 = np.percentile(mdd_g, 95), np.percentile(mdd_r, 95) ruin_g, ruin_r = float((mdd_g >= RUIN).mean()), float((mdd_r >= RUIN).mean()) dd30_g, dd30_r = float((mdd_g >= DDLIM).mean()), float((mdd_r >= DDLIM).mean()) rg = ruin_r / ruin_g if ruin_g > 1e-6 else float('inf') superlin.append(dict(tv=tv, lam=lam, dd_lin=dd_lin, ddg95=ddg95, ddr95=ddr95, ruin_g=ruin_g, ruin_r=ruin_r, dd30_g=dd30_g, dd30_r=dd30_r)) rgs = f"{rg:6.1f}" if np.isfinite(rg) else " inf" P(f" {tv:6.3f} {lam:5.2f} | {dd_lin:7.3f} {ddg95:8.3f} {ddr95:8.3f} {ddr95/ddg95:7.2f} | " f"{ruin_g:7.4f} {ruin_r:7.4f} {rgs} | {dd30_g:7.4f} {dd30_r:7.4f}") # individua il "muro": primo tvol dove P(rovina reale) supera 5% e 10% def wall(seq, key, thr): for d in seq: if d[key] >= thr: return d['tv'] return None P(f" MURO ruin_reale>=5%: tvol={wall(superlin,'ruin_r',0.05)} >=10%: tvol={wall(superlin,'ruin_r',0.10)} " f">=1%: tvol={wall(superlin,'ruin_r',0.01)}") P(f" MURO DD30_reale>=25%: tvol={wall(superlin,'dd30_r',0.25)} >=50%: tvol={wall(superlin,'dd30_r',0.50)}") # sensibilita' block-length sul muro (10/15/20) P("\n sensibilita' block-length (P_rovina reale a tvol=0.15 / 0.20 / 0.30):") for bl in (10, 15, 20): rr = np.random.default_rng(SEED + bl) pp = make_paths(r5, NPATH, HOR, bl, rr) row = [] for tv in (0.15, 0.20, 0.30): mdd, _ = path_maxdd(pp * (tv / v5)) row.append(f"tvol{tv:.2f}={float((mdd>=RUIN).mean()):.4f}") P(f" block={bl:2d}: " + " ".join(row)) # ---------------------------------------------------------------- C) FRONTIER TABLES (3 livelli confidenza) def frontier_table(name, variant_series, base_vol_override=None): """Costruisce e stampa la tabella di frontiera per un book-variant.""" r = np.asarray(pd.Series(variant_series).dropna().values, float) bvol = base_vol_override if base_vol_override else r.std() * math.sqrt(AY) rr = np.random.default_rng(SEED + zlib.crc32(name.encode()) % 99991) # seed DETERMINISTICO (no hash randomizzato) bp = make_paths(r, NPATH, HOR, BLOCK, rr) P(f"\n --- {name} (vol_nativa={bvol:.4f} sharpe={sharpe(r):+.3f} cagr_unlev={emp_cagr(r):+.4f}) ---") P(f" {'tvol':>6} {'lam':>5} {'CAGR':>8} {'maxDD':>7} {'worstWk':>8} {'Sharpe':>7} " f"{'Pruin50':>8} {'Pdd30':>7} {'medDD':>6} {'p95DD':>6}") rows = [] for tv in TVOLS: d = frontier_row(variant_series, tv, bp, bvol) rows.append(d) P(f" {d['tvol']:6.3f} {d['lam']:5.2f} {d['cagr']:+8.4f} {d['maxdd']:7.4f} {d['worst_wk']:+8.4f} " f"{d['sharpe']:7.3f} {d['p_ruin']:8.4f} {d['p_dd30']:7.4f} {d['med_dd']:6.3f} {d['p95_dd']:6.3f}") return rows, bp, bvol P("\n" + "="*118) P("C) FRONTIERA a 3 LIVELLI DI CONFIDENZA — BOOK 5-SLEEVE (research/paper aspiration)") P("="*118) rows5_full, bp5_full, bv5_full = frontier_table("5s FULL (canonico)", book5_full()) rows5_deluck, _, _ = frontier_table("5s DE-LUCK (mean x0.85)", book5_deluck(0.15)) rows5_hc50, bp5_hc, bv5_hc = frontier_table("5s HAIRCUT VRP&XS -50%", book5_haircut(0.50)) rows5_hc30, _, _ = frontier_table("5s HAIRCUT VRP&XS -30%", book5_haircut(0.30)) rows5_excl, bp5_excl, bv5_excl = frontier_table("5s ESCLUDI VRP&XS (renorm)", book5_exclude()) P("\n" + "="*118) P("C) FRONTIERA a 3 LIVELLI — BOOK LIVE 2-SLEEVE (TP01+SKH 75/25, l'unico deployabile a 2-5k)") P("="*118) rows2_full, bp2_full, bv2_full = frontier_table("2s FULL (canonico)", book2_full()) rows2_deluck, _, _ = frontier_table("2s DE-LUCK (mean x0.85)", book2_deluck(0.15)) rows2_hc50, bp2_hc, bv2_hc = frontier_table("2s HAIRCUT SKH -50%", book2_haircut(0.50)) rows2_excl, _, _ = frontier_table("2s ESCLUDI SKH -> TP01 puro", book2_exclude()) # ---------------------------------------------------------------- B) SYNTHETIC TAIL INJECTION P("\n" + "="*118) P("B) INIEZIONE DI CODA SINTETICA — calibrazione su worst-correlato dei singoli sleeve") P("="*118) # giorno-crash a livello book = tutti gli sleeve al loro peggior giorno IN CONTEMPORANEA (corr->1 in crisi) def worst_correlated_day(weights): tot = sum(weights.values()) return sum((w / tot) * float(pd.Series(SL[k]).min()) for k, w in weights.items()) def worst_correlated_week(weights): tot = sum(weights.values()) return sum((w / tot) * emp_worst_week(SL[k]) for k, w in weights.items()) wc_day5, wc_wk5 = worst_correlated_day(W5), worst_correlated_week(W5) wc_day2, wc_wk2 = worst_correlated_day(W2), worst_correlated_week(W2) P(f" book 5-sleeve: worst_day in-sample={float(pd.Series(book5_full()).min()):+.4f} " f"worst_correlato(iniettato)={wc_day5:+.4f} | worst_wk in-sample={emp_worst_week(book5_full()):+.4f} " f"worst_wk_correlato={wc_wk5:+.4f}") P(f" book 2-sleeve: worst_day in-sample={float(pd.Series(book2_full()).min()):+.4f} " f"worst_correlato(iniettato)={wc_day2:+.4f} | worst_wk in-sample={emp_worst_week(book2_full()):+.4f} " f"worst_wk_correlato={wc_wk2:+.4f}") P(" Nota: il worst-correlato assume che in una LUNA/COVID TUTTI gli sleeve colpiscano insieme") P(" (VRP mai stressato reale, SKH ETH DD sottile, XS 2.5y) -> stress deliberatamente severo.") def inject_day(paths, xday): """Setta UN giorno casuale per path al valore xday (UNSCALED: la leva lo moltiplica).""" P2 = paths.copy() col = np.random.default_rng(SEED + 7).integers(0, P2.shape[1], size=P2.shape[0]) P2[np.arange(P2.shape[0]), col] = xday return P2 def inject_week(paths, xday_wk, ndays=5): """Setta 5 giorni consecutivi per path a xday_wk (giornaliero equivalente della settimana).""" P2 = paths.copy() st = np.random.default_rng(SEED + 8).integers(0, P2.shape[1] - ndays, size=P2.shape[0]) for k in range(ndays): P2[np.arange(P2.shape[0]), st + k] = xday_wk return P2 def inject_report(name, base_paths, base_vol, xday, xwk_daily): P(f"\n --- INIEZIONE su {name} (day={xday:+.3f}, week={xwk_daily:+.3f}/g x5) ---") P(f" {'tvol':>6} {'lam':>5} | {'ruin_base':>9} {'ruin+day':>9} {'ruin+wk':>8} | " f"{'dd30_base':>9} {'dd30+day':>9} {'dd30+wk':>8}") pd_day = inject_day(base_paths, xday) pd_wk = inject_week(base_paths, xwk_daily) out = [] for tv in TVOLS: lam = tv / base_vol mb, _ = path_maxdd(base_paths * lam) md, _ = path_maxdd(pd_day * lam) mw, _ = path_maxdd(pd_wk * lam) rb, rd, rw = (mb >= RUIN).mean(), (md >= RUIN).mean(), (mw >= RUIN).mean() db, dd_, dw = (mb >= DDLIM).mean(), (md >= DDLIM).mean(), (mw >= DDLIM).mean() out.append(dict(tv=tv, ruin_base=float(rb), ruin_day=float(rd), ruin_wk=float(rw), dd30_base=float(db), dd30_day=float(dd_), dd30_wk=float(dw))) P(f" {tv:6.3f} {lam:5.2f} | {rb:9.4f} {rd:9.4f} {rw:8.4f} | {db:9.4f} {dd_:9.4f} {dw:8.4f}") return out # week daily-equivalent: distribuisci la settimana-crash su 5 giorni uguali composti def wk_daily(x_wk): return (1 + x_wk) ** (1 / 5.0) - 1.0 inj5_full = inject_report("5s FULL", bp5_full, bv5_full, wc_day5, wk_daily(wc_wk5)) inj5_hc = inject_report("5s HAIRCUT", bp5_hc, bv5_hc, wc_day5, wk_daily(wc_wk5)) inj2_full = inject_report("2s FULL", bp2_full, bv2_full, wc_day2, wk_daily(wc_wk2)) inj2_hc = inject_report("2s HAIRCUT", bp2_hc, bv2_hc, wc_day2, wk_daily(wc_wk2)) # ---------------------------------------------------------------- DELIVERABLE P("\n" + "="*118) P("DELIVERABLE — target-vol MASSIMO che sopravvive SIA all'iniezione di coda SIA all'haircut") P("="*118) # criterio di confidenza (sul book HAIRCUT, CON iniezione peggiore day/week, orizzonte 5y): # P(rovina>50%) < 5% AND P(DD>30%) < 20% RUIN_TOL = 0.05 DD30_TOL = 0.20 def max_safe_tvol(inj_rows): ok = None for d in inj_rows: worst_ruin = max(d['ruin_day'], d['ruin_wk']) worst_dd30 = max(d['dd30_day'], d['dd30_wk']) if worst_ruin < RUIN_TOL and worst_dd30 < DD30_TOL: ok = d['tv'] else: break return ok P(f" criterio: sotto l'iniezione PEGGIORE (day|week), P(rovina>50%)<{RUIN_TOL:.0%} AND P(DD>30%)<{DD30_TOL:.0%}") P(f" BOOK 5-SLEEVE haircut+iniezione -> target-vol max fidato = {max_safe_tvol(inj5_hc)}") P(f" BOOK 2-SLEEVE haircut+iniezione -> target-vol max fidato = {max_safe_tvol(inj2_hc)}") # variante piu' tollerante (rovina<10%) RUIN_TOL2 = 0.10 def max_safe_tvol2(inj_rows, rt): ok = None for d in inj_rows: if max(d['ruin_day'], d['ruin_wk']) < rt: ok = d['tv'] else: break return ok P(f" [tolleranza P(rovina)<10%] 5-sleeve={max_safe_tvol2(inj5_hc,0.10)} 2-sleeve={max_safe_tvol2(inj2_hc,0.10)}") # GAP full-vs-fidata a pari DD: a quale CAGR arrivi a maxDD 6.2%/9.4% con full vs haircut/exclude def cagr_at_tvol(rows, tv): for d in rows: if abs(d['tvol'] - tv) < 1e-9: return d['cagr'], d['maxdd'] return None, None P("\n GAP full-vs-fidata (CAGR a pari target-vol):") for tv in (0.06, 0.08, 0.10, 0.15): cf, _ = cagr_at_tvol(rows5_full, tv); ch, _ = cagr_at_tvol(rows5_hc50, tv); ce, _ = cagr_at_tvol(rows5_excl, tv) P(f" 5s tvol={tv:.3f}: full CAGR={cf:+.4f} haircut50={ch:+.4f} escludi={ce:+.4f} " f"gap(full-escludi)={cf-ce:+.4f}") for tv in (0.08, 0.10, 0.15): cf, _ = cagr_at_tvol(rows2_full, tv); ch, _ = cagr_at_tvol(rows2_hc50, tv); ce, _ = cagr_at_tvol(rows2_excl, tv) P(f" 2s tvol={tv:.3f}: full CAGR={cf:+.4f} haircutSKH50={ch:+.4f} escludi(TP01)={ce:+.4f} " f"gap(full-escludi)={cf-ce:+.4f}") # ---------------------------------------------------------------- €/giorno a 2k e 5k P("\n" + "="*118) P("TRADUZIONE €/GIORNO (CAGR de-luck & haircut, capitale 2k e 5k)") P("="*118) def eur_day(cagr, cap): return cap * cagr / 365.0 for label, rows in [("5s FULL", rows5_full), ("5s DE-LUCK", rows5_deluck), ("5s HAIRCUT50", rows5_hc50), ("2s FULL", rows2_full), ("2s HAIRCUT50", rows2_hc50), ("2s ESCLUDI(TP01)", rows2_excl)]: for tv in (0.06, 0.10): c, _ = cagr_at_tvol(rows, tv) if c is None: continue P(f" {label:18s} tvol={tv:.3f}: CAGR={c:+.4f} -> 2k={eur_day(c,2000):+.2f}€/g 5k={eur_day(c,5000):+.2f}€/g") # ---------------------------------------------------------------- dump with open(os.path.join(OUT, 'r0703_frontier_tail_report.txt'), 'w') as f: f.write("\n".join(rep)) def rows_to_df(rows): return pd.DataFrame(rows) with pd.ExcelWriter(os.path.join(OUT, 'r0703_frontier.xlsx')) if False else open(os.devnull,'w'): pass for nm, rows in [('5s_full',rows5_full),('5s_deluck',rows5_deluck),('5s_hc50',rows5_hc50), ('5s_hc30',rows5_hc30),('5s_excl',rows5_excl),('2s_full',rows2_full), ('2s_deluck',rows2_deluck),('2s_hc50',rows2_hc50),('2s_excl',rows2_excl)]: rows_to_df(rows).to_csv(os.path.join(OUT, f'r0703_front_{nm}.csv'), index=False) P("\n[report salvato in scratchpad: r0703_frontier_tail_report.txt + CSV per book/variante]")