"""r0701_portfolio_opt — MIGLIORARE IL PORTAFOGLIO ATTIVO SENZA NUOVE STRATEGIE (2026-07-01). (A) RI-OTTIMIZZAZIONE DEI PESI STATICI dei 4 sleeve (TP01/XS01/VRP01/SKH01). I pesi correnti (41.25/18.75/15/25) sono ad-hoc. Test onesto: - ottimizza SOLO in-sample (pre-cut), valuta OOS (post-cut), multi-cut {2024-01, 2024-07, 2025-01}; - criteri: MAXSH (max-Sharpe simulato), RP (inverse-vol), ERC (equal-risk-contribution), MINVAR-R (min-varianza con vincolo di ritorno >= ritorno dei pesi correnti in-sample); - null di riferimento: EW (25x4) e CURRENT (pesi correnti); - vincoli: 0.05 <= w_i <= 0.60, sum(w)=1; - sleeve con < MIN_IS_DAYS giorni in-sample al cut (XS01 al 2024-01) -> PINNED al peso corrente (l'ottimizzatore non ha dati per giudicarli), gli altri ottimizzati nel budget residuo; - il combine replica ESATTAMENTE src/portfolio/portfolio.combined_daily (outer-join, pesi rinormalizzati per-riga sugli sleeve attivi). Verdetto: MIGLIORA solo se l'uplift OOS vs CURRENT e' PERSISTENTE (positivo a ogni cut), non su una finestra sola. (B) GUARDIA-DRAWDOWN a livello portafoglio (replica del soft-guard vincente del tail-hedge lab 2026-06-23, portato sul 4-sleeve). CAUSALE: l'esposizione del giorno D usa l'equity fino a D-1. Trigger/re-entry sul NAV OMBRA (equity non-guardata: lezione stops_lab — sull'equity congelata a expo=0 il DD non rientra mai). Isteresi: de-risk quando DD>X, re-risk quando DD<0.4*X. Griglia modesta X in {3,4,5,6}%, derisk in {0, 0.5}. Selezione cella IN-SAMPLE (pre-cut, criterio Calmar = CAGR/MaxDD), verifica OOS multi-cut. NULL DECISIVO (lezione tp01-dvol-overlay): esposizione COSTANTE c calibrata in-sample per pareggiare il MaxDD della guardia — lo Sharpe di c*r e' INVARIANTE, quindi la guardia "vale" solo se a pari MaxDD trattiene piu' CAGR/Sharpe del semplice de-levering. (C) Combinazione A+B se entrambe reggono. NB (contesto): il thread meta-allocazione (2026-06-29) ha gia' mostrato che l'allocazione DINAMICA fra sleeve perde vs pesi fissi — qui NON la rifacciamo: (A) e' statico, (B) e' un overlay di esposizione aggregata, non riallocazione fra sleeve. Solo ricerca: NON tocca src/ ne' scripts/live/. Propone pesi, non li applica. uv run python scripts/research/r0701_portfolio_opt.py """ from __future__ import annotations import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np import pandas as pd from scipy.optimize import minimize from src.portfolio.sleeves import active_sleeves from src.portfolio.portfolio import HOLDOUT, DAYS_PER_YEAR CUTS = [pd.Timestamp(c, tz="UTC") for c in ("2024-01-01", "2024-07-01", "2025-01-01")] LO_W, HI_W = 0.05, 0.60 # vincoli pesi MIN_IS_DAYS = 120 # sotto -> sleeve PINNED al peso corrente (niente dati per giudicarlo) GUARD_TRIGGERS = (0.03, 0.04, 0.05, 0.06) GUARD_DERISK = (0.0, 0.5) GUARD_REC_FRAC = 0.4 # re-risk quando DD < 0.4*trigger (isteresi, come tail-hedge lab) # --------------------------------------------------------------------------- dati def load_matrix() -> tuple[pd.DataFrame, list[str], np.ndarray]: """Matrice daily OUTER-join dei 4 sleeve (NaN dove lo sleeve non e' ancora partito).""" sl = active_sleeves() names = [s.name for s in sl] w_cur = np.array([s.weight for s in sl], float) w_cur = w_cur / w_cur.sum() J = pd.concat({s.name: s.daily() for s in sl}, axis=1, join="outer").sort_index() J = J[J.notna().any(axis=1)] return J, names, w_cur def combo(J: pd.DataFrame, w: np.ndarray) -> pd.Series: """Replica ESATTA di portfolio.combined_daily: pesi rinormalizzati per-riga sugli attivi.""" active = J.notna().values * w rowsum = active.sum(axis=1, keepdims=True) wnorm = np.divide(active, rowsum, out=np.zeros_like(active), where=rowsum > 0) return pd.Series(np.nansum(np.nan_to_num(J.values) * wnorm, axis=1), index=J.index) def met(r: pd.Series | np.ndarray) -> dict: v = np.asarray(pd.Series(r).dropna().values, float) if len(v) < 20 or v.std() == 0: return dict(sh=0.0, cagr=0.0, dd=0.0, calmar=0.0, n=len(v)) eq = np.cumprod(1 + v); pk = np.maximum.accumulate(eq) yrs = len(v) / DAYS_PER_YEAR cagr = float(eq[-1] ** (1 / yrs) - 1) if eq[-1] > 0 else -1.0 dd = float(np.max((pk - eq) / pk)) return dict(sh=float(v.mean() / v.std() * np.sqrt(DAYS_PER_YEAR)), cagr=cagr, dd=dd, calmar=(cagr / dd if dd > 0 else 0.0), n=len(v)) def fmt_w(names, w): return " ".join(f"{n.split('_')[0]} {x*100:.1f}%" for n, x in zip(names, w)) # --------------------------------------------------------------------------- (A) ottimizzatori def _pinned_mask(J_is: pd.DataFrame, w_cur: np.ndarray) -> np.ndarray: """True = sleeve senza abbastanza storia in-sample -> peso fissato al corrente.""" return np.array([J_is[c].notna().sum() < MIN_IS_DAYS for c in J_is.columns]) def _solve(J_is, w_cur, objective, extra_cons=None, hi_bounds=None) -> np.ndarray: """SLSQP su pesi liberi (i pinned restano al corrente), bounds+somma, multi-start. hi_bounds: array di upper-bound per-sleeve (default HI_W) — per i cap STRUTTURALI.""" k = len(w_cur) hi = np.full(k, HI_W) if hi_bounds is None else np.asarray(hi_bounds, float) pin = _pinned_mask(J_is, w_cur) free = ~pin budget = 1.0 - w_cur[pin].sum() nf = int(free.sum()) def full_w(wf): w = w_cur.copy(); w[free] = wf return w cons = [dict(type="eq", fun=lambda wf: wf.sum() - budget)] if extra_cons: cons += [dict(type="ineq", fun=lambda wf, f=f: f(full_w(wf))) for f in extra_cons] bounds = [(LO_W, float(h)) for h in hi[free]] hif = hi[free] starts = [w_cur[free], np.full(nf, budget / nf)] rng = np.random.default_rng(7) for _ in range(4): x = rng.uniform(LO_W, hif); starts.append(x / x.sum() * budget) best, bval = w_cur.copy(), np.inf for x0 in starts: x0 = np.clip(x0, LO_W, hif); x0 = x0 / x0.sum() * budget res = minimize(lambda wf: objective(full_w(wf)), x0, method="SLSQP", bounds=bounds, constraints=cons, options=dict(maxiter=300, ftol=1e-10)) if res.success and res.fun < bval: bval, best = res.fun, full_w(res.x) best = np.clip(best, LO_W, hi) return best / best.sum() def opt_maxsh(J_is, w_cur): return _solve(J_is, w_cur, lambda w: -met(combo(J_is, w))["sh"]) def opt_minvar_ret(J_is, w_cur): """Min-varianza con vincolo: media in-sample >= media dei pesi correnti in-sample.""" mu_cur = float(combo(J_is, w_cur).mean()) return _solve(J_is, w_cur, lambda w: float(combo(J_is, w).var()), extra_cons=[lambda w: float(combo(J_is, w).mean()) - mu_cur]) def opt_rp(J_is, w_cur): """Inverse-vol (risk-parity naive) sulla vol in-sample di ogni sleeve (giorni attivi propri).""" pin = _pinned_mask(J_is, w_cur); free = ~pin vol = np.array([J_is[c].dropna().std() for c in J_is.columns]) w = w_cur.copy() iv = 1.0 / np.where(vol[free] > 0, vol[free], np.inf) w[free] = iv / iv.sum() * (1.0 - w_cur[pin].sum()) for _ in range(50): # clip ai bounds + rinormalizza w = np.clip(w, LO_W, HI_W) if abs(w.sum() - 1.0) < 1e-9: break adj = ~np.isclose(w, LO_W) & ~np.isclose(w, HI_W) if not adj.any(): break w[adj] += (1.0 - w.sum()) * (w[adj] / w[adj].sum()) return w / w.sum() def opt_erc(J_is, w_cur): """Equal-risk-contribution sulla cov in-sample pairwise-complete (submatrice dei non-pinned).""" pin = _pinned_mask(J_is, w_cur) cov = J_is.cov().values # pairwise-complete idx = np.where(~pin)[0] S = cov[np.ix_(idx, idx)] S = np.nan_to_num(S, nan=0.0) def obj(w): wf = w[idx] port = wf @ S @ wf if port <= 0: return 1e6 rc = wf * (S @ wf) / port return float(np.sum((rc - 1.0 / len(idx)) ** 2)) return _solve(J_is, w_cur, obj) def _struct_caps(J_is) -> np.ndarray: """Cap STRUTTURALI (prudenza, non statistica): VRP01 <= 15% (sleeve MODELLATO su DVOL ATM, stress-f non catturato — 'niente short-vol da modello in deploy'), XS01 <= 25% (STAT-MODE, storia ~2.5 anni). Gli ottimizzatori naive caricano VRP01 (vol 7.7% ma e' la vol del MODELLO).""" return np.array([0.15 if c.startswith("VRP") else (0.25 if c.startswith("XS") else HI_W) for c in J_is.columns]) def opt_maxsh_struct(J_is, w_cur): return _solve(J_is, w_cur, lambda w: -met(combo(J_is, w))["sh"], hi_bounds=_struct_caps(J_is)) CRITERIA = [("MAXSH", opt_maxsh), ("RP-invvol", opt_rp), ("ERC", opt_erc), ("MINVAR-R", opt_minvar_ret), ("MAXSH-STR", opt_maxsh_struct)] # --------------------------------------------------------------------------- (B) guardia-DD def dd_guard(r: pd.Series, trigger: float, derisk: float, rec_frac: float = GUARD_REC_FRAC): """Soft-guard causale: expo[t] decisa dal DD del NAV OMBRA (non-guardato) fino a t-1. DD>trigger -> expo=derisk; DD expo=1 (isteresi). Ritorna (serie, expo).""" v = r.values eq = np.cumprod(1 + v); pk = np.maximum.accumulate(eq) n = len(v); expo = np.ones(n); on = True for i in range(1, n): ddi = (pk[i - 1] - eq[i - 1]) / pk[i - 1] if ddi > trigger: on = False elif ddi < trigger * rec_frac: on = True expo[i] = 1.0 if on else derisk return pd.Series(expo * v, index=r.index), expo def const_expo_match_dd(r: pd.Series, target_dd: float) -> float: """Esposizione costante c (in-sample) che pareggia il MaxDD target — il null de-levering.""" lo, hi = 0.05, 1.0 if met(r)["dd"] <= target_dd: return 1.0 for _ in range(40): mid = 0.5 * (lo + hi) if met(pd.Series(mid * r.values, index=r.index))["dd"] > target_dd: hi = mid else: lo = mid return lo # --------------------------------------------------------------------------- report def section_A(J, names, w_cur): print("\n" + "=" * 108) print(" (A) RI-OTTIMIZZAZIONE PESI STATICI — ottimizza IN-SAMPLE (pre-cut), valuta OOS (post-cut), multi-cut") print("=" * 108) # correlazioni e vol per contesto (full, pairwise) C = J.corr() print("\n corr pairwise (full):") for i, a in enumerate(names): print(" " + a.split("_")[0].ljust(6) + " ".join(f"{C.iloc[i, j]:+.2f}" for j in range(len(names)))) print(" vol annua per-sleeve (full): " + " ".join(f"{n.split('_')[0]} {J[n].dropna().std()*np.sqrt(DAYS_PER_YEAR)*100:.1f}%" for n in names)) candidates = {} # nome -> pesi ottimizzati su pre-HOLDOUT (headline) J_is_main = J[J.index < HOLDOUT] for cname, fn in CRITERIA: candidates[cname] = fn(J_is_main, w_cur) candidates["EW"] = np.full(len(names), 1.0 / len(names)) caps = _struct_caps(J) w_ewstr = np.full(len(names), 1.0 / len(names)) # EW sotto i cap strutturali (null senza fit) for _ in range(20): over = w_ewstr > caps + 1e-12 if not over.any(): break excess = float((w_ewstr[over] - caps[over]).sum()) w_ewstr[over] = caps[over] room = w_ewstr < caps - 1e-12 # redistribuisci SOLO a chi ha spazio w_ewstr[room] += excess / max(int(room.sum()), 1) candidates["EW-STR"] = w_ewstr candidates["CURRENT"] = w_cur print(f"\n PESI (ottimizzati su in-sample pre-{HOLDOUT.date()}):") for cname, w in candidates.items(): print(f" {cname:<10s} {fmt_w(names, w)}") # headline: FULL + HOLD-OUT print(f"\n {'criterio':<10s} {'FULL Sh':>8s} {'FULL DD':>8s} {'FULL CAGR':>9s} | {'HOLD Sh':>8s} {'HOLD DD':>8s} {'HOLD CAGR':>9s}") rows = {} for cname, w in candidates.items(): f = met(combo(J, w)); h = met(combo(J[J.index >= HOLDOUT], w)) rows[cname] = (f, h) print(f" {cname:<10s} {f['sh']:>8.2f} {f['dd']*100:>7.1f}% {f['cagr']*100:>+8.1f}% |" f" {h['sh']:>8.2f} {h['dd']*100:>7.1f}% {h['cagr']*100:>+8.1f}%") # multi-cut: ottimizza su =cut, uplift vs CURRENT sulla stessa finestra OOS print("\n MULTI-CUT (pesi ri-ottimizzati a ogni cut sul solo pre-cut; ΔSh OOS vs CURRENT):") header = f" {'criterio':<10s}" + "".join(f" | {c.date()} Sh_oos ΔSh " for c in CUTS) print(header) persist = {} fixed = {"EW": candidates["EW"], "EW-STR": candidates["EW-STR"]} for cname, fn in CRITERIA + [("EW", None), ("EW-STR", None)]: cells = [] ok = True for cut in CUTS: J_is, J_oos = J[J.index < cut], J[J.index >= cut] w = fixed[cname] if cname in fixed else fn(J_is, w_cur) sh_o = met(combo(J_oos, w))["sh"] sh_c = met(combo(J_oos, w_cur))["sh"] d = sh_o - sh_c ok &= d > 0 cells.append(f" | {sh_o:>10.2f} {d:>+5.2f}") persist[cname] = ok print(f" {cname:<10s}" + "".join(cells) + (" <-- PERSISTENTE" if ok else "")) print(" ⚠ caveat: le 3 finestre OOS sono ANNIDATE (tutte contengono il 2025-26) — non 3 conferme") print(" indipendenti. Il test disgiunto vero e' il per-anno qui sotto.") # per-anno (finestre DISGIUNTE): ritorno annuo del candidato vs CURRENT, pesi fissi pre-2025 print("\n PER-ANNO (pesi headline fissi; Δret vs CURRENT, finestre disgiunte):") sel = ["MAXSH", "MINVAR-R", "MAXSH-STR", "EW", "EW-STR"] years = sorted(set(J.index.year)) print(" " + "anno".ljust(6) + "".join(f"{c:>12s}" for c in ["CURRENT"] + sel)) wins = {c: 0 for c in sel} for y in years: Jy = J[J.index.year == y] base_ret = float(np.prod(1 + combo(Jy, w_cur).values) - 1) row = f" {y} {base_ret*100:>+10.1f}%" for c in sel: ry = float(np.prod(1 + combo(Jy, candidates[c]).values) - 1) wins[c] += ry > base_ret row += f"{(ry-base_ret)*100:>+11.1f}p" print(row) print(" anni vinti vs CURRENT: " + " ".join(f"{c} {wins[c]}/{len(years)}" for c in sel)) # verdetto: la persistenza statistica NON basta — i pesi devono rispettare i cap STRUTTURALI # (VRP01<=15% perche' MODELLATO, XS01<=25% perche' STAT-MODE/storia corta), altrimenti l'uplift # sta comprando rischio-modello che il backtest non vede (punto cieco tipo CC01). winners = [c for c, ok in persist.items() if ok] respects = {c: bool(np.all(candidates[c] <= caps + 1e-3)) for c in candidates} # tol 0.1pp (jitter clip/renorm) struct_winners = [c for c in winners if respects[c]] print("\n cap strutturali: " + fmt_w(names, caps)) if winners: print(" persistenti: " + ", ".join(f"{c} ({'dentro i cap' if respects[c] else 'VIOLA i cap'})" for c in winners)) if struct_winners: wbest = candidates[struct_winners[0]] fb, fc = met(combo(J, wbest)), met(combo(J, w_cur)) hb, hc = met(combo(J[J.index >= HOLDOUT], wbest)), met(combo(J[J.index >= HOLDOUT], w_cur)) verdict = (f"MIGLIORA (OOS) — {struct_winners} persistente E dentro i cap strutturali. " f"Pesi candidati ({struct_winners[0]}): {fmt_w(names, wbest)}.\n" f" TRADE-OFF onesto: HOLD Sh {hc['sh']:.2f}->{hb['sh']:.2f}, HOLD CAGR " f"{hc['cagr']*100:+.1f}%->{hb['cagr']*100:+.1f}%, MA FULL Sh {fc['sh']:.2f}->{fb['sh']:.2f} " f"e FULL DD {fc['dd']*100:.1f}%->{fb['dd']*100:.1f}% — l'uplift e' un TILT di ritorno verso " f"gli sleeve research-grade (piu' SKH01/XS01, meno TP01), non un free lunch risk-adjusted. " f"NB: nessun OTTIMIZZATORE aggiunge nulla oltre questo null parameter-free " f"(MAXSH in-sample vince 1/8 anni: l'ottimizzazione qui overfitta, la de-concentrazione no).") elif winners: verdict = ("NON ADOTTABILE — l'uplift persistente esiste SOLO violando i cap strutturali " "(tutti i vincitori caricano VRP01>15%, sleeve MODELLATO il cui rischio di coda " "il backtest non cattura). Dentro i cap l'ottimizzatore converge ~ai pesi correnti " "e non e' persistente -> per il book reale I PESI CORRENTI REGGONO.") else: verdict = "NON MIGLIORA — nessun criterio batte i pesi CORRENTI in modo persistente. I pesi correnti reggono." print("\n VERDETTO (A): " + verdict) return candidates, persist, struct_winners def section_B(J, names, w_cur, label="CURRENT"): print("\n" + "=" * 108) print(f" (B) GUARDIA-DRAWDOWN a livello portafoglio (pesi {label}) — trigger X, de-risk d, isteresi 0.4X") print("=" * 108) r_full = combo(J, w_cur) # griglia completa su FULL/IS/HOLD per trasparenza (selezione = solo in-sample) r_is = r_full[r_full.index < HOLDOUT] r_ho = r_full[r_full.index >= HOLDOUT] b_is, b_ho, b_f = met(r_is), met(r_ho), met(r_full) print(f"\n baseline IS Sh {b_is['sh']:.2f} DD {b_is['dd']*100:.1f}% CAGR {b_is['cagr']*100:+.1f}%" f" | HOLD Sh {b_ho['sh']:.2f} DD {b_ho['dd']*100:.1f}% CAGR {b_ho['cagr']*100:+.1f}%" f" | FULL Sh {b_f['sh']:.2f} DD {b_f['dd']*100:.1f}%") print(f"\n {'cella':<16s} {'IS Sh':>6s} {'IS DD':>6s} {'IS CAGR':>8s} {'IS Calmar':>9s} {'%off':>5s} |" f" {'HOLD Sh':>7s} {'HOLD DD':>7s} {'HOLD CAGR':>9s}") grid = {} for trig in GUARD_TRIGGERS: for dr in GUARD_DERISK: g_full, expo = dd_guard(r_full, trig, dr) g_is = g_full[g_full.index < HOLDOUT]; g_ho = g_full[g_full.index >= HOLDOUT] mi, mh = met(g_is), met(g_ho) off = float((expo[: len(r_is)] < 1.0).mean()) grid[(trig, dr)] = dict(is_=mi, ho=mh, full=met(g_full)) print(f" X={trig*100:.0f}% d={dr:<4.1f} {mi['sh']:>6.2f} {mi['dd']*100:>5.1f}% {mi['cagr']*100:>+7.1f}%" f" {mi['calmar']:>9.2f} {off*100:>4.0f}% | {mh['sh']:>7.2f} {mh['dd']*100:>6.1f}% {mh['cagr']*100:>+8.1f}%") # selezione IN-SAMPLE (Calmar), poi null de-levering a pari MaxDD in-sample best_cell = max(grid, key=lambda k: grid[k]["is_"]["calmar"]) trig, dr = best_cell print(f"\n cella selezionata IN-SAMPLE (max Calmar): X={trig*100:.0f}% derisk={dr}") tgt_dd = grid[best_cell]["is_"]["dd"] c = const_expo_match_dd(r_is, tgt_dd) cnull_full = pd.Series(c * r_full.values, index=r_full.index) n_is, n_ho = met(cnull_full[cnull_full.index < HOLDOUT]), met(cnull_full[cnull_full.index >= HOLDOUT]) g = grid[best_cell] print(f" NULL de-levering: expo costante c={c:.2f} pareggia il MaxDD IS della guardia ({tgt_dd*100:.1f}%)") print(f" {'':<14s} {'IS Sh':>6s} {'IS CAGR':>8s} | {'HOLD Sh':>7s} {'HOLD DD':>7s} {'HOLD CAGR':>9s}") print(f" guardia {g['is_']['sh']:>6.2f} {g['is_']['cagr']*100:>+7.1f}% | {g['ho']['sh']:>7.2f}" f" {g['ho']['dd']*100:>6.1f}% {g['ho']['cagr']*100:>+8.1f}%") print(f" const c={c:.2f} {n_is['sh']:>6.2f} {n_is['cagr']*100:>+7.1f}% | {n_ho['sh']:>7.2f}" f" {n_ho['dd']*100:>6.1f}% {n_ho['cagr']*100:>+8.1f}%") # multi-cut: selezione sul pre-cut, valutazione OOS post-cut (guardia vs baseline vs null) inert = True print("\n MULTI-CUT (cella riscelta a ogni cut sul solo pre-cut; OOS post-cut):") print(f" {'cut':<12s} {'cella':<14s} {'OOS Sh(g)':>9s} {'OOS Sh(b)':>9s} {'ΔSh':>6s} {'OOS DD(g)':>9s}" f" {'OOS DD(b)':>9s} {'ΔCAGR':>7s} {'null c: ΔSh':>11s}") persist_sh, persist_dd = True, True for cut in CUTS: r_pre = r_full[r_full.index < cut] cells = {} for t in GUARD_TRIGGERS: for d in GUARD_DERISK: gpre, _ = dd_guard(r_pre, t, d) cells[(t, d)] = met(gpre)["calmar"] (t_, d_) = max(cells, key=cells.get) g_full, _ = dd_guard(r_full, t_, d_) # guard sul path completo (stato causale) g_oos = g_full[g_full.index >= cut] b_oos = r_full[r_full.index >= cut] gm, bm = met(g_oos), met(b_oos) cc = const_expo_match_dd(r_pre, met(dd_guard(r_pre, t_, d_)[0])["dd"]) nm = met(pd.Series(cc * b_oos.values, index=b_oos.index)) dsh = gm["sh"] - bm["sh"]; ddd = gm["dd"] - bm["dd"]; dcagr = gm["cagr"] - bm["cagr"] persist_sh &= dsh >= 0; persist_dd &= ddd < 0 inert &= abs(dsh) < 0.005 and abs(ddd) < 1e-4 print(f" {str(cut.date()):<12s} X={t_*100:.0f}% d={d_:<6.1f} {gm['sh']:>9.2f} {bm['sh']:>9.2f} {dsh:>+6.2f}" f" {gm['dd']*100:>8.1f}% {bm['dd']*100:>8.1f}% {dcagr*100:>+6.1f}pp {gm['sh']-nm['sh']:>+11.2f}") if inert: verdict = ("NON MIGLIORA — INERTE OOS: la cella scelta in-sample non scatta MAI post-cut " "(il DD OOS del 4-sleeve resta sotto il trigger: la diversificazione fa gia' il lavoro " "della guardia). Trigger piu' stretti (X=3-4%) scattano ma COSTANO Sharpe su HOLD. " "Coerente col thread meta-allocazione 2026-06-29 (drawdown-control ridondante).") elif persist_sh and persist_dd: verdict = f"MIGLIORA — la guardia X={trig*100:.0f}%/d={dr} taglia il DD OOS senza costo Sharpe persistente" elif persist_dd: verdict = ("TAGLIA IL DD MA COSTA — DD ridotto a ogni cut ma Sharpe/CAGR inferiori: " "trade-off assicurativo, confrontare col null de-levering prima di adottarla") else: verdict = "NON MIGLIORA — la guardia non riduce il DD OOS in modo persistente (o e' solo de-levering)" print("\n VERDETTO (B): " + verdict) return grid, best_cell def section_C(J, names, w_cur, candidates, struct_winners, grid, best_cell): print("\n" + "=" * 108) print(" (C) COMBINAZIONE A+B") print("=" * 108) wA = candidates[struct_winners[0]] if struct_winners else w_cur labA = struct_winners[0] if struct_winners else "CURRENT (A non adottabile)" trig, dr = best_cell r = combo(J, wA) g, _ = dd_guard(r, trig, dr) for lbl, s in [("baseline CURRENT", combo(J, w_cur)), (f"pesi {labA}", r), (f"pesi {labA} + guard X={trig*100:.0f}%/d={dr}", g)]: f = met(s); h = met(s[s.index >= HOLDOUT]) print(f" {lbl:<52s} FULL Sh {f['sh']:>5.2f} DD {f['dd']*100:>4.1f}% CAGR {f['cagr']*100:>+5.1f}%" f" | HOLD Sh {h['sh']:>5.2f} DD {h['dd']*100:>4.1f}% CAGR {h['cagr']*100:>+5.1f}%") if not struct_winners: print(" -> (A) non adottabile e (B) inerte OOS: la combinazione non ha una cella da proporre.") def main(): print("=" * 108) print(" r0701_portfolio_opt — pesi statici + guardia-DD sul portafoglio attivo a 4 sleeve") print(f" hold-out bloccato {HOLDOUT.date()}+ | vincoli pesi [{LO_W},{HI_W}] | cuts {[str(c.date()) for c in CUTS]}") print("=" * 108) J, names, w_cur = load_matrix() print(f"\n sleeve: {names}") print(f" pesi CORRENTI: {fmt_w(names, w_cur)}") print(f" finestra: {J.index[0].date()} -> {J.index[-1].date()} ({len(J)} giorni)") for n in names: s = J[n].dropna() print(f" {n:<16s} dal {s.index[0].date()} ({len(s)} giorni)") b_f = met(combo(J, w_cur)); b_h = met(combo(J[J.index >= HOLDOUT], w_cur)) print(f"\n BASELINE (pesi correnti): FULL Sh {b_f['sh']:.2f} DD {b_f['dd']*100:.1f}% CAGR {b_f['cagr']*100:+.1f}%" f" | HOLD Sh {b_h['sh']:.2f} DD {b_h['dd']*100:.1f}% CAGR {b_h['cagr']*100:+.1f}%") candidates, persistA, struct_winners = section_A(J, names, w_cur) grid, best_cell = section_B(J, names, w_cur) section_C(J, names, w_cur, candidates, struct_winners, grid, best_cell) if __name__ == "__main__": main()