"""GATE PORT06: position_size dei PAIRS live (improvement-sweep punto 8). Contesto. Tutta la validazione PR01 e' a pos=0.15 (lev 3 -> esposizione 0.45 della fetta), ma il live gira col position_size GLOBALE 0.5 a leva 2 -> esposizione 1.0 per coppia: ~2.2x il validato, su una famiglia SENZA stop (exit solo |z|<=z_exit o max_bars) e col DD grezzo piu' alto (ETH/BTC ~48%). Scelta deliberata (commento yml) ma mai gateata. Evidenza live: ADA/ETH 2026-06-05 net -8.52% = -4.26% sleeve a 0.5 (sarebbe -1.28% a 0.15). Confronto, a livello PORT06 (pesi cap canonici PAIRS 0.33 + SHAPE 0.0588): le 5 equity pairs ricostruite con pairs_sim alla LEVA LIVE (2x) e pos su griglia: LIVE-OGGI pos=0.50 (esposizione 1.00) P-0.25 pos=0.25 (0.50) P-0.20 pos=0.20 (0.40) P-0.15 pos=0.15 (0.30) <- pos canonica alla leva live Ancora CANONICO (pos 0.15 lev 3, eff 0.45) = le equity di build_everything. Parita' preliminare: pairs_sim(pos=0.15, lev=3) deve riprodurre le equity canoniche. GATE (sweep): scegliere il punto sul trade-off DD/ritorno nel range 0.15-0.25; promosso se vs LIVE-OGGI l'OOS Sharpe non peggiora e il DD (PORT06 e famiglia) scende. uv run python scripts/analysis/pairspos_port06_impact.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, IDX from scripts.analysis.pairs_research import pairs_sim from scripts.analysis.report_families import daily_from from scripts.strategies.PR01_pairs_reversion import PAIRS from scripts.portfolios._defs import PORTFOLIOS from src.portfolio import weighting as W LEV_LIVE = 2.0 GRID = [0.50, 0.25, 0.20, 0.15] def pair_equities(pos: float, lev: float): """{PR_AB: (equity giornaliera, dd_grezzo%, ret%)} con pairs_sim a (pos, lev).""" out = {} for a, b, p in PAIRS: r = pairs_sim(a, b, **{**p, "pos": pos, "lev": lev}) # pairs_sim ritorna dd e ret GIA' in percento out[f"PR_{a}{b}"] = (daily_from(r["eq_ts"], r["eq_v"]), float(r["dd"]), float(r["ret"])) return out def port_metrics(members: dict[str, pd.Series], p): ids = p.sleeve_ids dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids}) w = W.weight_vector(p.weighting, ids, dr, weights=p.weights, caps=p.caps, clusters=p.clusters, lookback=p.vol_lookback) drp = port_returns({i: members[i] for i in ids}, w) return metrics(drp), metrics(drp, lo=SPLIT) def fam_metrics(eqs: dict[str, pd.Series]): """Famiglia pairs standalone equal-weight.""" dr = port_returns(eqs) return metrics(dr), metrics(dr, lo=SPLIT) def main(): p = PORTFOLIOS["PORT06"] pair_ids = [s.sid for s in p.sleeves if s.sid.startswith("PR_")] print("=" * 100) print(" GATE PORT06 — position_size PAIRS live (lev 2x) | validazione storica: pos 0.15 lev 3") print(f" coppie: {pair_ids} | caps={p.caps}") print("=" * 100) print("\n[1] build_everything() canonico (cache)...") from src.portfolio.sleeves import all_sleeve_equities eq_base = dict(all_sleeve_equities()) # --- [2] PARITA': pairs_sim(0.15, lev3) == canonico --- print("\n[2] PARITA' pairs_sim(pos=0.15, lev=3) vs canonico:") canon = pair_equities(0.15, 3.0) parity_ok = True for sid in pair_ids: rep, _, _ = canon[sid] base = eq_base[sid] corr = base.pct_change().fillna(0).corr(rep.pct_change().fillna(0)) rb = (base.iloc[-1] / base.iloc[0] - 1) * 100 rr = (rep.iloc[-1] / rep.iloc[0] - 1) * 100 flag = "" if (corr > 0.999 and abs(rr - rb) <= max(1.0, abs(rb) * 0.01)) else " <-- MISMATCH" if flag: parity_ok = False print(f" {sid:<11s}corr={corr:.5f} ret canon {rb:+.0f}% vs replay {rr:+.0f}%{flag}") print(f" PARITA' {'OK' if parity_ok else 'FALLITA'}.") if not parity_ok: print(" >>> STOP.") return # --- [3] griglia pos alla leva live --- print(f"\n[3] PORT06 e famiglia PAIRS per pos (lev={LEV_LIVE:.0f}x) | OOS da {OOS_DATE}") print(f" {'variante':<12s}{'eff':>5s} | {'FULL Sh':>8s}{'FULL DD%':>9s}{'CAGR':>6s}" f" | {'OOS Sh':>7s}{'OOS DD%':>8s} | {'famSh':>6s}{'famDD%':>7s}" f" | {'worst pair DD% (grezzo)':>24s}") print(" " + "-" * 96) res = {} for pos in GRID: eqs = pair_equities(pos, LEV_LIVE) members = dict(eq_base) for sid in pair_ids: members[sid] = eqs[sid][0] f, o = port_metrics(members, p) ff, _fo = fam_metrics({sid: eqs[sid][0] for sid in pair_ids}) worst = max(((sid, eqs[sid][1]) for sid in pair_ids), key=lambda x: x[1]) res[pos] = (f, o, ff, worst, eqs) tag = "LIVE-OGGI" if pos == 0.50 else f"P-{pos:.2f}" print(f" {tag:<12s}{pos*LEV_LIVE:>5.2f} | {f['sharpe']:>8.2f}{f['dd']:>9.2f}{f['cagr']:>5.0f}%" f" | {o['sharpe']:>7.2f}{o['dd']:>8.2f} | {ff['sharpe']:>6.2f}{ff['dd']:>7.1f}" f" | {worst[0]} {worst[1]:>5.1f}") # --- [4] per-coppia al pos live vs candidato --- print("\n[4] Per-coppia (DD grezzo % | ret %) a pos 0.50 vs 0.20:") e50, e20 = res[0.50][4], res[0.20][4] for sid in pair_ids: print(f" {sid:<11s} DD {e50[sid][1]:>5.1f} -> {e20[sid][1]:>5.1f} " f"ret {e50[sid][2]:>+8.0f}% -> {e20[sid][2]:>+8.0f}%") # --- GATE: candidati vs LIVE-OGGI --- f_l, o_l = res[0.50][0], res[0.50][1] print("\n" + "=" * 100) print(" GATE vs LIVE-OGGI (pos 0.50): OOS Sharpe non peggiora E DD scende (PORT06)") print("=" * 100) for pos in (0.25, 0.20, 0.15): f_c, o_c = res[pos][0], res[pos][1] ok = (o_c["sharpe"] >= o_l["sharpe"] - 0.02 and o_c["dd"] < o_l["dd"] and f_c["sharpe"] >= f_l["sharpe"] - 0.02) print(f" pos {pos:.2f}: OOS Sh {o_l['sharpe']:.2f}->{o_c['sharpe']:.2f} " f"DD {o_l['dd']:.2f}->{o_c['dd']:.2f} | FULL Sh {f_l['sharpe']:.2f}->{f_c['sharpe']:.2f} " f"DD {f_l['dd']:.2f}->{f_c['dd']:.2f} => {'PROMOSSO' if ok else 'bocciato'}") if __name__ == "__main__": main()