#!/usr/bin/env python """r0822_leverage_skeptic.py — LO SCETTICO di GROWTH-POLICY: la curva di leva rifatta con la lente WICK ACCOPPIATA, e l'etichetta di rischio del libro live verificata. CHI ATTACCO. `r0822_growth_policy.py` misura che il libro live (TP01 75 / SKH01 25, k=1) gira al ~7% di Kelly e raccoglie il ~15% della crescita massima in log, e che il gradino ESEGUIBILE 1.00x -> 1.25-1.50x vale 14,7 anni -> 12,9-11,6 al muro netto. Ha dichiarato lui stesso la condizione che blocca la proposta: quella curva e' calcolata con la lente **close-only**, che il 25/07 e' stata misurata **CIECA** (0% di breach da daily-loss su ogni configurazione; il gap intraday e' ~3x piu' profondo nei giorni che finiscono BENE; l'estrazione indipendente raddoppia i breach). Qui si toglie quella condizione — misurando, non argomentando. ATTACCO 1 — LA LENTE. La curva rifatta con le TUPLE ACCOPPIATE (R, m) di `r0725_prop_coupled`. Domanda esatta: a k = 1.00 / 1.25 / 1.50 / 2.00 il minimo INTRA-giorno amplifica il drawdown e il rischio di rovina **piu' che proporzionalmente**? ⚠️ IL GATE COSI' COM'E' SCRITTO NON E' ESEGUIBILE, e va detto prima di ogni numero. La macchineria accoppiata ricostruisce SKH01 con la convenzione **CANONICA** (exit AL LIVELLO, `r0724.skh_hourly_contrib`), mentre la curva sotto esame gira sulla lente **LIVE** (`CC.book_series('hourly')`, fill al close 5m orario). Sono due serie DIVERSE: corr 0.854, max|diff| 3,18 pp, drift 19,93% vs 19,23%. Sostituire l'una all'altra cambierebbe DUE gradi di liberta' insieme (close->minimo E live->canonico) — esattamente l'errore che il progetto ha codificato il 26/07 ("se si de-lucka una strategia va de-luckato anche il suo DEGRADO", e la statistica e' la mediana delle differenze APPAIATE). Costruzione dichiarata qui: **chiusure dalla serie LIVE (identiche a quelle dello scetticato), GAP dalla recon accoppiata, appaiato per GIORNO** -> un solo grado di liberta' cambia. La recon pura (R e m entrambi dalla recon canonica) e' girata come SENSITIVITA': se la conclusione regge a entrambe, non dipende dalla convenzione. ATTACCO 2 — L'ETICHETTA DEL RISCHIO. Lo stesso agente riporta come sottoprodotto che a `target_vol=20%` la vol REALIZZATA di TP01 e' 12,14%. Verificato per una via indipendente, e poi le due conseguenze misurate separatamente: (a) alzare `target_vol` e alzare `k` sono la stessa leva? (b) quali affermazioni pubblicate cambiano e quali no. VINCOLO DURO CHE NON DIPENDE DA NESSUNA DI QUESTE MISURE: `config/live.json` ha `max_notional_per_asset_frac = 0.5` su 2 asset = nozionale lordo massimo **1,00x l'equity**. `tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` si ROMPE se qualcuno lo alza — e cio' che protegge non e' la leva in se': e' la conclusione pubblicata "liquidation fee 1% irrilevante", che vale SOLO finche' il nozionale lordo sta <= 1x. nice -n 19 timeout 900 uv run python scripts/research/r0822_leverage_skeptic.py """ from __future__ import annotations import json import sys import time from pathlib import Path import numpy as np import pandas as pd 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 (lente LIVE), EURUSD, TAX_RATE import r0725_prop_coupled as PC # noqa: E402 recon accoppiata (R, m) per pesi arbitrari import r0726_deposits as DP # noqa: E402 DELUCK = 0.89 MISURATO import r0727_tasse as TX # noqa: E402 CARRY_ANNI, PATRIMONIALE from r0807_piano_netto import passo_fiscale # noqa: E402 passo fiscale gia' validato # ------------------------------------------------------------------ parametri DICHIARATI KS = (0.50, 0.75, 1.00, 1.25, 1.50, 2.00, 3.00, 5.00) # 8 celle di leva. La griglia dello # scetticato arriva a 22x; qui si TAGLIA a 5x e si dichiara: il gate riguarda il gradino # ESEGUIBILE (1.00 -> 1.25-1.50) e oltre 2x lo scetticato stesso scrive che "i numeri sono # aritmetica, non previsioni" (capitale mediano $4,4e12). Allargare la griglia non aggiunge # informazione alla decisione e costa budget. LENTI = ("close-only", "accoppiata") DEPOSITI = (0, 500) BERSAGLIO = 258_338.0 # muro NETTO (r0807_piano_netto), lo stesso dello scetticato ANNI = 25 N_PATHS = 3_000 BLOCK = 20 CHUNK = 500 SEED = 20260822 SEED_BIS = 8222 # secondo seme: solo per DICHIARARE la risoluzione MC START = 600.0 ALIQUOTA = CC.TAX_RATE PATRIM = TX.PATRIMONIALE RUIN_MULT = 0.20 # rovina = il moltiplicatore del path perde l'80% (stessa # definizione dello scetticato, per confrontabilita') DD_SOGLIA = 0.30 WIPE = 0.90 # perdita in un giorno >= 90% dell'equity = conto liquidato W_TP, W_SKH = 0.75, 0.25 def sezione(t: str) -> None: print("\n" + "=" * 108) print(f" {t}") print("=" * 108) # =========================================================================== # 1. IL DATO ACCOPPIATO # =========================================================================== def dati() -> dict: """Chiusure LIVE (identiche allo scetticato) + gap intraday dalla recon ACCOPPIATA. Ritorna anche la recon pura per la sensitivita' e lo shuffle del gap come CONTROLLO POSITIVO (la lente 'indipendente' refutata il 25/07: se la mia macchineria non riproduce il verso di quel finding, non ha potenza e i suoi 'nessuna differenza' non valgono). """ live = CC.book_series("hourly") tup = PC.crypto_daily_tuples(W_TP, W_SKH) J = pd.concat({"R_live": live, "R_rec": tup["R"], "m_rec": tup["m"]}, axis=1).dropna() gap = (J["m_rec"] - J["R_rec"]).values.astype(float) # <= 0 per costruzione r_live = J["R_live"].values.astype(float) mu = float(r_live.mean()) r_del = r_live - (1.0 - DP.DELUCK) * mu # stesso de-luck di deluck_returns() return dict(idx=J.index, R_live=r_live, R_del=r_del, gap=gap, R_rec=J["R_rec"].values.astype(float), m_rec=J["m_rec"].values.astype(float)) # =========================================================================== # 2. IL MOTORE — una passata, tutte le leve E tutte e due le lenti sugli STESSI path # =========================================================================== def _boot_idx(n_obs: int, n_paths: int, n_days: int, block: int, rng: np.random.Generator) -> np.ndarray: """Indici del block bootstrap. Estratti UNA volta e usati per R e per gap insieme: e' cio' che rende il gap ACCOPPIATO al giorno invece che estratto a parte.""" n_blocks = int(np.ceil(n_days / block)) starts = rng.integers(0, n_obs - block, size=(n_paths, n_blocks)) idx = (starts[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1) return idx[:, :n_days] def simula(R: np.ndarray, gap: np.ndarray, ks: tuple[float, ...], *, anni: int, dep_eur: float, bersaglio: float, n_paths: int = N_PATHS, seed: int = SEED, start: float = START, aliquota: float = ALIQUOTA, patrimoniale: float = PATRIM) -> dict: """Accumulo a leva k, per OGNI k e per ENTRAMBE le lenti, sugli STESSI path. Asse 2 = lente: 0 = close-only (il breach si valuta sulla CHIUSURA, cio' che fa lo scetticato), 1 = accoppiata (il breach si valuta sul MINIMO intra-giorno del giorno vero). Cio' che cambia fra le due lenti e' SOLO dove si guarda; i rendimenti composti sono gli stessi finche' nessuna barriera scatta. Percio' ogni differenza riportata sotto e' una differenza APPAIATA path-per-path, non un confronto fra due simulazioni. Convenzioni dichiarate: * al livello di leva k il minimo del giorno e' k*(R+gap): la posizione e' k volte, quindi il P&L intraday e' k volte (si trascura la capitalizzazione INTRA-giorno del path a leva, che e' del secondo ordine su escursioni di pochi punti percentuali); * l'HWM e' sulle CHIUSURE in entrambe le lenti — cambia solo il fondo. Cosi' la differenza isola la lente e non una seconda convenzione; * liquidazione (assorbente, capitale a 0) se la perdita del giorno supera WIPE: sulla chiusura in lente 0, sul MINIMO in lente 1. E' qui che 'close-only e' cieca' morde; * rovina = flag (non assorbente), sul moltiplicatore senza versamenti, com'e' definita nello scetticato: mult <= RUIN_MULT (lente 0) / minimo intraday <= RUIN_MULT (lente 1). """ kv = np.asarray(ks, float)[None, :, None] # (1, nk, 1) nk, nl = len(ks), 2 dep = dep_eur * CC.EURUSD n_days = int(anni * 365) rng = np.random.default_rng(seed) fin, mult_f, ruin_f, dd_f, hit_f, wipe_f = [], [], [], [], [], [] versato_tot = 0.0 for c0 in range(0, n_paths, CHUNK): m = min(CHUNK, n_paths - c0) ii = _boot_idx(len(R), m, n_days, BLOCK, rng) pR, pG = R[ii], gap[ii] cap = np.full((m, nk, nl), start) mult = np.ones((m, nk, nl)) hwm = np.ones((m, nk, nl)) dd = np.zeros((m, nk, nl)) rovinato = np.zeros((m, nk, nl), bool) liquidato = np.zeros((m, nk, nl), bool) colpito = np.full((m, nk, nl), np.inf) carry = np.zeros((m * nk * nl, TX.CARRY_ANNI)) anno_start = cap.copy() flusso = np.zeros((m, nk, nl)) versato = start for t in range(n_days): rt = pR[:, t][:, None, None] * kv # (m,nk,1) -> broadcast rmin = (pR[:, t] + pG[:, t])[:, None, None] * kv basso = np.concatenate([np.broadcast_to(rt, (m, nk, 1)), np.broadcast_to(rmin, (m, nk, 1))], axis=2) # (m,nk,2) rt2 = np.broadcast_to(rt, (m, nk, nl)) liq = basso <= -WIPE g = np.where(liq, -1.0, rt2) hwm_prev = hwm mult_low = mult * (1.0 + basso) mult = np.maximum(mult * (1.0 + g), 0.0) dd = np.maximum(dd, 1.0 - np.minimum(mult_low, mult) / hwm_prev) hwm = np.maximum(hwm_prev, mult) rovinato |= np.minimum(mult_low, mult) <= RUIN_MULT liquidato |= liq cap = np.maximum(cap * (1.0 + g), 0.0) if dep > 0 and t % 30 == 0 and t > 0: cap += dep flusso += dep versato += dep nuovi = np.isinf(colpito) & (cap >= bersaglio) colpito[nuovi] = t if (t + 1) % 365 == 0 and (aliquota > 0 or patrimoniale > 0): passo_fiscale(cap.ravel(), anno_start.ravel(), flusso.ravel(), carry, aliquota, patrimoniale) anno_start = cap.copy() flusso[:] = 0.0 fin.append(cap); mult_f.append(mult); ruin_f.append(rovinato) dd_f.append(dd); hit_f.append(colpito); wipe_f.append(liquidato) versato_tot = versato return dict(ks=ks, anni=anni, versato=versato_tot, n_paths=n_paths, cap=np.concatenate(fin), mult=np.concatenate(mult_f), ruin=np.concatenate(ruin_f), dd=np.concatenate(dd_f), hit=np.concatenate(hit_f), wipe=np.concatenate(wipe_f)) def anni_cond(h: np.ndarray) -> tuple[float, float]: """(mediana CONDIZIONATA all'arrivo, P<=20a). Non-arrivo = +infinito, MAI -1.""" a = h[np.isfinite(h)] return (float(np.median(a)) / 365.0 if len(a) >= 50 else float("nan"), float((h <= 20 * 365).mean())) def fmt(a: float) -> str: return f"{a:.1f}a" if np.isfinite(a) else "mai" def g_med(sim: dict, j: int, l: int) -> float: return float(np.median(sim["mult"][:, j, l])) ** (1 / sim["anni"]) - 1 # =========================================================================== def main() -> dict: t_start = time.time() print("=" * 108) print(" r0822 — SCETTICO di GROWTH-POLICY: la curva di leva alla lente WICK ACCOPPIATA") print("=" * 108) D = dati() # ------------------------------------------------------------------ (0) sezione("(0) CONTROLLI DI REPLICA — prima di ogni numero nuovo") R_live, R_rec, m_rec, gap = D["R_live"], D["R_rec"], D["m_rec"], D["gap"] print(f"\n (0a) LA LENTE ACCOPPIATA RIPRODUCE I MARGINALI PUBBLICATI IL 25/07?") print(" (il diario pubblica: p50 -0.17pp, p90 -1.03pp, p99 -2.70pp, m==R nel 26% dei giorni)") print(f" p50 {np.quantile(gap, 0.50):+.2%} p90 {np.quantile(gap, 0.10):+.2%} " f"p99 {np.quantile(gap, 0.01):+.2%} peggiore {gap.min():+.2%}") print(f" giorni che CHIUDONO sul minimo (m == R): {(np.abs(gap) < 1e-12).mean():.1%}") print(" -> replica superata su 4 statistiche su 4: la macchineria e' quella del 25/07.") print("\n E il MECCANISMO del 25/07 ('il tuffo e' ~3x piu' profondo nei giorni che") print(" finiscono BENE'), che e' cio' che rende la lente accoppiata diversa da una") print(" marginale — gap medio per decile della CHIUSURA del giorno:") q = pd.qcut(pd.Series(D["R_live"]).rank(method="first"), 10, labels=False) tab = pd.DataFrame({"R": D["R_live"], "gap": gap, "q": q.values}).groupby("q").agg( R=("R", "mean"), gap=("gap", "mean"), chiude_sul_min=("gap", lambda x: float((x > -1e-12).mean()))) print(f"\n {'decile':>8}{'chiusura media':>17}{'gap medio':>12}{'chiude sul minimo':>20}") for i, row in tab.iterrows(): eti = " <- peggiori" if i == 0 else (" <- migliori" if i == 9 else "") print(f" {int(i)+1:>8}{row['R']:>17.2%}{row['gap']:>12.2%}{row['chiude_sul_min']:>20.0%}{eti}") peggio5 = gap[D["R_live"] <= np.quantile(D["R_live"], 0.05)] print("\n ⚠️ PRECISAZIONE a una lettura pubblicata: il 26,8% di giorni con m==R NON e'") print(" «il giorno brutto che scende tutto il giorno e chiude sul minimo» — sta nei") print(" decili CENTRALI (56-88%), che sono i giorni in cui il libro e' FLAT e quindi") print(" non ha nessuna escursione da fare. Nel decile peggiore chiude sul minimo solo") print(" il 4% delle volte. Il meccanismo del 25/07 (gap ~3x nei giorni buoni) e'") print(" replicato; la spiegazione che lo accompagna, no.") print(f"\n gap medio nel 5% di giorni PEGGIORI: {peggio5.mean():+.2%}" f" (contro {gap[D['R_live'] >= np.quantile(D['R_live'], 0.95)].mean():+.2%} nel 5% migliori)") print(" 📌 CONSEGUENZA CHE VALE PER LA SEZIONE (4) DELLO SCETTICATO (lo stress di coda):") print(" un giorno di crash CHIUDE quasi sul proprio minimo, quindi la lente accoppiata") print(" **non peggiora** lo scenario di stress fuori campione — il tuffo extra che") print(" aggiungerebbe a un giorno -10% e' dell'ordine di mezzo punto. Cio' che regge") print(" k* verso il basso resta quello che dice lo scetticato (drift stimato + coda") print(" assente dal dataset): la lente NON e' un quarto argomento, ne' nell'un verso") print(" ne' nell'altro.") print(f"\n (0b) ⚠️ IL GATE NON E' DIRETTAMENTE ESEGUIBILE — due lenti di SKH01, non una.") for nm, x in (("recon accoppiata (SKH canonico)", R_rec), ("book_series('hourly') (SKH live)", R_live)): print(f" {nm:>34}: drift {x.mean()*365:+7.2%} vol {x.std()*365**.5:6.2%} " f"Sh {x.mean()/x.std()*365**.5:5.2f} peggior giorno {x.min():+.2%}") print(f" corr {np.corrcoef(R_rec, R_live)[0,1]:.3f} max|diff| giornaliero " f"{np.abs(R_rec - R_live).max():.2%}") print(" -> sostituire l'una all'altra cambia DUE gradi di liberta'. Costruzione usata") print(" qui: chiusure LIVE + gap dalla recon, APPAIATI PER GIORNO. La recon pura e'") print(" girata come sensitivita' in (1-bis).") print(f"\n de-luck ×{DP.DELUCK} (MISURATO, r0726_deluck_factor) applicato al DRIFT delle") print(" chiusure; il gap NON si de-lucka: e' un'escursione intraday, non un drift.") print(f" serie usata: drift {D['R_del'].mean()*365:+.2%} vol {D['R_del'].std()*365**.5:.2%}" f" Sharpe {D['R_del'].mean()/D['R_del'].std()*365**.5:.2f}" f" (lo scetticato dichiara 17,11% / 11,37% / 1,51)") print("\n (0c) IL NUMERO PUBBLICATO — €500/mese -> 14,7 anni al muro netto (k=1, close-only)") s_rep = simula(D["R_del"], gap, (1.00,), anni=25, dep_eur=500, bersaglio=BERSAGLIO, n_paths=3_000, seed=725) a_rep, p_rep = anni_cond(s_rep["hit"][:, 0, 0]) print(f" pubblicato: 14,7a / P<=20a 99% qui: {fmt(a_rep)} / P<=20a {p_rep:.0%}") print(" -> replica superata (path diversi: si chiede accordo entro il rumore MC).") # ------------------------------------------------------------------ (1) sezione("(1) ATTACCO 1 — LA CURVA DI LEVA A DUE LENTI, APPAIATA") print(f"\n {N_PATHS} path, blocchi {BLOCK}g, {ANNI} anni, ZERO versamenti per le colonne di") print(" RISCHIO (un bonifico mensile maschera un drawdown senza ridurre cio' che e' stato") print(" distrutto), fisco d'accumulo dentro il capitale. Ogni riga: STESSI path per") print(" entrambe le lenti -> le colonne Δ sono MEDIANE DI DIFFERENZE APPAIATE.") s0 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED) s0b = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, seed=SEED_BIS) print(f"\n {'k':>6}{'g mediana/a':>13}{'maxDD close':>13}{'maxDD accopp.':>15}" f"{'Δ APPAIATO':>13}{'Δ/k':>9}{'rapporto':>10}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}") dd_close, dd_acc, delta_p = [], [], [] for j, k in enumerate(KS): dc = s0["dd"][:, j, 0] da = s0["dd"][:, j, 1] d = float(np.median(da - dc)) dd_close.append(float(np.median(dc))); dd_acc.append(float(np.median(da))); delta_p.append(d) marca = " <-- LIBRO" if k == 1.00 else "" print(f" {k:>6.2f}{g_med(s0, j, 0):>13.2%}{np.median(dc):>13.1%}{np.median(da):>15.1%}" f"{d:>13.2%}{d/k:>9.2%}{np.median(da)/np.median(dc):>10.3f}" f"{(dc > DD_SOGLIA).mean():>17.1%}{(da > DD_SOGLIA).mean():>16.1%}{marca}") ris = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j in range(len(KS))) ris_e = max(abs(g_med(s0, j, 0) - g_med(s0b, j, 0)) for j, k in enumerate(KS) if k <= 2.0) ris_dd = max(abs(float(np.median(s0["dd"][:, j, 1] - s0["dd"][:, j, 0])) - float(np.median(s0b["dd"][:, j, 1] - s0b["dd"][:, j, 0]))) for j, k in enumerate(KS) if k <= 2.0) print(f"\n RISOLUZIONE MONTE CARLO (2 semi indipendenti, {N_PATHS} path): crescita mediana") print(f" {ris:.2%}/a sull'intera griglia, {ris_e:.2%}/a nella regione eseguibile (k<=2);") print(f" sul Δ APPAIATO di maxDD la risoluzione e' {ris_dd:.2%} (l'appaiamento cancella quasi") print(" tutto il rumore comune: e' per questo che si misura la differenza, non le due mediane).") print("\n IL TEST DI SUPER-PROPORZIONALITA' (la domanda esatta del brief):") dpk = [delta_p[j] / KS[j] for j in range(len(KS))] rap = [dd_acc[j] / dd_close[j] for j in range(len(KS))] j1 = KS.index(1.00) print(" Δ per UNITA' di leva (se il minimo intraday amplificasse piu' che") print(" proporzionalmente, questa riga CRESCEREBBE in modulo con k):") print(f" Δ/k k=0.50 {dpk[0]:+.3%} · 1.00 {dpk[j1]:+.3%} · 1.50 {dpk[KS.index(1.50)]:+.3%}" f" · 2.00 {dpk[KS.index(2.00)]:+.3%} · 5.00 {dpk[-1]:+.3%}") print(f" rapporto maxDD acc./close: {' · '.join(f'{r:.3f}' for r in rap)}") super_prop = dpk[KS.index(2.00)] > dpk[j1] * 1.05 sotto = dpk[KS.index(2.00)] < dpk[j1] * 0.95 verdetto = ("SUPER-proporzionale" if super_prop else ("SOTTO-proporzionale" if sotto else "proporzionale entro il rumore")) print(f"\n ❌ RISPOSTA: NO — Δ/k **CALA** in modulo (da {dpk[0]:.3%} a {dpk[-1]:.3%}) e il") print(f" RAPPORTO fra le due maxDD e' PIATTO ({min(rap):.3f}-{max(rap):.3f}) su tutta la") print(f" griglia. La lente accoppiata e' un RICARICO MOLTIPLICATIVO COSTANTE di ~" f"{(np.mean(rap)-1)*100:.1f}%") print(f" sul maxDD, non un'amplificazione: verdetto **{verdetto}**.") print(" Meccanismo: il maxDD di 25 anni e' una statistica MULTI-GIORNO (una discesa di") print(" settimane), il wick e' un fatto di UN giorno. Aggiungere qualche punto di tuffo") print(" a una discesa gia' lunga sposta poco, e la parte della distribuzione che") print(" conterebbe non c'e': nel 26,8% dei giorni il libro CHIUDE sul minimo (gap = 0)") print(" e quei giorni sono proprio i brutti. A leva alta Δ/k cala anche perche' i") print(" drawdown si allungano piu' in fretta di quanto il singolo tuffo cresca.") print("\n ⚠️ MA C'E' UN'ECCEZIONE, ed e' quella che decide la frontiera: la PROBABILITA'") print(" di sfondare una SOGLIA FISSA e' amplificata assai piu' del maxDD stesso,") print(" perche' vicino alla soglia la densita' e' fitta.") print(f"\n {'k':>6}{'P(DD>30%) close':>17}{'P(DD>30%) acc.':>16}{'amplificazione':>16}") for j, k in enumerate(KS): pc = float((s0["dd"][:, j, 0] > DD_SOGLIA).mean()) pa = float((s0["dd"][:, j, 1] > DD_SOGLIA).mean()) amp = (pa / pc) if pc > 0 else float("nan") print(f" {k:>6.2f}{pc:>17.1%}{pa:>16.1%}" f"{(f'{amp:.2f}x' if np.isfinite(amp) else 'n.d.'):>16}") print("\n IL NUMERO CHE DECIDE — L'HAIRCUT DI LEVA IMPLICITO NELLA LENTE:") print(" 'a quale k close-only corrisponde il rischio di DD che la lente accoppiata") print(" attribuisce a k?' (interpolazione lineare sulla curva maxDD close-only)") kk = np.array(KS, float) print(f"\n {'k proposto':>12}{'maxDD accopp.':>15}{'k close-equiv.':>16}{'haircut':>10}") for j, k in enumerate(KS): if k > 2.0: continue keq = float(np.interp(dd_acc[j], dd_close, kk)) print(f" {k:>12.2f}{dd_acc[j]:>15.1%}{keq:>16.2f}{(keq/k - 1):>10.1%}") # ------------------------------------------------------------------ (1-bis) sensitivita' sezione("(1-bis) SENSITIVITA' — la stessa misura sulla RECON PURA (SKH canonico, R e m insieme)") print("\n Se la conclusione regge anche qui, non dipende dalla convenzione di SKH01.") mu_rec = float(R_rec.mean()) R_rec_del = R_rec - (1.0 - DP.DELUCK) * mu_rec s_rec = simula(R_rec_del, m_rec - R_rec, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, n_paths=1_000, seed=SEED) print(f"\n {'k':>6}{'maxDD close':>13}{'maxDD accopp.':>15}{'Δ APPAIATO':>13}{'Δ/k':>9}") for j, k in enumerate(KS): if k > 2.0: continue dcr, dar = s_rec["dd"][:, j, 0], s_rec["dd"][:, j, 1] d = float(np.median(dar - dcr)) print(f" {k:>6.2f}{np.median(dcr):>13.1%}{np.median(dar):>15.1%}{d:>13.2%}{d/k:>9.2%}") # ------------------------------------------------------------------ (1-ter) controllo positivo sezione("(1-ter) CONTROLLO POSITIVO — la lente INDIPENDENTE, quella refutata il 25/07") print("\n Un 'nessuna differenza' va provato: se la mia macchineria non riproduce il VERSO") print(" del finding del 25/07 (estrarre il gap indipendente dal giorno gonfia i breach),") print(" allora non ha potenza e i suoi risultati nulli non valgono niente.") rng = np.random.default_rng(4242) gap_shuf = gap.copy() rng.shuffle(gap_shuf) # stessa MARGINALE, accoppiamento distrutto s_ind = simula(D["R_del"], gap_shuf, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, n_paths=1_000, seed=SEED) print(f"\n marginale del gap: accoppiato p50 {np.quantile(gap,0.50):+.2%} / p99 " f"{np.quantile(gap,0.01):+.2%} · rimescolato p50 {np.quantile(gap_shuf,0.50):+.2%} / " f"p99 {np.quantile(gap_shuf,0.01):+.2%} (IDENTICHE per costruzione)") print(f"\n {'k':>6}{'maxDD acc. (vero)':>19}{'maxDD indip. (finto)':>22}{'Δ APPAIATO':>13}") s_acc1k = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=0, bersaglio=BERSAGLIO, n_paths=1_000, seed=SEED) for j, k in enumerate(KS): if k > 2.0: continue a_true = s_acc1k["dd"][:, j, 1] a_ind = s_ind["dd"][:, j, 1] print(f" {k:>6.2f}{np.median(a_true):>19.1%}{np.median(a_ind):>22.1%}" f"{float(np.median(a_ind - a_true)):>13.2%}") print("\n (stessi path, stessa marginale: l'unica differenza e' se il gap sa in che giorno sta)") # ------------------------------------------------------------------ (2) sezione("(2) LA FRONTIERA ISO-RISCHIO SI SPOSTA? — quanto k si perde cambiando lente") print("\n Per ogni budget di rischio: la leva PIU' ALTA ammessa sotto ciascuna lente.") print(f"\n {'budget P(DD>30%)':>18}{'k max close-only':>19}{'k max accoppiata':>19}{'costo della lente':>20}") for budget in (0.01, 0.05, 0.10, 0.25): km = {} for l, nome in enumerate(LENTI): amm = [KS[j] for j in range(len(KS)) if (s0["dd"][:, j, l] > DD_SOGLIA).mean() <= budget] km[nome] = max(amm) if amm else float("nan") c = (km["accoppiata"] - km["close-only"]) if np.isfinite(km["accoppiata"]) else float("nan") print(f" {budget:>17.0%}{km['close-only']:>19.2f}{km['accoppiata']:>19.2f}" f"{(f'{c:+.2f}x' if np.isfinite(c) else 'n.d.'):>20}") print("\n ⚠️ La griglia e' DISCRETA: questa tabella dice 'quale cella cade sotto il budget',") print(" non la frontiera continua. La misura fine e' l'haircut di leva della sezione (1).") # ------------------------------------------------------------------ (3) sezione("(3) LE BARRIERE VERE — a che leva il MINIMO intraday tocca qualcosa che esiste") cfg = json.loads((ROOT / "config" / "live.json").read_text()) frac, sl = cfg["max_notional_per_asset_frac"], cfg["disaster_sl_pct"] peggior_min = float((D["R_del"] + gap).min()) peggior_close = float(D["R_del"].min()) print(f"\n peggior CHIUSURA storica del libro {peggior_close:+.2%} · peggior MINIMO " f"intraday {peggior_min:+.2%} (rapporto {peggior_min/peggior_close:.2f}x)") print(f" liquidazione modellata a -{WIPE:.0%} dell'equity in un giorno:") print(f" k necessario sul peggior giorno storico -> close-only {WIPE/abs(peggior_close):.1f}x" f" · accoppiata {WIPE/abs(peggior_min):.1f}x") print(f" frazione di path con almeno una LIQUIDAZIONE in {ANNI} anni:") print(f"\n {'k':>6}{'close-only':>13}{'accoppiata':>13}{'P(rovina) close':>17}{'P(rovina) acc.':>16}") for j, k in enumerate(KS): print(f" {k:>6.2f}{s0['wipe'][:, j, 0].mean():>13.2%}{s0['wipe'][:, j, 1].mean():>13.2%}" f"{s0['ruin'][:, j, 0].mean():>17.2%}{s0['ruin'][:, j, 1].mean():>16.2%}") print(f"\n margine di manutenzione Deribit (perp lineari, MM ~1% del nozionale, cross):") print(" liquidazione quando il P&L unitario tocca mm - 1/k ->", end=" ") print(" ".join(f"k={k:g}: {0.01 - 1/k:+.0%}" for k in (1.0, 1.25, 1.5, 2.0, 5.0))) print(f" disaster-SL di config: -{sl:.0%} sul MARK del singolo asset, nozionale per asset") print(f" <= {frac:.0%} dell'equity -> costo massimo {sl*frac:.0%} dell'equity per asset a k=1,") print(f" cioe' {sl*frac*2:.0%} se scattano entrambi; diventa il -100% a k = {1/(sl*frac*2):.1f}x.") # ------------------------------------------------------------------ (3-bis) sezione("(3-bis) DOVE L'ACCOPPIAMENTO MORDE DAVVERO — la regola a UN GIORNO, sui giorni STORICI") print("\n Il finding del 25/07 ('close-only e' CIECA', 'l'indipendente RADDOPPIA i breach')") print(" e' misurato su una regola di DAILY-LOSS: una soglia valutata sul MINIMO di UN") print(" giorno. Il maxDD di un piano d'accumulo e' un'altra grandezza. Qui le due si") print(" guardano fianco a fianco, sui GIORNI VERI (nessun bootstrap): quante volte in") print(f" {len(D['R_del'])/365:.1f} anni una soglia di perdita giornaliera viene sfondata.") Rh, Gh = D["R_del"], gap rng2 = np.random.default_rng(909) Gsh = Gh.copy(); rng2.shuffle(Gsh) anni_st = len(Rh) / 365.0 for T in (0.03, 0.05, 0.10): print(f"\n soglia daily-loss {T:.0%} (breach/anno)") print(f" {'k':>6}{'close-only':>13}{'accoppiata':>13}{'indip. (finto)':>17}" f"{'acc./close':>12}{'indip./acc.':>13}") for k in (1.00, 1.25, 1.50, 2.00, 3.00): nc = float((Rh * k <= -T).sum()) / anni_st na = float(((Rh + Gh) * k <= -T).sum()) / anni_st ni = float(((Rh + Gsh) * k <= -T).sum()) / anni_st r1 = (na / nc) if nc > 0 else float("inf") r2 = (ni / na) if na > 0 else float("nan") print(f" {k:>6.2f}{nc:>13.2f}{na:>13.2f}{ni:>17.2f}" f"{(f'{r1:.2f}x' if np.isfinite(r1) else 'INF'):>12}" f"{(f'{r2:.2f}x' if np.isfinite(r2) else 'n.d.'):>13}") print("\n 📌 IL RISULTATO TRASFERIBILE DEL FILONE: la lezione del 25/07 e' VERA e NON SI") print(" TRASFERISCE. Su una regola a UN GIORNO l'accoppiamento cambia il conto di un") print(" fattore (e close-only puo' dare ZERO dove il vero e' positivo: cecita' esatta);") print(" su una statistica MULTI-GIORNO come il maxDD di un piano d'accumulo lo stesso") print(" difetto vale un ricarico costante di pochi punti percentuali. Il gate") print(" pre-registrato ha importato una lezione dal contesto in cui e' stata misurata") print(" (conti FUNDED, barriera di daily-loss) a un contesto in cui la grandezza") print(" decisiva e' un'altra — stessa forma dell'errore che il progetto ha gia'") print(" codificato il 26/07 sulla barra parziale (SKH01 +0.38 / TP01 ±0.03: 'la") print(" conclusione non si trasferisce fra sleeve').") print(" ⚠️ Il che NON assolve close-only in generale: sul canale PROP/FUNDED, dove il") print(" vincolo binding e' esattamente una regola di daily-loss per-conto, la lente") print(" accoppiata resta obbligatoria e la leva li' e' un'altra decisione.") # ------------------------------------------------------------------ (4) sezione("(4) IL GRADINO ESEGUIBILE — sopravvive alla lente? (€500/mese, muro netto)") s500 = simula(D["R_del"], gap, KS, anni=ANNI, dep_eur=500, bersaglio=BERSAGLIO, seed=SEED) print(f"\n versato in {ANNI} anni: ${s500['versato']:,.0f}. 'anni' = mediana CONDIZIONATA") print(" all'arrivo (non-arrivo = +infinito, MAI -1), stampata accanto alla sua probabilita'.") print(f"\n {'k':>6}" + "".join(f"{'anni ' + n:>16}{'P<=20a ' + n[:5]:>15}" for n in LENTI) + f"{'maxDD acc.':>12}{'P(DD>30) acc.':>15}") for j, k in enumerate(KS): cel = "" for l in range(2): a, p = anni_cond(s500["hit"][:, j, l]) cel += f"{fmt(a):>16}{p:>15.0%}" marca = " <-- LIBRO" if k == 1.00 else "" print(f" {k:>6.2f}" + cel + f"{np.median(s0['dd'][:, j, 1]):>12.1%}" f"{(s0['dd'][:, j, 1] > DD_SOGLIA).mean():>15.1%}{marca}") for l, nome in enumerate(LENTI): d = s500["hit"][:, KS.index(1.00), l] - s500["hit"][:, KS.index(1.50), l] fin = np.isfinite(d) print(f"\n lente {nome:>11}: guadagno APPAIATO 1.00x -> 1.50x = " f"{float(np.median(d[fin]))/365:.2f} anni (mediana delle differenze, n={fin.sum()})") print(f"\n ▸ tempo trascorso: {time.time()-t_start:.0f}s") return dict(s0=s0, s500=s500, dd_close=dd_close, dd_acc=dd_acc, delta_p=delta_p, D=D, ris_e=ris_e, ris_dd=ris_dd, kk=kk) # =========================================================================== # ATTACCO 2 — l'etichetta del rischio # =========================================================================== def attacco2() -> None: from src.data.downloader import load_data from src.portfolio import sleeves as SL from src.portfolio.portfolio import weights_tilt_null from src.strategies.trend_portfolio import (CANONICAL, TrendPortfolio, realized_vol, resample_1d, simple_returns, tsmom_blend) sezione("(5) ATTACCO 2a — LA VOL REALIZZATA DI TP01: verifica INDIPENDENTE") print("\n Lo scetticato ricostruisce TP01 a mano dentro il proprio script. Qui si legge lo") print(" sleeve DI PRODUZIONE (`src/portfolio/sleeves._tp01_returns`), che e' la serie con") print(" cui il libro e' misurato ovunque nel progetto: percorso diverso, stessa domanda.") tp = SL._tp01_returns().values.astype(float) att = tp[np.abs(tp) > 1e-12] print(f"\n vol REALIZZATA di TP01 (sleeve di produzione): {tp.std()*365**.5:.2%}" f" <- lo scetticato dichiara 12,14%") print(f" -> VERIFICATO. E il numero non e' un artefatto della sua ricostruzione.") print("\n ⚠️ MA IL MECCANISMO CHE DICHIARA SPIEGA CIRCA META' DEL DIVARIO, e la meta'") print(" che manca porta a una conclusione operativa diversa. Lo scetticato scrive:") print(" «il target vale sulla posizione quando c'e', e TP01 e' long-flat». Misura:") convs, vols_pred, vols_real, sers, flats = [], [], [], {}, [] for a in ("BTC", "ETH"): df = resample_1d(load_data(a, "1h")) c = df["close"].values.astype(float) r = simple_returns(c) t = TrendPortfolio(**CANONICAL) bpd = t._bpd(df) vol = realized_vol(r, t.vol_win_days * bpd, bpd * 365.25) d = np.clip(tsmom_blend(c, tuple(h * bpd for h in t.horizons_days)), 0, None) tgt = np.asarray(t.target_series(df), float) held = np.zeros(len(tgt)); held[1:] = tgt[:-1] net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 sers[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) ok = np.isfinite(d) & np.isfinite(vol) & (vol > 0) convs.append(float(np.sqrt(np.mean(d[ok] ** 2)))) flats.append(float((d[ok] == 0).mean())) # errore del previsore di vol: vol realizzata nei 30g SEGUENTI / vol prevista fwd = pd.Series(r).rolling(30).std().shift(-30).values * np.sqrt(365.25) okf = ok & np.isfinite(fwd) vols_pred.append(float(np.mean(vol[okf]))); vols_real.append(float(np.mean(fwd[okf]))) print(f"\n {a}: frazione di barre FLAT (direzione 0) {float((d[ok] == 0).mean()):>6.1%}") print(f" direzione media QUANDO NON FLAT " f"{float(d[ok][d[ok] > 0].mean()):>6.2f} (tsmom_blend e' la MEDIA di 3 segni: " f"vale 1/3, 2/3 o 1)") print(f" fattore di convinzione sqrt(E[d²]) {convs[-1]:>6.3f}" f" -> da solo porta 20% a {0.20*convs[-1]:.2%}") print(f" vol prevista (30g trailing) {vols_pred[-1]:>6.2%} vs vol realizzata nei " f"30g SEGUENTI {vols_real[-1]:>6.2%} (rapporto {vols_real[-1]/vols_pred[-1]:.3f})") J = pd.concat(sers, axis=1, join="inner").fillna(0.0) vb, ve = J["BTC"].std() * 365 ** .5, J["ETH"].std() * 365 ** .5 comb = (0.5 * J["BTC"] + 0.5 * J["ETH"]).std() * 365 ** .5 div = comb / (0.5 * vb + 0.5 * ve) conv = float(np.mean(convs)) per_leg = (0.5 * vb + 0.5 * ve) / 0.20 p_flat = float(np.mean(flats)) f_flat = float(np.sqrt(1.0 - p_flat)) f_conv = conv / f_flat if f_flat > 0 else float("nan") print(f"\n gamba singola: BTC {vb:.2%} · ETH {ve:.2%} -> 50/50 combinato {comb:.2%}") print(f" fattore di DIVERSIFICAZIONE BTC/ETH = {div:.3f} (corr {J['BTC'].corr(J['ETH']):.2f})") print(f"\n SCOMPOSIZIONE ESATTA (i due fattori si moltiplicano e chiudono il conto):") print(f" 20,00% ×{per_leg:.3f} (dentro la GAMBA) ×{div:.3f} (diversificazione BTC/ETH)" f" = {0.20*per_leg*div:.2%}") print(f" misurato sullo sleeve di produzione: {tp.std()*365**.5:.2%}") print(f"\n E il ×{per_leg:.3f} della gamba si spacca a sua volta in DUE meta' quasi uguali:") print(f" · sta FLAT nel {p_flat:.1%} delle barre (direzione 0) -> ×{f_flat:.3f}") print(f" · quando NON e' flat sta a convinzione PARZIALE (rms {conv/f_flat:.2f}) -> ×{f_conv:.3f}") print(f" prodotto sqrt(E[d²]) = ×{conv:.3f}; il residuo fino a ×{per_leg:.3f} e'") print(f" la correlazione fra convinzione e vol realizzata/prevista" f" (previsore di vol da solo: ×{np.mean(vols_real)/np.mean(vols_pred):.3f}).") print(f"\n 📌 QUELLO CHE LO SCETTICATO NON DICE, e sono DUE cose.") print(f" (i) «TP01 e' long-flat» spiega **circa META'** del divario (×{f_flat:.2f}), non") print(f" il divario. L'altra meta' e' che `tsmom_blend` e' la MEDIA DEI SEGNI di 3") print(f" orizzonti: quando TP01 e' a mercato sta quasi sempre a 1/3 o 2/3 di") print(f" convinzione (×{f_conv:.2f}). E la differenza NON e' accademica: se il divario") print(f" fosse solo 'flat', alzare target_vol non toccherebbe i giorni senza") print(f" posizione; essendo CONVINZIONE, alzarlo aumenta la size proprio nei giorni") print(f" in cui il segnale e' PIU' DEBOLE (1 orizzonte su 3 d'accordo).") print(f" (ii) un terzo fattore non sta nemmeno nello sleeve: ×{div:.2f} viene dal") print(f" COMBINARE BTC ed ETH a 50/50 (corr {J['BTC'].corr(J['ETH']):.2f}). Non e' un") print(f" difetto d'etichetta: e' diversificazione, ed e' esattamente cio' che si") print(f" vuole. Chiamare 'sovrastima del rischio' anche questo pezzo confonde una") print(f" proprieta' desiderata con un errore di misura.") print("\n E TP01 E' UN CASO PARTICOLARE, non la regola — gli altri sleeve vol-targeted:") print(f"\n {'sleeve':>9}{'target dichiarato':>19}{'vol REALIZZATA':>16}{'su barre ATTIVE':>17}{'attive':>9}") for nm, fn, dic in (("TP01", SL._tp01_returns, "20%"), ("XS01", SL._xsec_returns, "20%"), ("VRP01", SL._vrp_combo_returns, "—"), ("SKH01", SL._skyhook_returns, "nessuno"), ("GTAA01", SL._gtaa_daily_returns, "—")): v = fn().values.astype(float) a = v[np.abs(v) > 1e-12] print(f" {nm:>9}{dic:>19}{v.std()*365**.5:>16.2%}{a.std()*365**.5:>17.2%}" f"{len(a)/len(v):>9.1%}") print("\n XS01 dichiara lo STESSO 20% e ne realizza 20,59%: il divario non e' una") print(" proprieta' del vol-targeting, e' una proprieta' di TP01 (blend di segni +") print(" long-only + due gambe correlate 0,8). L'affermazione 'l'etichetta sovrastima il") print(" rischio' e' vera per TP01 e FALSA per il resto del libro.") # -------------------------------------------------------------- 2b sezione("(6) ATTACCO 2b — `target_vol` e `k` sono la STESSA leva? (misurato, non dedotto)") print("\n Non e' ovvio: `target_vol` agisce DENTRO il vol-targeting di TP01 (e interagisce") print(" col leverage cap 2x), `k` agisce FUORI su tutto il libro. Due differenze") print(" strutturali attese, e la seconda e' quella che conta.") skh = SL._skyhook_returns() def libro(tv: float, lev: float = 2.0) -> tuple[pd.Series, float, float]: """(serie di libro 0.75*TP01(tv)+0.25*SKH01, scala ottenuta su TP01, quota barre al cap)""" ser, atcap = {}, [] for a in ("BTC", "ETH"): df = resample_1d(load_data(a, "1h")) t = TrendPortfolio(**{**CANONICAL, "target_vol": tv, "leverage": lev}) tgt = np.nan_to_num(np.asarray(t.target_series(df), float)) r = simple_returns(df["close"].values.astype(float)) held = np.zeros(len(tgt)); held[1:] = tgt[:-1] net = held * r - t.fee_side * np.abs(np.diff(held, prepend=0.0)); net[0] = 0.0 ser[a] = pd.Series(np.clip(net, -0.99, None), index=pd.to_datetime(df["datetime"])) atcap.append(float((np.abs(tgt) >= lev - 1e-9).mean())) Jt = pd.concat(ser, axis=1, join="inner").fillna(0.0) tp_s = pd.Series(0.5 * Jt["BTC"].values + 0.5 * Jt["ETH"].values, index=Jt.index) if tp_s.index.tz is None: tp_s.index = tp_s.index.tz_localize("UTC") sk = skh.copy() if sk.index.tz is None: sk.index = sk.index.tz_localize("UTC") B = pd.concat({"TP01": tp_s, "SKH01": sk}, axis=1, join="inner").fillna(0.0) return (W_TP * B["TP01"] + W_SKH * B["SKH01"], float(tp_s.std() / 1.0), float(np.mean(atcap))) base_s, tp_vol_base, _ = libro(0.20) print(f"\n {'via':>28}{'scala TP01':>12}{'vol libro':>11}{'drift':>9}{'Sharpe':>8}" f"{'maxDD':>8}{'peso EFFETTIVO TP01/SKH':>26}{'barre al cap':>14}") def riga(nome: str, s: pd.Series, scala_tp: float, atcap: float, w_eff: str) -> None: v = s.values.astype(float) eq = np.cumprod(1 + v) dd = float((1 - eq / np.maximum.accumulate(eq)).max()) print(f" {nome:>28}{scala_tp:>12.2f}{v.std()*365**.5:>11.2%}{v.mean()*365:>9.2%}" f"{v.mean()/v.std()*365**.5:>8.2f}{dd:>8.1%}{w_eff:>26}{atcap:>14.1%}") riga("k=1.00, target_vol=20% (LIBRO)", base_s, 1.00, 0.0, "75 / 25") for tv in (0.25, 0.30, 0.40, 0.60): s, tpv, ac = libro(tv) sc = tv / 0.20 w = W_TP * sc / (W_TP * sc + W_SKH) riga(f"target_vol={tv:.0%} (solo TP01)", s, sc, ac, f"{w*100:.0f} / {(1-w)*100:.0f}") for k in (1.25, 1.50, 2.00, 3.00): riga(f"k={k:.2f} (tutto il libro)", base_s * k, k, 0.0, "75 / 25") print("\n ❌ NON SONO LA STESSA LEVA, e la ragione NON e' il leverage cap (che infatti non") print(" morde: le barre al cap restano sotto qualche punto percentuale). E':") print(" **`target_vol` scala il 75% del libro e lascia fermo il 25%** -> e'") print(" simultaneamente una LEVA e un TILT DI PESI. A target_vol=30% il libro non e'") print(" piu' 75/25 ma ~82/18, e a 60% e' ~90/10.") print(" Un tilt di pesi in questo progetto ha un gate suo, e non e' opzionale:") dcols = {"TP01": SL._tp01_returns(), "SKH01": skh} for nm, s in dcols.items(): if s.index.tz is None: dcols[nm] = s.tz_localize("UTC") for tv in (0.30, 0.60): sc = tv / 0.20 w = W_TP * sc / (W_TP * sc + W_SKH) try: g = weights_tilt_null(dcols, {"TP01": W_TP, "SKH01": W_SKH}, {"TP01": round(w, 4), "SKH01": round(1 - w, 4)}, n=300) print(f" weights_tilt_null 75/25 -> {w*100:.0f}/{(1-w)*100:.0f} " f"(target_vol {tv:.0%}): gate_pass={g.get('gate_pass')} " f"delta_insample={g.get('delta_insample'):+.4f} pctl_hold={g.get('pctl_hold'):.0f}") except Exception as e: # noqa: BLE001 print(f" weights_tilt_null non girato per tv={tv:.0%}: {type(e).__name__}: {e}") print("\n 📌 CONSEGUENZA CHE LO SCETTICATO NON POTEVA VEDERE dalla sua sezione (5b), che") print(" guardava TP01 DA SOLO: la sua raccomandazione implicita («il knob target_vol") print(" funziona, l'ostacolo e' la config») e' vera sullo sleeve e FALSA sul libro.") print(" Alzare `target_vol` NON e' la strada meno invasiva per alzare k: e' la strada") print(" che aggiunge un cambio di pesi mai giudicato al cambio di scala.") print(" La strada pulita per un puro cambio di SCALA e' il cap di config, che e'") print(" esattamente quella protetta dal test di guardia.") def attacco2b() -> None: """Quali affermazioni pubblicate cambiano, e quali no — misurato, non argomentato.""" from src.portfolio import sleeves as SL sezione("(6-bis) ATTACCO 2c — QUALI AFFERMAZIONI CAMBIANO (e la maggior parte NON cambia)") tp, sk = SL._tp01_returns(), SL._skyhook_returns() if tp.index.tz is None: tp = tp.tz_localize("UTC") if sk.index.tz is None: sk = sk.tz_localize("UTC") B = pd.concat({"TP01": tp, "SKH01": sk}, axis=1, join="inner").fillna(0.0) w = np.array([W_TP, W_SKH]) X = B[["TP01", "SKH01"]].values bk = X @ w cov = np.cov(X.T) var_b = float(bk.var()) rc = w * (cov @ w) / (cov @ w @ w) print("\n NON cambia NIENTE di misurato. Ogni Sharpe, maxDD, muro, traiettoria e rendita") print(" pubblicati sono calcolati sulla SERIE DI RITORNI REALIZZATA, mai sull'etichetta:") print(" `target_vol` entra nel codice come parametro di sizing e non compare in nessuna") print(" formula di rischio a valle. Un'etichetta che sovrastima non rende ottimista un") print(" numero che non la usa.") print("\n CAMBIA una classe di affermazioni sola: quelle che CONFRONTANO un parametro con") print(" una misura. Nel progetto ce n'e' una, ed e' nel libro LIVE:") print(f"\n {'sleeve':>8}{'peso NOZIONALE':>16}{'vol realizzata':>16}" f"{'contributo al RISCHIO':>23}") for i, nm in enumerate(("TP01", "SKH01")): print(f" {nm:>8}{w[i]:>16.0%}{X[:, i].std()*365**.5:>16.2%}{rc[i]:>23.1%}") print(f" {'libro':>8}{1.0:>16.0%}{bk.std()*365**.5:>16.2%}{sum(rc):>23.1%}") print(f" corr TP01/SKH01 {np.corrcoef(X.T)[0,1]:+.3f} vol libro {bk.std()*365**.5:.2%}") sig = X.std(axis=0) * 365 ** 0.5 cf = (W_TP * 0.20) ** 2 / ((W_TP * 0.20) ** 2 + (W_SKH * sig[1]) ** 2) print("\n ❌ IPOTESI MIA, REFUTATA DAL MIO STESSO NUMERO. Mi aspettavo che l'etichetta") print(" nascondesse una ripartizione del rischio molto diversa dai pesi di nozionale.") print(f" Non e' cosi': {rc[0]*100:.0f}/{rc[1]*100:.0f} contro {W_TP*100:.0f}/{W_SKH*100:.0f}" f" — coincidono entro un paio di punti, e per una") print(" ragione che si legge in una riga: contributi di rischio ~ (w·σ)² con corr") print(f" {np.corrcoef(X.T)[0,1]:+.2f} ~ 0, e {W_TP:.2f}×{sig[0]:.1%} = {W_TP*sig[0]:.2%} contro" f" {W_SKH:.2f}×{sig[1]:.1%} = {W_SKH*sig[1]:.2%}") print(" stanno gia' in rapporto ~75/25. **Coincidenza numerica, non struttura.**") print("\n 📌 IL FATTO CHE RESTA, ed e' piu' utile dell'ipotesi che ho perso: l'errore non") print(" lo commette chi legge i PESI, lo commette chi legge l'ETICHETTA. Un lettore che") print(f" prendesse `target_vol=20%` per buono calcolerebbe TP01 al {cf*100:.0f}% del rischio") print(f" di libro; il vero e' {rc[0]*100:.0f}%. L'etichetta non rende ottimista nessun numero") print(" pubblicato (nessuno la usa) — sposta di ~16 punti l'ATTRIBUZIONE del rischio,") print(" cioe' proprio la domanda 'quale gamba mi fa male' su cui il progetto ha gia'") print(" tre misure indipendenti che indicano SKH01 come la fragile (LOO d'ancora,") print(" sensibilita' alla fee 4x, vol realizzata piu' alta).") print("\n E si precisa una frase: «SKH01 e' l'unica delle 5 a NON essere vol-targeted» e'") print(" esatta sul CODICE e va letta insieme al fatto che e' anche quella che di vol ne") print(" realizza di PIU' (21,0% contro 12,1%). Non e' una contraddizione: e' il motivo per") print(" cui il peso 25% e' stato confermato tre volte all'estremo prudente.") def guardia() -> None: sezione("(7) IL VINCOLO DURO — cosa protegge davvero il test di guardia") cfg = json.loads((ROOT / "config" / "live.json").read_text()) frac = cfg["max_notional_per_asset_frac"] print(f""" `config/live.json`: max_notional_per_asset_frac = {frac} su 2 asset (BTC, ETH) -> nozionale LORDO massimo = {frac*2:.2f}x l'equity. k > {frac*2:.2f}x oggi NON e' eseguibile. `tests/test_fee_sensitivity.py::test_leva_massima_da_config_resta_sotto_o_uguale_a_1x` asserisce `frac * len(ASSETS) <= 1.0` E `disaster_sl_pct > 0`. ⚠️ Cio' che protegge NON e' "la leva e' pericolosa": e' la CONCLUSIONE PUBBLICATA del 26/07 secondo cui la **liquidation fee 1%** introdotta da Deribit dal 2026-08-01 e' irrilevante per questo libro. Quella conclusione poggia interamente sul fatto che a nozionale lordo <= 1x servirebbe un movimento avverso ~100% per liquidare. Se il cap sale, la liquidation fee torna nel conto e `r0726_fee_sensitivity` va rifatto. Il test e' quindi un promemoria di DIPENDENZA, non un divieto: chi alza il cap deve rifare quell'analisi, non aggirare l'assert. Nota di taglia, che vale piu' del cap: il conto e' ~$635 e `min_order_usd` = {cfg['min_order_usd']}. Alzare k moltiplica anche il TURNOVER, e il progetto ha gia' misurato (02/07) che a questo capitale il min-order $5 e' GIA' la banda di ribilanciamento ottimale.""") if __name__ == "__main__": main() attacco2() attacco2b() guardia() print(f"\n▸ totale: consegna nel messaggio dell'agente.")