chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera libreria "validata OOS" era artefatto di feed contaminato (print fantasma del feed Cerbero TESTNET + storico Binance/USDT). - Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE 50-82% barre flat; XRP/BNB non certificabili). - Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST con segnale residuo, da ri-validare in isolamento. - Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio, runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/ portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/ (preservati, non cancellati). Diario consolidato in un unico documento. - Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal + src/backtest/engine + load_data; tool dati certificati (rebuild_history, certify_feed, audit_feed, multi_source_check). - Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user