From 745ba7d066be188851d1cc8e370054f2bb818ba9 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 21 Jun 2026 20:01:20 +0000 Subject: [PATCH] research(portfolio): simula PREVDAY come overlay tail-hedge su TP01+XS01+VRP01 (NON deploy) Simulazione d'impatto (registry di produzione invariato): riscala i 3 sleeve attivi a (1-W) e aggiunge PREVDAY a peso W; sweep W {0,5,10,15,20%}. A 10%: - FULL Sharpe 1.68->1.88 (+0.20), maxDD 14.3%->9.9% (-31%): la gamba short ammortizza i crash storici. - HOLD-OUT Sharpe 1.66->1.97 (+0.31), ret +16.7->+19.0% (DD gia' bassissimo 3.4%). - 10% ~ ottimo di DD: oltre, lo Sharpe sale ma il maxDD smette di scendere (solo piu' rischio short). - per-anno: migliora/pareggia quasi ovunque; costa solo nel toro 2021 (premio hedge), paga nel bear 2022. Caveat: tutto IN-SAMPLE (i guadagni assumono che l'edge persista -> e' cio' che il forward-monitor verifica); outer-join gonfia il peso effettivo 2019-20 -> l'hold-out e' il read pulito a 10%. PREVDAY resta FORWARD-MONITOR. Lo script e' il riferimento per ri-valutare l'overlay a forward maturo. Diario: 2026-06-21-prevday-overlay-portfolio.md. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-21-prevday-overlay-portfolio.md | 68 +++++++++++ scripts/portfolio/prevday_overlay.py | 115 ++++++++++++++++++ 2 files changed, 183 insertions(+) create mode 100644 docs/diary/2026-06-21-prevday-overlay-portfolio.md create mode 100644 scripts/portfolio/prevday_overlay.py diff --git a/docs/diary/2026-06-21-prevday-overlay-portfolio.md b/docs/diary/2026-06-21-prevday-overlay-portfolio.md new file mode 100644 index 0000000..3bc5abf --- /dev/null +++ b/docs/diary/2026-06-21-prevday-overlay-portfolio.md @@ -0,0 +1,68 @@ +# PREVDAY come overlay di tail-hedge sul portafoglio — simulazione d'impatto (NON deploy) + +**Data:** 2026-06-21 (segue la trilogia fill-haircut / turnover-hedge / bootstrap) +**Script:** `scripts/portfolio/prevday_overlay.py` +**Esito:** a peso 10%, PREVDAY taglia il maxDD FULL del portafoglio **14.3% → 9.9% (−31%)** e alza +l'hold-out Sharpe **1.66 → 1.97 (+0.31)**. 10% è vicino all'ottimo di DD. MA è tutto IN-SAMPLE: il +prize si materializza solo SE l'edge di PREVDAY persiste forward. PREVDAY resta FORWARD-MONITOR. + +## Setup + +Simulazione che NON tocca il registry di produzione: prende il portafoglio attivo (TP01 55% + XS01 +25% + VRP01 20%), riscala i tre sleeve a (1−W) mantenendone le proporzioni, e aggiunge PREVDAY a +peso W. Sweep W ∈ {0,5,10,15,20%}. PREVDAY = libro 1h breakout-continuation, parametri congelati, +50/50 BTC+ETH, fee 0.10% RT. Outer-join del portafoglio: PREVDAY dal 2018, VRP01 dal 2021, XS01 dal +2024 → nel 2019-20 PREVDAY pesa di fatto >W (solo TP01 accanto); nell'hold-out 2025+ (tutti e 4 +attivi) pesa esattamente ~W → **l'HOLD-OUT è il confronto pulito a "10%"**. + +## Sweep peso overlay + +| peso PREVDAY | FULL Sharpe | FULL DD | HOLD Sharpe | HOLD DD | HOLD ret | +|--------------|------------:|--------:|------------:|--------:|---------:| +| BASELINE (55/25/20) | 1.68 | 14.3% | 1.66 | 3.4% | +16.7% | +| 5% | 1.80 | 11.1% | 1.83 | 3.3% | +17.8% | +| **10%** | **1.88** | **9.9%** | **1.97** | 3.3% | **+19.0%** | +| 15% | 1.93 | 10.3% | 2.06 | 3.3% | +20.2% | +| 20% | 1.95 | 10.6% | 2.09 | 3.4% | +21.4% | + +(PREVDAY standalone: FULL Sh 1.23 / DD 26.7%; HOLD Sh 1.28 / DD 10.8%.) + +## Lettura a 10% + +- **FULL: Sharpe +0.20 (1.68→1.88), maxDD 14.3%→9.9% (−4.4pp ≈ −31%).** Comportamento da tail-hedge: + la gamba short ammortizza i crash storici (2019-21, 2022). +- **HOLD-OUT: Sharpe +0.31 (1.66→1.97), ret +16.7%→+19.0%, DD 3.4%→3.3% (già bassissimo).** Nel + regime recente il beneficio è rendimento/Sharpe, non taglio DD. +- **10% ≈ ottimo di DD.** Oltre, lo Sharpe sale ancora (1.93→1.95) ma il maxDD FULL smette di + scendere (10.3→10.6%): stai solo aggiungendo rischio direzionale short. Argomento per ~10% in + chiave hedge (massimizza il taglio-coda per unità di rischio aggiunto). + +## Per anno (baseline → overlay 10%) + +| anno | ret | DD | +|------|-----|----| +| 2019 | +11.3 → +15.2 | 10.3 → 8.2 | +| 2020 | +51.1 → +53.1 | 8.4 → 6.3 | +| 2021 | +32.5 → +28.3 | 5.2 → 4.3 | +| 2022 | −3.0 → −1.6 | 3.7 → 3.0 | +| 2023 | +11.2 → +11.4 | 9.2 → 9.9 | +| 2024 | +24.4 → +25.7 | 3.9 → 3.4 | +| 2025 | +12.0 → +12.0 | 3.4 → 3.3 | +| 2026 | +4.2 → +6.2 | 2.6 → 2.2 | + +Migliora o pareggia quasi ovunque; costa solo nel toro 2021 (premio d'assicurazione atteso per un +hedge) e leggermente sul DD 2023; paga nel bear 2022 e nel 2026. + +## Verdetto + +L'overlay 10% è **attraente in simulazione** — taglia il drawdown FULL di ~31% e alza l'hold-out +Sharpe +0.31, con 10% vicino all'ottimo di DD. Ma: +1. **È in-sample.** I guadagni assumono che l'edge di PREVDAY persista — il forward-monitor esiste + proprio per verificarlo. Questa simulazione quantifica il PRIZE, non lo prova. +2. **Outer-join:** il taglio-DD storico è gonfiato dal peso effettivo >10% nel 2019-20; il read + pulito a 10% è l'hold-out (prize = Sharpe +0.31). +3. Incidentale: il 3-way TP01+XS01+VRP01 baseline qui fa FULL 1.68 / HOLD 1.66. + +**Azione: nessuna.** PREVDAY resta FORWARD-MONITOR (registry di produzione invariato). Quando il +track record forward avrà ~2-3 mesi, ri-valutare l'overlay 10% con la stessa metrica (taglio-DD + +hold-out Sharpe) su dati VERAMENTE fuori campione. Lo script è il riferimento per quel confronto. diff --git a/scripts/portfolio/prevday_overlay.py b/scripts/portfolio/prevday_overlay.py new file mode 100644 index 0000000..7d9ffd2 --- /dev/null +++ b/scripts/portfolio/prevday_overlay.py @@ -0,0 +1,115 @@ +"""SIMULAZIONE — PREVDAY come overlay di tail-hedge sul portafoglio attivo (NON deploy). + +PREVDAY (src/strategies/prevday_breakout) resta in FORWARD-MONITOR. Qui misuriamo SOLO, in +simulazione, cosa farebbe al portafoglio live (TP01 55% + XS01 25% + VRP01 20%) aggiungerlo come +overlay a peso W, riscalando i tre sleeve esistenti a (1-W) e tenendo le loro proporzioni. La +trilogia (fill-haircut/turnover/bootstrap) ha stabilito che PREVDAY e' un HEDGE di regime-down +(tutto il valore = gamba short) eseguibile a taglia reale: l'overlay si giudica sul TAGLIO DEL +DRAWDOWN del portafoglio, non sul ritorno. + +NB outer-join: PREVDAY parte dal 2018, XS01 dal 2024, VRP01 dal 2021. I pesi sono rinormalizzati +ogni giorno fra i soli sleeve con dato -> nel 2019-20 (solo TP01+PREVDAY) PREVDAY pesa di piu' del +target W; nell'hold-out 2025+ (tutti e 4 attivi) pesa esattamente ~W. Per questo l'HOLD-OUT e' il +confronto piu' pulito a "peso 10%". + + uv run python scripts/portfolio/prevday_overlay.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.backtest.harness import load +from src.strategies import prevday_breakout as pb +from src.portfolio.portfolio import StrategyPortfolio, Sleeve, metrics, HOLDOUT +from src.portfolio.sleeves import _tp01_returns, _xsec_returns, _vrp_combo_returns + +ASSETS = ("BTC", "ETH") +FEE_SIDE = 0.0005 +BASE_W = dict(TP01=0.55, XS01=0.25, VRP01=0.20) # proporzioni dei tre sleeve attivi +HEADLINE = 0.10 + + +def _prevday_returns() -> pd.Series: + """Rendimenti netti per-barra (1h) del libro PREVDAY 50/50 BTC+ETH (parametri congelati).""" + series = {} + for a in ASSETS: + df = load(a, "1h").reset_index(drop=True) + c = df["close"].values.astype(float) + r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0 + tgt = np.nan_to_num(pb.target(df), nan=0.0) + held = np.zeros(len(tgt)); held[1:] = tgt[:-1] + net = held * r - FEE_SIDE * np.abs(np.diff(tgt, prepend=tgt[0])) + series[a] = pd.Series(net, index=pd.to_datetime(df["datetime"], utc=True)) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + return pd.Series(0.5 * J.iloc[:, 0].values + 0.5 * J.iloc[:, 1].values, index=J.index) + + +def build_portfolio(series_cache: dict, w_prev: float) -> StrategyPortfolio: + """Portafoglio coi 3 sleeve riscalati a (1-w_prev) + PREVDAY a w_prev (0 = baseline).""" + sl = [ + Sleeve("TP01_trend_1d", BASE_W["TP01"] * (1 - w_prev), lambda s=series_cache["TP01"]: s), + Sleeve("XS01_xsec_hl", BASE_W["XS01"] * (1 - w_prev), lambda s=series_cache["XS01"]: s), + Sleeve("VRP01_shortvol", BASE_W["VRP01"] * (1 - w_prev), lambda s=series_cache["VRP01"]: s), + ] + if w_prev > 0: + sl.append(Sleeve("PREVDAY_hedge", w_prev, lambda s=series_cache["PREVDAY"]: s)) + return StrategyPortfolio(sl) + + +def line(label, m): + return (f" {label:<22s} Sh {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% " + f"CAGR {m['cagr']*100:>+6.1f}% | DD {m['maxdd']*100:>5.1f}% | n {m['n']}") + + +def main(): + print("=" * 92) + print(" PREVDAY OVERLAY (simulazione, NON deploy) — tail-hedge sul portafoglio TP01+XS01+VRP01") + print("=" * 92) + print(" Precalcolo sleeve...", flush=True) + cache = dict(TP01=_tp01_returns(), XS01=_xsec_returns(), + VRP01=_vrp_combo_returns(), PREVDAY=_prevday_returns()) + + print(f"\n PREVDAY standalone (per riferimento):") + from src.portfolio.portfolio import to_daily + pvd = to_daily(cache["PREVDAY"]) + print(line("PREVDAY full", metrics(pvd))) + print(line("PREVDAY hold-out", metrics(pvd[pvd.index >= HOLDOUT]))) + + print(f"\n SWEEP PESO OVERLAY (FULL | HOLD-OUT) — headline {HEADLINE*100:.0f}%:") + print(f" {'peso PREVDAY':<14s} {'FULL Sharpe':>11s} {'FULL DD':>9s} | {'HOLD Sharpe':>11s} {'HOLD DD':>9s} {'HOLD ret':>9s}") + rows = {} + for w in (0.0, 0.05, 0.10, 0.15, 0.20): + bt = build_portfolio(cache, w).backtest() + rows[w] = bt + tag = "BASELINE" if w == 0 else f"{w*100:.0f}%" + star = " <-- headline" if abs(w - HEADLINE) < 1e-9 else "" + print(f" {tag:<14s} {bt['full']['sharpe']:>11.2f} {bt['full']['maxdd']*100:>8.1f}% | " + f"{bt['holdout']['sharpe']:>11.2f} {bt['holdout']['maxdd']*100:>8.1f}% " + f"{bt['holdout']['ret']*100:>+8.1f}%{star}") + + base, ov = rows[0.0], rows[HEADLINE] + print(f"\n DETTAGLIO a {HEADLINE*100:.0f}% vs BASELINE:") + print(line("BASELINE FULL", base['full'])); print(line(f"OVERLAY{HEADLINE*100:.0f}% FULL", ov['full'])) + print(line("BASELINE HOLD", base['holdout'])); print(line(f"OVERLAY{HEADLINE*100:.0f}% HOLD", ov['holdout'])) + dSh = ov['holdout']['sharpe'] - base['holdout']['sharpe'] + dDD = (ov['holdout']['maxdd'] - base['holdout']['maxdd']) * 100 + print(f"\n >> HOLD-OUT: ΔSharpe {dSh:+.2f} | ΔmaxDD {dDD:+.1f}pp " + f"(tail-hedge = ci aspettiamo DD piu' basso)") + + print(f"\n PER ANNO (baseline -> overlay {HEADLINE*100:.0f}%): ret% / DD%") + yb, yo = base['yearly'], ov['yearly'] + for y in sorted(set(yb) | set(yo)): + b = yb.get(y, {}); o = yo.get(y, {}) + print(f" {y}: ret {b.get('ret',0)*100:>+7.1f}% -> {o.get('ret',0)*100:>+7.1f}% " + f"DD {b.get('dd',0)*100:>5.1f}% -> {o.get('dd',0)*100:>5.1f}%") + print("=" * 92) + print(" Nota: PREVDAY resta FORWARD-MONITOR. Questa e' una simulazione di impatto, non un deploy.") + + +if __name__ == "__main__": + main()