#!/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()