Files
PythagorasGoal/scripts/research/r0823_skeptic_leva.py
T
Adriano Dal Pastro e7ee0738a3 scettico(leva): il gradino 1,25x REGGE 5 attacchi su 6 — k max difendibile 1,40
Filone §53 SKEPTIC-LEVA. Sei attacchi con soglie dichiarate PRIMA, 10 numeri
pubblicati riprodotti prima di attaccarli. Sola lettura, zero rete, 127 s.

A1 DRIFT   REGGE   — il gradino diventa dannoso solo sotto +1,42%/anno di drift
                     (-2,70 SE, 0,25° pctl bootstrap a blocchi): fuori da IC95
                     [+4,78%] E da IC99 [+2,36%]. Il libro puo' perdere il 90,6%
                     del suo drift e il gradino resta non dannoso; il PEGGIOR
                     triennio mai realizzato (+5,35%) ci sta 3,8x sopra.
A2 SINISTRO REGGE  — Delta g(1,25) > 0 in 5/5 finestre giudicabili (2020-21 tolto
                     compreso). QUALIFICA misurata: nel biennio MOBILE peggiore
                     (dal 2021-10, drift -0,45%) il gradino COSTA -0,31%/anno =
                     -0,62% cumulato. Non falsifica per il criterio (li' perde il
                     LIBRO), ma il numero e' quello. Rapporto guadagno/costo 13:1.
A3 CODA    REGGE   — con Hill xi=0,43 (piu' severo del +0,34 pubblicato) il peggior
                     giorno strutturale a 1,25x e' 17,90% (<50%) e P(dimezzamento
                     10a) 0,100% contro 0,000%. CORREZIONE a §33: la coda muove k*
                     di 4,6 unita', non "1-2" — la conclusione regge (il drift ne
                     muove 6,2) ma il numero pubblicato e' ottimista ~3x.
A4 LENTE   REGGE   — sotto la lente accoppiata il ricarico del maxDD CALA con k
                     (1,2358 -> 1,2297 -> 1,2237): moltiplicativo, verso favorevole.
                     E la frequenza del disaster-SL e' invariante a k PER
                     COSTRUZIONE (innesco sul MARK, test di ri-piazzamento
                     RELATIVO): il numero di §40 va moltiplicato, non rifatto.
A5 INVARIANTE INCRINA — completo sul percorso FIDATO (0 violazioni su 576 combo:
                     il vero e' n_asset*min(WEIGHT*(W_TP01*lev+W_SKH), frac)*scala,
                     quindi l'invariante e' un LIMITE SUPERIORE). Ma nel ramo di
                     FALLBACK il denominatore del rapporto e' il WATERMARK, non
                     l'equity: leva vera fino a 5,00x GIA' a k=1,00 e 6,25x a 1,25.
                     La scala non apre il buco, lo MOLTIPLICA -> la guardia G3
                     della SPEC non e' una raffinatezza ed e' NON OPZIONALE, ma non
                     e' una chiusura. E il fattore 0,30 non e' un tetto di perdita
                     (§40: stop rotolante, passano -60% dall'ingresso).
A6 FUNDING REGGE   — rifatto col funding DENTRO e proporzionale a k (r_k = k*(r-f),
                     lineare per costruzione): il gradino vale +3,49a (muro mobile)
                     / +2,10a (muro congelato) contro +2,98a / +1,82a sulla lente
                     pubblicata. Il funding NON riduce il gradino: lo AUMENTA in
                     anni, perche' peggiora il caso base (17,28a contro 14,81a).
                     Ma il livello peggiora su tutta la colonna: k=1,25 col funding
                     (13,78a) resta peggio di k=1,00 senza (14,81a) -> il gradino
                     non ripaga il funding, lo attenua.

RICONCILIAZIONE: il «+1,8a / +EUR164» pubblicato NON era una discrepanza, era una
CONVENZIONE non dichiarata — e' la lettura a MURO CONGELATO (de-leva al traguardo),
riprodotta a 0,02a (+1,82a). Col muro che si muove con k il gradino vale +2,98a. La
differenza fra le due letture (1,2 anni) e' piu' grande di quasi tutti gli effetti
che questo progetto misura: va dichiarata. Il +EUR164 e' invece +EUR144 misurato
direttamente — la differenza E' l'ipotesi di linearita' dell'interpolazione.

K MASSIMO DIFENDIBILE = 1,40 (morde il peggior giorno strutturale <= 20%, soglia
AGGIUNTA da questo scettico); coi soli vincoli gia' dichiarati dal progetto sarebbe
1,67 (G6, il disaster-SL). Il 1,50x NON sopravvive: sfonda il 20% (21,48%) e sta al
90% del tetto G6. La scaletta SCALA_LADDER a 1,25 resta giusta: e' l'unico gate che
il progetto ha contro un parametro che nessun altro gate vede.

NON PORTATI: banda d'ancora del guadagno in ANNI (23x24 congiunte, fuori budget);
slippage a taglia crescente (la partecipazione 21,9% di una barra 5m diventa 27,4%
SUBITO, non a $5.000: va misurata PRIMA del gradino); costo di margine sopra 1x
(non esiste su perp lineare marginato); coda di venue (vive sul suo asse).

Nessun file di produzione toccato: git diff sui tracciati e' vuoto.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018M8Ncho6QV9FWLdyy4VyQf
2026-08-23 02:05:22 +00:00

998 lines
57 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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()