"""Recuperi e finestre in perdita su azioni USA e Treasury decennali, 1928-2025. Guida Rebalix «Come iniziare a investire», capitolo 14. Versione pubblica e riproducibile: legge `rendimenti-annuali.csv` (estratto il 9 ottobre 2026 dal workbook histretSP.xls di NYU / Aswath Damodaran, foglio «Returns by year»: colonne S&P 500 con dividendi, Treasury decennale, inflazione USA) e scrive `risultati.json` nella cartella di uscita. Solo libreria standard di Python 3; il grafico PNG viene prodotto soltanto se matplotlib è installato. Uso: python3 calcola.py # legge il CSV accanto allo script, scrive in ./out python3 calcola.py --out /percorso # cartella di uscita diversa (creata se manca) Definizioni (le stesse della guida): - recupero: a fine anno il valore torna almeno al massimo precedente osservato a fine anno; si riporta la durata massima fra gli episodi conclusi, più l'eventuale episodio aperto; - finestra in perdita: per ogni periodo intero di h anni che inizia a fine anno, valore finale inferiore a quello iniziale; le finestre si sovrappongono; - pesi azioni/Treasury ripristinati ogni anno; «reale» = corretto per l'inflazione USA; nessun costo, imposta o versamento. """ from __future__ import annotations import argparse, csv, json from pathlib import Path HERE = Path(__file__).resolve().parent PESI = [1.0, 0.8, 0.6, 0.4] ORIZZONTI = [5, 10, 15, 20] def leggi_csv(percorso: Path): with percorso.open(newline='') as f: righe = [r for r in csv.DictReader(f)] anni = [int(r['anno']) for r in righe] assert anni == list(range(1928, 2026)) and len(anni) == 98, 'attesi 98 anni contigui 1928-2025' return anni, [(float(r['azioni_USA']), float(r['Treasury_10_anni']), float(r['inflazione_USA'])) for r in righe] def serie_valori(dati, peso: float, reale: bool): valori = [1.0] for az, tr, infl in dati: r = peso * az + (1 - peso) * tr if reale: r = (1 + r) / (1 + infl) - 1 valori.append(valori[-1] * (1 + r)) return valori def analizza(anni_oss, valori): picco, episodi = 0, [] for i in range(1, len(valori)): if valori[i] >= valori[picco]: if i > picco + 1: episodi.append([i - picco, anni_oss[picco], anni_oss[i]]) picco = i piu_lungo = max(e[0] for e in episodi) aperto = None if picco == len(valori) - 1 else [anni_oss[picco], anni_oss[-1], valori[-1] / valori[picco] - 1] finestre = {str(h): [sum(1 for i in range(len(valori) - h) if valori[i + h] / valori[i] < 1), len(valori) - h] for h in ORIZZONTI} return {'recuperi_massimi': [e for e in episodi if e[0] == piu_lungo], 'episodio_aperto': aperto, 'finestre_negative': finestre} def grafico(anni_oss, valori, destinazione: Path) -> bool: try: import matplotlib matplotlib.use('Agg') import matplotlib.pyplot as plt except ImportError: return False base = valori[anni_oss.index(1999)] xs = [a for a in anni_oss if 1999 <= a <= 2013] ys = [100 * valori[anni_oss.index(a)] / base for a in xs] fig, ax = plt.subplots(figsize=(10, 4.5)) ax.plot(xs, ys, marker='o', linewidth=2.5) ax.axhline(100, linestyle='--') ax.set_title('Il potere d’acquisto può impiegare anni a recuperare', loc='left') ax.set_ylabel('Valore reale · fine 1999 = 100') fig.text(0.08, 0.02, 'Azioni USA · dividendi inclusi · valori a fine anno · inflazione USA. Elaborazione Rebalix su dati NYU/Damodaran.', fontsize=9) fig.tight_layout(rect=[0, 0.08, 1, 1]) fig.savefig(destinazione, dpi=180) plt.close(fig) return True def main(): ap = argparse.ArgumentParser(description=__doc__.split('\n')[0]) ap.add_argument('--csv', type=Path, default=HERE / 'rendimenti-annuali.csv') ap.add_argument('--out', type=Path, default=HERE / 'out') args = ap.parse_args() args.out.mkdir(parents=True, exist_ok=True) anni, dati = leggi_csv(args.csv) anni_oss = [anni[0] - 1] + anni # osservazioni a fine anno: fine 1927 = base 1 risultati = {} for peso in PESI: risultati[str(peso).rstrip('0').rstrip('.') if peso != 1.0 else '1'] = { 'nominale': analizza(anni_oss, serie_valori(dati, peso, False)), 'reale': analizza(anni_oss, serie_valori(dati, peso, True)), } (args.out / 'risultati.json').write_text(json.dumps(risultati, indent=2)) png = grafico(anni_oss, serie_valori(dati, 1.0, True), args.out / 'recupero-reale.png') print(f"scritto {args.out / 'risultati.json'}" + ('' if png else ' (grafico saltato: matplotlib non installato)')) if __name__ == '__main__': main()