Files
PythagorasGoal/scripts/research/r0703_frontier_tail.py
T
Adriano Dal Pastro eef200cf14 research(frontier): frontiera onesta del book — il 6% e' un miraggio di regime, gira gia' alla vol nativa
Misura (non ricerca segnali) della frontiera CAGR/DD/P-rovina vs
target-vol, pesata per confidenza per-sleeve, a 2k/5k. 7 agenti (3
filoni + refuter fat-tail + scettico), sanity bit-exact dei due book.

Reframe decisivo: il "6% attuale" NON e' un target, e' la vol realizzata
in risk-off; a lambda=1 il book LIVE 2-sleeve gira gia' a ~11% vol/9.4%
DD -> "saltare a 10%" e' assumere non-risk-off, non aggiungere leva.

- Target-vol fidato book LIVE (TP01+SKH01, unico eseguibile <20k):
  ~10% (banda 8-11%, lambda ~0.9). Scale-invariante: stesso % a 2k/5k.
- P(rovina-50%) e' la metrica SBAGLIATA (slack a 22%); il vincolo che
  morde e' P(DD>30%), super-lineare. Muro onesto ~12% (gap-through SKH
  reale non nei rendimenti modellati; 2-sleeve 4x crash-sensibile).
- Vol differenziata-per-confidenza REFUTATA: taglia la diversificazione;
  la bassa fiducia va nel DE-MEAN (haircut media), non nella leva.
- Reward onesto (HOLD<<FULL): ~EUR 0.3-0.7/g@2k, 0.8-1.8/g@5k; warm-up
  vs 6% risk-off = ~+EUR 0.2/g@2k. EUR 50/g resta ~130k (capitale+tempo).
- UNICA azione config (proposta, NON applicata): cap $300 -> equity/2,
  altrimenti a 5k il cap raffredda paradossalmente il book sotto il punto
  fidato (6% vol ceiling).

config/live.json NON toccato. Book invariato. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 08:43:59 +00:00

386 lines
20 KiB
Python

"""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]")