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>
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
# 2026-06-19 — Caccia al secondo sleeve: nessun diversificatore robusto (TP01-only resta)
|
||||
|
||||
Continuazione naturale del portafoglio: cercare un secondo sleeve SCORRELATO a TP01 (trend
|
||||
long-flat, in cash gran parte del tempo). Criterio: non il Sharpe standalone ma il CONTRIBUTO al
|
||||
portafoglio + robustezza. Tool: `scripts/portfolio/second_sleeve_hunt.py` (riusa le RV di trackE).
|
||||
|
||||
## Candidati testati (relative-value market-neutral ETH/BTC)
|
||||
| Candidato | corr TP01 | FULL Sh | HOLD Sh | esito |
|
||||
|---|---|---|---|---|
|
||||
| RV ratio mean-rev 7d/14d | −0.09/−0.05 | −1.36/−1.03 | −0.62/−0.76 | ☠️ morto (mean-rev dead, come sempre) |
|
||||
| RV ratio_trend / xs_momentum 30d | +0.04 | **0.56** | **1.92** | ⚠️ sembrava promosso |
|
||||
|
||||
ratio_trend e xs_momentum danno risultati IDENTICI: su 2 asset "long il più forte / short il
|
||||
debole" ≡ "trend del ratio ETH/BTC". È UN segnale (relative-momentum), non due.
|
||||
|
||||
## Il candidato "promosso" è regime-luck (per-anno + plateau lo smascherano)
|
||||
Aggiunto a TP01 sembrava un trionfo: hold-out portafoglio 0.31 → 1.18 (w20%) / 1.51 (w30%),
|
||||
corr +0.04. MA:
|
||||
- **Hold-out (1.92) >> FULL (0.56)**: bandiera rossa (immagine speculare della trappola di Fase 3).
|
||||
- **Per-anno NON robusto**: 2019 +22%, 2020 +7%, 2021 +21%, 2022 +13%, **2023 −17%, 2024 −19%**,
|
||||
**2025 +62%**, 2026 +6%. Due anni consecutivi negativi; il "guadagno" è concentrato nel 2025
|
||||
(ETH sottoperforma BTC in modo netto e sostenuto). FULL Sharpe mediocre 0.56, DD 41%.
|
||||
- **Nessun plateau**: l'hold-out Sharpe oscilla 0.25→1.92 al variare di (N, hold) → picco
|
||||
config+regime, non altopiano.
|
||||
- Il beneficio FULL al portafoglio è solo **+0.09 Sharpe** (la legittima diversificazione di uno
|
||||
sleeve scorrelato a Sharpe 0.56: √(1.30²+0.56²)≈1.42). Il resto del "miglioramento" è il 2025.
|
||||
|
||||
## Decisione: NON promosso — TP01-only resta il portafoglio deployato
|
||||
La stessa disciplina che ha bocciato i falsi positivi in-sample (Fasi 1-3) e cross-asset (frattali)
|
||||
deve bocciare questo falso positivo nel hold-out. Il relative-momentum BTC/ETH è un edge debole e
|
||||
regime-dipendente (2 anni a −17/−19%), il cui contributo robusto al portafoglio è marginale
|
||||
(+0.09 FULL); il grosso del beneficio è la fortuna del 2025. Aggiungerlo significherebbe
|
||||
scommettere sul ripetersi di quel regime.
|
||||
|
||||
**Lezione/criterio aggiornato per i futuri sleeve:** "migliora il hold-out" da solo NON basta (il
|
||||
hold-out è UN regime). Un secondo sleeve va promosso solo se: causale, corr bassa, **positivo nella
|
||||
maggioranza degli anni** (no 2 anni consecutivi rossi), **plateau** sui parametri, e migliora il
|
||||
portafoglio su FULL E hold-out — non solo per via di un singolo anno fortunato.
|
||||
|
||||
## Stato
|
||||
Portafoglio = **TP01-only** (difensivo, Sharpe FULL 1.30 / hold-out 0.31). `active_sleeves()`
|
||||
invariato. `second_sleeve_hunt.py` resta come tool per valutare candidati futuri col criterio
|
||||
corretto (contributo + breadth per-anno + plateau). Il relative-momentum BTC/ETH è in WATCHLIST,
|
||||
non deployato.
|
||||
@@ -0,0 +1,108 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user