feat(stability): sweep stabilità — fix TR01 mean(rets), XS01 phase-tranching K=3, z-stop pairs bocciato
Audit anti-overfit su tutte le 19 sleeve (diario 2026-06-11-stability-sweep.md): - FIX BasketTrendWorker: mean(rets) sui soli asset in posizione sovrappesava N/k a paniere parziale (1 long = 0.45 del capitale invece di 0.09) -> replay -44% vs ref +42%. Ora sum(rets)/N (convenzione canonica 1/N): replay +32% vs +42% (residuo = convenzione dichiarata). Solo statistica PAPER. - XS01 PHASE-TRANCHING (gate xs01_tranche_gate: plateau K=2 E K=3 promossi, PORT06 OOS Sh 10.07->10.15 DD 1.48->1.38, FULL pari): la fase del roll e' timing-luck (Sharpe daily 1.52-2.33, DD 13.8-33% sulle 12 fasi). Worker con param tranches (default 1), 3 sub-book sfasati hold/3 su capitale comune, migrazione status legacy, last_bar_ts solo-avanti; runner forward del param; _defs tranches=3; hourly_report aggrega i sub-book; validatore esteso e PASSATO (K=1 == xsec_sim esatto, K=3 == unione fasi esatto). - Disaster-cap z sui pairs: pre-registrato e BOCCIATO su tutti i criteri (coda OOS peggiora 4/6 coppie, Sharpe -10..-49%, plateau solo del danno; 5a conferma stop-su-MR). Record pairs_zstop_research.py; pairs restano senza stop. - Audit drift: regression-lock trendmax OK (parita' 1.00000, plateau 2.5/3.0/3.5 confermato), correlazioni cross-famiglia ~0 invariate; PORT06 rolling al 19-28mo pct (normale) ma FADE 120g al 2o percentile storico -> monitor in TODO (nessun ritocco parametri). - TODO: forming-bar ROT02/TSM01 era gia' fixato (v1.1.10), item chiuso. Test: pytest 99 passed; validate_honest_workers OK; validate_xsec_worker OK. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user