ef52ad6a79
src/portfolio/: Sleeve (serie rendimenti netti per-barra, causale/fee-aware) + StrategyPortfolio (combina N sleeve per peso su griglia giornaliera comune, metriche FULL/HOLD-OUT/per-anno + standalone per-sleeve, vs buy&hold). Registry sleeve attivi in sleeves.py: per ora SOLO TP01 (peso 100%); aggiungere = una riga (dopo validazione col gauntlet). Report (run_portfolio.py): TP01 FULL Sh 1.30 / DD 14.3% / ~€1.52/g, HOLD-OUT 0.31 / +3.5% (buy&hold -0.32 / -39%). Posizione corrente flat (difensivo). tests/test_portfolio.py (6 test). CLAUDE.md aggiornato (struttura + comando + come aggiungere uno sleeve). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
76 lines
2.9 KiB
Python
76 lines
2.9 KiB
Python
"""REPORT del portafoglio di strategie attivo (estensibile).
|
|
|
|
Costruisce il portafoglio dagli sleeve attivi (src/portfolio/sleeves.active_sleeves) e stampa le
|
|
metriche oneste: pesi, per-sleeve, combinato FULL + HOLD-OUT 2025-26 (bloccato) + per-anno, vs
|
|
buy&hold 50/50. Per ora c'e' solo TP01; aggiungere sleeve = una riga in sleeves.py.
|
|
|
|
uv run python scripts/portfolio/run_portfolio.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.data.downloader import load_data
|
|
from src.strategies.trend_portfolio import resample_1d, simple_returns
|
|
from src.portfolio.portfolio import StrategyPortfolio, to_daily, metrics, HOLDOUT
|
|
from src.portfolio.sleeves import active_sleeves
|
|
|
|
CAPITAL = 2000.0
|
|
|
|
|
|
def buy_hold_daily() -> pd.Series:
|
|
s = {}
|
|
for a in ("BTC", "ETH"):
|
|
df = resample_1d(load_data(a, "1h"))
|
|
s[a] = pd.Series(simple_returns(df["close"].values.astype(float)), index=pd.to_datetime(df["datetime"]))
|
|
J = pd.concat(s, axis=1, join="inner").fillna(0.0)
|
|
return to_daily(pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index))
|
|
|
|
|
|
def fmt(m, cap=CAPITAL):
|
|
yrs = m["n"] / 365.25
|
|
eur_day = (cap * m["ret"]) / (yrs * 365.25) if yrs > 0 else 0.0
|
|
return (f"Sh {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% CAGR {m['cagr']*100:>+6.1f}% | "
|
|
f"DD {m['maxdd']*100:>5.1f}% | ~€/g(2k) {eur_day:>+5.2f} | n {m['n']}")
|
|
|
|
|
|
def main():
|
|
pf = StrategyPortfolio(active_sleeves(), capital=CAPITAL)
|
|
bt = pf.backtest()
|
|
print("=" * 96)
|
|
print(f" PORTAFOGLIO DI STRATEGIE — {len(pf.sleeves)} sleeve | capitale {CAPITAL:,.0f} | hold-out {HOLDOUT.date()}+ bloccato")
|
|
print("=" * 96)
|
|
|
|
print("\n PESI:", " ".join(f"{k} {v*100:.0f}%" for k, v in bt["weights"].items()))
|
|
|
|
print("\n PER-SLEEVE (standalone):")
|
|
for name, d in bt["per_sleeve"].items():
|
|
print(f" {name:<16s} [{d['weight']*100:>3.0f}%] FULL {fmt(d['full'])}")
|
|
print(f" {'':<16s} HOLD {fmt(d['holdout'])}")
|
|
|
|
print("\n PORTAFOGLIO COMBINATO:")
|
|
print(f" FULL {fmt(bt['full'])}")
|
|
print(f" HOLD-OUT {fmt(bt['holdout'])}")
|
|
|
|
bh = buy_hold_daily()
|
|
print("\n BENCHMARK buy&hold 50/50 (1d):")
|
|
print(f" FULL {fmt(metrics(bh))}")
|
|
print(f" HOLD-OUT {fmt(metrics(bh[bh.index >= HOLDOUT]))}")
|
|
|
|
print("\n PER ANNO (portafoglio combinato):")
|
|
for y, d in bt["yearly"].items():
|
|
print(f" {y}: ret {d['ret']*100:>+7.1f}% DD {d['dd']*100:>5.1f}%")
|
|
|
|
print("\n POSIZIONI CORRENTI (ultima barra chiusa):")
|
|
for name, pos in pf.current_positions().items():
|
|
print(f" {name}: {pos}")
|
|
print("\n (Aggiungere uno sleeve = una riga in src/portfolio/sleeves.active_sleeves, dopo validazione.)")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|