Files
PythagorasGoal/scripts/portfolio/second_sleeve_hunt.py
Adriano Dal Pastro 18f22160b2 research: caccia al 2o sleeve — nessun diversificatore robusto, TP01-only resta
Tool second_sleeve_hunt.py: giudica i candidati per CONTRIBUTO al portafoglio (non Sharpe
standalone). RV mean-rev ETH/BTC morto (come sempre). RV relative-momentum (ratio_trend ==
xs_momentum) sembrava promosso (hold-out portafoglio 0.31->1.51) MA il per-anno + plateau lo
smascherano come REGIME-LUCK 2025: FULL Sh mediocre 0.56, 2 anni consecutivi negativi
(2023 -17%, 2024 -19%), guadagno concentrato nel 2025 (+62%), hold-out Sh non-plateau (0.25-1.92
al variare dei parametri). Beneficio FULL robusto solo +0.09 (diversificazione di uno sleeve
scorrelato debole). NON promosso: la disciplina che boccia i falsi positivi in-sample boccia
anche i falsi positivi nel hold-out. Criterio aggiornato: breadth per-anno + plateau, non solo
hold-out. Relative-momentum in WATCHLIST. Diario 2026-06-19-second-sleeve-hunt.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 19:28:51 +00:00

109 lines
4.9 KiB
Python

"""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()