diff --git a/scripts/research/combo_yearly_report.py b/scripts/research/combo_yearly_report.py new file mode 100644 index 0000000..1d07ad8 --- /dev/null +++ b/scripts/research/combo_yearly_report.py @@ -0,0 +1,99 @@ +"""Resoconto anno-per-anno della strategia combo (TP01+GTAA) + componenti, da $2.000. + +Per anno: PnL ($ e %), MaxDD (intra-anno), NumTrades, equity di fine anno (compounding da 2k). +Combo = blend 50/50 TP01(Deribit) + GTAA(IB) (crypto compoundato su grid giorni-di-borsa). +NumTrades: TP01 = cambi di target BTC/ETH (>0.05); GTAA = ribilanci MENSILI per-gamba (>2%). +Onesto: il combo parte dal 2019 (crypto). GTAA-solo dato anche su 10y come contesto. +""" +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" / "live")) +from src.data.downloader import load_data +from src.strategies.trend_portfolio import TrendPortfolio, CANONICAL, resample_1d +from src.portfolio.sleeves import _tp01_returns +from src.portfolio.gtaa import gtaa_returns, _exposure, _close, EQ_UNIVERSE +INITIAL = 2000.0 + + +def tp01_trades_per_year(): + tp = TrendPortfolio(**CANONICAL); cnt = {} + for a in ("BTC", "ETH"): + df = resample_1d(load_data(a, "1h")); tgt = tp.target_series(df) + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"])) + chg = pd.Series(np.abs(np.diff(tgt, prepend=tgt[0])) > 0.05, index=idx) + for y, c in chg.groupby(idx.year).sum().items(): + cnt[int(y)] = cnt.get(int(y), 0) + int(c) + return cnt + + +def gtaa_trades_per_year(): + # pesi giornalieri -> ribilancio MENSILE realistico -> conta gambe cambiate >2% + W = {} + for a in EQ_UNIVERSE: + ex = _exposure(_close(a)) / len(EQ_UNIVERSE) + W[a] = ex + Wd = pd.concat(W, axis=1).dropna() + Wm = Wd.resample("ME").last() # peso a fine mese + chg = (Wm.diff().abs() > 0.02).sum(axis=1) # gambe ribilanciate quel mese + return chg.groupby(chg.index.year).sum().astype(int).to_dict() + + +def yearly(ret: pd.Series, trades: dict, label: str, start_capital=INITIAL): + ret = ret.dropna().sort_index() + print(f"\n ===== {label} =====") + print(f" {'anno':6}{'eq inizio':>12}{'PnL $':>12}{'PnL %':>9}{'MaxDD %':>9}{'NumTrades':>11}{'eq fine':>12}") + eq = start_capital + for y in sorted(set(ret.index.year)): + r = ret[ret.index.year == y] + if len(r) < 5: + continue + eq0 = eq + curve = eq0 * np.cumprod(1 + r.values) + peak = np.maximum.accumulate(curve) + dd = float(np.max((peak - curve) / peak)) if len(curve) else 0.0 + eq = float(curve[-1]) + pnl = eq - eq0 + nt = trades.get(y, None) + print(f" {y:<6}{eq0:>12,.0f}{pnl:>+12,.0f}{(eq/eq0-1)*100:>+8.1f}%{dd*100:>8.1f}%" + f"{(str(nt) if nt is not None else '—'):>11}{eq:>12,.0f}") + tot = eq / start_capital - 1 + yrs = (ret.index[-1] - ret.index[0]).days / 365.25 + cagr = (eq / start_capital) ** (1 / yrs) - 1 if yrs > 0 else 0 + sh = float(r.mean()) if False else float(ret.mean() / ret.std() * np.sqrt(252)) + print(f" {'TOT':<6}{start_capital:>12,.0f}{eq-start_capital:>+12,.0f}{tot*100:>+8.1f}%" + f"{'':>9}{sum(v for v in trades.values()) if trades else 0:>11}{eq:>12,.0f}") + print(f" -> da ${start_capital:,.0f} a ${eq:,.0f} in {yrs:.1f}y | CAGR {cagr*100:+.1f}% | Sharpe {sh:.2f}") + + +def combo_daily(wc=0.5): + tp = _tp01_returns() + if tp.index.tz is None: + tp.index = tp.index.tz_localize("UTC") + eq = gtaa_returns().dropna() + grid = eq.index[eq.index >= tp.index[0]] + cum = (1 + tp).cumprod() + tpg = (cum.reindex(cum.index.union(grid)).ffill().reindex(grid)).pct_change() + J = pd.concat({"c": tpg, "e": eq.reindex(grid)}, axis=1).dropna() + return (wc * J["c"] + (1 - wc) * J["e"]).dropna() + + +def main(): + print("=" * 80) + print(" RESOCONTO STRATEGIA — da $2.000, anno per anno") + print("=" * 80) + tpt = tp01_trades_per_year(); gtt = gtaa_trades_per_year() + combo_tr = {y: tpt.get(y, 0) + gtt.get(y, 0) for y in set(tpt) | set(gtt)} + # COMBO (la strategia deployata) + yearly(combo_daily(), combo_tr, "COMBO TP01+GTAA 50/50 (deployabile, dal 2019)") + # componenti + tp = _tp01_returns(); tp.index = tp.index.tz_localize("UTC") if tp.index.tz is None else tp.index + yearly(tp, tpt, "solo TP01 (crypto, Deribit)") + g = gtaa_returns(); g10 = g[g.index >= (g.index[-1] - pd.Timedelta(days=3660))] + yearly(g10, gtt, "solo GTAA (equity, IB) — ULTIMI 10 ANNI") + + +if __name__ == "__main__": + main()