From 18f22160b20eb5d7f14d5ac2dcf50745988789a3 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Fri, 19 Jun 2026 19:28:51 +0000 Subject: [PATCH] =?UTF-8?q?research:=20caccia=20al=202o=20sleeve=20?= =?UTF-8?q?=E2=80=94=20nessun=20diversificatore=20robusto,=20TP01-only=20r?= =?UTF-8?q?esta?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- docs/diary/2026-06-19-second-sleeve-hunt.md | 44 ++++++++ scripts/portfolio/second_sleeve_hunt.py | 108 ++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 docs/diary/2026-06-19-second-sleeve-hunt.md create mode 100644 scripts/portfolio/second_sleeve_hunt.py diff --git a/docs/diary/2026-06-19-second-sleeve-hunt.md b/docs/diary/2026-06-19-second-sleeve-hunt.md new file mode 100644 index 0000000..112bb23 --- /dev/null +++ b/docs/diary/2026-06-19-second-sleeve-hunt.md @@ -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. diff --git a/scripts/portfolio/second_sleeve_hunt.py b/scripts/portfolio/second_sleeve_hunt.py new file mode 100644 index 0000000..18721b3 --- /dev/null +++ b/scripts/portfolio/second_sleeve_hunt.py @@ -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()