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:
2026-05-29 01:07:08 +02:00
parent 666c906907
commit 33e3e2a603
7 changed files with 638 additions and 0 deletions
+87
View File
@@ -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()