9d15506b05
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>
102 lines
4.4 KiB
Python
102 lines
4.4 KiB
Python
"""GATE — XS01 phase-tranching (K sub-book sfasati di hold/K, capitale comune).
|
|
|
|
`xs01_tranche_research.py` ha misurato timing-luck di fase reale (FULL Sharpe
|
|
1.87-2.87, DD 13.8-33.1% a seconda della fase di partenza, che live e' arbitraria).
|
|
Qui il confronto ONESTO su equity GIORNALIERA (stessa convenzione dei gate del
|
|
progetto: combine_portfolio.metrics) + gate PORT06 swap-sleeve:
|
|
[1] standalone: base (fase canonica) vs K=2 vs K=3, FULL e OOS, daily Sharpe/DD;
|
|
in piu' il range delle 12 fasi singole (il rischio che il tranching elimina).
|
|
[2] PORT06: members canonici con XS01 sostituito dalla variante tranched.
|
|
Criterio (tutti): OOS Sharpe portafoglio non peggiora (>-0.02) E DD non sale;
|
|
K=2 e K=3 devono essere ENTRAMBI >= base (plateau, non best-pick di K).
|
|
|
|
uv run python scripts/analysis/xs01_tranche_gate.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.analysis.combine_portfolio import port_returns, metrics, SPLIT, OOS_DATE
|
|
from scripts.analysis.report_families import daily_from
|
|
from scripts.analysis.xs01_tranche_research import xsec_trades
|
|
from scripts.strategies.XS01_cross_sectional import aligned_panel, HOLD, POS, LEV
|
|
from scripts.portfolios._defs import PORTFOLIOS
|
|
from src.portfolio.sleeves import all_sleeve_equities
|
|
from src.portfolio import weighting as W
|
|
|
|
|
|
def daily_equity_for(phases, M, ts):
|
|
"""Trade di tutte le fasi su capitale comune (peso 1/K) -> equity daily."""
|
|
K = len(phases)
|
|
allt = sorted([t for ph in phases for t in xsec_trades(phase=ph, M=M)],
|
|
key=lambda t: t[1])
|
|
cap = 1000.0
|
|
eq_ts, eq_v = [], []
|
|
for i, j, net in allt:
|
|
cap = max(cap + cap * POS * LEV * net / K, 10.0)
|
|
eq_ts.append(ts[j])
|
|
eq_v.append(cap)
|
|
return daily_from(eq_ts, eq_v)
|
|
|
|
|
|
def port_metrics(members, ids, clusters, caps):
|
|
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
|
|
w = W.weight_vector("cap", ids, dr, caps=caps, clusters=clusters)
|
|
drp = port_returns({i: members[i] for i in ids}, w)
|
|
return metrics(drp), metrics(drp, lo=SPLIT)
|
|
|
|
|
|
def main():
|
|
M = aligned_panel()
|
|
ts = pd.to_datetime(M.index, unit="ms", utc=True)
|
|
p = PORTFOLIOS["PORT06"]
|
|
print("=" * 92)
|
|
print(" GATE — XS01 phase-tranching | equity daily, OOS da", OOS_DATE)
|
|
print("=" * 92)
|
|
|
|
variants = {"base (fase 0)": [0],
|
|
"K=2 (fasi 0,6)": [0, 6],
|
|
"K=3 (fasi 0,4,8)": [0, 4, 8]}
|
|
eqs = {k: daily_equity_for(v, M, ts) for k, v in variants.items()}
|
|
|
|
print(f"\n [1] STANDALONE daily — {'config':<18}{'FULL Sh':>8}{'FULL DD%':>9}{'OOS Sh':>8}{'OOS DD%':>8}")
|
|
for k, e in eqs.items():
|
|
r = e.pct_change().fillna(0.0)
|
|
f, o = metrics(r), metrics(r, lo=SPLIT)
|
|
print(f" {k:<22}{f['sharpe']:>8.2f}{f['dd']:>9.2f}{o['sharpe']:>8.2f}{o['dd']:>8.2f}")
|
|
# range delle 12 fasi singole (daily): il rischio di fase che il tranching elimina
|
|
fs, os_ = [], []
|
|
for ph in range(HOLD):
|
|
r = daily_equity_for([ph], M, ts).pct_change().fillna(0.0)
|
|
fs.append(metrics(r)["sharpe"])
|
|
os_.append(metrics(r, lo=SPLIT)["sharpe"])
|
|
print(f" 12 fasi singole: FULL Sh {min(fs):.2f}-{max(fs):.2f} | OOS Sh {min(os_):.2f}-{max(os_):.2f}")
|
|
|
|
eq_base = dict(all_sleeve_equities())
|
|
ids, cl, caps = list(p.sleeve_ids), p.clusters, p.caps
|
|
print(f"\n [2] PORT06 swap-sleeve — {'config':<18}{'FULL Sh':>8}{'FULL DD%':>9}{'OOS Sh':>8}{'OOS DD%':>8}")
|
|
f0, o0 = port_metrics(eq_base, ids, cl, caps)
|
|
print(f" {'ATTUALE (base)':<22}{f0['sharpe']:>8.2f}{f0['dd']:>9.2f}{o0['sharpe']:>8.2f}{o0['dd']:>8.2f}")
|
|
verdicts = []
|
|
for k in ("K=2 (fasi 0,6)", "K=3 (fasi 0,4,8)"):
|
|
mem = dict(eq_base)
|
|
mem["XS01"] = eqs[k]
|
|
f1, o1 = port_metrics(mem, ids, cl, caps)
|
|
ok = (o1["sharpe"] >= o0["sharpe"] - 0.02 and o1["dd"] <= o0["dd"] + 1e-9
|
|
and f1["sharpe"] >= f0["sharpe"] - 0.02 and f1["dd"] <= f0["dd"] + 1e-9)
|
|
verdicts.append(ok)
|
|
print(f" {k:<22}{f1['sharpe']:>8.2f}{f1['dd']:>9.2f}{o1['sharpe']:>8.2f}{o1['dd']:>8.2f}"
|
|
f" {'OK' if ok else 'NO'}")
|
|
print(f"\n => {'PROMOSSO (plateau K=2 e K=3)' if all(verdicts) else 'NON promosso (serve plateau su entrambi i K)'}")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|