18f22160b2
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>
109 lines
4.9 KiB
Python
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()
|