#!/usr/bin/env python """r0726_deluck_factor.py — DECOMPOSIZIONE del fattore de-luck ×0.6 del 25/07. IL PROBLEMA. `r0725_capcurve.py` applica `b = b - 0.4*mean(b)` ("de-luck ×0.6": taglia il 40% del DRIFT, lascia la vol) sopra un book gia' calcolato con SKH01 sulla lente `hourly`. Il ×0.6 fu scelto **a occhio**, e CLAUDE.md registrava gia' il sospetto: *"il ×0.6 sopra il path live rischia di contare DUE VOLTE la degradazione SKH01"*. Il fattore mescola due cose che ora si possono separare e misurare: (a) **fortuna d'ancora** — i numeri canonici sono un punto alto dello spazio d'ancora (misurato il 26/07 sul Sharpe: FULL 97.0 pctl, HOLD 99.6 pctl). Qui serve sul **DRIFT**, perche' e' il drift che il ×0.6 taglia. (b) **degradazione del path live** — che per SKH01 e' misurata **NEGATIVA** (il live e' MEGLIO del backtest su entrambi i lati: uscite +0.081 di Sharpe FULL di book su 23/23 offset, ingressi +0.38 di Sharpe di sleeve) e per TP01 e' ~zero (±0.03 su 24 ancore). Questo script misura (a) e la riporta accanto a (b), per due book: * il book ATTIVO 5-sleeve (33/15/12/20/20) * il book LIVE Deribit (TP01/SKH01 75/25) — quello che gira davvero METODO: identico a `r0726_loo_deluck.py` (stesse serie d'ancora, stesse estrazioni congiunte uniformi indipendenti), ma la metrica e' il **drift annualizzato** (media giornaliera x 365.25) e il CAGR, non il Sharpe. Il fattore onesto e' `mediana / canonico` sul drift. NON decide da solo il valore da usare: la componente (b) sugli INGRESSI di SKH01 e' in corso di misura de-luckata (`r0726_skh_sigcache.py` + `r0726_skh_live_book.py`). Qui si stabilisce quanto del ×0.6 e' spiegato dall'ancora — cioe' quanto ne resta da attribuire al live. uv run python scripts/research/r0726_deluck_factor.py [--draws N] """ from __future__ import annotations import argparse import sys from pathlib import Path import numpy as np ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) import r0726_loo_deluck as L # noqa: E402 from src.portfolio.portfolio import HOLDOUT, combine_outer, metrics # noqa: E402 DPY = 365.25 BOOKS = { "book ATTIVO 5-sleeve (33/15/12/20/20)": L.WEIGHTS, "book LIVE Deribit (TP01/SKH01 75/25)": {"TP01_trend_1d": 0.75, "SKH01_skyhook": 0.25}, } def drift_and_cagr(cols: dict, weights: dict) -> tuple[float, float, float]: """(drift annualizzato, CAGR, vol annualizzata) del book a quei pesi.""" w = {k: weights[k] for k in cols} s = combine_outer(cols, w) r = np.asarray(s.dropna().values, float) m = metrics(s) return float(r.mean() * DPY), float(m["cagr"]), float(r.std() * np.sqrt(DPY)) def main() -> None: ap = argparse.ArgumentParser() ap.add_argument("--draws", type=int, default=2000) args = ap.parse_args() print("=" * 100) print(" r0726 — DECOMPOSIZIONE del fattore de-luck ×0.6 (25/07)") print("=" * 100) print(" Il ×0.6 taglia il DRIFT del book (b - 0.4*mean(b)). Quanto di quel taglio e'") print(" spiegato dalla FORTUNA D'ANCORA, e quanto resterebbe da attribuire al path live?") S = L.build_all() rng = np.random.default_rng(L.SEED) cfgs = [{n: int(rng.choice(L.SPEC[n][1])) for n in L.SPEC} for _ in range(args.draws)] canon_cfg = {n: L.SPEC[n][2] for n in L.SPEC} for label, W in BOOKS.items(): names = list(W) print("\n" + "=" * 100) print(f" {label}") print("=" * 100) def cols_at(cfg): return {n: S[n][cfg[n]] for n in names} d_can, c_can, v_can = drift_and_cagr(cols_at(canon_cfg), W) rows = np.array([drift_and_cagr(cols_at(c), W) for c in cfgs], float) d_med, c_med, v_med = np.median(rows, axis=0) d_lo, d_hi = np.percentile(rows[:, 0], [10, 90]) c_lo, c_hi = np.percentile(rows[:, 1], [10, 90]) n_anch = int(np.prod([len(L.SPEC[n][1]) for n in names])) print(f" spazio d'ancora di questo book: {n_anch:,} configurazioni " f"({' x '.join(str(len(L.SPEC[n][1])) for n in names)})") print(f"\n {'':<24}{'canonico':>11}{'pctl':>8}{'MEDIANA':>11}{'p10':>10}{'p90':>10}") print(f" {'drift annualizzato':<24}{d_can*100:10.2f}%" f"{float((rows[:, 0] < d_can).mean()*100):7.1f}%{d_med*100:10.2f}%" f"{d_lo*100:9.2f}%{d_hi*100:9.2f}%") print(f" {'CAGR':<24}{c_can*100:10.2f}%" f"{float((rows[:, 1] < c_can).mean()*100):7.1f}%{c_med*100:10.2f}%" f"{c_lo*100:9.2f}%{c_hi*100:9.2f}%") print(f" {'vol annualizzata':<24}{v_can*100:10.2f}%{'':>8}{v_med*100:10.2f}%") f_drift = d_med / d_can if d_can else float("nan") f_cagr = c_med / c_can if c_can else float("nan") print(f"\n >>> FATTORE D'ANCORA sul drift : ×{f_drift:.3f} (il ×0.6 ne assume ×0.600)") print(f" >>> FATTORE D'ANCORA sul CAGR : ×{f_cagr:.3f}") residuo = 0.6 / f_drift if f_drift else float("nan") print(f" >>> residuo implicito da attribuire al PATH LIVE: ×{residuo:.3f}") if residuo < 1.0: print(f" cioe' il ×0.6 assume che il live tolga un altro " f"{(1-residuo)*100:.0f}% di drift OLTRE l'ancora.") else: print(f" cioe' il ×0.6 e' CONSERVATIVO anche prima di contare il live.") print("\n" + "=" * 100) print(" COMPONENTE (b) — degradazione del path live, gia' misurata") print("=" * 100) print(" SKH01 uscite : +0.081 Sharpe FULL di book, 23/23 offset (26/07, banda appaiata)") print(" SKH01 ingressi: +0.38 Sharpe di sleeve, mediana su 3 offset x 2 asset (26/07)") print(" -> in corso la versione de-luckata a 8 offset, a livello di BOOK") print(" TP01 barra parziale: -0.031 FULL / +0.118 HOLD su 24 ancore = trascurabile") print(" => il segno di (b) e' NON-NEGATIVO su tutto cio' che e' stato misurato.") print(" Un fattore <1 per il live non e' sostenuto da nessuna misura del progetto.") if __name__ == "__main__": main()