7226946911
Giro di validazione scettica (walk-forward, plateau, stress, scomposizione): - PAIRS: config PER-COPPIA -> config UNIVERSALE (n50 z2 zx0.75 mb72), niente cherry-picking. Plateau confermato (heatmap 20/20 Sharpe>1) + walk-forward (ETH/BTC 11/12 finestre+, BTC/LTC 9/10). Scartata BNB/ETH (overfit). 5 coppie robuste. - TSM01: gross 0.45->0.30 (stesso Sharpe, DD 22->15%); corr reale con ROT02 = 0.62 (non 0.53); diversificatore, non motore. Robusto (36/36 config OOS+). - Confluenza multi-TF SCARTATA: overfit (taglia 97% trade, ~40 in 8 anni, Sharpe crolla). - MASTER: numeri sobri onesti -> OOS Sharpe 7.7/DD 2.3% e' regime calmo 2024-25 (ottimistico ~50%); worst-DD 90g ~6%, Sharpe atteso ~5, regge leva 2x+slippage. Config robusta: equal-weight, leva 2x, cap pairs ~30-35% (sono ~57% del rischio). Quanto trovato regge l'esame anti-overfit; numeri comunicati sobri. Doc/CLAUDE/memoria aggiornati. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
95 lines
4.6 KiB
Python
95 lines
4.6 KiB
Python
"""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.62 -> fattore
|
|
parzialmente indipendente, utile come diversificatore (NON come motore di ritorno:
|
|
rende meno di ROT02 a parita' di OOS). DD basso.
|
|
|
|
Anti-overfit: edge su ALTOPIANO (36/36 config orizzonti x thr x regime_n restano OOS+),
|
|
walk-forward stabile (4 anni up, 2 piatti per risk-off, mai un anno negativo), regge
|
|
fee 0.40% RT. Gran parte del DD basso viene dall'overlay risk-off SMA100 (condiviso),
|
|
la struttura multi-orizzonte aggiunge ~+38pp OOS e alza lo Sharpe 0.58->1.07.
|
|
Default gross=0.30 (era 0.45): stesso Sharpe ma DD 22%->15% (scelta robusta, non la piu' redditizia).
|
|
|
|
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.30, 0.30 # gross 0.30 (anti-overfit): stesso Sharpe di 0.45, DD piu' basso
|
|
|
|
|
|
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()
|