"""Verifica indipendente + ricerca TSM01 — Time-Series Momentum multi-orizzonte. Long-only, multi-crypto, bassa frequenza. Per ogni asset il segnale è il CONSENSO dei segni del momentum su più orizzonti lunghi (3/6/12 mesi); si tengono equal-weight gli asset con consenso pieno positivo. Overlay risk-off: cash se BTC < SMA100. Distinta da ROT02 (cross-sectional ranking): qui conta la PERSISTENZA assoluta lenta di ogni asset, non la classifica relativa. Correlazione con ROT02 ~0.53 -> fattore parzialmente indipendente, utile come diversificatore. DD basso (22% full / 12% OOS). Engine onesto: pesi a close[i] da soli rendimenti passati, realizzo i->i+1, fee one-way fee_rt/2 sul turnover. NETTO, leva implicita gross. OOS = ultimo 30%. """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) from scripts.analysis.honest_lab import available_assets, FEE_RT from scripts.analysis.honest_rotation import build_panel GROSS, OOS_FRAC = 0.45, 0.30 def tsmom_sim(horizons=(63, 126, 252), thr=1.0, regime_n=100, gross=GROSS, fee_rt=FEE_RT, oos_frac=0.0, cheat=False): """horizons in giorni. thr=1.0 -> consenso pieno (tutti i segni positivi).""" panel = build_panel(available_assets(), "1d") cols = list(panel.columns); P = panel.values; T, N = P.shape rets = np.zeros_like(P); rets[1:] = P[1:] / P[:-1] - 1 years = panel.index.year.values btc = P[:, cols.index("BTC")] bma = pd.Series(btc).rolling(regime_n).mean().values start = max(max(horizons) + 1, regime_n + 1, int(T * (1 - oos_frac)) if oos_frac else 0) cap = 1000.0; w = np.zeros(N); eq = [cap]; yearly = {} eq_ts: list = []; eq_v: list = [] for i in range(start, T - 1): risk_on = btc[i] > bma[i] if not np.isnan(bma[i]) else False wi = i + 1 if cheat else i # cheat: usa il futuro (test no-look-ahead) score = np.zeros(N) for h in horizons: score += np.sign(P[wi] / P[wi - h] - 1) score /= len(horizons) chosen = [j for j in range(N) if score[j] >= thr] if risk_on else [] nw = np.zeros(N) for j in chosen: nw[j] = gross / len(chosen) cap -= cap * np.abs(nw - w).sum() * (fee_rt / 2); w = nw cap = max(cap * (1 + float(np.dot(w, rets[i + 1]))), 10.0) eq.append(cap) eq_ts.append(panel.index[i + 1]); eq_v.append(cap) y = int(years[i]); yearly[y] = yearly.get(y, 0.0) + float(np.dot(w, rets[i + 1])) * 100 eq = np.array(eq); peak = np.maximum.accumulate(eq) dd = float(np.max((peak - eq) / peak) * 100) yrs = (panel.index[-1] - panel.index[start]).days / 365.25 or 1 rets_d = np.diff(eq) / eq[:-1] sharpe = float(np.mean(rets_d) / np.std(rets_d) * np.sqrt(365)) if np.std(rets_d) > 0 else 0.0 return dict(ret=(cap / 1000 - 1) * 100, cagr=((cap / 1000) ** (1 / yrs) - 1) * 100, dd=dd, sharpe=sharpe, yearly=yearly, eq_ts=eq_ts, eq_v=eq_v, pos_years=sum(1 for v in yearly.values() if v > 0), n_years=len(yearly)) def main(): print("=" * 90) print(" TSM01 — TSMOM multi-orizzonte (3/6/12m consenso pieno) + risk-off SMA100") print("=" * 90) # no-look-ahead: cheat deve esplodere base = tsmom_sim() ch = tsmom_sim(cheat=True) print(f" no-look-ahead: onesto FULL={base['ret']:+.0f}% vs cheat(futuro)={ch['ret']:+.0f}% -> " f"{'OK (il cheat esplode -> niente leak)' if ch['ret'] > base['ret'] * 2 else 'CONTROLLARE'}") o = tsmom_sim(oos_frac=1 - OOS_FRAC) hi = tsmom_sim(fee_rt=0.002) print(f"\n FULL {base['ret']:+.0f}% CAGR {base['cagr']:.0f}% DD {base['dd']:.0f}% " f"Sharpe {base['sharpe']:.2f} anni+ {base['pos_years']}/{base['n_years']}") print(f" OOS {o['ret']:+.0f}% DD {o['dd']:.0f}% | fee 0.40% RT: FULL {hi['ret']:+.0f}%") print(" Per-anno: " + " ".join(f"{y}:{v:+.0f}%" for y, v in sorted(base["yearly"].items()))) if __name__ == "__main__": main()