Files
PythagorasGoal/scripts/research/combo_leverage_sim.py
T
2026-06-23 12:44:54 +00:00

75 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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()