"""XS01 phase-tranching — PRE-REGISTRATO: il roll non-sovrapposto di XS01 (entry a i, exit a i+hold, re-entry) ha una FASE arbitraria (dipende dal primo indice valido). Se l'esito dipende dalla fase, c'e' timing-luck: dividere il book in K sub-book sfasati di hold/K barre (capitale 1/K ciascuno) e' un ensemble di fase che riduce la varianza SENZA parametri fittati (K=2 e K=3 riportati entrambi, nessun best-pick). Test (griglia fissata qui, completa): [1] SENSIBILITA' DI FASE: xsec_sim base con offset di partenza 0..hold-1 -> dispersione di Sharpe/ret/DD (FULL e OOS). Se la dispersione e' piccola, il tranching non serve (verdetto onesto, fine). [2] TRANCHING: K in {2, 3} sub-book sfasati equal-capital -> Sharpe/DD/ret FULL e OOS vs la MEDIA e il WORST delle fasi singole. Criterio (tutti necessari): il tranched riduce la dispersione (per costruzione) e il suo Sharpe OOS >= worst-fase OOS con DD <= mediana fasi; fee identiche (il tranching NON cambia il turnover per unita' di capitale). uv run python scripts/analysis/xs01_tranche_research.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(PROJECT_ROOT)) from scripts.strategies.XS01_cross_sectional import ( aligned_panel, UNIVERSE, LB, HOLD, FEE_RT, LEV, POS, OOS_FRAC) def xsec_trades(phase: int = 0, lb: int = LB, hold: int = HOLD, fee_rt: float = FEE_RT, split_frac: float = 0.0, M=None): """Replica ESATTA della logica di xsec_sim ma parte da max(lb, split)+phase e ritorna la lista trade (i, j, net) — stessa matematica, fee = 2*fee_rt.""" C = M[UNIVERSE].values n = len(C) logC = np.log(C) split = int(n * split_frac) fee = 2 * fee_rt out = [] last = -1 i = max(lb, split) + phase while i < n - hold: if i <= last: i += 1 continue dm = logC[i] - logC[i - lb] dm = dm - dm.mean() gw = np.sum(np.abs(dm)) if gw < 1e-9: i += 1 continue w = -dm / gw book = float(np.sum(w * (logC[i + hold] - logC[i]))) out.append((i, i + hold, book - fee)) last = i + hold i += 1 return out def equity_from(trades, ts, pos=POS, lev=LEV, weight=1.0): """Equity compounding a punti-exit (convenzione xsec_sim), peso per tranche.""" cap = 1000.0 eq_ts, eq_v = [], [] for i, j, net in sorted(trades, key=lambda t: t[1]): cap = max(cap + cap * pos * lev * net * weight, 10.0) eq_ts.append(ts[j]) eq_v.append(cap) return pd.Series(eq_v, index=pd.DatetimeIndex(eq_ts)) def metrics(trades, ts, yrs_span): rets = [t[2] * POS for t in trades] n = len(rets) sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(n / yrs_span)) if n > 1 and np.std(rets) > 0 else 0.0 eq = equity_from(trades, ts) pk = eq.cummax() dd = float(((pk - eq) / pk).max() * 100) if len(eq) else 0.0 ret = float((eq.iloc[-1] / 1000 - 1) * 100) if len(eq) else 0.0 return dict(n=n, ret=ret, sharpe=sharpe, dd=dd) def combined_metrics(branches, ts, yrs_span): """K tranche -> un'unica equity: pesa ogni trade 1/K sul capitale comune.""" K = len(branches) allt = sorted([t for b in branches for t in b], key=lambda t: t[1]) cap = 1000.0 eq_ts, eq_v, rets = [], [], [] for i, j, net in allt: rets.append(net * POS / K) cap = max(cap + cap * POS * LEV * net / K, 10.0) eq_ts.append(ts[j]) eq_v.append(cap) eq = pd.Series(eq_v, index=pd.DatetimeIndex(eq_ts)) pk = eq.cummax() n = len(rets) sharpe = float(np.mean(rets) / np.std(rets) * np.sqrt(n / yrs_span)) if n > 1 and np.std(rets) > 0 else 0.0 return dict(n=n, ret=float((eq.iloc[-1] / 1000 - 1) * 100), sharpe=sharpe, dd=float(((pk - eq) / pk).max() * 100)) def run(): M = aligned_panel() ts = pd.to_datetime(M.index, unit="ms", utc=True) n = len(M) print("=" * 96) print(" XS01 phase-tranching — sensibilita' di fase + ensemble K=2/3 | griglia pre-registrata") print("=" * 96) for tag, split in (("FULL", 0.0), ("OOS", 1 - OOS_FRAC)): yrs_span = (ts[-1] - ts[max(int(n * split), LB)]).days / 365.25 or 1 rows = [] for ph in range(HOLD): tr = xsec_trades(phase=ph, split_frac=split, M=M) rows.append(metrics(tr, ts, yrs_span)) sh = np.array([r["sharpe"] for r in rows]) dd = np.array([r["dd"] for r in rows]) rt = np.array([r["ret"] for r in rows]) print(f"\n [{tag}] sensibilita' di fase (12 fasi):") print(f" Sharpe: min {sh.min():.2f} | med {np.median(sh):.2f} | max {sh.max():.2f} | std {sh.std():.2f}") print(f" ret%: min {rt.min():+.0f} | med {np.median(rt):+.0f} | max {rt.max():+.0f}") print(f" DD%: min {dd.min():.1f} | med {np.median(dd):.1f} | max {dd.max():.1f}") for K in (2, 3): branches = [xsec_trades(phase=int(p), split_frac=split, M=M) for p in np.linspace(0, HOLD, K, endpoint=False)] m = combined_metrics(branches, ts, yrs_span) print(f" K={K} tranched: Sharpe {m['sharpe']:.2f} | ret {m['ret']:+.0f}% | " f"DD {m['dd']:.1f}% | trade {m['n']}") print("\n criterio: tranched-Sharpe OOS >= worst-fase OOS e DD <= mediana fasi; " "se la dispersione di fase e' gia' piccola -> NON SERVE (verdetto onesto).") if __name__ == "__main__": run()