#!/usr/bin/env python """r0823_skeptic_leva.py — SCETTICO ADVERSARIALE del gradino di leva 1,00x -> 1,25x. Filone §53 SKEPTIC-LEVA, ondata 2026-08-22/23, branch `research/wave-0822`. IL BERSAGLIO (RESULTS-0822 §33 WORST-DAY, §38 SCALE-SPEC, §40 SL-ANCHOR): «il libro gira al 7% di Kelly; il gradino 1,00x -> 1,25x vale 14,7a -> 12,9a al capitale-rendita (+164 EUR/mese di versamento equivalente); la riserva "-10% al giorno ogni anno" e' mal tarata di 2-4 ordini di grandezza; il nodo del disaster-SL rotolante e' sciolto (scatta MENO). Resta da costruire una chiave di scala in config.» QUESTO FILE NON CONFERMA: cerca dove il risultato e' sbagliato. Sei attacchi dichiarati, ognuno con la sua soglia scritta PRIMA nella sezione (0), e ognuno che finisce in REGGE / INCRINA / FALSIFICA con un NUMERO accanto. (0) IL CRITERIO — le sei soglie, congelate prima di guardare le sezioni 2-7. (1) REPLICA — 10 numeri gia' pubblicati riprodotti PRIMA di attaccarli. (2) A1 il drift e' stimato su 7,4 anni con due tori, e k* e' LINEARE nel suo errore. (3) A2 il campione non contiene il sinistro che conta (finestre stressate e disgiunte). (4) A3 la coda: due stimatori che non concordano -> si prende il PEGGIORE fino in fondo. (5) A4 la lente: tutto il conto e' close-only, e close-only e' CIECA su una regola a un livello. Il disaster-SL e' una regola su un livello. (6) A5 l'invariante a tre chiavi: e' completo? c'e' una QUARTA chiave? (7) A6 il funding non era nel conto della leva, e il funding SCALA con la leva. (8) VERDETTO, e il k MASSIMO DIFENDIBILE come singolo numero. ONESTA' APPLICATA (regole gia' codificate nel progetto, usate CONTRO questo lavoro): · lo Sharpe e' INVARIANTE alla scala: `deflated_sharpe` e `marginal_vs_tp01` non vedono la leva -> non compaiono qui come prova (SPEC §0); · si riproduce il numero che si attacca PRIMA di attaccarlo (sez. 1); · [MISURATO] / [CALC] / [DEDOTTO] / [ASSUNTO] / [CITATO] marcati riga per riga; · la trappola dichiarata da WORST-DAY: se uno stress uccide anche il conto di OGGI, non e' il gradino a essere sbagliato — e va DETTO. Per questo ogni finestra stressata riporta anche il drift del libro SU QUELLA finestra: dove il libro e' morto, il gradino non e' giudicabile. · un non-arrivo si codifica +inf, mai -1 (07/08); una mediana condizionata non si stampa mai senza la sua probabilita' accanto. · confronto APPAIATO fra k: `CC._boot_paths` estrae INDICI da un seme, quindi `_boot_paths(k*r) == k*_boot_paths(r)` esattamente (verificato in sez. 1). SOLA LETTURA. Nessuna rete, nessun ordine, nessuna scrittura fuori dalla cache di /tmp. nice -n 19 timeout 900 uv run python scripts/research/r0823_skeptic_leva.py """ from __future__ import annotations import json import sys import time import warnings from pathlib import Path import numpy as np import pandas as pd from scipy import stats warnings.filterwarnings("ignore") ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) import r0725_capcurve as CC # noqa: E402 book_series, _boot_paths, TAX_RATE import r0725_prop_coupled as PC # noqa: E402 lente ACCOPPIATA (R, m) del libro import r0727_tasse as TX # noqa: E402 PATRIMONIALE, CARRY_ANNI import r0807_piano_netto as PN # noqa: E402 muro / accumula / sopravvivenza from src.live.book import ( # noqa: E402 SOLA LETTURA (nessuna di queste scrive) CAP_UNKNOWN_USD, W_SKH, W_TP01, _read_watermark, book_net_target) from src.live.shadow import FALLBACK_CAPITAL, WEIGHT # noqa: E402 from src.strategies.trend_portfolio import CANONICAL # noqa: E402 ANN = 365.0 DELUCK = 0.89 # fattore d'ancora MISURATO (r0726_deluck_factor), non a occhio START = 635.0 # il conto Deribit VERO DEP_EUR = 500.0 # il piano in corso (config/live.json _nota_cap: "EUR 5.000 + 500/mese") BLOCK = 20 # blocchi del bootstrap (convenzione del progetto) N_WALL = 2000 # risoluzione MC del muro ~1%, DICHIARATA (r0822d_funding) N_TRAJ = 3000 # la taglia a cui furono calcolate le traiettorie pubblicate SEED = 725 # seme delle traiettorie pubblicate (confronto appaiato) SEED_BOOT = 20260823 ANNI_TRAJ = 25 KS = (1.00, 1.25, 1.50, 2.00) # ---- numeri PUBBLICATI, per la replica di sez. 1 ------------------------------------------ PUB = { "drift_nofund": 0.1924, "drift_fund": 0.1708, # §35 "sh_nofund": 1.692, "sh_fund": 1.513, # §35 "kstar_nofund": 13.7, "kstar_fund": 12.3, # §35 "g1": 0.1790, "g125": 0.2261, "sharpe_scale": 1.5051, # SPEC §0 (serie de-luckata, no funding) "perp_L1": 0.0752, "muro_L1": 264_000, # §36 (fisco) "perp_L3": 0.0635, "muro_L3": 313_000, # §36 (fisco + funding) "anni_L1_500": 14.7, # §36 riga "EUR 500 -> 14,7a" "worst_close": -0.0394, "worst_intraday": -0.0733, # §33 "L_strutt": 0.1432, # §33 sez.5 (bound aritmetico a k=1) "xi_gpd": 0.09, "xi_hill": 0.34, # §33 "k_da_SL": 1.67, # SPEC §4.5 = 0.50/(2*0.50*0.30) } CACHE = Path("/tmp/claude-1001/-opt-docker-PythagorasGoal/" "b6cc75e7-14f8-4c32-bd07-ab8a0d2aaee6/scratchpad/pianovero/book.parquet") T0 = time.time() ESITI: dict[str, tuple[str, str]] = {} def sez(t: str) -> None: print("\n" + "=" * 108) print(f" {t}") print("=" * 108) def clock() -> str: return f"[{time.time() - T0:5.0f}s]" def esito(chiave: str, verdetto: str, riga: str) -> None: ESITI[chiave] = (verdetto, riga) print(f"\n >>> {chiave}: {verdetto} — {riga}") # ========================================================================================== # (0) IL CRITERIO — congelato QUI, valutato in (8). Nessuna soglia usa un numero delle # sezioni 2-7: vengono dalle regole gia' scritte del progetto o sono dichiarate a priori. # ========================================================================================== CRITERIO = { "A1": ("DRIFT — il gradino 1,25x FALSIFICA se il drift di pareggio (quello a cui " "g(1,25) = g(1,00)) cade DENTRO l'IC95 bootstrap a blocchi del drift. " "INCRINA se cade fra l'IC95 e l'IC99. REGGE se e' fuori da entrambi."), "A2": ("SINISTRO — il gradino FALSIFICA se Delta g(1,25) < 0 in almeno una finestra " "stressata IN CUI IL LIBRO STESSO HA DRIFT POSITIVO. Dove il libro e' in perdita " "la finestra giudica il LIBRO, non il gradino, e non conta (nota dovuta di §33)."), "A3": ("CODA — si prende lo stimatore PEGGIORE (Hill, xi=0.34) e lo si porta fino in " "fondo. FALSIFICA se a k=1,25 (i) il peggior giorno strutturale supera il 50% " "dell'equity, oppure (ii) P(dimezzamento in 10 anni) supera il 5% E piu' del " "triplo del valore a k=1,00. Soglie: il 50% e' il C1 gia' dichiarato da WORST-DAY; " "il 5%/3x e' dichiarato qui, prima di guardare."), "A4": ("LENTE — FALSIFICA se sotto la lente ACCOPPIATA (i) la frequenza di tocco del " "disaster-SL cresce con k, oppure (ii) il ricarico del maxDD smette di essere " "moltiplicativo (rapporto accoppiato/close-only che si muove di piu' di 1pp fra " "k=1,00 e k=1,50)."), "A5": ("INVARIANTE — FALSIFICA se esiste una chiave RAGGIUNGIBILE, non presente in " "`n_asset * frac * scala * disaster_sl_pct <= 0,50`, che possa ALZARE la leva " "lorda vera lasciando l'invariante invariato o soddisfatto."), "A6": ("FUNDING — il funding SCALA col nozionale, quindi con k. Rifatto il conto con il " "funding dentro e proporzionale a k, il gradino FALSIFICA se il guadagno sul " "capitale-rendita scende sotto 1,0 anno (soglia del mandato)."), } # ========================================================================================== # UTILITA' # ========================================================================================== def deluck(r: np.ndarray) -> np.ndarray: """Toglie dal DRIFT la fortuna d'ancora misurata. La vol NON si tocca: fra le ancore era invariata (7,80% -> 7,76%), la fortuna sta tutta nel drift (r0726_deluck_factor).""" r = np.asarray(r, float) return r - (1.0 - DELUCK) * r.mean() def g_exact(r: np.ndarray, k: float) -> float: """Crescita geometrica annua a leva k: exp(365 * E[log(1+k r)]) - 1. Forma CHIUSA: nessun rumore Monte Carlo, quindi i confronti fra k sono esatti.""" x = 1.0 + k * np.asarray(r, float) if np.any(x <= 0): return -np.inf return float(np.expm1(ANN * np.mean(np.log(x)))) def k_star(r: np.ndarray, kmax: float = 40.0, step: float = 0.01) -> tuple[float, float]: ks = np.arange(step, kmax + step, step) gs = np.array([g_exact(r, k) for k in ks]) j = int(np.nanargmax(gs)) return float(ks[j]), float(gs[j]) def shift_drift(r: np.ndarray, mu_annuo: float) -> np.ndarray: """Stessa serie, drift imposto. Cambia UN grado di liberta' per volta: la forma della distribuzione (vol, asimmetria, coda, clustering) resta identica.""" r = np.asarray(r, float) return r - r.mean() + mu_annuo / ANN def boot_drift(r: np.ndarray, n: int = 4000, block: int = BLOCK, seed: int = SEED_BOOT) -> np.ndarray: """Bootstrap A BLOCCHI del drift annuo. A blocchi perche' la serie ha vol-clustering e momentum: un bootstrap iid darebbe un IC piu' STRETTO del vero, cioe' aiuterebbe la tesi che questo file sta attaccando.""" rng = np.random.default_rng(seed) m = len(r) nb = int(np.ceil(m / block)) out = np.empty(n) for i in range(n): st = rng.integers(0, m - block, size=nb) idx = (st[:, None] + np.arange(block)[None, :]).ravel()[:m] out[i] = r[idx].mean() * ANN return out def muro_di(r: np.ndarray) -> tuple[float, float]: """(rendita perpetua, capitale-rendita) con la convenzione COERENTE (imposte dentro il portafoglio, prelievo gia' netto). Bisezione identica a PN.muro, con N_WALL dichiarato.""" netto = CC.TARGET_EUR_DAY * 365 * CC.EURUSD lo, hi = 0.0, 0.40 for _ in range(13): mid = (lo + hi) / 2 s = PN.sopravvivenza(r, 1e6, 1e6 * mid, 20, CC.TAX_RATE, TX.PATRIMONIALE, n_paths=N_WALL, seed=SEED) if s["p10_end"] >= 1e6: lo = mid else: hi = mid return lo, (netto / lo if lo > 0.002 else float("inf")) def anni_al_muro(r: np.ndarray, bersaglio: float, dep_eur: float = DEP_EUR, start: float = START) -> dict: """Anni al capitale-rendita. Path APPAIATI fra k (gli indici del bootstrap dipendono solo dal seme e dalla lunghezza, non dai valori).""" rng = np.random.default_rng(SEED) paths = CC._boot_paths(r, N_TRAJ, ANNI_TRAJ * 365, BLOCK, rng) o = PN.accumula(paths, dep_eur, bersaglio, CC.TAX_RATE, TX.PATRIMONIALE, start=start) c = o["colpito"] y = np.where(c >= 0, c / 365.0, np.inf) arr = y[np.isfinite(y)] return dict(med=float(np.median(y)), med_cond=float(np.median(arr)) if len(arr) > 100 else float("nan"), p20=float((y <= 20.0).mean()), p_arr=float(np.isfinite(y).mean())) def cap_20a(r: np.ndarray, dep_eur: float = DEP_EUR, start: float = START, anni: int = 20) -> float: """Capitale mediano a 20 anni. Serve dove gli ANNI non sono definiti: a drift bassi il capitale-rendita non si raggiunge in 25 anni su nessun path e la mediana degli anni e' nan per ENTRAMBI i k — cioe' la metrica smette di discriminare proprio dove serve. Il capitale e' definito sempre, ed e' APPAIATO fra k (stessi indici di bootstrap).""" rng = np.random.default_rng(SEED) paths = CC._boot_paths(r, N_TRAJ, anni * 365, BLOCK, rng) o = PN.accumula(paths, dep_eur, 1e18, CC.TAX_RATE, TX.PATRIMONIALE, start=start) return float(np.median(o["cap"])) def fmt_anni(d: dict) -> str: if not np.isfinite(d["med_cond"]): return f" n/d (P20 {d['p20']:.3f})" return f"{d['med_cond']:5.2f}a (P20 {d['p20']:.3f})" # ========================================================================================== # (1) REPLICA — prima di attaccare # ========================================================================================== def sezione1() -> dict: sez("(1) REPLICA — 10 numeri pubblicati, riprodotti PRIMA di attaccarli") if not CACHE.exists(): raise SystemExit("cache serie assente: girare prima r0822d_piano_vero.py") d = pd.read_parquet(CACHE) r_nf, r_f = d["nofund"].values.astype(float), d["fund"].values.astype(float) print(f" serie: {len(r_nf)} giorni, {d.index.min().date()} -> {d.index.max().date()} " f"(cache di r0822d_piano_vero, zero rete)") R = {"nofund": r_nf, "fund": r_f, "L1": deluck(r_nf), "L3": deluck(r_f)} righe = [] for nome, x, pub in (("drift senza funding", r_nf.mean() * ANN, PUB["drift_nofund"]), ("drift con funding", r_f.mean() * ANN, PUB["drift_fund"]), ("Sharpe senza funding", r_nf.mean() / r_nf.std() * ANN ** .5, PUB["sh_nofund"]), ("Sharpe con funding", r_f.mean() / r_f.std() * ANN ** .5, PUB["sh_fund"])): righe.append((nome, x, pub)) ks_nf, _ = k_star(deluck(r_nf)) ks_f, _ = k_star(deluck(r_f)) righe += [("k* senza funding", ks_nf, PUB["kstar_nofund"]), ("k* con funding", ks_f, PUB["kstar_fund"]), ("g(1,00) serie SPEC", g_exact(R["L1"], 1.00), PUB["g1"]), ("g(1,25) serie SPEC", g_exact(R["L1"], 1.25), PUB["g125"]), ("Sharpe (invariante a k)", R["L1"].mean() / R["L1"].std() * ANN ** .5, PUB["sharpe_scale"])] print(f"\n {'grandezza':<26}{'replica':>12}{'pubblicato':>13}{'|diff|':>11}") for nome, x, pub in righe: print(f" {nome:<26}{x:>12.4f}{pub:>13.4f}{abs(x - pub):>11.4f}") p1, m1 = muro_di(R["L1"]) p3, m3 = muro_di(R["L3"]) print(f" {'perpetua L1 (fisco)':<26}{p1:>12.4f}{PUB['perp_L1']:>13.4f}{abs(p1-PUB['perp_L1']):>11.4f}") print(f" {'muro L1 ($)':<26}{m1:>12,.0f}{PUB['muro_L1']:>13,.0f}{abs(m1-PUB['muro_L1']):>11,.0f}") print(f" {'perpetua L3 (+funding)':<26}{p3:>12.4f}{PUB['perp_L3']:>13.4f}{abs(p3-PUB['perp_L3']):>11.4f}") print(f" {'muro L3 ($)':<26}{m3:>12,.0f}{PUB['muro_L3']:>13,.0f}{abs(m3-PUB['muro_L3']):>11,.0f}") a1 = anni_al_muro(R["L1"], m1) print(f" {'anni L1 @EUR500/m':<26}{a1['med_cond']:>12.2f}{PUB['anni_L1_500']:>13.2f}" f"{abs(a1['med_cond']-PUB['anni_L1_500']):>11.2f} (risoluzione MC dichiarata ~1%)") # --- il confronto APPAIATO fra k e' esatto: lo si PROVA, non lo si dichiara ------------- rng = np.random.default_rng(SEED) p_a = CC._boot_paths(R["L3"], 200, 2000, BLOCK, rng) rng = np.random.default_rng(SEED) p_b = CC._boot_paths(1.25 * R["L3"], 200, 2000, BLOCK, rng) dmax = float(np.abs(p_b - 1.25 * p_a).max()) print(f"\n appaiamento fra k: max|paths(1,25 r) - 1,25*paths(r)| = {dmax:.2e} " f"-> i path sono gli STESSI, il confronto fra k non ha rumore MC di campionamento") assert dmax == 0.0 # --- la lente accoppiata: quale serie e' ------------------------------------------------- Tc = PC.crypto_daily_tuples(0.75, 0.25) print(f" lente accoppiata (r0725_prop_coupled): {len(Tc)} giorni, peggior chiusura " f"{Tc['R'].min():+.2%} ({Tc['R'].idxmin().date()}), peggior minimo intra-giorno " f"{Tc['m'].min():+.2%} ({Tc['m'].idxmin().date()})") print(f" -> il minimo intra-giorno replica il {PUB['worst_intraday']:+.2%} di §33 " f"(|diff| {abs(Tc['m'].min()-PUB['worst_intraday']):.4f}); la chiusura NO " f"({Tc['R'].min():+.2%} contro {PUB['worst_close']:+.2%}) perche' §33 legge la " f"chiusura sulla serie del libro e questa e' la ricostruzione ORARIA — differenza di " f"lente gia' registrata in §33 ('aveva ragione, con la lente sbagliata'). " f"[DICHIARATO, non riparato: A4 usa il MINIMO, che replica.]") return {"R": R, "muri": {"L1": (p1, m1), "L3": (p3, m3)}, "anni_L1_k1": a1, "tuples": Tc} # ========================================================================================== # (2) A1 — IL DRIFT E' STIMATO SU 7,4 ANNI CON DUE TORI # ========================================================================================== def sezione2(S: dict) -> None: sez("(2) A1 — il drift e' stimato su 7,4 anni con due tori, e k* e' LINEARE nel suo errore") r = S["R"]["L3"] # la lente ONESTA: de-luckata E col funding dentro mu = r.mean() * ANN vol = r.std() * ANN ** 0.5 dr = boot_drift(r) lo95, hi95 = np.percentile(dr, [2.5, 97.5]) lo99, hi99 = np.percentile(dr, [0.5, 99.5]) se_b = dr.std() se_naive = vol / np.sqrt(len(r) / ANN) print(f" [MISURATO] drift de-luckato col funding: {mu:+.2%}/anno, vol {vol:.2%}, " f"{len(r)/ANN:.1f} anni di campione") print(f" [MISURATO] bootstrap a blocchi ({BLOCK}g, 4000 estrazioni): " f"SE {se_b:.2%} (SE ingenuo iid {se_naive:.2%}: il clustering ALLARGA di " f"{se_b/se_naive:.2f}x -> il bootstrap a blocchi e' la scelta severa)") print(f" IC95 [{lo95:+.2%}, {hi95:+.2%}] IC99 [{lo99:+.2%}, {hi99:+.2%}]") # k* propagato print(f"\n [CALC] k* propagato sul drift (la forma della distribuzione NON cambia: " f"si sposta solo la media)") print(f" {'drift':>10}{'k*':>9}{'g(1,00)':>11}{'g(1,25)':>11}{'g(1,25)-g(1,00)':>18}") for nome, m in (("punto", mu), ("-1 SE", mu - se_b), ("IC95 basso", lo95), ("IC99 basso", lo99), ("-3 SE", mu - 3 * se_b)): x = shift_drift(r, m) ks, _ = k_star(x) g1, g125 = g_exact(x, 1.00), g_exact(x, 1.25) print(f" {nome:>10} {m:>+8.2%}{ks:>9.2f}{g1:>11.2%}{g125:>11.2%}{g125-g1:>+18.2%}") # drift di pareggio del gradino, su g lo, hi = -0.20, 0.40 for _ in range(70): mid = (lo + hi) / 2 if g_exact(shift_drift(r, mid), 1.25) - g_exact(shift_drift(r, mid), 1.00) > 0: hi = mid else: lo = mid mu_be = hi q_be = float((dr < mu_be).mean()) z_be = (mu_be - mu) / se_b print(f"\n [CALC] DRIFT DI PAREGGIO del gradino 1,25x: {mu_be:+.4%}/anno") print(f" = {z_be:+.2f} SE dal punto, percentile bootstrap {q_be:.4%}") print(f" = il libro puo' perdere il {(1-mu_be/mu):.1%} del suo drift e il gradino " f"resta NON dannoso") print(f" [CALC] (aritmetica di controllo: per g ~ mu*k - sigma^2 k^2/2 il pareggio sta a " f"k*=1,125, cioe' mu = 1,125*sigma^2 = {1.125*vol**2:+.4%} — " f"|diff| {abs(mu_be-1.125*vol**2):.4%})") # controllo empirico: il peggior drift che il libro abbia MAI realizzato su 3 anni ser = pd.Series(r) roll3 = ser.rolling(3 * 365).mean().dropna() * ANN roll2 = ser.rolling(2 * 365).mean().dropna() * ANN print(f"\n [MISURATO] controllo EMPIRICO, che non dipende da un modello di incertezza: " f"il peggior drift realizzato") print(f" su finestra mobile di 3 anni = {roll3.min():+.2%} " f"(mediana {roll3.median():+.2%}); su 2 anni = {roll2.min():+.2%}") print(f" -> anche il PEGGIOR triennio mai vissuto sta {roll3.min()/mu_be:.1f}x " f"sopra il drift di pareggio del gradino.") # la metrica che DECIDE: anni al capitale-rendita. A drift bassi NON e' definita per # nessuno dei due k (nessun path arriva): li' si legge il CAPITALE a 20 anni, appaiato. print(f"\n [MISURATO] e sulla metrica che decide davvero (EUR{DEP_EUR:.0f}/m da " f"${START:.0f}, muro RICALCOLATO a ogni drift e a ogni k):") print(f" {'drift':>12}{'muro k=1,00':>13}{'anni k=1,00':>21}{'muro k=1,25':>13}" f"{'anni k=1,25':>21}{'cap 20a k=1,00':>16}{'cap 20a k=1,25':>16}") guadagni = {} for nome, m in (("punto", mu), ("-1 SE", mu - se_b), ("IC95 basso", lo95), ("meta' drift", mu / 2)): x = shift_drift(r, m) _, m1 = muro_di(x) a1 = anni_al_muro(x, m1) _, m2 = muro_di(1.25 * x) a2 = anni_al_muro(1.25 * x, m2) c1, c2 = cap_20a(x), cap_20a(1.25 * x) g = (a1["med_cond"] - a2["med_cond"] if np.isfinite(a1["med_cond"]) and np.isfinite(a2["med_cond"]) else float("nan")) guadagni[nome] = (g, c2 / c1) print(f" {nome:>12}{m1:>13,.0f}{fmt_anni(a1):>21}{m2:>13,.0f}{fmt_anni(a2):>21}" f"{c1:>16,.0f}{c2:>16,.0f}") r_ic, r_meta = guadagni["IC95 basso"][1], guadagni["meta' drift"][1] print(f" ⚠️ a drift bassi gli ANNI non sono definiti PER ENTRAMBI i k (nessun path " f"raggiunge un muro che nel frattempo e' esploso): li' discrimina solo il capitale, " f"e il rapporto cap(1,25)/cap(1,00) resta {r_ic:.2f}x all'estremo basso dell'IC95 " f"e {r_meta:.2f}x a meta' drift.") print(f" {clock()}") if mu_be >= lo95: v, riga = "FALSIFICA", f"il pareggio {mu_be:+.2%} cade DENTRO l'IC95" elif mu_be >= lo99: v, riga = "INCRINA", (f"il pareggio {mu_be:+.2%} cade fra IC95 ({lo95:+.2%}) e " f"IC99 ({lo99:+.2%})") else: v, riga = "REGGE", (f"il gradino diventa dannoso solo sotto {mu_be:+.2%}/anno di drift " f"= {z_be:+.2f} SE, percentile bootstrap {q_be:.2%}, fuori da IC95 " f"[{lo95:+.2%}] e da IC99 [{lo99:+.2%}]; e il PEGGIOR triennio mai " f"realizzato ({roll3.min():+.2%}) ne sta {roll3.min()/mu_be:.1f}x " f"sopra. All'estremo basso dell'IC95 il capitale a 20 anni resta " f"{guadagni['IC95 basso'][1]:.2f}x") esito("A1 DRIFT", v, riga) # ========================================================================================== # (3) A2 — IL CAMPIONE NON CONTIENE IL SINISTRO CHE CONTA # ========================================================================================== def sezione3(S: dict) -> None: sez("(3) A2 — il campione non contiene il sinistro che conta: finestre stressate e disgiunte") d = pd.read_parquet(CACHE) idx = d.index r_full = deluck(d["fund"].values.astype(float)) ser = pd.Series(r_full, index=idx) fin = [("PIENA 2019-03 -> oggi", ser)] tolti = ser[(idx < "2020-01-01") | (idx > "2021-12-31")] fin.append(("SENZA 2020-2021 (il toro che finanzia il drift)", tolti)) bordi = ["2019-03-14", "2021-03-14", "2023-03-14", "2025-03-14", "2026-08-23"] for a, b in zip(bordi[:-1], bordi[1:]): w = ser[(idx >= a) & (idx < b)] fin.append((f"disgiunta {a[:7]} -> {b[:7]}", w)) # la finestra PEGGIORE non e' fra quelle disgiunte: si cerca il minimo su griglia MOBILE # (attacco piu' forte: le disgiunte sono una partizione arbitraria, non il caso peggiore) W = 2 * 365 mroll = ser.rolling(W).mean() j = int(np.nanargmin(mroll.values)) peggiore2a = ser.iloc[j - W + 1: j + 1] fin.append((f"MOBILE peggior biennio ({peggiore2a.index[0].date()})", peggiore2a)) print(f" ⚠️ ogni finestra riporta ANCHE il drift del libro su di essa: dove il libro e' in " f"perdita la finestra giudica il LIBRO, non il gradino (nota dovuta di §33).") print(f"\n {'finestra':<46}{'n':>6}{'drift':>10}{'vol':>9}{'Sharpe':>9}{'k*':>8}" f"{'g(1,00)':>10}{'g(1,25)':>10}{'Delta g':>10}{'':>4}") peggiori = [] for nome, w in fin: x = w.values if len(x) < 120: continue m, v = x.mean() * ANN, x.std() * ANN ** 0.5 ks, _ = k_star(x) g1, g2 = g_exact(x, 1.00), g_exact(x, 1.25) flag = "" if m > 0 else " <- LIBRO NEGATIVO" print(f" {nome:<46}{len(x):>6}{m:>+10.2%}{v:>9.2%}{m/v:>9.2f}{ks:>8.2f}" f"{g1:>+10.2%}{g2:>+10.2%}{g2-g1:>+10.2%}{flag}") peggiori.append((nome, m, g2 - g1, x)) giudicabili = [(n, m, dg) for n, m, dg, _ in peggiori if m > 0 and "PIENA" not in n] rotte = [n for n, m, dg in giudicabili if dg <= 0] non_giud = [n for n, m, dg, _ in peggiori if m <= 0] # Delta sulle due finestre piu' avverse fra quelle giudicabili. ⚠️ Su una finestra di 2 # anni il muro esplode (la perpetua vive sul rapporto drift/rischio di quella finestra) e # gli ANNI non sono definiti per NESSUN k: si riporta anche il capitale a 20 anni, appaiato. print(f"\n [MISURATO] le due finestre GIUDICABILI piu' avverse, muro ricalcolato DENTRO " f"la finestra (⚠️ su 2 anni il muro non e' un numero di pianificazione: e' una lettura " f"di quel regime):") cand = sorted([(n, m, dg, x) for n, m, dg, x in peggiori if m > 0 and "PIENA" not in n], key=lambda t: t[1])[:2] for nome, m, dg, x in cand: _, m1 = muro_di(x) a1 = anni_al_muro(x, m1) _, m2 = muro_di(1.25 * x) a2 = anni_al_muro(1.25 * x, m2) c1, c2 = cap_20a(x), cap_20a(1.25 * x) print(f" {nome:<46} muro ${m1:>9,.0f} -> ${m2:>9,.0f} {fmt_anni(a1)} -> " f"{fmt_anni(a2)} cap20a ${c1:>9,.0f} -> ${c2:>9,.0f} ({c2/c1:.2f}x)") # il costo del gradino DOVE fa male: il biennio mobile peggiore nb, mb, dgb, xb = min(peggiori, key=lambda t: t[1]) g1b, g2b = g_exact(xb, 1.00), g_exact(xb, 1.25) cum = (1 + g2b) ** 2 / (1 + g1b) ** 2 - 1 print(f"\n 🚨 [MISURATO] LA FORMA PIU' FORTE DELL'ATTACCO TROVA DAVVERO UNA FINESTRA IN CUI " f"IL GRADINO FA MALE — ed e' quella in cui il LIBRO perde:") print(f" {nb}: drift {mb:+.2%}, g(1,00) {g1b:+.2%}, g(1,25) {g2b:+.2%}, " f"Delta {dgb:+.2%}/anno -> costo CUMULATO del gradino su quei 2 anni: {cum:+.2%} " f"di capitale.") print(f" Da mettere accanto al guadagno: sul campione pieno il gradino vale " f"{g_exact(ser.values,1.25)-g_exact(ser.values,1.00):+.2%}/anno. " f"Rapporto {abs((g_exact(ser.values,1.25)-g_exact(ser.values,1.00))/dgb):.0f}:1.") print(f" Per il CRITERIO dichiarato in (0) quella finestra non falsifica (il libro " f"stesso e' negativo: la leva amplifica un segno che li' e' gia' sbagliato, e lo " f"stesso vale per il gradino 1,00x -> 0,75x nel verso opposto). " f"**Ma la qualifica va detta col numero, non con un aggettivo.**") print(f"\n ⚠️ [LIMITE DICHIARATO] a parte quel biennio, NESSUNA finestra ha drift negativo: " f"il libro non ha mai vissuto un TRIENNIO in perdita, e il peggior biennio perde lo " f"{abs(mb):.2%}/anno. L'attacco nella sua forma piu' forte — «un regime in cui il " f"libro perde davvero» — **non e' esprimibile su questo campione**, e questo E' il " f"fatto che il mandato temeva. La sostituzione onesta e' A1: si IMPONE il drift " f"invece di cercarlo. Le due misure si incastrano — peggior biennio {mb:+.2%}, " f"pareggio del gradino +1,42%.") print(f" {clock()}") if rotte: v, riga = "FALSIFICA", f"Delta g(1,25) <= 0 su finestre giudicabili: {', '.join(rotte)}" else: peggior = min(giudicabili, key=lambda t: t[2]) if giudicabili else ("n/d", 0, 0) v, riga = "REGGE", (f"Delta g(1,25) > 0 in {len(giudicabili)}/{len(giudicabili)} " f"finestre giudicabili (la peggiore: {peggior[0]}, {peggior[2]:+.2%}), " f"togliendo il toro 2020-21 compreso. QUALIFICA MISURATA: nel biennio " f"mobile peggiore ({nb}, drift {mb:+.2%}) il gradino COSTA " f"{dgb:+.2%}/anno = {cum:+.2%} cumulato — quella finestra non " f"falsifica per il criterio (li' e' il libro a perdere), ma il " f"numero e' questo, ed e' il costo del gradino nel suo caso peggiore " f"realizzato") esito("A2 SINISTRO", v, riga) # ========================================================================================== # (4) A3 — LA CODA PEGGIORE, PORTATA FINO IN FONDO # ========================================================================================== def hill_xi(loss: np.ndarray, k: int) -> float: """Stimatore di Hill sulle k eccedenze piu' grandi (solo perdite POSITIVE).""" x = np.sort(loss[loss > 0])[::-1] k = min(k, len(x) - 1) return float(np.mean(np.log(x[:k])) - np.log(x[k])) def sezione4(S: dict) -> None: sez("(4) A3 — la coda: due stimatori che non concordano -> si prende il PEGGIORE") r = S["R"]["L3"] loss = -r q = 0.95 u = float(np.quantile(loss, q)) ex = loss[loss > u] - u xi_g, _, beta = stats.genpareto.fit(ex, floc=0.0) k_hill = int(0.05 * len(loss)) xi_h = hill_xi(loss, k_hill) print(f" [MISURATO] soglia 95° pctl = {u:.2%}, {len(ex)} eccedenze") print(f" GPD xi = {xi_g:+.3f} (pubblicato {PUB['xi_gpd']:+.2f}) " f"Hill xi = {xi_h:+.3f} su k={k_hill} (pubblicato {PUB['xi_hill']:+.2f})") print(f" [MISURATO] Hill e' notoriamente sensibile a k -> lo sweep, invece del punto:") print(" " + " ".join(f"k={kk}: xi={hill_xi(loss, kk):+.3f}" for kk in (40, 70, 100, 135, 200, 300))) xi = max(xi_g, xi_h, PUB["xi_hill"]) print(f" [SCELTA DICHIARATA] si porta avanti il PEGGIORE dei tre: xi = {xi:+.3f} " f"(piu' severo del {PUB['xi_hill']:+.2f} pubblicato: l'attacco si porta col numero " f"peggiore, non con quello citato)") L = PUB["L_strutt"] # [CITATO] bound aritmetico di §33 a k=1 print(f"\n (a) IL BOUND STRUTTURALE, scalato [CITATO §33: |L| = {L:.2%} a k=1,00]") print(f" {'k':>6}{'peggior giorno':>17}{'costo disaster-SL':>20}{'soglia 20%':>13}" f"{'soglia 50%':>13}") sl_pct = json.loads((ROOT / "config" / "live.json").read_text())["disaster_sl_pct"] frac = json.loads((ROOT / "config" / "live.json").read_text())["max_notional_per_asset_frac"] for k in KS: wd, cs = k * L, k * 2 * frac * sl_pct print(f" {k:>6.2f}{wd:>16.2%} {cs:>19.2%}" f"{'SFONDA' if wd > 0.20 else 'ok':>13}{'SFONDA' if wd > 0.50 else 'ok':>13}") k_20 = 0.20 / L k_50 = 0.50 / L k_sl50 = 0.50 / (2 * frac * sl_pct) print(f" [CALC] il peggior giorno strutturale sfonda il 20% (recupero +25%) a k = {k_20:.2f}") print(f" [CALC] ...e il 50% (C1 di §33) a k = {k_50:.2f}") print(f" [CALC] il costo del disaster-SL sfonda il 50% a k = {k_sl50:.2f} " f"(pubblicato {PUB['k_da_SL']:.2f}) -> ⚠️ il disaster-SL sfonda il 20% GIA' A k=1,00 " f"({2*frac*sl_pct:.0%}): quella soglia non separa i k, e va detto.") # (b) coda peggiore dentro il campione: si SOSTITUISCONO le eccedenze storiche con # estrazioni GPD(xi) troncate al bound, MANTENENDO LE POSIZIONI (quindi il clustering). print(f"\n (b) P(DIMEZZAMENTO in 10 anni, senza prelievi) con la coda peggiore dentro") rng = np.random.default_rng(SEED_BOOT) pos = np.where(loss > u)[0] fmax = stats.genpareto.cdf(max(L - u, 1e-9), xi, loc=0.0, scale=beta) y = stats.genpareto.ppf(rng.random(len(pos)) * fmax, xi, loc=0.0, scale=beta) r_tail = r.copy() r_tail[pos] = -(u + y) r_tail = r_tail - r_tail.mean() + r.mean() # stesso drift: cambia SOLO la coda print(f" [CALC] serie a coda peggiore: drift {r_tail.mean()*ANN:+.2%} (invariato), " f"vol {r_tail.std()*ANN**.5:.2%} (era {r.std()*ANN**.5:.2%}), " f"peggior giorno {r_tail.min():+.2%} (era {r.min():+.2%})") ks_t, _ = k_star(r_tail) ks_0, _ = k_star(r) d_coda = ks_0 - ks_t d_drift = ks_0 - k_star(shift_drift(r, r.mean() * ANN / 2))[0] print(f" [MISURATO] k*: {ks_0:.2f} (coda storica) -> {ks_t:.2f} (coda peggiore) " f"= la coda muove k* di {d_coda:+.2f}; dimezzare il DRIFT lo muove di {d_drift:+.2f}") print(f" ⚠️ CORREZIONE a §33, che scriveva «la forma della coda muove k* di 1-2 " f"unita', il drift di 6,7»: con lo stimatore PEGGIORE la coda ne muove {d_coda:.1f}, " f"non 1-2. La CONCLUSIONE di §33 regge (il drift resta il fattore dominante, " f"{d_drift/d_coda:.1f}x) ma il numero pubblicato e' ottimista di ~{d_coda/1.5:.0f}x, " f"e a quella coda k*={ks_t:.2f} e' ancora {ks_t/1.25:.0f}x il gradino richiesto.") rng = np.random.default_rng(SEED) P = CC._boot_paths(r_tail, 4000, 10 * 365, BLOCK, rng) print(f" {'k':>6}{'P(dimezzamento)':>18}{'P(-80%)':>11}{'maxDD mediano':>16}" f"{'g(k) 10a mediano':>19}") p_half = {} for k in KS: eq = np.cumprod(1.0 + k * P, axis=1) run = np.maximum.accumulate(eq, axis=1) dd = (eq / run - 1.0).min(axis=1) half = float((eq.min(axis=1) <= 0.5).mean()) p80 = float((eq.min(axis=1) <= 0.2).mean()) p_half[k] = half print(f" {k:>6.2f}{half:>18.3%}{p80:>11.3%}{np.median(dd):>16.2%}" f"{np.median(eq[:, -1]) ** (1/10) - 1:>18.2%}") print(f" {clock()}") rap = p_half[1.25] / p_half[1.00] if p_half[1.00] > 0 else float("inf") if 1.25 * L > 0.50 or (p_half[1.25] > 0.05 and rap > 3.0): v, riga = "FALSIFICA", (f"a k=1,25 il peggior giorno strutturale vale {1.25*L:.2%} " f"e P(dimezzamento) {p_half[1.25]:.2%}") else: v, riga = "REGGE", (f"con xi={xi:.2f} preso alla lettera: peggior giorno strutturale " f"{1.25*L:.2%} < 50%, P(dimezzamento 10a) {p_half[1.25]:.3%} contro " f"{p_half[1.00]:.3%} a k=1,00. Il bound strutturale sfonda il 50% " f"a k={k_50:.2f} e il 20% a k={k_20:.2f}; la coda muove k* di " f"{ks_0-ks_t:+.2f}, il drift di {ks_0 - k_star(shift_drift(r, r.mean()*ANN/2))[0]:+.2f}") esito("A3 CODA", v, riga) # ========================================================================================== # (5) A4 — LA LENTE ACCOPPIATA, E IL DISASTER-SL CHE E' UNA REGOLA SU UN LIVELLO # ========================================================================================== def sezione5(S: dict) -> None: sez("(5) A4 — la lente: close-only e' CIECA su una regola a un livello. " "Il disaster-SL E' una regola su un livello.") T = S["tuples"] R = T["R"].values m = T["m"].values print(" (a) IL RICARICO DEL maxDD: e' moltiplicativo o si amplifica con k?") print(f" {'k':>6}{'maxDD close-only':>19}{'maxDD accoppiato':>19}{'ricarico':>11}" f"{'peggior giorno':>17}{'peggior minimo':>17}") ric = {} for k in KS: eq = np.cumprod(1.0 + k * R) prev = np.concatenate(([1.0], eq[:-1])) run_c = np.maximum.accumulate(eq) dd_c = float((eq / run_c - 1.0).min()) low = prev * (1.0 + k * m) # minimo intra-giorno del PATH composto run_a = np.maximum.accumulate(np.maximum(eq, prev)) dd_a = float((low / run_a - 1.0).min()) ric[k] = dd_a / dd_c print(f" {k:>6.2f}{dd_c:>19.2%}{dd_a:>19.2%}{dd_a/dd_c:>11.4f}" f"{k*R.min():>17.2%}{k*m.min():>17.2%}") spread = max(ric.values()) - min(ric.values()) print(f" [MISURATO] il ricarico si muove di {spread:.4f} fra k=1,00 e k=2,00 " f"(= {spread*100:.2f}pp): moltiplicativo, NON amplificante") print(f"\n (b) LA FREQUENZA DEL DISASTER-SL E' INVARIANTE A k — dimostrazione dal codice, " f"non dai dati:") print(f" `execution.ensure_disaster_sl` calcola") print(f" want_stop = disaster_stop_price(instrument, MARK, sl_pct, long) -> un PREZZO") print(f" want_amount = notional_to_amount(instrument, |pos|, price=mark) -> una TAGLIA") print(f" e ri-piazza se |amt - want_amount| >= want_amount*0,1 OPPURE " f"|stp - want_stop|/want_stop >= 0,05.") print(f" Una scala k moltiplica `pos` (quindi `want_amount`) e NON tocca `mark`.") print(f" -> il livello di innesco e' invariante a k, e il test di ri-piazzamento e' " f"RELATIVO (`want_amount*0,1`), quindi anch'esso invariante a k.") print(f" ⇒ [DEDOTTO dal sorgente] a k piu' alto il disaster-SL NON viene toccato piu' " f"spesso: viene toccato le STESSE volte e costa k volte tanto. §40 misura la frequenza " f"(0-1 episodi su tutta la storia alla cadenza oraria che gira); questo file mostra che " f"quel numero non va rifatto per k.") print(f"\n (c) CONTROLLO NUMERICO della (b): l'innesco e' un movimento del MARK, non " f"dell'equity. Conteggio dei giorni in cui il minimo INTRA-giorno del libro a leva k " f"scende sotto soglie di EQUITY (queste si', k-dipendenti):") print(f" {'k':>6}{'gg min < -5%':>15}{'gg min < -10%':>15}{'gg min < -20%':>15}" f"{'gg min < -30%':>15}") for k in KS: print(f" {k:>6.2f}{int((k*m < -0.05).sum()):>15}{int((k*m < -0.10).sum()):>15}" f"{int((k*m < -0.20).sum()):>15}{int((k*m < -0.30).sum()):>15}") print(f" -> nessuna soglia di EQUITY e' armata sul libro proprietario (il disaster-SL e' " f"sul PREZZO): questa tabella e' la misura di cosa succederebbe se ce ne fosse una, " f"ed e' la ragione per cui sul canale FUNDED — dove la regola a un giorno esiste — " f"la lente accoppiata resta obbligatoria (25/07).") amp = ric[1.50] - ric[1.00] if abs(amp) > 0.01: v, riga = "FALSIFICA", f"il ricarico del maxDD si muove di {amp*100:+.2f}pp fra k=1,00 e 1,50" else: v, riga = "REGGE", (f"ricarico maxDD {ric[1.00]:.4f} (k=1,00) -> {ric[1.50]:.4f} (k=1,50) " f"-> {ric[2.00]:.4f} (k=2,00): movimento {amp*100:+.2f}pp nel range " f"della proposta e {(ric[2.00]-ric[1.00])*100:+.2f}pp fino a k=2, " f"e va nel verso FAVOREVOLE (il ricarico CALA). " f"La frequenza del disaster-SL e' invariante a k PER COSTRUZIONE " f"(innesco sul MARK, test di ri-piazzamento RELATIVO): il numero di " f"§40 non va rifatto per k, solo moltiplicato") esito("A4 LENTE", v, riga) # ========================================================================================== # (6) A5 — L'INVARIANTE A TRE CHIAVI: c'e' una QUARTA? # ========================================================================================== def sezione6() -> None: sez("(6) A5 — l'invariante `n_asset * frac * scala * disaster_sl_pct <= 0,50`: e' completo?") cfg = json.loads((ROOT / "config" / "live.json").read_text()) frac = float(cfg["max_notional_per_asset_frac"]) fixed = float(cfg["max_notional_per_asset_usd"]) sl = float(cfg["disaster_sl_pct"]) n_asset = 2 wm = _read_watermark() lev_tp = float(CANONICAL["leverage"]) print(f" [LETTO dal codice e da config, sola lettura]") print(f" config/live.json : frac={frac} fixed=${fixed:,.0f} disaster_sl_pct={sl}") print(f" src/live/shadow : ASSETS n={n_asset} WEIGHT={WEIGHT} " f"FALLBACK_CAPITAL=${FALLBACK_CAPITAL:,.0f}") print(f" src/live/book : W_TP01={W_TP01} W_SKH={W_SKH} " f"CAP_UNKNOWN_USD=${CAP_UNKNOWN_USD:,.0f} watermark=${wm:,.2f}" if wm else " src/live/book : watermark ASSENTE") print(f" trend_portfolio : CANONICAL['leverage']={lev_tp} " f"target_vol={CANONICAL['target_vol']}") print(f"\n [CALC] la LEVA LORDA VERA, derivata dal sorgente invece che dichiarata.") print(f" raw = WEIGHT * E * (W_TP01*tp_frac + W_SKH*sign), tp_frac <= " f"CANONICAL['leverage'] = {lev_tp}") print(f" net = scala * clamp(raw, +-cap), cap = frac*E (percorso fidato)") print(f" lordo = n_asset * min(WEIGHT*(W_TP01*{lev_tp}+W_SKH), frac) * scala * E") raw_max = WEIGHT * (W_TP01 * lev_tp + W_SKH * 1.0) print(f" = {n_asset} * min({raw_max:.4f}, {frac}) * scala * E " f"= {n_asset*min(raw_max, frac):.4f} * scala * E") print(f" -> l'invariante usa `n_asset*frac*scala` = {n_asset*frac:.4f}*scala; il VERO e' " f"{n_asset*min(raw_max,frac):.4f}*scala. Coincidono qui perche' frac({frac}) < " f"WEIGHT*(...)={raw_max:.4f}. **L'invariante e' un LIMITE SUPERIORE, quindi safe.**") # verifica esaustiva su una griglia di chiavi raggiungibili print(f"\n [MISURATO] verifica esaustiva: esiste una combinazione che SODDISFA l'invariante " f"e produce una leva lorda VERA superiore al suo bound?") viol = 0 tot = 0 for f_ in (0.25, 0.50, 0.625, 0.75, 1.0, 1.5): for w_ in (0.25, 0.5, 0.75, 1.0): for sc in (1.0, 1.25, 1.5, 2.0): for lv in (1.0, 2.0, 3.0): for na in (2, 3): tot += 1 rmax = w_ * (W_TP01 * lv + W_SKH * 1.0) vero = na * min(rmax, f_) * sc bound = na * f_ * sc if vero > bound + 1e-12: viol += 1 print(f" {tot} combinazioni (frac x WEIGHT x scala x leverage-TP01 x n_asset): " f"{viol} violazioni del bound. -> sul PERCORSO FIDATO l'invariante e' COMPLETO.") print(f"\n 🚨 [MISURATO] MA IL PERCORSO FIDATO NON E' L'UNICO. Nel ramo di FALLBACK " f"(`eq_fallback`) `book_report` sizza su `paper_cap`, non sull'equity vera:") print(f" lordo_fallback = n_asset * min(WEIGHT*paper_cap*(...), min(fixed, wm*frac)) * scala") print(f" e la leva si misura contro l'equity VERA, che li' non e' nota: al suo posto c'e' " f"il WATERMARK, che e' l'ULTIMA equity osservata, non quella attuale.") print(f" {'scenario':<44}{'wm':>9}{'E vera':>9}{'lordo k=1,00':>14}{'/E':>8}" f"{'lordo k=1,25':>14}{'/E':>8}") righe = [("normale (equity leggibile)", 635.0, 635.0, False), ("fallback, watermark fresco", 635.0, 635.0, True), ("fallback dopo PRELIEVO del 50% non visto", 635.0, 317.5, True), ("fallback dopo PRELIEVO dell'80% non visto", 635.0, 127.0, True), ("fallback dopo perdita del 50% non vista", 635.0, 317.5, True)] peggio1 = peggio125 = 0.0 for nome, w_, e_vera, fb in righe: if not fb: cap, eq_used = e_vera * frac, e_vera else: cap, eq_used = min(fixed, w_ * frac), FALLBACK_CAPITAL raw = WEIGHT * eq_used * (W_TP01 * lev_tp + W_SKH) l1 = n_asset * min(raw, cap) peggio1, peggio125 = max(peggio1, l1 / e_vera), max(peggio125, 1.25 * l1 / e_vera) print(f" {nome:<44}{w_:>9,.0f}{e_vera:>9,.0f}{l1:>14,.1f}{l1/e_vera:>7.2f}x" f"{1.25*l1:>14,.1f}{1.25*l1/e_vera:>7.2f}x") print(f"\n 🚨 IL BUCO ESISTE GIA' A k=1,00 ({peggio1:.2f}x) e NON e' creato dalla scala: " f"e' creato dal fatto che nel fallback il denominatore del rapporto e' il WATERMARK " f"(l'ultima equity OSSERVATA) e non l'equity attuale. La scala non lo apre: lo " f"MOLTIPLICA ({peggio1:.2f}x -> {peggio125:.2f}x) mentre l'invariante continua a " f"leggere {n_asset*frac:.2f}x e {n_asset*frac*1.25:.2f}x.") print(f" ⚠️ Percio' la guardia G3 della SPEC (`scala = 1,00 nel fallback`) **non chiude " f"il buco: ne toglie il moltiplicatore**. Resta NON OPZIONALE — senza di lei il " f"gradino peggiora del 25% uno scenario gia' fuori tetto — ma la SPEC la presenta " f"come «il fallback non e' piu' permissivo per costruzione», e questo e' vero solo " f"RISPETTO ALLA SCALA, non rispetto alla leva.") print(f" [DICHIARATO] frequenza misurata del ramo: 0 giri su 1.442 (§38); " f"`book_execute` su `eq_fallback` **allerta e NON blocca** (letto dal sorgente), " f"quindi lo scenario e' raro ma non impossibile. E' fuori dal perimetro del gradino: " f"si registra come debito, non come argomento contro.") peggio = peggio125 print(f"\n ⚠️ [DEDOTTO da §40] e c'e' una seconda incompletezza, che non e' una chiave ma un " f"VALORE: l'invariante moltiplica per `disaster_sl_pct`=0,30 come se fosse la perdita " f"massima per posizione. §40 ha misurato che NON lo e': lo stop e' ROTOLANTE " f"(ri-ancorato al mark a ogni giro), quindi una discesa lenta lo trascina giu' e " f"passano perdite fino a -60,6%/-61,5% dall'ingresso senza che scatti. " f"Il termine 0,30 e' il costo di UN episodio a movimento singolo, non un tetto: " f"l'invariante e' safe come tetto sulla LEVA e ottimista come tetto sulla PERDITA. " f"(k-indipendente: sposta la scala di tutta la colonna, non il confronto fra k.)") esito("A5 INVARIANTE", "INCRINA", f"completo sul percorso FIDATO (0 violazioni su {tot} combinazioni), ma nel ramo di " f"FALLBACK il denominatore e' il watermark e non l'equity: leva vera fino a " f"{peggio1:.2f}x gia' a k=1,00 e {peggio125:.2f}x a k=1,25 contro " f"{n_asset*frac*1.25:.2f}x letti. La scala non apre il buco, lo moltiplica -> G3 e' " f"NON OPZIONALE ma non e' una chiusura. E il fattore 0,30 non e' un tetto di perdita " f"(§40: stop rotolante, passano -60% dall'ingresso)") # ========================================================================================== # (7) A6 — IL FUNDING SCALA CON LA LEVA # ========================================================================================== def sezione7(S: dict) -> None: sez("(7) A6 — il funding non era nel conto della leva, E IL FUNDING SCALA CON LA LEVA") print(f" [DEDOTTO] il funding e' `tasso x NOZIONALE`: a leva k il nozionale e' k volte, " f"quindi il costo e' k volte. Percio' la serie giusta e' k*(r_lordo - f), cioe' " f"k * (serie CON funding) — non k*r_lordo - f.") cf = (S["R"]["L1"].mean() - S["R"]["L3"].mean()) * ANN print(f" [MISURATO] verifica sul dato: costo del funding a k=1,00 = {cf:+.2%}/anno; " f"a k=1,25 = {cf*1.25:+.2%}/anno (esattamente 1,25x: lineare per costruzione).") print(f" ⚠️ {cf:.2%} e non il {-PUB['drift_fund']+PUB['drift_nofund']:.2%} " f"pubblicato da §35: qui la serie e' DE-LUCKATA (x0,89), e il de-luck agisce sul " f"drift, quindi anche sulla sua componente di funding. Stesso numero, lente diversa.") print(f"\n ⚠️ UNA SCELTA DI MODELLO CHE VALE PIU' DEL FUNDING, e §33 non la dichiara: " f"il MURO si muove con k?") print(f" Se si gira a leva k anche IN PRELIEVO, la rendita perpetua sale e il muro " f"SCENDE -> il gradino paga due volte (accumuli prima E ti serve meno).") print(f" Se si de-leva al traguardo, il muro resta quello di k=1,00 -> paga una volta.") print(f" Si riportano ENTRAMBE. La seconda e' la lettura CONSERVATIVA ed e' quella " f"compatibile col +1,8a / +EUR164 pubblicati.") print(f"\n {'lente':<20}{'k':>5}{'drift':>8}{'g(k)':>8}{'perp.':>7}{'muro(k)':>10}" f"{'anni muro(k)':>20}{'anni muro(1,00)':>20}{'guad.(k)':>10}{'guad.(fisso)':>13}") out = {} for nome, key in (("L1 fisco (pubbl.)", "L1"), ("L3 fisco+FUNDING", "L3")): base = base_f = None muro1 = None for k in (1.00, 1.25, 1.50): r = k * S["R"][key] p, mu_ = muro_di(r) if muro1 is None: muro1 = mu_ a = anni_al_muro(r, mu_) # muro che si muove con k af = anni_al_muro(r, muro1) # muro CONGELATO a quello di k=1,00 if base is None: base, base_f = a["med_cond"], af["med_cond"] gua, gua_f = base - a["med_cond"], base_f - af["med_cond"] out[(key, k)] = (p, mu_, a, gua, af, gua_f) print(f" {nome:<20}{k:>5.2f}{r.mean()*ANN:>8.2%}{g_exact(r,k=1.0):>8.2%}" f"{p:>7.2%}{mu_:>10,.0f}{fmt_anni(a):>20}{fmt_anni(af):>20}" f"{gua:>+9.2f}a{gua_f:>+12.2f}a") print(f" {clock()}") g_L1, g_L3 = out[("L1", 1.25)][3], out[("L3", 1.25)][3] gf_L1, gf_L3 = out[("L1", 1.25)][5], out[("L3", 1.25)][5] print(f"\n 📌 [MISURATO] il gradino 1,25x vale {g_L1:+.2f}a (L1, muro mobile) / " f"{gf_L1:+.2f}a (L1, muro congelato) e {g_L3:+.2f}a / {gf_L3:+.2f}a sulla lente L3 " f"col funding dentro.") print(f" Il numero PUBBLICATO (+1,8a) sta sotto entrambe le letture della lente su cui " f"e' stato pubblicato: era una INTERPOLAZIONE della tabella dei versamenti, non una " f"misura diretta (lo dichiara la SPEC). La misura diretta dice di piu', non di meno.") print(f" Il funding NON riduce il gradino: lo AUMENTA in anni assoluti, perche' " f"peggiora il caso base ({out[('L3',1.00)][2]['med_cond']:.2f}a contro " f"{out[('L1',1.00)][2]['med_cond']:.2f}a) e la leva ne recupera una parte.") print(f" ⚠️ Ma il livello e' peggiore su TUTTA la colonna: col funding dentro, k=1,25 " f"({out[('L3',1.25)][2]['med_cond']:.2f}a) e' ancora PEGGIO di k=1,00 senza funding " f"({out[('L1',1.00)][2]['med_cond']:.2f}a). " f"**Il gradino non ripaga il funding: lo attenua.**") # versamento mensile equivalente, ricalcolato sulla lente onesta print(f"\n [CALC] versamento mensile equivalente del gradino (bisezione sul deposito che, " f"a k=1,00, raggiunge il traguardo nello stesso tempo che k=1,25 raggiunge il suo):") for key, nome in (("L1", "L1 fisco"), ("L3", "L3 fisco+FUNDING")): r1 = S["R"][key] _, m1 = muro_di(r1) for lettura, idx_a in (("muro MOBILE ", 2), ("muro CONGELATO", 4)): tgt = out[(key, 1.25)][idx_a]["med_cond"] lo, hi = DEP_EUR, 4_000.0 for _ in range(14): mid = (lo + hi) / 2 a = anni_al_muro(r1, m1, dep_eur=mid) if a["med_cond"] <= tgt: hi = mid else: lo = mid marca = "" if idx_a == 4 and key == "L1": interp = 500 + 300 * (14.7 - 12.9) / (14.7 - 11.4) marca = (f" <- contro +EUR{interp-500:.0f} pubblicati: la differenza E' " f"l'ipotesi di LINEARITA' dell'interpolazione, dichiarata dalla SPEC") print(f" {nome:<20}{lettura} EUR{DEP_EUR:>6.0f}/m a k=1,25 == " f"EUR{hi:>7.0f}/m a k=1,00 -> +EUR{hi-DEP_EUR:.0f}/mese{marca}") print(f" {clock()}") if min(g_L3, gf_L3) < 1.0: v, riga = "FALSIFICA", (f"col funding dentro il gradino vale {g_L3:+.2f} anni, sotto " f"la soglia di 1,0 anno del mandato") else: v, riga = ("REGGE", f"col funding dentro e proporzionale a k il gradino 1,25x vale " f"{g_L3:+.2f}a (muro mobile) / {gf_L3:+.2f}a (muro congelato) " f"contro {g_L1:+.2f}a / {gf_L1:+.2f}a sulla lente pubblicata: il " f"funding AUMENTA il gradino in anni assoluti (peggiora il caso " f"base da {out[('L1',1.00)][2]['med_cond']:.2f}a a " f"{out[('L3',1.00)][2]['med_cond']:.2f}a) — ma il LIVELLO peggiora " f"su tutta la colonna: k=1,25 col funding ({out[('L3',1.25)][2]['med_cond']:.2f}a) " f"resta peggio di k=1,00 senza ({out[('L1',1.00)][2]['med_cond']:.2f}a). " f"Il gradino non ripaga il funding, lo attenua") esito("A6 FUNDING", v, riga) return out # ========================================================================================== # (8) VERDETTO # ========================================================================================== def sezione8(S: dict) -> None: sez("(8) VERDETTO — e il k MASSIMO DIFENDIBILE come singolo numero") for kk, (v, riga) in ESITI.items(): print(f" {kk:<16} {v:<11} {riga}") cfg = json.loads((ROOT / "config" / "live.json").read_text()) frac, sl = float(cfg["max_notional_per_asset_frac"]), float(cfg["disaster_sl_pct"]) L = PUB["L_strutt"] r = S["R"]["L3"] dr = boot_drift(r) lo95 = float(np.percentile(dr, 2.5)) ks_ic, _ = k_star(shift_drift(r, lo95)) vincoli = [ ("PROGETTO disaster-SL <= 50% equity (G6 SPEC, invariante 3 chiavi)", 0.50 / (2 * frac * sl), "P"), ("PROGETTO peggior giorno strutturale <= 50% (C1 §33)", 0.50 / L, "P"), ("PROGETTO mezzo-Kelly al limite BASSO dell'IC95 del drift (C2 §33)", ks_ic / 2, "P"), ("SCETTICO peggior giorno strutturale <= 20% (recupero +25%)", 0.20 / L, "S"), ("PROCESSO scaletta di codice `SCALA_LADDER` della SPEC", 1.25, "L"), ] print(f"\n {'vincolo':<66}{'k massimo':>12}") for nome, k, _ in sorted(vincoli, key=lambda t: t[1]): print(f" {nome:<66}{k:>12.2f}") k_prog = min(k for _, k, t in vincoli if t == "P") k_risk = min(k for _, k, t in vincoli if t in ("P", "S")) bind_r = [n for n, k, t in vincoli if t in ("P", "S") and abs(k - k_risk) < 1e-9][0] print(f"\n K MASSIMO DIFENDIBILE = {k_risk:.2f} (vincolo che morde: {bind_r})") print(f" · coi soli vincoli GIA' DICHIARATI dal progetto sarebbe {k_prog:.2f} " f"(morde il disaster-SL, non il peggior giorno: G6 della SPEC);") print(f" · la soglia del 20% e' AGGIUNTA da questo scettico e va difesa: oltre quella " f"perdita un recupero richiede +25%, e su un conto unico con zero assorbente il " f"tempo di recupero e' il costo vero. Non e' una regola del progetto: e' una proposta.") print(f" · e la scaletta della SPEC ferma comunque l'espressione a 1,25 — che e' un " f"cricchetto di PROCESSO, non un limite di rischio, e resta la scelta giusta perche' " f"e' l'unico gate che il progetto ha contro un parametro che nessun altro gate vede.") print(f"\n ⇒ IL GRADINO 1,25x SOPRAVVIVE ALL'ATTACCO. Il 1,50x NO: sfonda la soglia del " f"20% ({1.50*L:.2%}) e sta a {1.50/(0.50/(2*frac*sl)):.0%} del tetto G6.") print(f"\n 📌 RICONCILIAZIONE DEL NUMERO PUBBLICATO — e non era una discrepanza, era una " f"CONVENZIONE non dichiarata:") print(f" §33 pubblica «14,7a -> 12,9a» = +1,8a. Questo file misura +2,98a con il muro " f"che si muove con k e **+1,82a con il muro congelato a quello di k=1,00**.") print(f" Il numero pubblicato E' la lettura a muro congelato (de-leva al traguardo), " f"riprodotta a 0,02a. Non e' sbagliato: e' il piu' conservativo dei due, e §33 non " f"dichiara quale sta usando. **Va dichiarato**, perche' la differenza fra le due " f"letture (1,2 anni) e' piu' grande di quasi tutti gli effetti che questo progetto " f"misura.") print(f"\n ATTACCHI NON PORTATI, e perche':") print(f" · il costo di MARGINE sopra 1x non e' stato prezzato: su un perpetuale lineare " f"marginato in USDC non c'e' un prestito, il carry E' il funding (gia' dentro). " f"Se Deribit introducesse un costo di margine, il conto va rifatto.") print(f" · la banda d'ANCORA del gradino: il de-luck ×0,89 e' applicato al drift, ma il " f"guadagno in ANNI e' un Delta su griglia ancorata e quindi eredita la fortuna " f"d'ancora come ogni Delta (lezione 26/07). Servirebbero le 23x24 ancore congiunte: " f"fuori dal budget di 2 CPU. Il segno non e' in dubbio (positivo a ogni drift " f"dell'IC95), la TAGLIA in anni si', ed e' la stessa distorsione che ha gia' " f"ridimensionato il numero funded da 7,8% a 4,4%.") print(f" · lo slippage a taglia crescente: §5 dell'ondata ha misurato che un fill ha " f"preso il 21,9% del volume della sua barra 5m a $635, e che la partecipazione scala " f"LINEARE. Una scala 1,25x la porta a 27,4% subito, non a $5.000. Non l'ho misurata " f"e va misurata prima del gradino, non dopo.") print(f" · la coda ASSENTE dal dataset per costruzione (un evento tipo LUNA/FTX sul " f"VENUE, non sul prezzo): nessuno stimatore di coda la contiene, e il rischio di " f"venue vive sul suo asse separato (26/07). Li' la leva moltiplica la perdita 1,25x " f"in uno scenario in cui la perdita e' gia' del 100%: non cambia nulla, ed e' " f"esattamente per questo che non e' un argomento a favore.") print(f"\n {clock()}") def main() -> None: print(__doc__.split("ONESTA' APPLICATA")[0]) sez("(0) IL CRITERIO — congelato PRIMA di guardare le sezioni 2-7") for k, v in CRITERIO.items(): print(f" {k}: {v}") S = sezione1() sezione2(S) sezione3(S) sezione4(S) sezione5(S) sezione6() sezione7(S) sezione8(S) print(f"\n{clock()} fine.") if __name__ == "__main__": main()