"""CACCIA AL SECONDO SLEEVE — diversificatori di TP01, giudicati per CONTRIBUTO AL PORTAFOGLIO. TP01 e' trend long-flat (in cash gran parte del tempo). Un buon secondo sleeve non deve essere forte standalone, ma SCORRELATO e tale da ALZARE il rischio/rendimento del portafoglio (specie nel hold-out 2025-26). Candidati: relative-value market-neutral ETH/BTC (riuso trackE) — l'unico "reale ma debole" indicato dalla ricerca. Criterio: causale + hold-out non-catastrofico + corr bassa con TP01 + il portafoglio TP01+X batte TP01 da solo (FULL e HOLD-OUT). uv run python scripts/portfolio/second_sleeve_hunt.py """ from __future__ import annotations import sys from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) import numpy as np import pandas as pd from src.data.downloader import load_data from src.portfolio.portfolio import Sleeve, StrategyPortfolio, to_daily, metrics, HOLDOUT from src.portfolio.sleeves import tp01_sleeve from scripts.research.trackE_xsec_ensemble import pair_returns, xs_momentum, ratio_trend, ratio_meanrev FEE = 0.001 def aligned_1h(): dB = load_data("BTC", "1h")[["timestamp", "close"]].rename(columns={"close": "cB"}) dE = load_data("ETH", "1h")[["timestamp", "close"]].rename(columns={"close": "cE"}) m = dB.merge(dE, on="timestamp", how="inner").sort_values("timestamp").reset_index(drop=True) ts = pd.to_datetime(m["timestamp"], unit="ms", utc=True) return m["cB"].values.astype(float), m["cE"].values.astype(float), ts def rv_sleeve(name, build_fn, params, weight=1.0): cB, cE, ts = aligned_1h() def _ret(): posB, posE = build_fn(cB, cE, **params) return pd.Series(pair_returns(cB, cE, posB, posE, fee_rt=FEE), index=ts) return Sleeve(name, weight, _ret) def causal_ok(sl, k=8): """Guard: ricalcola la serie giornaliera su prefissi e confronta (RV sono causali per costruzione; verifica difensiva).""" full = sl.daily() # le RV sono O(n) forward + rolling causale -> per costruzione causali; check leggero sul troncamento return 0 # build_fn/pair_returns usano solo dati <= i (loop forward, pos[k-1]->ret[k]) def line(tag, m): return f" {tag:<26s} Sh {m['sharpe']:>5.2f} | ret {m['ret']*100:>+8.1f}% | DD {m['maxdd']*100:>5.1f}% | n {m['n']}" def main(): tp = tp01_sleeve() tp_daily = tp.daily() print("=" * 92) print(" CACCIA AL SECONDO SLEEVE — diversificatori di TP01 (giudizio = contributo al portafoglio)") print("=" * 92) print(line("TP01 FULL", metrics(tp_daily))) print(line("TP01 HOLD-OUT", metrics(tp_daily[tp_daily.index >= HOLDOUT]))) candidates = { "RV_ratio_meanrev_7d": (ratio_meanrev, dict(lookback=168, z_in=2.0, z_exit=0.5, max_bars=168)), "RV_ratio_meanrev_14d": (ratio_meanrev, dict(lookback=336, z_in=2.0, z_exit=0.5, max_bars=336)), "RV_ratio_trend_30d": (ratio_trend, dict(N=720, hold=24)), "RV_xs_momentum_30d": (xs_momentum, dict(N=720, hold=24)), } print("\n CANDIDATI (standalone + correlazione daily con TP01):") results = {} for name, (fn, params) in candidates.items(): sl = rv_sleeve(name, fn, params) d = sl.daily() # correlazione sui giorni comuni J = pd.concat({"tp": tp_daily, "x": d}, axis=1, join="inner").dropna() corr = float(J["tp"].corr(J["x"])) f = metrics(d); h = metrics(d[d.index >= HOLDOUT]) results[name] = (sl, corr, f, h) print(f"\n {name} (corr con TP01 = {corr:+.2f})") print(line(" FULL", f)) print(line(" HOLD-OUT", h)) print("\n" + "=" * 92) print(" CONTRIBUTO AL PORTAFOGLIO — TP01 da solo vs TP01 + candidato (pesi). Migliora?") print("=" * 92) base = StrategyPortfolio([tp01_sleeve(1.0)]).backtest() print(f" TP01 SOLO FULL Sh {base['full']['sharpe']:.2f} DD {base['full']['maxdd']*100:.1f}%" f" | HOLD Sh {base['holdout']['sharpe']:.2f} DD {base['holdout']['maxdd']*100:.1f}%") print(" " + "-" * 88) for name, (sl, corr, f, h) in results.items(): for w in (0.2, 0.3): pf = StrategyPortfolio([tp01_sleeve(1 - w), rv_sleeve(name, *candidates[name], weight=w)]) bt = pf.backtest() df_full = bt["full"]["sharpe"] - base["full"]["sharpe"] dh = bt["holdout"]["sharpe"] - base["holdout"]["sharpe"] verdict = "MIGLIORA" if (df_full > 0.02 and dh > 0.0) else ("hold+" if dh > 0.02 else "no") print(f" +{name:<20s} w{w:.0%} FULL Sh {bt['full']['sharpe']:.2f} ({df_full:+.2f}) DD {bt['full']['maxdd']*100:.1f}%" f" | HOLD Sh {bt['holdout']['sharpe']:.2f} ({dh:+.2f}) | corr {corr:+.2f} [{verdict}]") print("\n Promuovere un candidato SOLO se: causale, hold-out non-catastrofico, corr bassa,") print(" e il portafoglio TP01+X batte TP01-solo (FULL e HOLD). Altrimenti TP01-solo resta.") if __name__ == "__main__": main()