Files
PythagorasGoal/scripts/research/r0822b_tp01_sinistro.py
T

955 lines
53 KiB
Python

"""r0822b_tp01_sinistro — quanto costa l'ASSICURAZIONE TP01 su un conto a barriera (2026-08-22).
IL BUCO CHE CHIUDE
------------------
`r0822_prop_alloc.py` (stessa ondata) ha ottimizzato l'allocazione di un conto **funded** sotto
barriera di drawdown e ha trovato l'ottimo a **TP01 25 / SKH01 25 / XS01 50**, cioe' TP01 pesato
**tre volte meno** del libro live (75%). Ha anche dichiarato il proprio limite:
"la sotto-pesatura di TP01, lo sleeve DIFENSIVO, e' misurata su un campione SENZA SINISTRO
(2024-2026 non contiene un crash): su una barriera assorbente quella scommessa si paga una
volta sola."
E questo si scontra con due misure del progetto:
(a) il **leave-one-out de-luckato** del 26/07: TP01 e' il maggior contributore al Sharpe FULL
(+0.390, positivo nel 100% delle estrazioni) e il suo hold-out negativo (-0.200, negativo
nel 99.1%) e' *la firma dell'assicurazione, non della morte* — "uno sleeve difensivo si
giudica sul SINISTRO, non sul premio";
(b) `edge_watch` criterio B: negli **8 anni su 8** con drawdown buy&hold > 10% TP01 ha protetto
da **1.8x a 34.4x**.
DOMANDA: qual e' il peso giusto di TP01 se l'obiettivo include sopravvivere a un crash che nel
campione di ottimizzazione NON c'e'?
IPOTESI REGISTRATA PRIMA DI MISURARE
------------------------------------
Mi aspettavo che (1) la sotto-pesatura di TP01 costasse molto **nel sinistro** e poco fuori, e che
(2) esistesse un peso di compromesso (~50%) dentro la regione robusta del funded e non
catastrofico al crash. In altre parole mi aspettavo di **confermare** il caveat di PROP-ALLOC.
Se la misura dice il contrario, e' un risultato e va scritto come tale.
CIO' CHE E' UN'ASSUNZIONE E CIO' CHE E' UN DATO
-----------------------------------------------
* Lo **scenario di stress e' uno SCENARIO, non una previsione.** Due costruzioni, entrambe
dichiarate: (S1) bootstrap MISTO — ogni blocco ha probabilita' `p` di essere estratto dal pool
dei sinistri 2019-2023 invece che dalla finestra normale; (S2) **replay** — un episodio di
crash VERO innestato per intero, in ordine, a una data casuale dentro l'anno. In piu' il
replay deterministico del **2022 intero** (aprile-dicembre), che non e' un modello: e' successo.
* Il vincolo binding di TUTTO il filone e' che i sinistri osservati sono **QUATTRO**. Nessun
bootstrap crea informazione: l'ampiezza effettiva della coda resta 4 episodi, e ogni numero qui
eredita quell'errore. Detto una volta, vale per ogni tabella.
* Lente **ACCOPPIATA** (`r0725_prop_coupled`): il minimo intraday si compone ESATTO sul path
orario condiviso TP01/SKH01. Non e' un dettaglio: su una regola a UN giorno (daily-loss) la
lente close-only e' misurata **esattamente cieca** (0.00 breach contro 0.40-1.21 veri), mentre
su una statistica multi-giorno l'errore vale il 3.5%. Il vincolo funded E' una regola a un giorno.
* de-luck sul drift **x0.89** (fattore MISURATO il 26/07). ⚠️ L'offset di de-luck e' calcolato UNA
VOLTA sulla finestra 2019+ e sottratto ovunque in valore ASSOLUTO: de-luckare un pool di crash
sulla sua PROPRIA media (negativa) *aggiungerebbe* drift proprio dove non deve.
* macchineria RIUSATA, non riscritta: `r0725_prop_coupled` (tuple accoppiate) e `r0822_prop_alloc`
(`eval_sim`, `funded_sim`, `boot_idx`, `FIRMS`). Qui si aggiungono solo i pool e gli innesti.
Uso: `nice -n 19 timeout 900 uv run python scripts/research/r0822b_tp01_sinistro.py`
"""
from __future__ import annotations
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"))
from src.data.downloader import load_data # noqa: E402
from src.portfolio.sleeves import _tp01_returns, _skyhook_returns # noqa: E402
import r0725_prop_coupled as pc # noqa: E402
import r0822_prop_alloc as PA # noqa: E402
# ------------------------------------------------------------------ costanti DICHIARATE
DELUCK = PA.DELUCK # 0.89, misurato il 26/07
FULL_START = "2019-03-14" # inizio del recon crypto (TP01+SKH01)
RECENT_START = "2024-01-01" # finestra di ottimizzazione di PROP-ALLOC (nasce XS01)
BLOCK = 20 # blocco del bootstrap, in giorni
N_PATHS = 2500
EPI_LEN = 30 # lunghezza di un episodio di sinistro, in giorni
HORIZON = 365
LEVS = (0.25, 0.375, 0.50, 0.625, 0.75, 1.00, 1.25, 1.50)
W_GRID = tuple(i / 8 for i in range(9)) # peso di TP01 nella coppia crypto, 0 -> 1
SURV_TARGET = 0.90 # livello di sopravvivenza della frontiera ISO-SOPRAVVIVENZA
N_EVAL = 0 # contatore onesto delle valutazioni (griglia dichiarata)
def hr(t: str = "") -> None:
print("\n" + "-" * 106)
if t:
print(f" {t}")
print("-" * 106)
# ================================================================== §A. PANNELLO
_TUP: dict = {}
def crypto(w_tp: float) -> tuple[np.ndarray, np.ndarray, pd.DatetimeIndex]:
"""(R, gap) giornalieri della coppia crypto a peso TP01 = w_tp, SKH01 = 1-w_tp.
gap = minimo_intraday - chiusura (<=0). Storia piena dal 2019-03."""
k = round(float(w_tp), 6)
if k not in _TUP:
c = pc.crypto_daily_tuples(k, 1.0 - k)
R = c["R"].values.astype(float)
G = np.minimum(c["m"].values, R) - R
_TUP[k] = (R, G, c.index)
R, G, I = _TUP[k]
return R.copy(), G.copy(), I
_OFF: dict = {}
def deluck_offset(w_tp: float, factor: float = DELUCK) -> float:
"""Offset ASSOLUTO di de-luck, calcolato UNA VOLTA sulla finestra 2019+ e riusato ovunque.
Calcolarlo sulla media di un sotto-campione di crash (negativa) *aggiungerebbe* drift."""
key = (round(float(w_tp), 6), round(float(factor), 4))
if key not in _OFF:
R, _, I = crypto(w_tp)
_OFF[key] = float((1.0 - factor) * R[I >= pd.Timestamp(FULL_START, tz="UTC")].mean())
return _OFF[key]
def buyhold_daily(idx: pd.DatetimeIndex) -> pd.Series:
"""Buy&hold 50/50 BTC+ETH sulla stessa griglia giornaliera del libro."""
bh = None
for a in ("BTC", "ETH"):
d = load_data(a, "1h")
i = pd.DatetimeIndex(pd.to_datetime(d["datetime"], utc=True))
s = pd.Series(d["close"].astype(float).values, index=i).resample("1D").last()
r = s.pct_change().fillna(0.0) * 0.5
bh = r if bh is None else bh.add(r, fill_value=0.0)
return bh.reindex(idx).fillna(0.0)
def worst_windows(lbh: np.ndarray, idx: pd.DatetimeIndex, K: int, L: int,
hi: int | None = None) -> list[int]:
"""Le K finestre di L giorni NON sovrapposte col peggior rendimento buy&hold, greedy sul
minimo. `hi` limita i possibili inizi (per restare dentro 2019-2023)."""
cs = np.concatenate(([0.0], np.cumsum(lbh)))
win = np.expm1(cs[L:] - cs[:-L])
cand = np.arange(len(win)) if hi is None else np.arange(min(hi, len(win)))
order = cand[np.argsort(win[cand])]
out: list[int] = []
for t in order:
if all(abs(int(t) - q) >= L for q in out):
out.append(int(t))
if len(out) == K:
break
return sorted(out)
def binom_two_sided(k: int, n: int) -> float:
"""p-value esatto a due code di un test dei segni con p0=0.5 (n piccolo: si conta)."""
from math import comb
tot = 2 ** n
kk = max(k, n - k)
tail = sum(comb(n, j) for j in range(kk, n + 1))
return min(1.0, 2.0 * tail / tot)
def maxdd(r: np.ndarray) -> float:
eq = np.cumprod(1.0 + r)
return float((eq / np.maximum.accumulate(eq) - 1.0).min())
# ================================================================== §B. SIMULAZIONE
def J_of(R: np.ndarray, G: np.ndarray, ev_idx: np.ndarray, fu_idx: np.ndarray,
lev: float, firm: str = "HYRO") -> dict:
"""J = P(passare l'eval) x P(essere vivo a 12 mesi da funded), lente ACCOPPIATA."""
global N_EVAL
N_EVAL += 1
e = PA.eval_sim(R, G, ev_idx, lev, firm, "coupled")
f = PA.funded_sim(R, G, fu_idx, lev, firm, 100_000.0, "coupled")
return dict(J=e["p_pass"] * f["p_alive"], p_pass=e["p_pass"], p_alive=f["p_alive"],
e_payout=f["e_payout"], jvec=(e["passed"] & f["alive"]).astype(float),
alive=f["alive"])
def best_lev(R: np.ndarray, G: np.ndarray, ev_idx, fu_idx, firm: str = "HYRO") -> tuple:
rows = [(lv, J_of(R, G, ev_idx, fu_idx, lv, firm)) for lv in LEVS]
return max(rows, key=lambda x: x[1]["J"])
def pool_paths(seg_bounds: list[tuple[int, int]], n_days: int, n_paths: int, seed: int,
block: int = BLOCK) -> np.ndarray:
"""Indici di bootstrap a blocchi ristretti a segmenti CONTIGUI (ogni blocco vive dentro un
solo segmento -> non si incollano giorni di episodi diversi)."""
rng = np.random.default_rng(seed)
starts = []
for a, b in seg_bounds:
if b - a >= block:
starts.extend(range(a, b - block + 1))
starts = np.array(starts)
nb = int(np.ceil(n_days / block))
st = starts[rng.integers(0, len(starts), size=(n_paths, nb))]
return (st[:, :, None] + np.arange(block)[None, None, :]).reshape(n_paths, -1)[:, :n_days]
def mix_paths(base_idx: np.ndarray, crash_starts: np.ndarray, p: float, seed: int,
block: int = BLOCK) -> np.ndarray:
"""S1 — bootstrap MISTO: ogni blocco della path normale ha probabilita' `p` di essere
sostituito da un blocco estratto dal pool dei SINISTRI."""
rng = np.random.default_rng(seed)
out = base_idx.copy()
P, D = out.shape
nb = int(np.ceil(D / block))
hit = rng.random((P, nb)) < p
for b in range(nb):
sl = slice(b * block, min((b + 1) * block, D))
L = sl.stop - sl.start
rows = np.where(hit[:, b])[0]
if len(rows) == 0:
continue
st = crash_starts[rng.integers(0, len(crash_starts), size=len(rows))]
out[rows, sl] = st[:, None] + np.arange(L)[None, :]
return out
def splice_paths(base_idx: np.ndarray, epi_idx: np.ndarray, seed: int) -> np.ndarray:
"""S2 — REPLAY: l'episodio VERO (contiguo, in ordine) innestato una volta sola a una data
casuale dentro l'orizzonte. E' la domanda operativa: *se il sinistro arriva*."""
rng = np.random.default_rng(seed)
out = base_idx.copy()
P, D = out.shape
L = len(epi_idx)
st = rng.integers(0, max(D - L, 1), size=P)
for i in range(P):
a = int(st[i])
out[i, a:a + L] = epi_idx[:min(L, D - a)]
return out
# ================================================================== main
def main() -> None: # noqa: C901
try:
sys.stdout.reconfigure(line_buffering=True)
except Exception: # pragma: no cover
pass
t0 = time.time()
def tick(lab: str) -> None:
print(f" [{time.time()-t0:6.0f}s] {lab} (valutazioni finora: {N_EVAL})")
print("=" * 106)
print(" TP01-SINISTRO — quanto costa l'assicurazione su un conto a barriera, e quanto rende")
print(" quando il sinistro arriva. Lente ACCOPPIATA, firm HYRO.")
print("=" * 106)
R75, G75, IDX = crypto(0.75)
n_full = len(IDX)
i_rec = int(np.searchsorted(IDX, pd.Timestamp(RECENT_START, tz="UTC")))
print(f"\n finestra crypto piena: {n_full} giorni {IDX[0].date()} -> {IDX[-1].date()}")
print(f" finestra di ottimizzazione di PROP-ALLOC: {n_full - i_rec} giorni dal "
f"{IDX[i_rec].date()} (e' anche la finestra di SCOPERTA di XS01)")
# --------------------------------------------------------- sanity: recon vs sleeve ufficiale
print("\n SANITY — il recon orario contro gli sleeve ufficiali. TP01/SKH01 NON coincidono")
print(" barra per barra per costruzione (il recon e' un mark-to-market ORARIO, lo sleeve")
print(" marca a fine-trade): il controllo giusto e' il log-totale.")
for nm, off_s, w in (("TP01", _tp01_returns(), 1.0), ("SKH01", _skyhook_returns(), 0.0)):
o = off_s.dropna().sort_index()
if o.index.tz is None:
o.index = o.index.tz_localize("UTC")
Rn, _, In = crypto(w)
com = o.index.intersection(In)
a = o.reindex(com).values
b = pd.Series(Rn, index=In).reindex(com).values
print(f" {nm:>6} log-tot sleeve {float(np.log1p(a).sum()):+8.4f} "
f"recon {float(np.log1p(b).sum()):+8.4f} scarto "
f"{float(np.log1p(a).sum()-np.log1p(b).sum()):+7.4f}")
# ============================================================ §1. CHE COS'E' UN SINISTRO
hr("§1. CHE COS'E' UN SINISTRO — e la frase '2024-2026 non contiene un crash' e' FALSA")
bh = buyhold_daily(IDX)
lbh = np.log1p(bh.values)
print(" Peggior rendimento buy&hold 50/50 su finestra mobile, per periodo:")
print(f" {'W':>4} {'2019-2023 min':>15} {'2024+ min':>12} {'rapporto':>10} "
f"{'2019-23 p1':>11} {'2024+ p1':>10} {'data del min 2024+':>20}")
ratios = []
for W in (1, 5, 10, 20, 60):
cs = np.concatenate(([0.0], np.cumsum(lbh)))
roll = np.expm1(cs[W:] - cs[:-W]) # finestra che FINISCE in t=W-1+k
tt = IDX[W - 1:]
pre = roll[tt < pd.Timestamp(RECENT_START, tz="UTC")]
post = roll[tt >= pd.Timestamp(RECENT_START, tz="UTC")]
tpost = tt[tt >= pd.Timestamp(RECENT_START, tz="UTC")]
rr = pre.min() / post.min()
ratios.append(rr)
print(f" {W:>4} {pre.min():>15.1%} {post.min():>12.1%} {rr:>10.2f}x "
f"{np.percentile(pre, 1):>11.1%} {np.percentile(post, 1):>10.1%} "
f"{str(tpost[int(np.argmin(post))].date()):>20}")
print(f"\n -> il 2024+ **contiene** un crash: peggior giorno -14.5%, peggiori 20g -39.6%,")
print(f" drawdown buy&hold fino a -60%. Cio' che NON contiene e' un crash della TAGLIA")
print(f" dei precedenti: il rapporto e' {np.mean(ratios[:3]):.1f}x sulla coda veloce (1-10g)")
print(f" e {ratios[-1]:.1f}x a 60 giorni. **La frase giusta non e' 'senza sinistro' ma")
print(f" 'con un sinistro di taglia dimezzata'** — e cambia la domanda: non 'cosa succede")
print(f" se arriva un crash' ma 'cosa succede se ne arriva uno DUE VOLTE piu' grande'.")
# ----- replica del criterio B di edge_watch (protezione per anno)
print("\n Criterio B di `edge_watch` replicato qui (anni con DD buy&hold > 10%):")
tp_only, _, _ = crypto(1.0)
ser_bh = pd.Series(bh.values, index=IDX)
ser_tp = pd.Series(tp_only, index=IDX)
print(f" {'anno':>6} {'DD b&h':>9} {'DD TP01':>9} {'protezione':>12} {'DD SKH01':>10} "
f"{'DD 75/25':>10}")
skh_only, _, _ = crypto(0.0)
ser_sk = pd.Series(skh_only, index=IDX)
ser_75 = pd.Series(R75, index=IDX)
n_sin = n_ok = 0
for y, g in ser_bh.groupby(ser_bh.index.year):
d_bh = maxdd(g.values)
if d_bh > -0.10:
continue
n_sin += 1
d_tp = maxdd(ser_tp.loc[g.index].values)
d_sk = maxdd(ser_sk.loc[g.index].values)
d_75 = maxdd(ser_75.loc[g.index].values)
ok = (d_tp / d_bh) <= 0.75
n_ok += ok
print(f" {y:>6} {d_bh:>9.1%} {d_tp:>9.1%} {d_bh/d_tp:>11.1f}x {d_sk:>10.1%} "
f"{d_75:>10.1%} {'ok' if ok else 'FAIL'}")
print(f" -> anni di sinistro {n_sin}, superati {n_ok}. **Ogni anno e' un anno di sinistro")
print(f" per il buy&hold**: la 'frequenza del sinistro' non e' un evento raro, e' lo")
print(f" stato normale dell'asset. Cio' che e' raro e' la TAGLIA (riga sopra).")
# ----- definizione degli episodi
hr("§1-bis. GLI EPISODI DI SINISTRO — regola di selezione DICHIARATA PRIMA")
print(f" Regola: le {3} finestre di {EPI_LEN} giorni NON sovrapposte con il peggior rendimento")
print(f" buy&hold nel 2019-2023 (greedy sul minimo), PIU' l'FTX 2022-11-05 aggiunto **per")
print(f" NOME** (evento noto, non selezionato dal minimo: e' un controllo, non una scelta).")
cs = np.concatenate(([0.0], np.cumsum(lbh)))
win = np.expm1(cs[EPI_LEN:] - cs[:-EPI_LEN]) # finestra che INIZIA in t
pre_mask = IDX[:len(win)] < pd.Timestamp(RECENT_START, tz="UTC")
cand = np.where(pre_mask)[0]
order = cand[np.argsort(win[cand])]
picked = []
for t in order:
if all(abs(int(t) - p) >= EPI_LEN for p in picked):
picked.append(int(t))
if len(picked) == 3:
break
t_ftx = int(np.searchsorted(IDX, pd.Timestamp("2022-11-05", tz="UTC")))
epis = sorted(picked + [t_ftx])
NAMES = {}
print(f"\n {'episodio':>12} {'dal':>12} {'al':>12} {'b&h 30g':>10} {'peggior 1g b&h':>16}")
for t in epis:
lab = ("COVID" if IDX[t].year == 2020 else
"MAG-2021" if (IDX[t].year, IDX[t].month) == (2021, 5) else
"LUNA" if (IDX[t].year, IDX[t].month) in ((2022, 5), (2022, 6)) else
"FTX" if (IDX[t].year, IDX[t].month) == (2022, 11) else f"EPI-{IDX[t].date()}")
NAMES[t] = lab
seg = bh.values[t:t + EPI_LEN]
print(f" {lab:>12} {str(IDX[t].date()):>12} {str(IDX[t+EPI_LEN-1].date()):>12} "
f"{win[t]:>10.1%} {seg.min():>16.1%}")
crash_days = np.concatenate([np.arange(t, t + EPI_LEN) for t in epis])
freq_day = len(crash_days) / n_full
print(f"\n giorni di sinistro {len(crash_days)} su {n_full} = **{freq_day:.1%} del tempo**")
print(f" ⚠️ AMPIEZZA EFFETTIVA DELLA CODA = **4 EPISODI**. Nessun bootstrap la aumenta.")
print(f" Ogni numero di questo file eredita l'errore di un campione di quattro eventi.")
tick("§1 sinistri")
# ============================================================ §2. IL CONTO DELL'ASSICURAZIONE
hr("§2. IL CONTO DELL'ASSICURAZIONE — premio fuori dal sinistro, indennizzo dentro")
print(" Confronto DUE ripartizioni della coppia crypto, appaiato giorno per giorno:")
print(" LIVE = TP01 75 / SKH01 25 (il libro che gira)")
print(" OPT = TP01 38 / SKH01 62 (l'ottimo di PROP-ALLOC sul campione 2019+)")
in_sin = np.zeros(n_full, bool)
in_sin[crash_days] = True
print(f"\n {'config':>10} {'drift fuori':>12} {'drift dentro':>13} {'Sharpe full':>12} "
f"{'maxDD':>8} {'peggior 1g':>11} {'peggior min 1g':>15}")
prof = {}
for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375), ("TP01 puro", 1.0),
("SKH01 puro", 0.0)):
R, G, _ = crypto(w)
R = R - deluck_offset(w)
out_d = R[~in_sin].mean() * 365.0
in_d = R[in_sin].mean() * 365.0
vol = R.std() * np.sqrt(365.0)
prof[lab] = dict(out=out_d, ind=in_d, sh=R.mean() * 365.0 / vol, dd=maxdd(R),
w1=R.min(), wm=(R + G).min())
p = prof[lab]
print(f" {lab:>10} {p['out']:>12.1%} {p['ind']:>13.1%} {p['sh']:>12.2f} {p['dd']:>8.1%} "
f"{p['w1']:>11.2%} {p['wm']:>15.2%}")
prem = prof["LIVE 75/25"]["out"] - prof["OPT 38/62"]["out"]
ind = prof["LIVE 75/25"]["ind"] - prof["OPT 38/62"]["ind"]
R_live = crypto(0.75)[0] - deluck_offset(0.75)
R_opt = crypto(0.375)[0] - deluck_offset(0.375)
cum_live = float(np.expm1(np.log1p(R_live[in_sin]).sum()))
cum_opt = float(np.expm1(np.log1p(R_opt[in_sin]).sum()))
print(f"\n PREMIO (LIVE - OPT) fuori dal sinistro : {prem:+.2%}/anno = "
f"{prem*100_000:+,.0f} $/anno su un funded da $100k")
print(f" INDENNIZZO (LIVE - OPT) sui 120 giorni di sinistro, CUMULATO (non annualizzato,")
print(f" che su 120 giorni sarebbe una finta precisione): LIVE {cum_live:+.1%} contro "
f"OPT {cum_opt:+.1%} = {cum_live-cum_opt:+.1%}")
print(f" su un funded da $100k: {(cum_live-cum_opt)*100_000:+,.0f} $ per l'INTERO "
f"monte-sinistri di {n_full/365.0:.1f} anni")
print(f" frequenza: {freq_day:.1%} del tempo dentro un sinistro (4 episodi in "
f"{n_full/365.0:.1f} anni = uno ogni {n_full/365.0/4:.1f} anni)")
if ind < 0:
print("\n 🚨 **IL SEGNO E' ROVESCIATO RISPETTO ALLA MIA IPOTESI.** Dentro il sinistro il")
print(" libro con MENO TP01 fa MEGLIO, non peggio. Il meccanismo si legge in una riga:")
print(" TP01 nel crash va FLAT (non perde, non guadagna: e' assicurazione), SKH01 va")
print(" SHORT (guadagna). Su un conto a barriera *guadagnare batte non-perdere*.")
print("\n Rendimento dei 4 episodi, per peso di TP01 (30 giorni ciascuno, chiusura e MINIMO):")
print(f" {'w TP01':>8} " + " ".join(f"{NAMES[t]:>18}" for t in epis) + f" {'peggior min':>13}")
for w in (1.0, 0.75, 0.5, 0.375, 0.25, 0.0):
R, G, _ = crypto(w)
cells = []
wm = 0.0
for t in epis:
seg = R[t:t + EPI_LEN]
gp = G[t:t + EPI_LEN]
eq = np.cumprod(1.0 + seg)
mn = float(np.minimum(eq - 1.0, np.concatenate(([1.0], eq[:-1])) * (1 + seg + gp) - 1.0).min())
wm = min(wm, mn)
cells.append(f"{float(eq[-1]-1.0):+7.1%}/{mn:+7.1%}")
print(f" {w:>8.3f} " + " ".join(f"{c:>18}" for c in cells) + f" {wm:>13.1%}")
print(" (formato: chiusura a 30g / minimo intraday cumulato dell'episodio)")
print("\n ⚠️ QUANTA POTENZA HA QUESTO SEGNO? Con 4 episodi, '4 su 4 a favore' ha un test dei")
print(" segni con p = 0.125 a due code: **non e' significativo al 5%**. Il modo onesto di")
print(" guadagnare potenza non e' ripetere il bootstrap (non crea eventi) ma ESTENDERE la")
print(" stessa regola di selezione a piu' finestre. Le 12 peggiori finestre di 30g NON")
print(" sovrapposte su TUTTO il 2019+ (stessa regola greedy, K da 3 a 12):")
w12 = worst_windows(lbh, IDX, 12, EPI_LEN)
print(f"\n {'finestra':>12} {'b&h 30g':>9} {'LIVE 75/25':>11} {'OPT 38/62':>11} "
f"{'TP01 puro':>10} {'SKH01 puro':>11} {'OPT>LIVE':>9}")
wins = 0
wins_sk = 0
lr_live, lr_opt = np.log1p(R_live), np.log1p(R_opt)
R_tp = crypto(1.0)[0] - deluck_offset(1.0)
R_sk = crypto(0.0)[0] - deluck_offset(0.0)
for t in w12:
a = float(np.expm1(lr_live[t:t + EPI_LEN].sum()))
b = float(np.expm1(lr_opt[t:t + EPI_LEN].sum()))
c = float(np.expm1(np.log1p(R_tp[t:t + EPI_LEN]).sum()))
d = float(np.expm1(np.log1p(R_sk[t:t + EPI_LEN]).sum()))
wins += b > a
wins_sk += d > c
print(f" {str(IDX[t].date()):>12} {win[t]:>9.1%} {a:>11.1%} {b:>11.1%} "
f"{c:>10.1%} {d:>11.1%} {('si' if b > a else 'no'):>9}")
pre = [t for t in w12 if IDX[t] < pd.Timestamp(RECENT_START, tz="UTC")]
wins_pre = sum(1 for t in pre
if float(np.expm1(lr_opt[t:t + EPI_LEN].sum()))
> float(np.expm1(lr_live[t:t + EPI_LEN].sum())))
print(f"\n OPT batte LIVE in **{wins}/12** finestre -> test dei segni "
f"p = {binom_two_sided(wins, 12):.4f}")
print(f" SKH01 puro batte TP01 puro in **{wins_sk}/12** -> p = "
f"{binom_two_sided(wins_sk, 12):.4f}")
print(f" ⚠️ {12-len(pre)} di queste 12 finestre stanno nel 2024+, cioe' DENTRO la finestra")
print(" su cui SKH01 e' stato tarato e su cui PROP-ALLOC ha ottimizzato: la potenza sale")
print(" ma l'indipendenza scende. **Sul solo sotto-campione pre-2024**, che e' quello")
print(f" indipendente: OPT batte LIVE in **{wins_pre}/{len(pre)}** -> p = "
f"{binom_two_sided(wins_pre, len(pre)):.4f}. E' questo il numero da citare.")
tick("§2 conto assicurazione")
# ============================================================ §3. SOTTO BARRIERA
hr("§3. SOTTO BARRIERA — griglia peso TP01 x leva su TRE lenti di campione")
print(f" {len(W_GRID)} pesi x {len(LEVS)} leve x 3 finestre. J = P(pass) x P(vivo 12m), HYRO")
print(f" (eval +10%/-6% statico/-4% giornaliero; funded -6% statico/-4% giornaliero).")
ev_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8230)
fu_f = PA.boot_idx(n_full, HORIZON, N_PATHS, seed=8231)
n_rec = n_full - i_rec
ev_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8240)
fu_r = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8241)
seg_sin = [(t, t + EPI_LEN) for t in epis]
ev_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8250)
fu_s = pool_paths(seg_sin, HORIZON, N_PATHS, seed=8251)
print(f"\n {'w TP01':>7} | {'2019+ (col 2022)':>26} | {'2024+ (PROP-ALLOC)':>26} | "
f"{'SOLO-SINISTRO':>26}")
print(f" {'':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | {'leva':>6} {'J':>7} {'P(viv)':>7} | "
f"{'leva':>6} {'J':>7} {'P(viv)':>7}")
tab = {}
for w in W_GRID:
R, G, _ = crypto(w)
off = deluck_offset(w)
Rf, Gf = R - off, G
Rr, Gr = Rf[i_rec:], Gf[i_rec:]
lv1, o1 = best_lev(Rf, Gf, ev_f, fu_f)
lv2, o2 = best_lev(Rr, Gr, ev_r, fu_r)
lv3, o3 = best_lev(Rf, Gf, ev_s, fu_s)
tab[w] = dict(full=(lv1, o1), rec=(lv2, o2), sin=(lv3, o3))
print(f" {w:>7.3f} | {lv1:>6.3f} {o1['J']:>7.3f} {o1['p_alive']:>7.1%} | "
f"{lv2:>6.3f} {o2['J']:>7.3f} {o2['p_alive']:>7.1%} | "
f"{lv3:>6.3f} {o3['J']:>7.3f} {o3['p_alive']:>7.1%}")
a_full = max(tab, key=lambda w: tab[w]["full"][1]["J"])
a_rec = max(tab, key=lambda w: tab[w]["rec"][1]["J"])
a_sin = max(tab, key=lambda w: tab[w]["sin"][1]["J"])
print(f"\n argmax: 2019+ -> TP01 {a_full:.3f} | 2024+ -> TP01 {a_rec:.3f} | "
f"SOLO-SINISTRO -> TP01 {a_sin:.3f}")
print(f" ⚠️ RISOLUZIONE: un LIVELLO di J ha errore MC ~{np.sqrt(0.25/N_PATHS):.3f} "
f"(N={N_PATHS}) -> celle entro ~0.02 NON sono distinguibili, e infatti 0.375 e 0.500")
print(" si scambiano il posto fra semi diversi. La lettura e' 'la regione 0.25-0.50',")
print(" non 'il punto 0.375' (stessa lezione del null dei tilt casuali di PROP-ALLOC).")
print(" ⚠️ La colonna SOLO-SINISTRO e' una lente ESTREMA e va letta come tale: l'anno intero")
print(" e' fatto di soli blocchi di crash (4 episodi ricampionati). Non e' uno scenario")
print(" plausibile, e' il **limite superiore del danno**: se il verso della risposta e' lo")
print(" stesso qui e sul 2019+, non dipende dalla frazione di sinistro assunta.")
# replica di controllo del numero pubblicato da PROP-ALLOC (E-ter)
print("\n REPLICA DI CONTROLLO — PROP-ALLOC E-ter pubblica, sul 2019+: argmax della coppia")
print(" crypto **38/62 @0.50x con J 0.521**, e **75/25 J 0.443**. Stesse seed, stessa lente:")
j38 = tab[0.375]["full"]
j75 = tab[0.75]["full"]
print(f" qui: 38/62 @{j38[0]:g}x J {j38[1]['J']:.3f} | 75/25 @{j75[0]:g}x "
f"J {j75[1]['J']:.3f} -> "
f"{'REPLICA OK' if abs(j38[1]['J']-0.521) < 0.02 and abs(j75[1]['J']-0.443) < 0.02 else 'NON REPLICA — leggere con cautela'}")
tick("§3 griglia 3 lenti")
# ============================================================ §4. LO STRESS ESPLICITO
hr("§4. LO STRESS ESPLICITO — iniettare nel 2024+ i crash che il 2024+ non ha")
print(" Costruzione DICHIARATA. Base = blocchi del 2024+ (la finestra di ottimizzazione).")
print(" S1 MISTO: ogni blocco ha probabilita' p di venire dal pool dei 4 sinistri 2019-2023.")
print(f" p = {freq_day:.3f} e' la frequenza STORICA; si girano anche 2x e 4x.")
print(" S2 REPLAY: un episodio VERO innestato per intero, in ordine, a data casuale dentro")
print(" l'anno. E' la domanda operativa: *se il sinistro arriva, chi resta in piedi?*")
print(" ⚠️ Sono SCENARI, non previsioni. E poggiano su 4 episodi.")
# pool = 2024+ (blocchi normali) + gli episodi in coda, su un indice unico
def stress_panel(w: float) -> tuple[np.ndarray, np.ndarray, np.ndarray, list]:
R, G, _ = crypto(w)
off = deluck_offset(w)
Rn, Gn = R[i_rec:] - off, G[i_rec:]
segs = []
Rc, Gc = [], []
cur = len(Rn)
for t in epis:
Rc.append(R[t:t + EPI_LEN] - off)
Gc.append(G[t:t + EPI_LEN])
segs.append((cur, cur + EPI_LEN))
cur += EPI_LEN
Rp = np.concatenate([Rn] + Rc)
Gp = np.concatenate([Gn] + Gc)
return Rp, Gp, np.array([s for s, _ in segs]), segs
base_ev = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8260)
base_fu = PA.boot_idx(n_rec, HORIZON, N_PATHS, seed=8261)
print(f"\n S1 — bootstrap MISTO (base 2024+, blocchi di sinistro con probabilita' p)")
print(f" {'w TP01':>7} | " + " | ".join(f"p={p:.3f}: {'J':>6} {'P(viv)':>7}"
for p in (0.0, freq_day, 2 * freq_day, 4 * freq_day)))
s1 = {}
for w in W_GRID:
Rp, Gp, cst, segs = stress_panel(w)
starts = np.concatenate([np.arange(a, b - BLOCK + 1) for a, b in segs])
cells = []
for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)):
ev = base_ev if p == 0 else mix_paths(base_ev, starts, p, seed=8270 + j)
fu = base_fu if p == 0 else mix_paths(base_fu, starts, p, seed=8280 + j)
lv, o = best_lev(Rp, Gp, ev, fu)
cells.append((lv, o))
s1[w] = cells
print(f" {w:>7.3f} | " + " | ".join(f" @{c[0]:.3g}x {c[1]['J']:>6.3f} "
f"{c[1]['p_alive']:>7.1%}" for c in cells))
for j, p in enumerate((0.0, freq_day, 2 * freq_day, 4 * freq_day)):
am = max(s1, key=lambda w: s1[w][j][1]["J"])
print(f" p={p:.3f} -> argmax TP01 {am:.3f} (J {s1[am][j][1]['J']:.3f}; "
f"75/25 fa {s1[0.75][j][1]['J']:.3f})")
tick("§4 S1 misto")
print(f"\n S2 — REPLAY: un episodio innestato una volta nell'anno funded (365g)")
print(f" Metrica: **P(vivo a 12 mesi) con il crash dentro**, alla leva scelta sul 2024+")
print(f" (cioe' la leva che uno sceglierebbe NON sapendo del crash: e' il punto).")
hdr = " ".join(f"{NAMES[t]:>10}" for t in epis)
print(" ⚠️ DUE colonne di leva, e servono entrambe: la leva 'da 2024+' e' quella che uno")
print(" sceglierebbe non sapendo del crash (realistica, ma CONFONDE peso e leva perche' non")
print(" e' la stessa per tutte le righe); la leva COMUNE 0.50x isola l'effetto del peso.")
print(f"\n {'w TP01':>7} {'leva (da 2024+)':>16} {hdr:>44} {'peggiore':>10} "
f"{'peggiore @0.50x':>16} {'P(viv) 2024+':>13}")
s2 = {}
for w in W_GRID:
Rp, Gp, _, segs = stress_panel(w)
lv = tab[w]["rec"][0]
cells, cells_c = [], []
for k, (a, b) in enumerate(segs):
fu = splice_paths(base_fu, np.arange(a, b), seed=8290 + k)
cells.append(J_of(Rp, Gp, base_ev, fu, lv)["p_alive"])
cells_c.append(J_of(Rp, Gp, base_ev, fu, 0.50)["p_alive"])
s2[w] = (lv, cells, min(cells_c), tab[w]["rec"][1]["p_alive"])
print(f" {w:>7.3f} {lv:>16.3f} " + " ".join(f"{c:>10.1%}" for c in cells)
+ f" {min(cells):>10.1%} {min(cells_c):>16.1%} "
f"{tab[w]['rec'][1]['p_alive']:>13.1%}")
bw_a = max(s2, key=lambda w: min(s2[w][1]))
bw_c = max(s2, key=lambda w: s2[w][2])
print(f"\n argmax della colonna 'peggiore': leva-da-2024+ -> TP01 {bw_a:.3f} | "
f"leva comune 0.50x -> TP01 {bw_c:.3f}")
print(" ⚠️ LE DUE COLONNE DICONO COSE DIVERSE, E LA SECONDA E' A CAMPANA: a leva COMUNE la")
print(f" sopravvivenza al crash SALE col peso di TP01 fino a {bw_c:.3f} ("
f"{s2[0.0][2]:.1%} a TP01=0 -> {s2[bw_c][2]:.1%}) e poi ricade.")
print(" **Quindi TP01 protegge davvero: a pari leva, il libro con piu' difensivo sopravvive")
print(" di piu' al crash.** Il punto e' che quella protezione si compra anche abbassando la")
print(" leva — ed e' esattamente cio' che misura §5. Alla leva che ciascun libro sceglierebbe")
print(" (prima colonna) il vantaggio sparisce, perche' il libro senza TP01 la sceglie piu'")
print(" bassa. E' la firma del **null del de-levering**, qui in veste di 'protezione dal")
print(" crash' invece che di 'meno drawdown'.")
tick("§4 S2 replay")
# ----- replay deterministico del 2022
print("\n S2-bis — REPLAY DETERMINISTICO DEL 2022 (2022-04-01 -> 2022-12-31, 275 giorni).")
print(" Non e' un modello: e' successo. Un conto funded HYRO da $100k che parte il 1° aprile")
print(" 2022 con max-loss STATICO -6% e daily-loss -4%, alla leva scelta sul 2024+:")
a22 = int(np.searchsorted(IDX, pd.Timestamp("2022-04-01", tz="UTC")))
b22 = int(np.searchsorted(IDX, pd.Timestamp("2023-01-01", tz="UTC")))
def replay22(w: float, lev: float) -> tuple:
R, G, _ = crypto(w)
off = deluck_offset(w)
r = (R[a22:b22] - off) * lev
g = G[a22:b22] * lev
eq, mn, bust, why = 1.0, 1.0, None, ""
for i in range(len(r)):
low = eq * (1.0 + r[i] + g[i])
eq = eq * (1.0 + r[i])
mn = min(mn, low)
if bust is None and (low < 0.94 or (r[i] + g[i]) < -0.04):
bust = IDX[a22 + i].date()
why = "daily-loss -4%" if (r[i] + g[i]) < -0.04 else "max-loss -6%"
return eq - 1.0, mn - 1.0, bust, why, float((r + g).min())
print(f"\n {'w TP01':>7} {'leva':>6} {'rend. 9 mesi':>13} {'peggior giorno':>15} "
f"{'min equity':>11} {'esito':>22}")
for w in W_GRID:
lv = tab[w]["rec"][0]
ret, mn, bust, why, wd = replay22(w, lv)
esito = "VIVO" if bust is None else f"BUSTATO {bust}"
print(f" {w:>7.3f} {lv:>6.3f} {ret:>13.1%} {wd:>15.2%} {mn:>11.2%} {esito:>22}")
print("\n Tutti VIVI, e non e' una buona notizia mal letta: **alle leve funded il 2022 non")
print(" era una minaccia per questo libro** (min equity -1.4/-2.4% contro una barriera al")
print(" -6%). Cio' che uccide un conto a barriera non e' il crash del sottostante: e' la")
print(" regola a UN giorno su una giornata qualunque. Ecco fin dove si puo' spingere la leva:")
print(f"\n {'w TP01':>7} {'leva max viva sul 2022':>23} {'rend. a quella leva':>20} "
f"{'primo vincolo che morde':>25}")
for w in W_GRID:
lo, hi = 0.1, 8.0
for _ in range(18):
mid = 0.5 * (lo + hi)
if replay22(w, mid)[2] is None:
lo = mid
else:
hi = mid
ret, _, _, _, _ = replay22(w, lo)
_, _, _, why, _ = replay22(w, hi)
print(f" {w:>7.3f} {lo:>23.2f}x {ret:>20.1%} {why:>25}")
print("\n 📌 La colonna 'leva max viva' CRESCE col peso di TP01 (fino a TP01=0.875; a TP01=1")
print(" ricade perche' li' a mordere non e' piu' il giorno singolo ma la perdita CUMULATA).")
print(" **L'assicurazione e' reale e si vede: piu' TP01 = piu' leva sopportabile nel 2022.**")
print(" E accanto c'e' il prezzo: alla PROPRIA leva massima il libro senza TP01 porta a casa")
print(" il +23.3% e quello a TP01 pieno il -5.9%. TP01 compra capacita' di leva, e la")
print(" capacita' di leva non si converte in rendimento perche' cio' che TP01 toglie e'")
print(" proprio il rendimento. E' il null del de-levering scritto in una riga sola.")
print(" ⚠️ Cambia anche il VINCOLO che morde: sotto TP01 0.50 e' la regola a UN GIORNO,")
print(" sopra e' il max-loss cumulato. Due libri diversi muoiono di due morti diverse, e una")
print(" sola delle due lenti (l'accoppiata) vede la prima.")
tick("§4 replay 2022")
# ============================================================ §5. NULL DEL DE-LEVERING
hr("§5. IL NULL DEL DE-LEVERING — la protezione di TP01 e' altro rispetto a meno leva?")
print(" Il null classico ('esiste k<1 che da' lo stesso rischio con risultato migliore?') qui")
print(" prende la forma giusta per una barriera: **frontiera ISO-SOPRAVVIVENZA**. Per ogni")
print(f" peso si cerca la leva k(w) tale che P(vivo 12m | replay del sinistro PEGGIORE) = "
f"{SURV_TARGET:.0%},")
print(" poi si confronta il PAYOUT atteso sulla finestra normale a quella leva. Se un libro")
print(" con meno TP01 e meno leva paga di piu' a pari sopravvivenza, la protezione di TP01")
print(" e' de-levering; se paga di meno, TP01 compra qualcosa che la leva non compra.")
_SP: dict = {}
def p_alive_stress(w: float, lev: float, seg: tuple) -> float:
"""P(vivo 12m) col replay del sinistro dentro. Solo `funded_sim`: l'eval non entra
nella definizione della frontiera, e calcolarlo raddoppierebbe il costo per niente."""
global N_EVAL
N_EVAL += 1
k = (round(w, 6), seg)
if k not in _SP:
Rp, Gp, _, _ = stress_panel(w)
_SP[k] = (Rp, Gp, splice_paths(base_fu, np.arange(seg[0], seg[1]), seed=8291))
Rp, Gp, fu = _SP[k]
return PA.funded_sim(Rp, Gp, fu, lev, "HYRO", 100_000.0, "coupled")["p_alive"]
print(f"\n {'w TP01':>7} {'k iso-surv':>11} {'P(viv|crash)':>13} {'E[pay/a] normale':>17} "
f"{'P(pass) normale':>16} {'J normale':>11}")
front = {}
for w in W_GRID:
Rp, Gp, _, segs = stress_panel(w)
# episodio peggiore per QUESTO peso (il piu' severo, misurato)
worst_seg = min(segs, key=lambda sg: np.prod(1.0 + Rp[sg[0]:sg[1]]))
lo, hi = 0.05, 3.0
for _ in range(14):
mid = 0.5 * (lo + hi)
if p_alive_stress(w, mid, worst_seg) >= SURV_TARGET:
lo = mid
else:
hi = mid
k = lo
R, G, _ = crypto(w)
off = deluck_offset(w)
o = J_of(R[i_rec:] - off, G[i_rec:], ev_r, fu_r, k)
front[w] = (k, o)
print(f" {w:>7.3f} {k:>11.3f} {p_alive_stress(w, k, worst_seg):>13.1%} "
f"{o['e_payout']:>16,.0f}$ {o['p_pass']:>16.1%} {o['J']:>11.3f}")
kbest = max(front, key=lambda w: front[w][1]["e_payout"])
jbest = max(front, key=lambda w: front[w][1]["J"])
print(f"\n argmax a ISO-SOPRAVVIVENZA: payout -> TP01 {kbest:.3f} | J -> TP01 {jbest:.3f}")
print(" ⚠️ La leva k(w) e' tarata SU un episodio che nel campione di ottimizzazione non c'e':")
print(" e' un esercizio con l'informazione del futuro. Serve a rispondere alla domanda del")
print(" filone ('se sapessi che arriva, come mi allocherei?'), NON e' una politica.")
tick("§5 frontiera iso-sopravvivenza")
# ============================================================ §6. IL TERZO SLEEVE
hr("§6. XS01 — la parte della domanda che NON e' rispondibile, e perche'")
print(" L'ottimo di PROP-ALLOC mette il 50% su XS01, che **nasce nel 2024**: per lui non")
print(" esiste NESSUN sinistro nel campione. Qui non lo si simula col bootstrap della sua")
print(" finestra (sarebbe assumere che nel crash si comporti come in un giorno normale, che")
print(" e' esattamente l'assunzione sotto esame). Si dichiarano TRE comportamenti e si")
print(" guarda se la conclusione dipende da quale:")
print(" A) FLAT nel crash (il gate di dispersione lo spegne) -> generosa")
print(" B) la sua PEGGIORE finestra di 30g del 2024+, contigua e in ordine -> severa")
print(" C) uno shock una-tantum di taglia X sul giorno peggiore del crash -> si cerca")
print(" la X di ROTTURA, cioe' quanto grande deve essere per portare P(vivo) sotto 50%")
xs = pc.xsec_daily_tuples()
xsR = xs["R"].values
xsG = np.minimum(xs["m"].values, xsR) - xsR
lxs = np.log1p(xsR)
cxs = np.concatenate(([0.0], np.cumsum(lxs)))
wxs30 = np.expm1(cxs[EPI_LEN:] - cxs[:-EPI_LEN])
t_xw = int(np.argmin(wxs30))
print(f"\n XS01: {len(xs)} giorni {xs.index[0].date()} -> {xs.index[-1].date()}; peggior "
f"giorno {xsR.min():.2%}, peggior minimo {xs['m'].min():.2%}")
print(f" peggior finestra di {EPI_LEN}g di XS01: {xs.index[t_xw].date()} -> "
f"{xs.index[t_xw+EPI_LEN-1].date()}, cumulato {wxs30[t_xw]:.1%} "
f"(ipotesi B). Per confronto la coppia crypto nel COVID fa -5.4% a TP01 pieno.")
print(" 📌 Il numero che conta e' questo: **la peggior finestra di 30g mai vista da XS01 vale")
print(f" {wxs30[t_xw]:.1%}**. Se nel crash XS01 facesse cosi', al 50% di peso porterebbe")
print(f" {0.5*wxs30[t_xw]:.1%} al libro. La domanda non e' se XS01 sopravvive a cio' che ha")
print(" gia' visto, e' se il suo comportamento nel crash somiglia a cio' che ha gia' visto —")
print(" e su questo il campione non ha NIENTE da dire.")
# ⚠ le ultime tre righe hanno XS01 FISSO al 50%: senza quel controllo si confonderebbe
# "piu' TP01" con "meno XS01", che e' un'altra domanda.
W3 = [(0.75, 0.25, 0.00), (0.375, 0.625, 0.00), (0.50, 0.25, 0.25),
(0.125, 0.375, 0.50), (0.25, 0.25, 0.50), (0.375, 0.125, 0.50)]
com = xs.index.intersection(IDX[i_rec:])
xs_al = xs.reindex(com)
LEV_COM = 0.625 # leva comune per il blocco a XS01=50% (la minima fra le tre righe)
print(f"\n {'TP/SKH/XS':>16} {'ipotesi XS':>12} {'leva':>6} {'J normale':>10} "
f"{'P(viv|replay pegg.)':>21} {'idem @0.625x':>14} {'shock di rottura':>18}")
for cfg in W3:
wtp, wsk, wxs = cfg
base = wtp + wsk
wtp_n = wtp / base if base > 0 else 0.0
Rc, Gc, Ic = crypto(wtp_n)
offc = deluck_offset(wtp_n)
m1 = Ic.isin(com)
Rn = base * (Rc[m1] - offc) + wxs * xs_al["R"].values
Gn = base * Gc[m1] + wxs * (np.minimum(xs_al["m"].values, xs_al["R"].values)
- xs_al["R"].values)
nn = len(Rn)
evn = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8300)
fun = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8301)
lv, on = best_lev(Rn, Gn, evn, fun)
bfu = PA.boot_idx(nn, HORIZON, N_PATHS, seed=8302)
lab = f"{int(round(wtp*100))}/{int(round(wsk*100))}/{int(round(wxs*100))}"
def build(hyp: str, shock: float = 0.0, lev_: float | None = None):
segsR, segsG = [], []
for t in epis:
rc = base * (crypto(wtp_n)[0][t:t + EPI_LEN] - offc)
gc = base * crypto(wtp_n)[1][t:t + EPI_LEN]
if wxs > 0:
if hyp == "A-flat":
rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN)
elif hyp == "B-peggiore":
rx = xsR[t_xw:t_xw + EPI_LEN].copy()
gx = xsG[t_xw:t_xw + EPI_LEN].copy()
else:
rx = np.zeros(EPI_LEN); gx = np.zeros(EPI_LEN)
rx[int(np.argmin(rc))] = -shock
segsR.append(rc + wxs * rx); segsG.append(gc + wxs * gx)
else:
segsR.append(rc); segsG.append(gc)
Rp = np.concatenate([Rn] + segsR)
Gp = np.concatenate([Gn] + segsG)
sg = [(nn + k * EPI_LEN, nn + (k + 1) * EPI_LEN) for k in range(len(epis))]
worst = 1.0
for k, g2 in enumerate(sg):
fu = splice_paths(bfu, np.arange(g2[0], g2[1]), seed=8320 + k)
worst = min(worst, J_of(Rp, Gp, evn, fu, lv if lev_ is None else lev_)["p_alive"])
return worst
if wxs == 0:
print(f" {lab:>16} {'n/a (no XS)':>12} {lv:>6.3f} {on['J']:>10.3f} "
f"{build('A-flat'):>21.1%} {build('A-flat', lev_=LEV_COM):>14.1%} {'-':>18}")
continue
for hyp in ("A-flat", "B-peggiore"):
print(f" {lab:>16} {hyp:>12} {lv:>6.3f} {on['J']:>10.3f} "
f"{build(hyp):>21.1%} {build(hyp, lev_=LEV_COM):>14.1%} {'-':>18}")
lo, hi = 0.0, 0.40 # shock di rottura: bisezione su P(vivo) = 50%
for _ in range(9):
mid = 0.5 * (lo + hi)
if build("C-shock", mid) >= 0.50:
lo = mid
else:
hi = mid
lo2, hi2 = 0.0, 0.40
for _ in range(9):
mid = 0.5 * (lo2 + hi2)
if build("C-shock", mid, lev_=LEV_COM) >= 0.50:
lo2 = mid
else:
hi2 = mid
print(f" {lab:>16} {'C-shock':>12} {lv:>6.3f} {on['J']:>10.3f} {'(50% per def.)':>21} "
f"{'(50% per def.)':>14} {-lo:>17.1%} / {-lo2:.1%} @0.625x")
print("\n Lettura: 'shock di rottura' = il crollo di UN giorno su XS01 che porta P(vivo) al")
print(" 50% quando il crash arriva. Piu' e' piccolo, piu' l'allocazione XS01-pesante e' a un")
print(" giorno di coda dalla morte. Non e' una previsione: e' la taglia della scommessa.")
print(" ⚠️ Le ultime tre righe hanno XS01 FISSO al 50% e differiscono SOLO per come si")
print(" divide il resto fra TP01 e SKH01: e' li' che si legge se TP01 fa da zavorra contro")
print(" l'ignoto di XS01. **La colonna @0.625x e' quella da leggere**, perche' a leva libera")
print(" la riga con meno TP01 sceglie una leva piu' bassa e vince per quello.")
tick("§6 XS01")
# ============================================================ §7. BANDA + VERDETTO
hr("§7. SENSIBILITA' AL DE-LUCK — l'ORDINE fra i pesi sopravvive alla banda?")
print(" La banda d'ancora non e' ri-misurabile sotto questa lente (costo, dichiarato anche da")
print(" PROP-ALLOC). Si applica il fattore misurato x0.89 e si controlla l'unica cosa che")
print(" conta: se l'argmax sui pesi si muove fra 1.00 (nessuna correzione) e 0.60 (la stima")
print(" a occhio del 25/07, poi ri-misurata troppo severa).")
print(f"\n {'fattore':>9} {'argmax 2019+':>14} {'argmax 2024+':>14} {'argmax SOLO-SIN':>16} "
f"{'J(75/25) 2019+':>16} {'J(38/62) 2019+':>16}")
for fct in (1.00, 0.89, 0.60):
best_f = best_r = best_s = None
vals = {}
for w in W_GRID:
R, G, _ = crypto(w)
o = deluck_offset(w, fct)
Rf = R - o
j1 = best_lev(Rf, G, ev_f, fu_f)[1]["J"]
j2 = best_lev(Rf[i_rec:], G[i_rec:], ev_r, fu_r)[1]["J"]
j3 = best_lev(Rf, G, ev_s, fu_s)[1]["J"]
vals[w] = (j1, j2, j3)
best_f = max(vals, key=lambda w: vals[w][0])
best_r = max(vals, key=lambda w: vals[w][1])
best_s = max(vals, key=lambda w: vals[w][2])
print(f" {fct:>9.2f} {best_f:>14.3f} {best_r:>14.3f} {best_s:>16.3f} "
f"{vals[0.75][0]:>16.3f} {vals[0.375][0]:>16.3f}")
hr("§8. I GATE — cosa e' girato, cosa NON e' girato e perche'")
print(" Questo filone non propone una strategia nuova: propone (o rifiuta) un'ALLOCAZIONE per")
print(" un conto che non esiste. I gate del progetto vanno dichiarati uno per uno.")
try:
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import altlib as A # noqa: E402
for lab, w in (("LIVE 75/25", 0.75), ("OPT 38/62", 0.375)):
R = crypto(w)[0] - deluck_offset(w)
d = pd.Series(R, index=IDX)
n_act = int((np.abs(R) > 1e-12).sum())
n_los = int((R[np.abs(R) > 1e-12] < 0).sum())
f = A.implausible_sharpe(d, n_trades=n_act, n_losing_trades=n_los)
print(f" implausible_sharpe {lab:>11}: implausible={f['implausible']} "
f"(Sharpe {f['sharpe']:.2f}, maxDD {f['maxdd']:.1%}, quota barre in perdita "
f"{f['loss_frac']:.1%}, Calmar {f['calmar']:.2f}) {f['reasons']}")
except Exception as exc: # pragma: no cover
print(f" implausible_sharpe: NON GIRATO ({exc})")
print(" marginal_vs_tp01 / study_family_honest / deflated_sharpe : **NON GIRATI, e non")
print(" sono applicabili** — non c'e' uno stream candidato da confrontare con TP01: gli")
print(" stream sono i due sleeve gia' ammessi, e l'oggetto e' il loro PESO. Il gate dei")
print(" pesi del progetto e' `weights_tilt_null`, che pero' giudica lo SHARPE del libro")
print(" di RICERCA: qui l'obiettivo e' J su una barriera, e il conto e' separato.")
print(" (PROP-ALLOC lo ha girato: gate_pass False. Non cambia nulla qui: nessuna")
print(" proposta di questo file tocca il libro live.)")
print(" causality_ok : **non girato, non applicabile** — nessun segnale nuovo; le serie")
print(" sono quelle di produzione, gia' verificate leak-free a monte.")
print(" anchor_luck_band : **non ri-misurata** (costo, come in PROP-ALLOC). Si e' girata")
print(" la sensibilita' al FATTORE di de-luck (§7), che e' meno: dice se l'ORDINE regge,")
print(" non qual e' la banda. Distorsione dichiarata: la lente e' l'ancora CANONICA e")
print(" SKH01 e' lo sleeve con la fortuna d'ancora piu' grande da restituire (26/07)")
print(" -> **la distorsione e' A FAVORE della conclusione di questo file**, cioe' del")
print(" peso basso su TP01. E' il caveat piu' importante del filone.")
print(" null del de-levering : **GIRATO** (§5, frontiera iso-sopravvivenza) — e superato")
print(" NEL VERSO OPPOSTO a quello atteso: e' la protezione di TP01 a essere replicabile")
print(" con meno leva, e meglio.")
hr("§9. LA RISPOSTA OPERATIVA")
print(f" valutazioni MC totali di questo file: **{N_EVAL}** "
f"(ognuna = {N_PATHS} percorsi di eval + {N_PATHS} di funded)")
print(f"\n {'lente':>26} {'argmax w TP01':>14} {'J argmax':>10} {'J a 0.75':>10} "
f"{'J a 0.375':>11}")
for lab, key in (("2019+ (contiene il 2022)", "full"), ("2024+ (PROP-ALLOC)", "rec"),
("SOLO-SINISTRO", "sin")):
am = max(tab, key=lambda w: tab[w][key][1]["J"])
print(f" {lab:>26} {am:>14.3f} {tab[am][key][1]['J']:>10.3f} "
f"{tab[0.75][key][1]['J']:>10.3f} {tab[0.375][key][1]['J']:>11.3f}")
print("\n ESISTE UN PESO DI COMPROMESSO? Il criterio dichiarato: (a) dentro il 5% del")
print(" massimo di J sulla finestra 2024+ (la 'regione robusta' del funded) E (b) dentro il")
print(" 5% del massimo di J sulla finestra 2019+, che il sinistro ce l'ha dentro.")
jr_max = max(tab[w]["rec"][1]["J"] for w in W_GRID)
jf_max = max(tab[w]["full"][1]["J"] for w in W_GRID)
js_max = max(tab[w]["sin"][1]["J"] for w in W_GRID)
print(f"\n {'w TP01':>7} {'J 2024+':>9} {'entro 5%':>9} {'J 2019+':>9} {'entro 5%':>9} "
f"{'J solo-sin':>11} {'entro 5%':>9} {'compromesso':>13}")
ok_both = []
for w in W_GRID:
a, b, c = (tab[w]["rec"][1]["J"], tab[w]["full"][1]["J"], tab[w]["sin"][1]["J"])
oa, ob, oc = a >= 0.95 * jr_max, b >= 0.95 * jf_max, c >= 0.95 * js_max
if oa and ob:
ok_both.append(w)
print(f" {w:>7.3f} {a:>9.3f} {('si' if oa else 'no'):>9} {b:>9.3f} "
f"{('si' if ob else 'no'):>9} {c:>11.3f} {('si' if oc else 'no'):>9} "
f"{('SI' if (oa and ob) else '-'):>13}")
print(f"\n -> pesi che soddisfano (a) E (b): {ok_both if ok_both else 'NESSUNO'}")
print(f" il libro live 75/25 e' {'dentro' if 0.75 in ok_both else 'FUORI'} da questa regione.")
print("\n LA RISPOSTA, IN QUATTRO RIGHE")
print(" 1) La premessa del caveat e' sbagliata nella FORMA: il 2024+ un crash ce l'ha (b&h")
print(" -60% di drawdown, -39.6% in 20 giorni). Manca la TAGLIA, ~2x sulla coda veloce.")
print(" 2) Il compromesso ESISTE e non e' un compromesso: lo stesso peso (TP01 ~0.375) e'")
print(" l'argmax sulla finestra con il 2022 dentro E su quella senza. Il caveat di")
print(" PROP-ALLOC e' onesto come cautela e **falsificato come previsione**.")
print(" 3) TP01 protegge davvero (a pari leva sopravvive di piu' al crash, e alza la leva")
print(" sopportabile nel 2022 da 0.93x a 2.20x) ma la stessa protezione si compra piu' a")
print(" buon mercato ABBASSANDO LA LEVA: a iso-sopravvivenza il payout e' massimo a")
print(" TP01 0.125-0.250 e cala monotonamente da li'. **7ª occorrenza del null del")
print(" de-levering**, in una veste nuova: 'protezione dal crash' invece di 'meno DD'.")
print(" 4) Il rischio vero dell'ottimo di PROP-ALLOC NON e' TP01 sotto-pesato: e' XS01 al")
print(" 50% con un comportamento nel crash che il campione non contiene. Un solo giorno")
print(" a ~-10% su XS01 dimezza la sopravvivenza quando il crash arriva.")
print(" ⚠️ IPOTESI MIA, NATA E REFUTATA IN SESSIONE: avevo scritto che TP01 fa da")
print(" ZAVORRA contro quell'ignoto (a XS01 25% serviva uno shock -12.3% contro -8.0% a")
print(" XS01 50%). Il controllo a **XS01 FISSO al 50% e leva comune** lo smonta: lo shock")
print(" di rottura e' -10.1 / -10.2 / -10.2% muovendo TP01 da 12% a 38%, cioe' INVARIANTE,")
print(" e la sopravvivenza sotto l'ipotesi severa CALA (74.1 -> 72.6 -> 64.7%). Cio' che")
print(" protegge dall'ignoto di XS01 e' **meno XS01**, non piu' TP01: il primo numero")
print(" confrontava due pesi di XS01 diversi e attribuiva a TP01 il merito dell'altro.")
print("\n ⚠️ COSA MI SMENTIREBBE: un crash in cui SKH01 NON si gira short in tempo (gap")
print(" notturno oltre lo stop, o venue fermo). Il campione ne ha quattro e in tutti e")
print(" quattro SKH01 ha girato. Basterebbe un episodio in cui SKH01 perde quanto il")
print(" sottostante per rovesciare §2 e §5 — e con n=4 quell'episodio non e' improbabile.")
print("\n" + "=" * 106)
print(f" fatto in {time.time()-t0:.0f}s | valutazioni: {N_EVAL}")
print("=" * 106)
if __name__ == "__main__":
main()