diff --git a/scripts/research/combo_leverage_sim.py b/scripts/research/combo_leverage_sim.py new file mode 100644 index 0000000..d14871e --- /dev/null +++ b/scripts/research/combo_leverage_sim.py @@ -0,0 +1,74 @@ +"""Simulazione LEVA 1x/2x/3x su COMBO (TP01+GTAA) e TP01-solo, da $2k e $5k. + +Leva modellata onestamente: ritorno_giorno = L*r - (L-1)*financing/252 (costo del nozionale preso a +prestito ~8%/anno blended: perp funding crypto + margin IB). MaxDD calcolato sul PERCORSO LEVATO REALE +(non scalato: il compounding peggiora il DD oltre ×L). Check RUINA/margin-call: se l'equity tocca la +soglia di liquidazione (perdita cumulata >= 1/L del picco -> margin call). + +CLAUDE.md: la leva NON e' la scorciatoia; raddoppia (e oltre) il drawdown. Caso base = 1x. +""" +import sys +from pathlib import Path +import numpy as np, pandas as pd +ROOT = Path(__file__).resolve().parents[2] +sys.path.insert(0, str(ROOT)); sys.path.insert(0, str(ROOT / "scripts" / "research")) +from combo_yearly_report import combo_daily +from src.portfolio.sleeves import _tp01_returns + +FIN = 0.08 # costo finanziamento annuo sul nozionale preso a prestito (perp funding + margin IB), blended + + +def lever(ret: pd.Series, L: float) -> pd.Series: + return L * ret - (L - 1) * FIN / 252.0 + + +def analyze(ret: pd.Series, L: float, cap0: float): + r = lever(ret.dropna().sort_index(), L) + curve = cap0 * np.cumprod(1 + r.values) + peak = np.maximum.accumulate(curve) + dd = (peak - curve) / peak + maxdd = float(np.max(dd)) + # margin call: perdita dal picco >= 1/L (a leva L, un drawdown del sottostante di 1/L azzera il margine) + ruin = bool(np.any(dd >= 1.0 / L - 1e-9)) if L > 1 else False + yrs = (r.index[-1] - r.index[0]).days / 365.25 + cagr = (curve[-1] / cap0) ** (1 / yrs) - 1 if yrs > 0 and curve[-1] > 0 else -1 + sh = float(r.mean() / r.std() * np.sqrt(252)) if r.std() > 0 else 0 + worst_y = min((np.prod(1 + r[r.index.year == y].values) - 1) for y in sorted(set(r.index.year))) + return dict(L=L, final=float(curve[-1]), cagr=cagr, maxdd=maxdd, sharpe=sh, ruin=ruin, + worst_y=float(worst_y), perday=(curve[-1] - cap0) / yrs / 365) + + +def main(): + print("=" * 92) + print(" LEVA su COMBO vs TP01-solo — percorso reale (fin 8%/anno sul prestito), 2019-2026 (~7.3y)") + print("=" * 92) + strat = {"COMBO TP01+GTAA": combo_daily(), "TP01 solo (crypto)": _tp01_returns()} + for nm, r in strat.items(): + if r.index.tz is None: + r.index = r.index.tz_localize("UTC") + for cap0 in (2000.0, 5000.0): + print(f"\n ##### capitale iniziale ${cap0:,.0f} #####") + print(f" {'strategia':20}{'leva':>5}{'CAGR':>8}{'MaxDD':>8}{'Sharpe':>8}{'pegg.anno':>10}{'$/giorno':>10}{'eq fine':>12}{' RUINA?':>9}") + for nm, r in strat.items(): + for L in (1, 2, 3): + a = analyze(r, L, cap0) + flag = "MARGIN-CALL" if a["ruin"] else "ok" + print(f" {nm:20}{L:>4}x{a['cagr']*100:>7.1f}%{a['maxdd']*100:>7.1f}%{a['sharpe']:>8.2f}" + f"{a['worst_y']*100:>9.1f}%{('$'+format(a['perday'],',.0f')):>10}{('$'+format(a['final'],',.0f')):>12}{flag:>11}") + # per-anno del COMBO a 2x e 3x da 2k (dettaglio) + print(f"\n ##### COMBO per-anno a leva, da $2.000 #####") + cd = combo_daily() + for L in (2, 3): + print(f"\n --- COMBO {L}x ---") + print(f" {'anno':6}{'PnL %':>9}{'MaxDD %':>9}{'eq fine':>11}") + eq = 2000.0; r = lever(cd, L) + for y in sorted(set(r.index.year)): + ry = r[r.index.year == y] + if len(ry) < 5: continue + eq0 = eq; curve = eq0 * np.cumprod(1 + ry.values); peak = np.maximum.accumulate(curve) + ddp = float(np.max((peak - curve) / peak)); eq = float(curve[-1]) + print(f" {y:<6}{(eq/eq0-1)*100:>+8.1f}%{ddp*100:>8.1f}%{('$'+format(eq,',.0f')):>11}") + + +if __name__ == "__main__": + main()