Files
PythagorasGoal/scripts/research/r0703_vrpimp_anchor.py
T
Adriano Dal Pastro 26f8d27a61 research(wave-0703): migliora+proteggi VRP01 — 7 filoni, 0 miglioramenti, anchor-audit VRP01 chiuso (4/4 sleeve)
Goal: migliorare la strategia short-vol (famiglia Albimarini/VRP01) e
proteggerla dai DD. Workflow 26 agenti (7 filoni + 2 lenti avversariali
+ scettico incrociato). Esito: NON migliorabile; la protezione DD si
compra SOLO con la size.

- Griglia 288 strutture: nessuna batte VRP01 (DSR 0.000; meta' griglia
  = 3a occorrenza "0-perdite/Sharpe implausibile" dopo CC01/ALB-A).
- 4 overlay DD (exit-spike/SL-MTM/ala-coda/cooldown): 4/4 REFUTED dal
  null de-levering — la protezione crash vive gia' nel gate IV-rank.
- Gate nuovi: 4o fallimento su 4 (l'alpha e' il binario IV-rank>0.30).
- Sizing: 12% deploy ~ 0.27 Kelly onesto (anti-rovina); disambiguazione
  unita' obbligatoria vs 12% peso book (0.014 Kelly, fattore 19x).
- Gate term-structure VIX/VXV su SPX (dSh +0.90, DSR 0.992) = confound
  di modello al 100% -> nuova regola: riprezzare term-structure-consistent
  prima di credere a un gate vol su strutture BS-flat.
- ANCHOR-AUDIT VRP01 CHIUSO: primo sleeve SENZA luck (fase canonica =
  peggiore delle 7 -> numeri di ammissione conservativi). Audit anchor
  ora completo su 4/4 sleeve ancorati.

Book/pesi INVARIATI. Nessun nuovo sleeve. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-03 07:05:19 +00:00

485 lines
24 KiB
Python

#!/usr/bin/env python
"""r0703_vrpimp_anchor.py — FILONE 5: AUDIT ANCHOR-LUCK di VRP01 (le 7 fasi settimanali).
VRP01 apre un put credit spread ogni 7 GIORNI-INDICE a partire da i=60 del join px/DVOL
(gap-free dal 2021-03-24) e lo tiene a scadenza. L'ancora e' quindi il GIORNO DELLA SETTIMANA
di apertura: 7 fasi a priori (i0 = 60+phase, phase in 0..6). E' l'unico sleeve ancorato non
ancora auditato dopo TP01 (24 ancore, P=0.86), XS01 (10 fasi) e SKH01 (23 offset).
Domande (ricerca di PROTEZIONE, non di alpha):
0. REPLICA bit-exact della sleeve canonica (_vrp_combo_returns) prima di tutto.
1. Banda delle 7 fasi a parametri IDENTICI (gate IV-rank/VRP ricalcolati causali per fase,
automatico: i gate si valutano all'indice d'ingresso della fase): Sh FULL/HOLD, maxDD,
worst-week, per-anno. Percentile della fase canonica.
2. Banda f skew-caveat {0.6, 0.8, 1.0, 1.3} (pricing BS flat su DVOL ATM).
3. ENSEMBLE delle 7 fasi (1/7 ciascuna) + null del DE-LEVERING (lezione TP01xDVOL:
una riduzione DD replicabile scalando la size non vale nulla).
4. Block-bootstrap P(spike) alla maniera dello scettico TP01 (r0702_skeptic_offset.py):
lo spike della fase canonica e' speciale o e' il massimo atteso di 7 stime correlate?
5. Impatto sul book 5-sleeve (TP 33/XS 15/VRP 12/SKH 20/GTAA 20, combine_outer) alla fase
canonica / mediana / peggiore / ensemble.
6. Ri-verdetto dei numeri di ammissione (FULL 1.10 / HOLD 0.60 / DD 12%) alla fase mediana.
Macchineria RIUSATA, non riscritta: helper del pricing e config presi in sola lettura da
src/portfolio/sleeves (_bs_put, _strike_from_delta, VRP_CFG) — il motore replica quindi la
sleeve canonica per costruzione, e la replica e' VERIFICATA bit-exact in PART 0. (Il motore
r0702_alb_structure usa load_tf di research_lab: qui serve il path della sleeve, resample_1d
su load_data, per la bit-exactness.) Nessun file di produzione toccato. Nessuna selezione
sull'hold-out (nessuna grid: 7 fasi = famiglia a priori, parametri congelati).
Regole standing rispettate: niente short-vol da modello in deploy (esito = conoscenza);
niente '7D' con origin (qui il ciclo e' index-based come il canonico, niente resample
settimanale); pandas 2.x niente .view('int64').
uv run python scripts/research/r0703_vrpimp_anchor.py
"""
from __future__ import annotations
import sys
from functools import lru_cache
from pathlib import Path
import numpy as np
import pandas as pd
ROOT = Path("/opt/docker/PythagorasGoal")
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
sys.path.insert(0, str(ROOT))
import altlib as al # noqa: E402 (harness di progetto: _sh/_dd_ret/HOLDOUT)
from src.data.downloader import load_data # noqa: E402
from src.strategies.trend_portfolio import resample_1d # noqa: E402
from src.portfolio.sleeves import ( # noqa: E402 (SOLA LETTURA)
VRP_CFG, _bs_put, _strike_from_delta, _vrp_weekly_asset, _vrp_combo_returns, _HL_DIR)
from src.portfolio.portfolio import combine_outer, to_daily # noqa: E402
HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC")
WKY = 365.25 / 7.0
F_BAND = (0.6, 0.8, 1.0, 1.3)
PHASES = tuple(range(7))
ASSETS = ("BTC", "ETH")
B_BOOT = 4000
DAYNAME = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
ADMISSION = dict(full=1.10, hold=0.60, dd=0.12) # numeri dichiarati in CLAUDE.md
# ===========================================================================
# motore per-fase (specchio 1:1 di sleeves._vrp_weekly_asset, param i0 e f)
# ===========================================================================
@lru_cache(maxsize=4)
def joined(asset: str) -> pd.DataFrame:
"""Stesso join px/DVOL della sleeve canonica (resample_1d su load_data 1h)."""
df = resample_1d(load_data(asset, "1h"))
s = pd.Series(df["close"].values.astype(float), index=pd.to_datetime(df["datetime"]))
if s.index.tz is None:
s.index = s.index.tz_localize("UTC")
dv = pd.read_parquet(_HL_DIR / f"dvol_{asset.lower()}.parquet")
d = pd.Series(dv["close"].values.astype(float),
index=pd.to_datetime(dv["timestamp"], unit="ms", utc=True))
return pd.concat({"px": s, "dvol": d}, axis=1, join="inner").sort_index().dropna()
def vrp_weekly_phase(asset: str, phase: int = 0, f: float = 1.0) -> pd.Series:
"""Replica ESATTA di _vrp_weekly_asset con i0 = 60+phase e fattore premio f.
I gate (VRP>0, IV-rank espandente, crash-skip) sono ricalcolati CAUSALI all'indice
d'ingresso di ciascuna fase (dvf[:i]) — nessun riuso della sequenza gate di fase 0."""
J = joined(asset)
px = J["px"].values; dvf = J["dvol"].values / 100.0; idx = J.index
n = len(px); cfg = VRP_CFG; tn = cfg["tenor_d"]; T = tn / 365.25
rets = {}
i = 60 + phase
while i + tn < n:
S0 = px[i]; sig = dvf[i]
skip = False
if cfg["gate_vrp"] and i >= 31: # VRP>0 causale
rv = np.std(np.diff(np.log(px[i - 30:i + 1]))) * np.sqrt(365.25)
if (sig - rv) <= 0:
skip = True
if not skip and (cfg["gate_ivr"] > 0 or cfg["crash_skip"] < 1.0) and i >= 60:
ivr = float((dvf[:i] < dvf[i]).mean()) # IV-rank espandente
if cfg["gate_ivr"] > 0 and ivr < cfg["gate_ivr"]:
skip = True
if cfg["crash_skip"] < 1.0 and ivr > cfg["crash_skip"]:
skip = True
if skip:
rets[idx[i + tn]] = 0.0; i += tn; continue
Ks = _strike_from_delta(S0, T, sig, cfg["short_delta"])
Kl = _strike_from_delta(S0, T, sig, cfg["long_delta"])
net_prem = (_bs_put(S0, Ks, T, sig) - _bs_put(S0, Kl, T, sig)) * f
S1 = px[i + tn]
payoff = max(0.0, Ks - S1) - max(0.0, Kl - S1)
pnl = net_prem - payoff - cfg["fee_frac"] * abs(net_prem)
rets[idx[i + tn]] = pnl / Ks
i += tn
return pd.Series(rets)
@lru_cache(maxsize=64)
def combo_weekly(phase: int, f: float = 1.0) -> pd.Series:
"""Book 50/50 BTC+ETH per fase (stessa inner-join su date di scadenza del canonico)."""
rB = vrp_weekly_phase("BTC", phase, f)
rE = vrp_weekly_phase("ETH", phase, f)
return pd.concat({"B": rB, "E": rE}, axis=1, join="inner").mean(axis=1).sort_index()
def to_daily_lumped(wk: pd.Series) -> pd.Series:
"""Convenzione _vrp_combo_returns: lump del rendimento settimanale sul giorno di scadenza."""
days = pd.date_range(wk.index.min().normalize(), wk.index.max().normalize(),
freq="1D", tz="UTC")
daily = pd.Series(0.0, index=days)
daily.loc[wk.index.normalize()] = wk.values
return daily
# ===========================================================================
# metriche
# ===========================================================================
def m_wk(r: pd.Series) -> dict:
"""Metriche sul ciclo settimanale naturale (convenzione dei numeri di ammissione)."""
r = r.dropna()
if len(r) < 3 or r.std() == 0:
return dict(sh=0.0, sh_h=0.0, cagr=0.0, dd=0.0, worst=0.0, act=0.0, n=len(r))
def _s(x):
return float(x.mean() / x.std() * np.sqrt(WKY)) if len(x) > 2 and x.std() > 0 else 0.0
eq = np.cumprod(1 + r.values); pk = np.maximum.accumulate(eq)
yrs = len(r) / WKY
return dict(sh=_s(r), sh_h=_s(r[r.index >= HOLDOUT]),
cagr=float(eq[-1] ** (1 / yrs) - 1) if yrs > 0 and eq[-1] > 0 else -1.0,
dd=float(np.max((pk - eq) / pk)), worst=float(r.min()),
act=float((r != 0.0).mean()), n=int(len(r)))
def sh_daily(s: pd.Series) -> float:
return al._sh(s)
def worst_7d(daily: pd.Series) -> float:
"""Peggior finestra di 7 giorni compounding, ancor-free (per l'ensemble)."""
lr = np.log1p(daily.values)
if len(lr) < 8:
return 0.0
cs = np.concatenate([[0.0], np.cumsum(lr)])
w = cs[7:] - cs[:-7]
return float(np.expm1(w.min()))
def pctl_of(vals: np.ndarray, v0: float) -> float:
return float(((vals < v0).sum() + 0.5 * (vals == v0).sum()) / len(vals) * 100.0)
def per_year(r: pd.Series) -> dict:
return {int(y): float(np.prod(1 + g.values) - 1) for y, g in r.groupby(r.index.year)}
# ===========================================================================
# PART 0 — replica bit-exact
# ===========================================================================
def part0() -> None:
print("=" * 100)
print("PART 0 — REPLICA della sleeve canonica (sanity bit-exact vs sleeves._vrp_combo_returns)")
print("=" * 100)
for a in ASSETS:
mine = vrp_weekly_phase(a, phase=0, f=VRP_CFG["f"])
ref = _vrp_weekly_asset(a)
assert len(mine) == len(ref), f"P0 len mismatch {a}: {len(mine)} vs {len(ref)}"
assert (mine.index == ref.index).all(), f"P0 index mismatch {a}"
dmax = float(np.max(np.abs(mine.values - ref.values)))
assert dmax == 0.0, f"P0 valori non bit-exact {a}: max|diff|={dmax:.2e}"
print(f" [{a}] settimanale fase-0 == _vrp_weekly_asset: bit-exact "
f"(n={len(mine)}, max|diff|={dmax:.1e})")
mine_d = to_daily_lumped(combo_weekly(0, VRP_CFG["f"]))
ref_d = _vrp_combo_returns()
assert len(mine_d) == len(ref_d) and (mine_d.index == ref_d.index).all(), "P0 grid daily"
dmax = float(np.max(np.abs(mine_d.values - ref_d.values)))
assert dmax == 0.0, f"P0 combo daily non bit-exact: {dmax:.2e}"
print(f" [COMBO] daily-lumped fase-0 == _vrp_combo_returns: bit-exact (n={len(mine_d)})")
for a in ASSETS:
J = joined(a)
gaps = J.index.to_series().diff().dt.total_seconds().dropna() / 86400.0
print(f" [{a}] join px/DVOL: {len(J)} giorni {J.index.min().date()} -> "
f"{J.index.max().date()}, gap>1g: {int((gaps > 1).sum())} "
f"(gap-free -> ogni fase = giorno-della-settimana FISSO)")
# ===========================================================================
# PART 1 — le 7 fasi a parametri identici (f=1.0)
# ===========================================================================
def part1() -> dict:
print("\n" + "=" * 100)
print("PART 1 — LE 7 FASI (parametri canonici, f=1.0). Ancora canonica = fase 0.")
print("=" * 100)
rows = {}
print(f" {'fase':<6}{'apre':>5}{'n_wk':>6}{'att%':>6}{'ShFULL':>8}{'ShHOLD':>8}"
f"{'CAGR':>8}{'maxDD':>8}{'worst-wk':>9}")
for p in PHASES:
wk = combo_weekly(p, 1.0)
entry_wd = int(pd.DatetimeIndex(wk.index - pd.Timedelta(days=VRP_CFG["tenor_d"]))
.dayofweek.min())
wd_n = pd.DatetimeIndex(wk.index).dayofweek.nunique()
mm = m_wk(wk)
rows[p] = dict(mm, entry_wd=entry_wd, wd_n=wd_n, wk=wk)
tag = " <- CANONICA" if p == 0 else ""
print(f" {p:<6}{DAYNAME[entry_wd]:>5}{mm['n']:>6}{mm['act']*100:>5.0f}%"
f"{mm['sh']:>8.2f}{mm['sh_h']:>8.2f}{mm['cagr']*100:>+7.1f}%"
f"{mm['dd']*100:>7.1f}%{mm['worst']*100:>+8.2f}%{tag}")
assert wd_n == 1, f"fase {p}: weekday non unico ({wd_n})"
for key, label in (("sh", "Sh FULL"), ("sh_h", "Sh HOLD"), ("dd", "maxDD"),
("worst", "worst-week"), ("cagr", "CAGR")):
v = np.array([rows[p][key] for p in PHASES])
print(f" banda {label:<11}: min {v.min():+.3f} / mediana {np.median(v):+.3f} / "
f"max {v.max():+.3f} | canonica {v[0]:+.3f} "
f"(pctl {pctl_of(v, v[0]):.0f}° su 7 fasi)")
print("\n per-anno (f=1.0) — dispersione di fase attraverso il 2022 (LUNA/FTX):")
yrs = sorted({y for p in PHASES for y in per_year(rows[p]["wk"])})
print(" fase " + "".join(f"{y:>9}" for y in yrs))
for p in PHASES:
py = per_year(rows[p]["wk"])
print(f" {p:<6}" + "".join(f"{py.get(y, float('nan'))*100:>+8.1f}%" for y in yrs))
return rows
# ===========================================================================
# PART 2 — banda f (caveat skew: pricing BS flat su DVOL ATM)
# ===========================================================================
def part2() -> None:
print("\n" + "=" * 100)
print("PART 2 — BANDA f {0.6, 0.8, 1.0, 1.3} x 7 fasi (skew non esplicito -> banda, non punto)")
print("=" * 100)
print(f" {'f':>5} | {'ShFULL min/med/max':>22} {'(canon)':>8} | {'ShHOLD min/med/max':>22} "
f"{'(canon)':>8} | {'maxDD min/med/max':>20} {'(canon)':>8} | {'worst min':>9}")
for f in F_BAND:
mm = [m_wk(combo_weekly(p, f)) for p in PHASES]
sf = np.array([m["sh"] for m in mm]); shh = np.array([m["sh_h"] for m in mm])
dd = np.array([m["dd"] for m in mm]); wo = np.array([m["worst"] for m in mm])
print(f" {f:>5.1f} | {sf.min():>6.2f} {np.median(sf):>6.2f} {sf.max():>6.2f} "
f"{sf[0]:>7.2f} | {shh.min():>6.2f} {np.median(shh):>6.2f} {shh.max():>6.2f} "
f"{shh[0]:>7.2f} | {dd.min()*100:>5.1f} {np.median(dd)*100:>5.1f} "
f"{dd.max()*100:>5.1f}% {dd[0]*100:>6.1f}% | {wo.min()*100:>+8.2f}%")
# ===========================================================================
# PART 3 — ensemble delle 7 fasi + null del de-levering
# ===========================================================================
def phase_matrix(f: float = 1.0) -> pd.DataFrame:
cols = {f"p{p}": to_daily_lumped(combo_weekly(p, f)) for p in PHASES}
M = pd.concat(cols, axis=1, join="outer").sort_index()
lo = max(c.index.min() for c in cols.values())
hi = min(c.index.max() for c in cols.values())
return M.loc[(M.index >= lo) & (M.index <= hi)].fillna(0.0)
def part3(rows: dict) -> dict:
print("\n" + "=" * 100)
print("PART 3 — ENSEMBLE delle 7 fasi (1/7 ciascuna) + NULL DEL DE-LEVERING")
print("=" * 100)
M = phase_matrix(1.0)
ens = M.mean(axis=1)
can = M["p0"]
def _stats(s, name):
dd = al._dd_ret(s)
out = dict(sh=sh_daily(s), sh_h=sh_daily(s[s.index >= HOLDOUT]), dd=dd,
w7=worst_7d(s),
cagr=float(np.prod(1 + s.values) ** (365.25 / len(s)) - 1))
print(f" {name:<28} ShFULL {out['sh']:>5.2f} ShHOLD {out['sh_h']:>5.2f} "
f"CAGR {out['cagr']*100:>+5.1f}% maxDD {out['dd']*100:>5.1f}% "
f"worst-7g {out['w7']*100:>+6.2f}%")
return out
print(" (griglia daily comune alle 7 fasi; Sharpe daily ~= Sharpe weekly del lump)")
s_can = _stats(can, "CANONICA fase-0")
s_ens = _stats(ens, "ENSEMBLE 7 fasi (1/7)")
# NULL DEL DE-LEVERING: lo Sharpe e' scale-free -> scalare la canonica non lo cambia.
# Cerco lambda t.c. maxDD(lam*canonica) ~= maxDD(ensemble) e confronto CAGR a pari DD.
lams = np.linspace(0.30, 1.00, 141)
dds = np.array([al._dd_ret(can * lam) for lam in lams])
k = int(np.argmin(np.abs(dds - s_ens["dd"])))
lam = float(lams[k]); scaled = can * lam
cagr_sc = float(np.prod(1 + scaled.values) ** (365.25 / len(scaled)) - 1)
print(f"\n NULL de-levering: canonica x lambda={lam:.2f} -> maxDD {dds[k]*100:.1f}% "
f"(~ ensemble {s_ens['dd']*100:.1f}%), Sharpe INVARIATO {sh_daily(scaled):.2f}, "
f"CAGR {cagr_sc*100:+.1f}% (ensemble {s_ens['cagr']*100:+.1f}%)")
# LENTE ONESTA sul numero dell'ensemble: lo Sharpe daily (sopra) e' GONFIATO dalla
# convenzione del lump — le 7 fasi lumpano su giorni DISGIUNTI (corr daily ~0 per
# costruzione) ma i 7 spread concorrenti condividono ~6/7 dell'esposizione economica.
# Ricompound a blocchi di 7 giorni (tutti gli offset) = P&L settimanale del libro
# sfalsato con la correlazione cross-fase RIPRISTINATA.
def sh_7d_blocks(s: pd.Series) -> np.ndarray:
lr = np.log1p(s.values)
shs = []
for o in range(7):
m = (len(lr) - o) // 7
if m < 30:
continue
w = np.expm1(lr[o:o + 7 * m].reshape(m, 7).sum(axis=1))
shs.append(float(w.mean() / w.std() * np.sqrt(WKY)) if w.std() > 0 else 0.0)
return np.array(shs)
print("\n LENTE ONESTA (Sharpe su blocchi 7g non-overlap, banda sui 7 offset di blocco):")
for name, s in (("CANONICA fase-0", can), ("ENSEMBLE 7 fasi", ens)):
vf = sh_7d_blocks(s); vh = sh_7d_blocks(s[s.index >= HOLDOUT])
print(f" {name:<20} FULL min/med/max {vf.min():.2f}/{np.median(vf):.2f}/{vf.max():.2f}"
+ (f" | HOLD {vh.min():.2f}/{np.median(vh):.2f}/{vh.max():.2f}" if len(vh) else ""))
print(" -> il numero citabile dell'ensemble e' quello a blocchi 7g, NON lo Sh daily del lump.")
print(" NB: anche il maxDD (canonica E ensemble) e' su equity a date di REGOLAMENTO —")
print(" il mark-to-market intra-settimana degli spread aperti non e' catturato (caveat noto).")
beats = (s_ens["sh"] > s_can["sh"] + 1e-9) and (s_ens["cagr"] > cagr_sc + 1e-9)
print(f"\n -> l'ensemble batte il de-levering? {'SI' if beats else 'NO'} "
f"(serve Sh_ens > Sh_can E CAGR_ens > CAGR de-levered a pari DD; il CAGR e' "
f"convention-free, lo Sh daily dell'ensemble NO — vedi lente onesta)")
print(" NB deploy: a $600-2k un solo spread min-size satura il libro -> 7 tranche da 1/7")
print(" NON sono eseguibili (stesso muro del tranching TP01); l'ensemble e' una LENTE di")
print(" misura (de-lucking della stima), non una modifica proponibile del book.")
return dict(ens=s_ens, can=s_can, lam=lam, cagr_scaled=cagr_sc, beats=beats)
# ===========================================================================
# PART 4 — block-bootstrap P(spike) (metodo r0702_skeptic_offset)
# ===========================================================================
def _sh_mat(R: np.ndarray) -> np.ndarray:
mu = R.mean(axis=1); sd = R.std(axis=1)
with np.errstate(divide="ignore", invalid="ignore"):
out = np.where(sd > 0, mu / sd, 0.0)
return np.nan_to_num(out) * np.sqrt(365.25)
def block_boot(M: np.ndarray, B: int, block: int, rng) -> dict:
n, K = M.shape
nblocks = int(np.ceil(n / block))
g0s, gmaxs = [], []
done = 0
while done < B:
b = min(500, B - done)
starts = rng.integers(0, n, size=(b, nblocks))
idx = (starts[:, :, None] + np.arange(block)[None, None, :]) % n
idx = idx.reshape(b, -1)[:, :n]
R = M[idx]
Sh = np.stack([_sh_mat(R[:, :, k]) for k in range(K)], axis=1)
med_others = np.empty_like(Sh)
for h in range(K):
others = np.delete(Sh, h, axis=1)
med_others[:, h] = np.median(others, axis=1)
g = Sh - med_others
g0s.append(g[:, 0]); gmaxs.append(g.max(axis=1))
done += b
return dict(g0=np.concatenate(g0s), gmax=np.concatenate(gmaxs))
def part4() -> dict:
print("\n" + "=" * 100)
print("PART 4 — BLOCK-BOOTSTRAP: lo spike della fase canonica e' speciale o massimo atteso di 7?")
print("=" * 100)
M = phase_matrix(1.0)
out = {}
for wname, W in (("HOLD-OUT 2025+", M[M.index >= HOLDOUT]),
("FULL 2021-26", M)):
Mv = W.values
sh = _sh_mat(Mv.T)
g0 = float(sh[0] - np.median(sh[1:]))
corr = np.corrcoef(Mv.T); iu = np.triu_indices(len(PHASES), 1)
print(f"\n [{wname}] {Mv.shape[0]} giorni | Sh fase-0 {sh[0]:+.3f}, mediana altre "
f"{np.median(sh[1:]):+.3f}, spike g0 = {g0:+.3f} | corr daily fra fasi: "
f"mediana {np.median(corr[iu]):+.3f} (lump su giorni diversi -> quasi-ortogonali "
f"per costruzione)")
for blk in (14, 28, 56):
bs = block_boot(Mv, B_BOOT, blk, np.random.default_rng(73 + blk))
p_any = float(np.mean(bs["gmax"] >= g0))
p_le0 = float(np.mean(bs["g0"] <= 0.0))
ci = np.percentile(bs["g0"], [2.5, 97.5])
print(f" block={blk:>2}: P(spike di UNA QUALSIASI fase >= {g0:+.2f}) = {p_any:.3f}"
f" | P(g0<=0) = {p_le0:.3f} | CI95 g0 [{ci[0]:+.2f},{ci[1]:+.2f}]")
out[(wname, blk)] = p_any
# finestre annuali: la fase migliore e' stabile o gira?
print("\n finestre annuali — Sh per fase, pctl della canonica, best-fase:")
print(f" {'finestra':<9}" + "".join(f"{'p'+str(p):>8}" for p in PHASES)
+ f"{'pctl p0':>9}{'best':>6}")
idx_years = sorted(set(M.index.year))
for y in idx_years:
W = M[M.index.year == y]
if len(W) < 60:
continue
sh = _sh_mat(W.values.T)
print(f" {y:<9}" + "".join(f"{v:>8.2f}" for v in sh)
+ f"{pctl_of(sh, sh[0]):>8.0f}°{int(np.argmax(sh)):>6}")
return out
# ===========================================================================
# PART 5 — impatto sul book 5-sleeve
# ===========================================================================
def part5(rows: dict) -> dict:
print("\n" + "=" * 100)
print("PART 5 — BOOK 5-SLEEVE (TP 33/XS 15/VRP 12/SKH 20/GTAA 20) con VRP01 per fase")
print("=" * 100)
from src.portfolio.sleeves import (_gtaa_daily_returns, _skyhook_returns,
_tp01_returns, _xsec_returns)
tp = to_daily(_tp01_returns())
fixed = dict(TP=tp, XS=to_daily(_xsec_returns()), SKH=to_daily(_skyhook_returns()),
GTAA=to_daily(_gtaa_daily_returns()))
W = dict(TP=0.33, XS=0.15, VRP=0.12, SKH=0.20, GTAA=0.20)
lo = tp.index.min()
def book(vrp_daily):
s = combine_outer(dict(fixed, VRP=vrp_daily), W, lo=lo)
return dict(sh=al._sh(s), sh_h=al._sh(s[s.index >= HOLDOUT]), dd=al._dd_ret(s),
dd_h=al._dd_ret(s[s.index >= HOLDOUT]))
res = {}
print(f" {'variante VRP':<22}{'ShFULL':>8}{'ShHOLD':>8}{'maxDD':>8}{'DD hold':>9}")
for p in PHASES:
res[p] = book(to_daily_lumped(rows[p]["wk"]))
tag = " <- CANONICA (book corrente)" if p == 0 else ""
print(f" fase {p} ({DAYNAME[rows[p]['entry_wd']]}){'':<10}{res[p]['sh']:>8.2f}"
f"{res[p]['sh_h']:>8.2f}{res[p]['dd']*100:>7.1f}%{res[p]['dd_h']*100:>8.1f}%{tag}")
M = phase_matrix(1.0)
res["ens"] = book(M.mean(axis=1))
print(f" {'ENSEMBLE 7 fasi':<22}{res['ens']['sh']:>8.2f}{res['ens']['sh_h']:>8.2f}"
f"{res['ens']['dd']*100:>7.1f}%{res['ens']['dd_h']*100:>8.1f}%")
for key, label in (("sh", "ShFULL"), ("sh_h", "ShHOLD"), ("dd", "maxDD")):
v = np.array([res[p][key] for p in PHASES])
print(f" banda book {label:<7}: min {v.min():.3f} / mediana {np.median(v):.3f} / "
f"max {v.max():.3f} | canonica {v[0]:.3f} (pctl {pctl_of(v, v[0]):.0f}°)")
print(" (il peso VRP e' 12%: la banda di fase si diluisce ~0.12 nel book — atteso stretto)")
return res
# ===========================================================================
# PART 6 — ri-verdetto dei numeri di ammissione alla fase mediana
# ===========================================================================
def part6(rows: dict) -> None:
print("\n" + "=" * 100)
print("PART 6 — RI-VERDETTO dei numeri di ammissione VRP01 (FULL 1.10 / HOLD 0.60 / DD 12%)")
print("=" * 100)
sf = np.array([rows[p]["sh"] for p in PHASES])
shh = np.array([rows[p]["sh_h"] for p in PHASES])
dd = np.array([rows[p]["dd"] for p in PHASES])
wo = np.array([rows[p]["worst"] for p in PHASES])
print(f" Sh FULL : dichiarato {ADMISSION['full']:.2f} | canonica {sf[0]:.2f} "
f"(pctl {pctl_of(sf, sf[0]):.0f}°) | mediana fasi {np.median(sf):.2f} | "
f"banda [{sf.min():.2f}, {sf.max():.2f}]")
print(f" Sh HOLD : dichiarato {ADMISSION['hold']:.2f} | canonica {shh[0]:.2f} "
f"(pctl {pctl_of(shh, shh[0]):.0f}°) | mediana fasi {np.median(shh):.2f} | "
f"banda [{shh.min():.2f}, {shh.max():.2f}]")
print(f" maxDD : dichiarato {ADMISSION['dd']*100:.0f}% | canonica {dd[0]*100:.1f}% "
f"(pctl {pctl_of(dd, dd[0]):.0f}°) | mediana fasi {np.median(dd)*100:.1f}% | "
f"banda [{dd.min()*100:.1f}%, {dd.max()*100:.1f}%]")
print(f" worst-wk: canonica {wo[0]*100:+.2f}% | mediana {np.median(wo)*100:+.2f}% | "
f"peggiore {wo.min()*100:+.2f}%")
print("\n Regola 2026-07-02: i numeri di VRP01 si citano d'ora in poi CON la banda di fase.")
def main() -> None:
part0()
rows = part1()
part2()
part3(rows)
part4()
part5(rows)
part6(rows)
print("\nFatto (r0703_vrpimp_anchor).")
if __name__ == "__main__":
main()