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>
This commit is contained in:
@@ -0,0 +1,148 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user