Files
PythagorasGoal/scripts/analysis/projection_3y.py
T
Adriano Dal Pastro 5a219ca8e5 feat(analysis): proiezione 3 anni capitale PORT06 (live 2x, esclude 2024)
Script di proiezione: partendo da 1000 EUR con capitale che compone e
puntata che cresce, stima guadagno giornaliero e traiettoria a 3 anni.
Riscalata sul sizing LIVE (pos 0.15 x 2x) vs backtest 3x; ROT02/TSM01
usano gross fisso (non riscalano). Esclude il 2024 (anno eccezionale)
e include haircut ~50% per lo scenario sobrio prudente.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-30 19:29:07 +00:00

149 lines
5.9 KiB
Python

"""Proiezione a 3 anni del portafoglio live (PORT06) ESCLUDENDO il 2024.
Risponde a: "partendo da 1000 EUR, con capitale che compone e puntata che cresce
di conseguenza, quale guadagno giornaliero atteso e quale traiettoria a 3 anni?"
Punti chiave (perche' i numeri differiscono da report_families):
- report_families/Portfolio.backtest usano le curve sleeve NATIVE a leva 3x.
- Il container LIVE (src.portfolio.runner via portfolios.yml) gira a pos 0.15 x 2x.
- Il PnL giornaliero scala ESATTAMENTE con la leva: pnl = cap * pos * lev * (ret - fee),
stesso segnale -> ratio 2/3 per gli sleeve leverati.
- ROT02 e TSM01 NON si riscalano: usano `gross` (0.45 / 0.30), indipendente dalla leva
e identico fra backtest e worker live.
- Il 2024 e' escluso perche' anno eccezionale (crypto +; gonfia ogni stima).
CAVEAT (le proiezioni sono OTTIMISTICHE):
- 2021-2025 e' quasi tutto bull/recovery; poco orso/flat prolungato nel campione.
- Dati alt = testnet (volume sottile, fill/slippage NON modellati).
- OOS singolo (2024-25) = regime calmo -> ~50% ottimistico (vedi report_families (D)).
Lo scenario SOBRIO (haircut ~50%) e' il numero prudente su cui pianificare.
Run: uv run python scripts/analysis/projection_3y.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.portfolios._defs import PORTFOLIOS
from src.portfolio.sleeves import all_sleeve_equities, sleeve_returns_df
from scripts.analysis.combine_portfolio import port_returns
# sleeve la cui esposizione NON dipende dalla leva (gross-based) -> non si riscalano
NOSCALE = {"ROT02_rot", "TSM01"}
NATIVE_LEV = 3.0 # leva delle curve sleeve in combine_portfolio
EXCLUDE_YEAR = 2024
START_CAPITAL = 1000.0
def live_portfolio_returns(live_leverage: float = 2.0) -> pd.Series:
"""Rendimenti giornalieri di PORT06 al sizing LIVE (pos 0.15 x `live_leverage`).
Riscala gli sleeve leverati di live_leverage/NATIVE_LEV; ROT/TSM invariati."""
p = PORTFOLIOS["PORT06"]
p.leverage = live_leverage
p.weighting = "cap"
p.caps = {"PAIRS": 0.33}
eq = all_sleeve_equities()
dr = sleeve_returns_df(p.sleeve_ids)
w = p.weight_vector(dr)
scale = live_leverage / NATIVE_LEV
eq_live = {}
for sid in p.sleeve_ids:
r = eq[sid].pct_change().fillna(0.0)
r = r if sid in NOSCALE else r * scale
eq_live[sid] = (1 + r).cumprod()
pr = port_returns(eq_live, w)
pr.index = pd.to_datetime(pr.index)
return pr
def stats_ex_year(pr: pd.Series, exclude: int = EXCLUDE_YEAR) -> dict:
ex = pr[pr.index.year != exclude]
n = len(ex)
years = n / 365.0
tot = (1 + ex).prod() - 1
cagr = (1 + tot) ** (1 / years) - 1
yvals = [(1 + g).prod() - 1 for _, g in ex.groupby(ex.index.year)]
return {
"days": n, "years": years, "total": tot, "cagr": cagr,
"year_median": float(np.median(yvals)), "year_mean": float(np.mean(yvals)),
"daily_mean_eur": float(ex.mean() * START_CAPITAL),
"daily_median_eur": float(ex.median() * START_CAPITAL),
"daily_std_eur": float(ex.std() * START_CAPITAL),
"pos_days": float((ex > 0).mean()),
"per_year": {int(y): float((1 + g).prod() - 1) for y, g in pr.groupby(pr.index.year)},
}
def project(annual: float, years: int = 3, start: float = START_CAPITAL) -> list[dict]:
"""Capitale che compone; la puntata cresce col capitale -> EUR/giorno cresce."""
rows, cap = [], start
for yr in range(1, years + 1):
s = cap
cap = cap * (1 + annual)
rows.append({"year": yr, "start": s, "end": cap,
"gain": cap - s, "eur_per_day": (cap - s) / 365.0})
return rows
def years_to_target(daily_target: float, annual: float, start: float = START_CAPITAL) -> float:
"""Anni per raggiungere un certo EUR/giorno componendo (capitale = target*365/cagr)."""
cap_needed = daily_target * 365.0 / annual
if cap_needed <= start:
return 0.0
return float(np.log(cap_needed / start) / np.log(1 + annual))
def main():
pr = live_portfolio_returns(live_leverage=2.0)
s = stats_ex_year(pr)
print("=" * 72)
print(" PORT06 LIVE (pos 0.15 x 2x) — proiezione 3 anni, ESCLUSO 2024")
print("=" * 72)
print(" Rendimento live per anno:")
for y, v in s["per_year"].items():
flag = " <-- ESCLUSO" if y == EXCLUDE_YEAR else ""
print(f" {y}: {v * 100:+6.1f}%{flag}")
print()
print(f" CAGR (escl 2024): {s['cagr'] * 100:5.1f}% "
f"[{s['years']:.2f} anni di dati]")
print(f" anno mediano: {s['year_median'] * 100:5.1f}%")
print(f" anno medio: {s['year_mean'] * 100:5.1f}%")
print(f" EUR/giorno su 1000: media {s['daily_mean_eur']:.2f} | "
f"mediana {s['daily_median_eur']:.2f} | std {s['daily_std_eur']:.2f}")
print(f" giorni positivi: {s['pos_days'] * 100:.1f}%")
print()
scenarios = [
("CAGR backtest escl-2024", s["cagr"]),
("anno mediano", s["year_median"]),
("SOBRIO (haircut ~50%)", s["cagr"] * 0.5),
]
for name, g in scenarios:
print(f" -- 3 anni @ {g * 100:.0f}%/anno ({name}) --")
for r in project(g):
print(f" anno {r['year']}: {r['start']:7.0f} -> {r['end']:7.0f} EUR "
f"(+{r['gain']:5.0f}, ~{r['eur_per_day']:4.2f} EUR/g medi)")
print()
print(" -- Target 50 EUR/giorno (reality check) --")
for name, g in scenarios[:1] + scenarios[2:]:
cap_needed = 50.0 * 365.0 / g
t = years_to_target(50.0, g)
print(f" @ {g * 100:.0f}%/anno: servono ~{cap_needed:,.0f} EUR schierati "
f"-> da 1000 EUR, ~{t:.0f} anni componendo")
print(" => il collo di bottiglia e' il CAPITALE iniziale, non la strategia.")
if __name__ == "__main__":
main()