feat(explore): esplora 9 famiglie alternative -> PAIRS (nuovo edge forte) + TSM01
Esplorazione onesta con agenti paralleli su harness condiviso (explore_lab.py): ingresso close[i], netto fee, OOS, DD basso, attenzione fee. 7 famiglie su 9 sono rumore (stagionalita' oraria/mensile, cross-sectional reversal, opening-range, lead-lag BTC->alt, continuation intraday) e l'harness le rifiuta senza falsi positivi. Due edge reali verificati indipendentemente: - PR01 Pairs: spread reversion market-neutral su log-ratio z-score (ETH/BTC, LTC/ETH, ADA/ETH). ETH/BTC CAGR 144% Sharpe 4.04 OOS DD 17% 8/9 anni, corr mercato ~0.02, no-look-ahead verificato, regge fee 0.40%/coppia. Fee su 2 gambe (worker da estendere). - TSM01: TSMOM multi-orizzonte 3/6/12m + risk-off, distinto da ROT02 (corr 0.53), DD 22%/12% OOS, mai un anno negativo, regge fee 0.40%. Payoff: aggiungere i pairs (quasi scorrelati ~0.05) al MASTER -> CAGR 47->66%, DD 5.2->3.8% full / 4.7->3.3% OOS, Sharpe OOS 4.33->6.86 (combine_v2.py). Fix: explore_lab.get_df ora produce timestamp ms reale per 1d/4h (era placeholder). Diario 2026-05-29-exploration.md + nota CLAUDE.md. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user