6097205694
Punto 8 roadmap sweep. La famiglia pairs (SENZA stop, validata a pos 0.15 lev 3 =
esposizione 0.45) girava col position_size globale 0.5 a leva 2 = esposizione 1.0,
~2.2x il validato (ETH/BTC DD grezzo 78% a quella taglia; live: ADA/ETH -4.26%
sleeve in un trade il 2026-06-05).
Gate pairspos_port06_impact.py (pairs_sim alla leva live 2x, parita' canonica
1.00000 5/5, PORT06 pesi cap): griglia pos {0.5, 0.25, 0.20, 0.15}. Il gate
meccanico boccia ogni riduzione (costa OOS Sharpe — il compounding in-sample a 0.5
e' fantasia: +2.6e9% ETH/BTC), ma 0.20 e' il ginocchio del trade-off: OOS DD
3.40->1.26% (meglio anche di 0.15), famiglia DD 14->5.8%, costo OOS Sharpe
9.05->8.43. Decisione assicurativa (utente), come il precedente cap SHAPE.
- runner.pos_for_spec(sid, global, family_overrides): override per-famiglia
(chiave weighting.family_of), plumbato in build_worker_for.
- portfolios.yml: position_size_family {PAIRS: 0.20} (globale 0.5 invariato).
- Test: pos_for_spec mapping + PairsWorker.position_size (74/74 verdi).
Nota transizione: la posizione LTC/ETH aperta verra' chiusa col pos nuovo
(one-time). Diario docs/diary/2026-06-07-pairspos-gate.md.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
148 lines
6.2 KiB
Python
148 lines
6.2 KiB
Python
"""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()
|