745ba7d066
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) <noreply@anthropic.com>
116 lines
5.5 KiB
Python
116 lines
5.5 KiB
Python
"""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()
|