"""r0701_portfolio_skeptic — VERIFICA AVVERSARIALE del tilt EW-STR (TP30/XS25/VRP15/SKH30). Oggetto: la proposta di r0701_portfolio_opt (diario 2026-07-01-portfolio-weights-ddguard.md) di spostare i pesi del portafoglio a 4 sleeve da CURRENT (41.25/18.75/15/25) a EW-STR (30/25/15/30). Claim: HOLD Sh 2.21->2.35, HOLD CAGR +16.0->+19.7%, multi-cut +0.06/+0.13/+0.14, 7/8 anni vinti. Linee di attacco (agente scettico): 1. RIPRODUZIONE indipendente via src.portfolio.StrategyPortfolio (path di PRODUZIONE, non lo script dell'agente). 2. SELEZIONE-SULL'HOLD-OUT DI 2° ORDINE via SKH01/XS01: SKH01 fu ammesso (2026-06-23) perche' alzava l'hold-out 2025-26; XS01 fu affinato (blend+gate) conoscendo l'hold-out. Un tilt verso di loro eredita quella selezione? Test: (a) per-anno SOLO 2019-2024; (b) tilt scomposti (solo-XS con SKH pinned 25, solo-SKH con XS pinned); (c) finestre OOS DISGIUNTE (i 3 cut del diario sono ANNIDATI); (d) contributo per-sleeve all'uplift hold-out. 3. "7/8 ANNI VINTI": quale metrica (ritorno, non Sharpe), margini per anno, Sharpe per-anno. 4. PLATEAU: griglia 2.5pp fra CURRENT e EW-STR (TP x SKH, VRP=15, XS=residuo cap 25). 5. REALISMO: pesi RINORMALIZZATI per era (outer-join: nel 2019-20 attivi solo TP+SKH!), haircut d'esecuzione su SKH01/XS01 come DRAG (r' = r - h*mean(r), modello-costi, non de-levering). 6. FORKING PATHS: quante config viste sull'hold-out; null di tilt CASUALI cap-respecting — se quasi ogni tilt anti-TP01 batte CURRENT sull'hold-out, il claim e' generico, non informativo. Solo lettura/analisi: NON tocca src/ ne' scripts/live/. uv run python scripts/research/r0701_portfolio_skeptic.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 src.portfolio.sleeves import active_sleeves from src.portfolio.portfolio import StrategyPortfolio, HOLDOUT, DAYS_PER_YEAR, metrics CUTS = [pd.Timestamp(c, tz="UTC") for c in ("2024-01-01", "2024-07-01", "2025-01-01")] LO_W = 0.05 # pesi per PREFISSO sleeve (ordine risolto sui nomi del registry) W_CURRENT = {"TP01": 0.4125, "XS01": 0.1875, "VRP01": 0.15, "SKH01": 0.25} W_EWSTR = {"TP01": 0.30, "XS01": 0.25, "VRP01": 0.15, "SKH01": 0.30} W_ONLY_XS = {"TP01": 0.35, "XS01": 0.25, "VRP01": 0.15, "SKH01": 0.25} # SKH pinned al corrente W_ONLY_SKH = {"TP01": 0.3625, "XS01": 0.1875, "VRP01": 0.15, "SKH01": 0.30} # XS pinned al corrente CAPS = {"TP01": 0.60, "XS01": 0.25, "VRP01": 0.15, "SKH01": 0.60} def prefix(name: str) -> str: return name.split("_")[0] def wvec(names: list[str], wd: dict) -> np.ndarray: v = np.array([wd[prefix(n)] for n in names], float) return v / v.sum() def combo(J: pd.DataFrame, w: np.ndarray) -> pd.Series: """Combine outer-join con pesi rinormalizzati per-riga (stessa semantica di combined_daily; verificata sotto contro il path di produzione).""" act = J.notna().values * w rs = act.sum(axis=1, keepdims=True) wn = np.divide(act, rs, out=np.zeros_like(act), where=rs > 0) return pd.Series(np.nansum(np.nan_to_num(J.values) * wn, axis=1), index=J.index) def met(s) -> dict: return metrics(pd.Series(s).dropna()) # metriche di PRODUZIONE (src.portfolio) def sh(s) -> float: return met(s)["sharpe"] def yr_ret(s) -> float: v = pd.Series(s).dropna().values return float(np.prod(1 + v) - 1) def main(): print("=" * 110) print(" r0701_portfolio_skeptic — verifica avversariale EW-STR (30/25/15/30) vs CURRENT (41.25/18.75/15/25)") print("=" * 110) sl = active_sleeves() names = [s.name for s in sl] J = pd.concat({s.name: s.daily() for s in sl}, axis=1, join="outer").sort_index() J = J[J.notna().any(axis=1)] w_cur = wvec(names, W_CURRENT) w_ew = wvec(names, W_EWSTR) print(f"\n 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)") # ------------------------------------------------ 1) RIPRODUZIONE (path di produzione) print("\n" + "-" * 110) print(" [1] RIPRODUZIONE INDIPENDENTE — StrategyPortfolio.combined_daily (produzione), non lo script agente") print("-" * 110) for lbl, wd in [("CURRENT", W_CURRENT), ("EW-STR", W_EWSTR)]: for s, sleeve_w in zip(sl, [wd[prefix(n)] for n in names]): s.weight = sleeve_w port = StrategyPortfolio(sl) full = port.combined_daily() hold = port.combined_daily(lo=HOLDOUT) # check che il mio combine coincida col path di produzione mine = combo(J, wvec(names, wd)) dmax = float(np.max(np.abs(mine.values - full.values))) f, h = met(full), met(hold) print(f" {lbl:<8s} FULL Sh {f['sharpe']:.2f} DD {f['maxdd']*100:.1f}% CAGR {f['cagr']*100:+.1f}%" f" | HOLD Sh {h['sharpe']:.2f} DD {h['maxdd']*100:.1f}% CAGR {h['cagr']*100:+.1f}%" f" (|combine mio - produzione|max = {dmax:.2e})") print(" multi-cut ΔSh OOS EW-STR vs CURRENT (finestre ANNIDATE, come nel diario):") for cut in CUTS: Jo = J[J.index >= cut] d = sh(combo(Jo, w_ew)) - sh(combo(Jo, w_cur)) print(f" cut {cut.date()} ΔSh {d:+.2f}") # ------------------------------------------------ 2) SELEZIONE-SULL'HOLD-OUT DI 2° ORDINE print("\n" + "-" * 110) print(" [2] SELEZIONE HOLD-OUT DI 2° ORDINE — SKH01 ammesso PERCHE' alzava l'hold-out; XS01 affinato idem") print("-" * 110) # (d prima: contesto) Sharpe standalone per-sleeve, pre-2025 vs hold-out print(" Sharpe standalone per sleeve (pre-2025 | hold-out 2025+):") for n in names: s = J[n].dropna() print(f" {n:<16s} IS {sh(s[s.index < HOLDOUT]):>5.2f} | HOLD {sh(s[s.index >= HOLDOUT]):>5.2f}") # (a) per-anno SOLO 2019-2024 (pre-selezione di SKH01/holdout) print("\n (a) per-anno SOLO 2019-2024 (finestra NON toccata dalla selezione hold-out):") print(f" {'anno':<6s} {'ret CUR':>9s} {'ret EW-STR':>10s} {'Δret':>8s} | {'Sh CUR':>7s} {'Sh EW-STR':>9s} {'ΔSh':>6s}") wins_ret_pre = wins_sh_pre = 0 years_pre = [y for y in sorted(set(J.index.year)) if y < 2025] for y in years_pre: Jy = J[J.index.year == y] rc, re = combo(Jy, w_cur), combo(Jy, w_ew) dr = yr_ret(re) - yr_ret(rc); dsh = sh(re) - sh(rc) wins_ret_pre += dr > 0; wins_sh_pre += dsh > 0 print(f" {y:<6d} {yr_ret(rc)*100:>+8.1f}% {yr_ret(re)*100:>+9.1f}% {dr*100:>+7.1f}p |" f" {sh(rc):>7.2f} {sh(re):>9.2f} {dsh:>+6.2f}") print(f" -> vittorie EW-STR 2019-2024: ritorno {wins_ret_pre}/{len(years_pre)}, Sharpe {wins_sh_pre}/{len(years_pre)}") Jpre = J[J.index < HOLDOUT] print(f" pre-2025 aggregato: Sh CUR {sh(combo(Jpre, w_cur)):.2f} vs EW-STR {sh(combo(Jpre, w_ew)):.2f}" f" (ΔSh {sh(combo(Jpre, w_ew)) - sh(combo(Jpre, w_cur)):+.2f})") # (b) tilt scomposti print("\n (b) tilt SCOMPOSTI (chi porta l'uplift?): HOLD Sh + multi-cut ΔSh vs CURRENT") for lbl, wd in [("solo-XS (SKH pinned 25): TP35 /XS25/VRP15/SKH25", W_ONLY_XS), ("solo-SKH (XS pinned 18.75): TP36.25/XS18.75/VRP15/SKH30", W_ONLY_SKH), ("EW-STR: TP30 /XS25/VRP15/SKH30", W_EWSTR)]: w = wvec(names, wd) hold_sh = sh(combo(J[J.index >= HOLDOUT], w)) cuts_d = [sh(combo(J[J.index >= c], w)) - sh(combo(J[J.index >= c], w_cur)) for c in CUTS] print(f" {lbl:<58s} HOLD Sh {hold_sh:.2f} multi-cut " + " ".join(f"{d:+.2f}" for d in cuts_d)) # (c) finestre OOS DISGIUNTE (i cut del diario sono annidati) print("\n (c) finestre OOS DISGIUNTE (ΔSh e Δret EW-STR vs CURRENT):") windows = [("2024-01 -> 2024-07", "2024-01-01", "2024-07-01"), ("2024-07 -> 2025-01", "2024-07-01", "2025-01-01"), ("2025-01 -> fine ", "2025-01-01", None)] for lbl, lo, hi in windows: m = (J.index >= pd.Timestamp(lo, tz="UTC")) if hi: m &= (J.index < pd.Timestamp(hi, tz="UTC")) Jw = J[m] rc, re = combo(Jw, w_cur), combo(Jw, w_ew) print(f" {lbl} ΔSh {sh(re) - sh(rc):+.2f} Δret {(yr_ret(re) - yr_ret(rc))*100:+.1f}pp") # (d) contributo per-sleeve all'uplift hold-out (pesi statici: sul hold-out tutti attivi) print("\n (d) contributo per-sleeve al Δritorno annualizzato sull'hold-out (Δw_i x mean_i x 365):") Jh = J[J.index >= HOLDOUT] for n in names: dw = W_EWSTR[prefix(n)] - W_CURRENT[prefix(n)] mu = float(Jh[n].dropna().mean()) * DAYS_PER_YEAR print(f" {n:<16s} Δw {dw*100:>+6.2f}pp x ret_ann {mu*100:>+6.1f}% = {dw*mu*100:>+5.2f}pp/anno") # ------------------------------------------------ 3) "7/8 ANNI VINTI" print("\n" + "-" * 110) print(" [3] '7/8 ANNI VINTI' — metrica = RITORNO composto per anno (non Sharpe). Tabella completa + margini") print("-" * 110) print(f" {'anno':<6s} {'ret CUR':>9s} {'ret EW-STR':>10s} {'Δret':>8s} | {'Sh CUR':>7s} {'Sh EW-STR':>9s} {'ΔSh':>6s} |" f" {'DD CUR':>7s} {'DD EW':>7s}") wins_ret = wins_sh = 0 years = sorted(set(J.index.year)) for y in years: Jy = J[J.index.year == y] rc, re = combo(Jy, w_cur), combo(Jy, w_ew) mc, me = met(rc), met(re) dr = yr_ret(re) - yr_ret(rc); dsh = sh(re) - sh(rc) wins_ret += dr > 0; wins_sh += dsh > 0 print(f" {y:<6d} {yr_ret(rc)*100:>+8.1f}% {yr_ret(re)*100:>+9.1f}% {dr*100:>+7.1f}p |" f" {sh(rc):>7.2f} {sh(re):>9.2f} {dsh:>+6.2f} | {mc['maxdd']*100:>6.1f}% {me['maxdd']*100:>6.1f}%") print(f" -> vittorie EW-STR: RITORNO {wins_ret}/{len(years)} | SHARPE {wins_sh}/{len(years)}" f" (2026 = anno parziale)") # ------------------------------------------------ 4) PLATEAU print("\n" + "-" * 110) print(" [4] PLATEAU — griglia 2.5pp: TP x SKH, VRP=15 fisso, XS = residuo (cap 25). HOLD Sh | min multi-cut ΔSh") print("-" * 110) tps = [0.30, 0.325, 0.35, 0.375, 0.40, 0.4125] skhs = [0.25, 0.275, 0.30] Jh = J[J.index >= HOLDOUT] hold_cur = sh(combo(Jh, w_cur)) print(f" (HOLD Sh CURRENT = {hold_cur:.2f}; celle con XS>25% = viola cap, marcate *)") hdr = " TP\\SKH " + "".join(f"{s*100:>18.1f}%" for s in skhs) print(hdr) for tp in tps: row = f" {tp*100:>6.2f}%" for skh in skhs: xs = 1.0 - 0.15 - tp - skh tag = "*" if xs > CAPS["XS01"] + 1e-9 else " " if xs < LO_W - 1e-9: row += f"{'—':>19s}" continue w = np.array([{"TP01": tp, "XS01": xs, "VRP01": 0.15, "SKH01": skh}[prefix(n)] for n in names]) hs = sh(combo(Jh, w)) dmin = min(sh(combo(J[J.index >= c], w)) - sh(combo(J[J.index >= c], w_cur)) for c in CUTS) row += f" {hs:>5.2f}|{dmin:+.2f}{tag}" print(row) # ------------------------------------------------ 5) REALISMO print("\n" + "-" * 110) print(" [5] REALISMO — pesi EFFETTIVI per era (outer-join rinormalizza!) + haircut d'esecuzione") print("-" * 110) # pesi rinormalizzati per era eras = [("2019-2020 (TP+SKH)", "2019-06-01"), ("2021-2023 (TP+VRP+SKH)", "2022-06-01"), ("2024+ (tutti)", "2025-06-01")] print(" pesi EFFETTIVI (rinormalizzati sugli sleeve attivi) per era:") for lbl, probe in eras: t = pd.Timestamp(probe, tz="UTC") i = J.index.get_indexer([t], method="nearest")[0] act = J.iloc[i].notna().values for wl, w in [("CURRENT", w_cur), ("EW-STR ", w_ew)]: wn = w * act; wn = wn / wn.sum() s = " ".join(f"{prefix(n)} {x*100:.0f}%" for n, x in zip(names, wn) if act[list(names).index(n)]) print(f" {lbl:<24s} {wl}: {s}") print(" -> nel 2019-23 EW-STR tiene SKH01 al 40-50% effettivo (research-grade, book 230m).") # haircut d'esecuzione come DRAG (r' = r - h*mean_full(r)): modello-costi, non de-levering print("\n haircut esecuzione (drag costante = h x mean ritorno full dello sleeve):") print(f" {'scenario':<34s} {'CUR HOLD Sh':>11s} {'EW HOLD Sh':>11s} {'ΔSh':>6s} | {'CUR HOLD CAGR':>13s} {'EW HOLD CAGR':>13s}") scen = [("nessun haircut", {}, 0.0), ("SKH01 -20%", {"SKH01"}, 0.20), ("SKH01 -30%", {"SKH01"}, 0.30), ("SKH01+XS01 -20%", {"SKH01", "XS01"}, 0.20), ("SKH01+XS01 -30%", {"SKH01", "XS01"}, 0.30)] for lbl, targets, h in scen: Jx = J.copy() for n in names: if prefix(n) in targets: mu = float(J[n].dropna().mean()) Jx[n] = J[n] - h * mu # drag solo dove lo sleeve e' attivo (NaN restano NaN) Jxh = Jx[Jx.index >= HOLDOUT] rc, re = combo(Jxh, w_cur), combo(Jxh, w_ew) mc, me = met(rc), met(re) print(f" {lbl:<34s} {mc['sharpe']:>11.2f} {me['sharpe']:>11.2f} {me['sharpe']-mc['sharpe']:>+6.2f} |" f" {mc['cagr']*100:>+12.1f}% {me['cagr']*100:>+12.1f}%") # ------------------------------------------------ 6) FORKING PATHS print("\n" + "-" * 110) print(" [6] FORKING PATHS — config viste sull'hold-out + null di tilt casuali cap-respecting") print("-" * 110) n_configs = 7 + 8 # 7 vettori pesi (MAXSH,RP,ERC,MINVAR-R,MAXSH-STR,EW,EW-STR) + 8 celle guardia print(f" config valutate sull'hold-out nello script agente: >= {n_configs} (7 pesi + 8 celle guardia),") print(" EW-STR costruito DOPO aver visto EW vincere (ammesso nel diario).") rng = np.random.default_rng(42) caps = np.array([CAPS[prefix(n)] for n in names]) N = 500 dsh_hold, dsh_full, all_cuts_pos = [], [], 0 Jh = J[J.index >= HOLDOUT] hold_c = sh(combo(Jh, w_cur)); full_c = sh(combo(J, w_cur)) cuts_J = {c: J[J.index >= c] for c in CUTS} cuts_base = {c: sh(combo(cuts_J[c], w_cur)) for c in CUTS} for _ in range(N): w = rng.dirichlet(np.ones(len(names))) for _ in range(60): # proiezione su [LO_W, caps], somma 1 w = np.clip(w, LO_W, caps) d = 1.0 - w.sum() if abs(d) < 1e-9: break if d > 0: room = caps - w w += d * room / max(room.sum(), 1e-12) else: room = w - LO_W w += d * room / max(room.sum(), 1e-12) dh = sh(combo(Jh, w)) - hold_c dsh_hold.append(dh) dsh_full.append(sh(combo(J, w)) - full_c) all_cuts_pos += all(sh(combo(cuts_J[c], w)) - cuts_base[c] > 0 for c in CUTS) dsh_hold = np.array(dsh_hold); dsh_full = np.array(dsh_full) dew = sh(combo(Jh, w_ew)) - hold_c print(f" {N} tilt casuali dentro i cap (VRP<=15, XS<=25, w>=5): quota che batte CURRENT") print(f" su HOLD-OUT: {float((dsh_hold > 0).mean())*100:.0f}% (mediana ΔSh {np.median(dsh_hold):+.2f})") print(f" su FULL: {float((dsh_full > 0).mean())*100:.0f}% (mediana ΔSh {np.median(dsh_full):+.2f})") print(f" su TUTTI e 3 cut: {all_cuts_pos/N*100:.0f}%") print(f" percentile di EW-STR (ΔSh HOLD {dew:+.2f}) fra i tilt casuali: {float((dsh_hold < dew).mean())*100:.0f}°") print(" lettura: se quasi ogni tilt cap-respecting batte CURRENT sull'hold-out, il 'vince OOS'") print(" e' una proprieta' del PERIODO (hold-out pro-SKH/XS per costruzione), non del vettore EW-STR.") print("\n" + "=" * 110) print(" fine verifica — vedi addendum nel diario 2026-07-01-portfolio-weights-ddguard.md") print("=" * 110) if __name__ == "__main__": main()