research(frontier): frontiera onesta del book — il 6% e' un miraggio di regime, gira gia' alla vol nativa

Misura (non ricerca segnali) della frontiera CAGR/DD/P-rovina vs
target-vol, pesata per confidenza per-sleeve, a 2k/5k. 7 agenti (3
filoni + refuter fat-tail + scettico), sanity bit-exact dei due book.

Reframe decisivo: il "6% attuale" NON e' un target, e' la vol realizzata
in risk-off; a lambda=1 il book LIVE 2-sleeve gira gia' a ~11% vol/9.4%
DD -> "saltare a 10%" e' assumere non-risk-off, non aggiungere leva.

- Target-vol fidato book LIVE (TP01+SKH01, unico eseguibile <20k):
  ~10% (banda 8-11%, lambda ~0.9). Scale-invariante: stesso % a 2k/5k.
- P(rovina-50%) e' la metrica SBAGLIATA (slack a 22%); il vincolo che
  morde e' P(DD>30%), super-lineare. Muro onesto ~12% (gap-through SKH
  reale non nei rendimenti modellati; 2-sleeve 4x crash-sensibile).
- Vol differenziata-per-confidenza REFUTATA: taglia la diversificazione;
  la bassa fiducia va nel DE-MEAN (haircut media), non nella leva.
- Reward onesto (HOLD<<FULL): ~EUR 0.3-0.7/g@2k, 0.8-1.8/g@5k; warm-up
  vs 6% risk-off = ~+EUR 0.2/g@2k. EUR 50/g resta ~130k (capitale+tempo).
- UNICA azione config (proposta, NON applicata): cap $300 -> equity/2,
  altrimenti a 5k il cap raffredda paradossalmente il book sotto il punto
  fidato (6% vol ceiling).

config/live.json NON toccato. Book invariato. 168 test verdi.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-03 08:43:59 +00:00
parent 116abf5203
commit eef200cf14
4 changed files with 1412 additions and 0 deletions
+352
View File
@@ -0,0 +1,352 @@
"""FILONE 1 — LA CURVA FRONTIERA (r0703_frontier_curve.py)
MISURA (non ricerca segnali) della frontiera rischio/rendimento dei DUE book che GIA' esistono:
- book RESEARCH 5-sleeve: TP01 33 / XS01 15 / VRP01 12 / SKH01 20 / GTAA01 20 (combine_outer)
- book LIVE 2-sleeve: TP01 75 / SKH01 25 (cio' che gira su soldi reali su Deribit)
Per decidere a quale TARGET-VOL conviene girare il book a capitale piccolo (2k / 5k).
LEVA STUDIATA = moltiplicatore lambda sui rendimenti giornalieri del book combinato:
r_scaled(t) = clip(lambda * r_book(t), -1, None) (clip: non si perde >100% in un giorno)
Sweep di lambda per coprire target-vol annuo ~5% -> ~30%. Sharpe INVARIANTE per costruzione
(lo verifichiamo: cambia solo con l'attivazione del clip a leva alta).
CONFIDENZA PER-SLEEVE (anti-alllucinazione #2): 3 livelli. La de-luck e l'haircut sono un
HAIRCUT SULLA MEDIA (edge), applicato DE-MEANando lo sleeve: s' = s - h*mean(s).
=> std invariata (stessa vol/coda), drift ridotto. Quindi tra i 3 livelli cambia SOLO il drift
(CAGR / P-rovina via minor deriva), NON la forma della coda -> il messaggio e' la BANDA.
(a) FULL : book as-is.
(b) DE-LUCK : rimuove l'anchor timing-luck (audit 4/4):
SKH01 mean x0.60 (canonica = 93-98 pctl di 23 offset, ~+0.5 HOLD di fortuna),
TP01 mean x0.85 (hold-out 0.31 = migliore di 24 ancore, mediana 0.04).
VRP01/XS01 NON haircuttati qui: le loro ancore canoniche sono CONSERVATIVE
(VRP 7 pctl = peggiore; XS 15 pctl di DD). GTAA: nessuna ancora-luck.
(c) HAIRCUT : de-luck + taglio -50% della MEDIA degli sleeve a FIDUCIA BASSA sull'edge
(VRP01 premio MODELLATO, f di stress mai catturato; XS01 STAT-MODE, 2.5y).
= la "frontiera fidata". Il GAP full<->haircut e' il margine di onesta'.
Variante extra: EXCL = escludi del tutto VRP01+XS01 (ultra-conservativo, riferimento).
Per il book LIVE 2-sleeve (niente VRP/XS): haircut = de-luck + SKH01 mean a x0.50 dall'originale
(SKH e' la gamba a fiducia MEDIA: research, ETH DD margine sottile vs 30%).
TAIL (anti-allucinazione #1): la coda NON scala lineare-Gaussiano. Quantificato con:
- BLOCK BOOTSTRAP (blocchi 10 e 20g, B=5000) sui rendimenti REALI del book (fat-tail+autocorr):
P(rovina-50%|5y) = P(equity tocca <=50% del capitale iniziale in un path di 5y)
P(DD>30%|5y) = P(max drawdown peak-to-trough > 30% in 5y)
- BENCHMARK GAUSSIANO iid (stessa media/vol) -> il GAP misura il contributo fat-tail/autocorr.
- INIEZIONE DI CODA SINTETICA: un giorno-crash = 1.5x il peggior giorno storico, ~1 volta per
path di 5y -> sensibilita' al "il campione puo' non contenere il crash peggiore".
Nessuna selezione su hold-out: FULL e HOLD affiancati solo per PROVARE che la frontiera non e'
un artefatto in-sample. NON tocca src/config/live/tests: importa solo (sola lettura) i builder.
Output CSV+report testuale in scratchpad. Girare: uv run python scripts/research/r0703_frontier_curve.py
"""
from __future__ import annotations
import sys, os, time
sys.path.insert(0, '/opt/docker/PythagorasGoal')
sys.path.insert(0, '/opt/docker/PythagorasGoal/scripts/research/alt')
import numpy as np
import pandas as pd
from src.portfolio.sleeves import (_tp01_returns, _skyhook_returns, _xsec_returns,
_vrp_combo_returns, _gtaa_daily_returns)
from src.portfolio.portfolio import combine_outer, to_daily, HOLDOUT
DPY = 365.25
OUT = '/tmp/claude-1001/-opt-docker-PythagorasGoal/e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad'
os.makedirs(OUT, exist_ok=True)
RNG = np.random.default_rng(20260703)
# ---------------------------------------------------------------- sleeves
def build_sleeves() -> dict[str, pd.Series]:
d = {}
for nm, fn in [("TP01", _tp01_returns), ("XS01", _xsec_returns), ("VRP01", _vrp_combo_returns),
("SKH01", _skyhook_returns), ("GTAA01", _gtaa_daily_returns)]:
d[nm] = to_daily(fn())
return d
def demean_haircut(s: pd.Series, h: float) -> pd.Series:
"""s' = s - h*mean(s): riduce il drift del fattore (1-h), std INVARIATA."""
if h == 0.0:
return s
return s - h * float(s.mean())
# per-livello: fattore di haircut sulla MEDIA per sleeve (h)
LEVELS_5 = {
"FULL": dict(TP01=0.0, XS01=0.0, VRP01=0.0, SKH01=0.0, GTAA01=0.0),
"DELUCK": dict(TP01=0.15, XS01=0.0, VRP01=0.0, SKH01=0.40, GTAA01=0.0),
"HAIRCUT": dict(TP01=0.15, XS01=0.5, VRP01=0.5, SKH01=0.40, GTAA01=0.0),
"EXCL": dict(TP01=0.15, XS01=1.0, VRP01=1.0, SKH01=0.40, GTAA01=0.0), # escludi VRP+XS (mean->0)
}
W5 = {"TP01": 0.33, "XS01": 0.15, "VRP01": 0.12, "SKH01": 0.20, "GTAA01": 0.20}
LEVELS_2 = {
"FULL": dict(TP01=0.0, SKH01=0.0),
"DELUCK": dict(TP01=0.15, SKH01=0.40),
"HAIRCUT": dict(TP01=0.15, SKH01=0.50), # SKH = gamba fiducia MEDIA -> taglio piu' profondo
}
W2 = {"TP01": 0.75, "SKH01": 0.25}
def make_book(sleeves: dict, weights: dict, hair: dict) -> pd.Series:
cols = {nm: demean_haircut(sleeves[nm], hair.get(nm, 0.0)) for nm in weights}
return combine_outer(cols, weights)
# ---------------------------------------------------------------- metrics (full sample, scaled)
def full_metrics(book: pd.Series, lam: float) -> dict:
r = np.clip(lam * book.values.astype(float), -1.0, None)
s = pd.Series(r, index=book.index)
eq = np.cumprod(1.0 + r)
pk = np.maximum.accumulate(eq)
yrs = len(r) / DPY
dd = float(np.max((pk - eq) / pk))
cagr = float(eq[-1] ** (1 / yrs) - 1) if eq[-1] > 0 else -1.0
shp = float(r.mean() / r.std() * np.sqrt(DPY)) if r.std() > 0 else 0.0
ww = float(((1.0 + s).resample('168h').prod() - 1.0).min()) # worst-week (168h, non '7D')
vol = float(r.std() * np.sqrt(DPY))
return dict(sharpe=shp, cagr=cagr, maxdd=dd, worstweek=ww, vol=vol)
# ---------------------------------------------------------------- block bootstrap engine
def block_index(n: int, L: int, npath: int, B: int) -> np.ndarray:
"""Matrice (B, npath) di indici in [0,n) via block-bootstrap circolare-troncato (blocchi L)."""
nblk = int(np.ceil(npath / L))
starts = RNG.integers(0, n - L + 1, size=(B, nblk)) # start di ogni blocco
offs = np.arange(L)
idx = (starts[:, :, None] + offs[None, None, :]).reshape(B, nblk * L)[:, :npath]
return idx
def ruin_dd(Rmat: np.ndarray, lam: float) -> tuple[float, float]:
"""P(rovina-50%|5y), P(DD>30%|5y) da una matrice (B,npath) di rendimenti NATIVI, scalata per lam.
Versione veloce: la matrice bootstrap (o la coda iniettata, o il gauss iid) e' PRE-costruita una
volta per (book,level); qui si applica solo lo scaling di leva + cumprod. Numerica identica a
boot_probs/gauss_probs, senza ri-gather/ri-generazione per ogni target-vol."""
R = np.clip(lam * Rmat, -1.0, None)
eq = np.cumprod(1.0 + R, axis=1)
ruin = float((eq.min(axis=1) <= 0.5).mean())
pk = np.maximum.accumulate(eq, axis=1)
dd = ((pk - eq) / pk).max(axis=1)
return ruin, float((dd > 0.30).mean())
def boot_probs(r_native: np.ndarray, lam: float, idx: np.ndarray,
inject_crash: float | None = None) -> tuple[float, float]:
"""P(rovina-50%|5y), P(DD>30%|5y) su path bootstrap.
inject_crash: se non None, in OGNI path sostituisce 1 giorno casuale con questo rendimento
NATIVO (pre-scala) -> stress "una coda sintetica per 5 anni"."""
R = r_native[idx].copy() # (B, npath) nativi
if inject_crash is not None:
B, npath = R.shape
pos = RNG.integers(0, npath, size=B)
R[np.arange(B), pos] = inject_crash
R = np.clip(lam * R, -1.0, None)
eq = np.cumprod(1.0 + R, axis=1)
ruin = float((eq.min(axis=1) <= 0.5).mean()) # tocca 50% del capitale iniziale
pk = np.maximum.accumulate(eq, axis=1)
dd = ((pk - eq) / pk).max(axis=1)
dd30 = float((dd > 0.30).mean())
return ruin, dd30
def gauss_probs(mu: float, sd: float, npath: int, B: int) -> tuple[float, float]:
"""Benchmark iid-Gaussiano con stessa media/std giornaliera (post-scala)."""
R = RNG.normal(mu, sd, size=(B, npath))
R = np.clip(R, -1.0, None)
eq = np.cumprod(1.0 + R, axis=1)
ruin = float((eq.min(axis=1) <= 0.5).mean())
pk = np.maximum.accumulate(eq, axis=1)
dd = ((pk - eq) / pk).max(axis=1)
return ruin, float((dd > 0.30).mean())
# ---------------------------------------------------------------- main
def run():
t0 = time.time()
print("building sleeves...", flush=True)
sl = build_sleeves()
NPATH = int(round(5 * DPY)) # 5 anni ~ 1826 giorni-calendario
B = 5000
TV_GRID = np.round(np.arange(0.05, 0.305, 0.01), 3) # 5%..30% target-vol (fine)
TV_FINE = np.round(np.arange(0.04, 0.305, 0.005), 4) # per la ricerca del punto-decisione
books = {
"5SLEEVE": dict(weights=W5, levels=LEVELS_5),
"2LIVE": dict(weights=W2, levels=LEVELS_2),
}
# ---- costruzione book per livello + sanity + banda di confidenza
print("\n" + "=" * 78)
print("SANITY + BANDA DI CONFIDENZA (FULL vs HOLD affiancati)")
print("=" * 78)
built = {} # (book,level) -> pd.Series (native, lam=1)
for bk, cfg in books.items():
for lv, hair in cfg["levels"].items():
b = make_book(sl, cfg["weights"], hair)
built[(bk, lv)] = b
# print sanity for this book, all levels
print(f"\n[{bk}] weights={cfg['weights']}")
print(f" {'level':9s} {'nat.vol':>8s} {'FULL Sh':>8s} {'HOLD Sh':>8s} "
f"{'FULL CAGR':>10s} {'HOLD CAGR':>10s} {'FULL DD':>8s}")
for lv in cfg["levels"]:
b = built[(bk, lv)]
mF = full_metrics(b, 1.0)
bh = b[b.index >= HOLDOUT]
mH = full_metrics(bh, 1.0)
print(f" {lv:9s} {mF['vol']*100:7.2f}% {mF['sharpe']:8.3f} {mH['sharpe']:8.3f} "
f"{mF['cagr']*100:9.2f}% {mH['cagr']*100:9.2f}% {mF['maxdd']*100:7.2f}%")
# ---- anchor band (citata dagli audit; non ricalcolata qui)
print("\n" + "-" * 78)
print("BANDA D'ANCORA (dove il DE-LUCK e' ancorato — audit 4/4, diari 2026-07-02/03):")
print(" TP01 : hold-out 0.31 = MIGLIORE di 24 ancore orarie (mediana 0.04, banda [-0.13,+0.30],")
print(" P~0.86 che un'ancora mostri tale spike per caso) -> de-luck mean x0.85")
print(" SKH01: canonica = 93-98 pctl di 23 offset (griglia 230/690m), ~+0.5 HOLD di fortuna sul")
print(" book; gate DD<30% fallisce in 15/23 offset -> de-luck mean x0.60")
print(" XS01 : canonica = 15 pctl di DD (10 fasi) -> CONSERVATIVA sul DD, nessun de-luck edge")
print(" VRP01: canonica = PEGGIORE (7 pctl, 7 giorni) -> CONSERVATIVA, nessun de-luck edge")
print(" GTAA : validato OOS 30y indipendente, nessuna ancora-luck")
print(" => il DE-LUCK haircutta SOLO gli sleeve con ancora-FORTUNATA (TP01/SKH01).")
# ---- frontiera + bootstrap
rows = [] # per CSV
print("\n" + "=" * 78)
print("FRONTIERA + BLOCK-BOOTSTRAP (B=%d, 5y=%d gg, blocchi 10 & 20)" % (B, NPATH))
print("=" * 78)
# crash sintetico per book: 1.5x il peggior giorno storico del book FULL (nativo)
for bk, cfg in books.items():
native_min = float(built[(bk, "FULL")].values.min())
crash = 1.5 * native_min
n = len(built[(bk, "FULL")])
idx10 = block_index(n, 10, NPATH, B)
idx20 = block_index(n, 20, NPATH, B)
print(f"\n########## BOOK {bk} (n={n} gg, worst-day nativo {native_min*100:.2f}%, "
f"crash sintetico {crash*100:.2f}%) ##########")
for lv in cfg["levels"]:
b = built[(bk, lv)]
r_nat = b.values.astype(float)
bh = b[b.index >= HOLDOUT]
r_hold = bh.values.astype(float)
nat_vol = float(r_nat.std() * np.sqrt(DPY))
# PRE-costruzione (una volta per level) delle matrici bootstrap NATIVE + coda + gauss:
R20 = r_nat[idx20] # (B, npath) block-20
R10 = r_nat[idx10] # (B, npath) block-10
R20c = R20.copy() # coda sintetica iniettata 1x/path
_pos = RNG.integers(0, R20c.shape[1], size=R20c.shape[0])
R20c[np.arange(R20c.shape[0]), _pos] = crash
Z = RNG.standard_normal(R20.shape) # normali std fisse (gauss iid)
mu0 = float(r_nat.mean()); sd0 = float(r_nat.std())
print(f"\n--- {bk} / {lv} (native vol {nat_vol*100:.2f}%, "
f"FULL Sh {full_metrics(b,1.0)['sharpe']:.3f}, "
f"HOLD Sh {full_metrics(bh,1.0)['sharpe']:.3f}) ---")
hdr = (f" {'tvol':>5s} {'lam':>5s} {'Sh':>6s} {'CAGRf':>7s} {'CAGRh':>7s} "
f"{'maxDDf':>7s} {'wWeek':>7s} | {'Pruin20':>8s} {'Pdd30_20':>9s} "
f"{'Pruin10':>8s} {'Pdd30_10':>9s} | {'Gruin':>7s} {'Gdd30':>7s} | "
f"{'Pruin+cr':>9s} {'Pdd30+cr':>9s}")
print(hdr)
for tv in TV_GRID:
lam = tv / nat_vol
mF = full_metrics(b, lam)
mH = full_metrics(bh, lam)
r20, d20 = ruin_dd(R20, lam)
r10, d10 = ruin_dd(R10, lam)
# gaussiano iid con media/std della serie SCALATA (Rg = lam*mu0 + lam*sd0*Z)
gr, gd = ruin_dd((lam * mu0) + (lam * sd0) * Z, 1.0)
# tail injection (blocco 20)
rc, dc = ruin_dd(R20c, lam)
print(f" {tv*100:4.0f}% {lam:5.2f} {mF['sharpe']:6.2f} "
f"{mF['cagr']*100:6.1f}% {mH['cagr']*100:6.1f}% "
f"{mF['maxdd']*100:6.1f}% {mF['worstweek']*100:6.1f}% | "
f"{r20*100:7.2f}% {d20*100:8.2f}% {r10*100:7.2f}% {d10*100:8.2f}% | "
f"{gr*100:6.2f}% {gd*100:6.2f}% | {rc*100:8.2f}% {dc*100:8.2f}%")
rows.append(dict(book=bk, level=lv, target_vol=tv, lam=round(lam, 3),
sharpe=round(mF['sharpe'], 3), cagr_full=round(mF['cagr'], 4),
cagr_hold=round(mH['cagr'], 4), maxdd_full=round(mF['maxdd'], 4),
worstweek=round(mF['worstweek'], 4),
p_ruin50_b20=round(r20, 4), p_dd30_b20=round(d20, 4),
p_ruin50_b10=round(r10, 4), p_dd30_b10=round(d10, 4),
p_ruin50_gauss=round(gr, 4), p_dd30_gauss=round(gd, 4),
p_ruin50_crash=round(rc, 4), p_dd30_crash=round(dc, 4)))
df = pd.DataFrame(rows)
csv = os.path.join(OUT, 'frontier_curve.csv')
df.to_csv(csv, index=False)
print(f"\nCSV completo -> {csv}")
# ---- punto OGGI (lam=1) marcato
print("\n" + "=" * 78)
print("PUNTO OPERATIVO OGGI (lambda=1, book as-run):")
for bk, cfg in books.items():
b = built[(bk, "FULL")]
m = full_metrics(b, 1.0)
print(f" {bk}: native vol {m['vol']*100:.2f}% => target-vol as-run ~{m['vol']*100:.1f}%, "
f"lambda=1, FULL maxDD {m['maxdd']*100:.2f}%")
# ---- NUMERO-DECISIONE: max CAGR con P(rovina-50%|5y) <= 1%
print("\n" + "=" * 78)
print("NUMERO-DECISIONE: max target-vol/CAGR con P(rovina-50%|5y) <= 1.0%")
print(" (headline = block-20 baseline; verifica block-10 e coda-iniettata)")
print("=" * 78)
dec = []
for bk, cfg in books.items():
n = len(built[(bk, "FULL")])
idx20 = block_index(n, 20, NPATH, B)
idx10 = block_index(n, 10, NPATH, B)
native_min = float(built[(bk, "FULL")].values.min())
crash = 1.5 * native_min
for lv in cfg["levels"]:
b = built[(bk, lv)]
r_nat = b.values.astype(float)
bh = b[b.index >= HOLDOUT]
nat_vol = float(r_nat.std() * np.sqrt(DPY))
R20 = r_nat[idx20]; R10 = r_nat[idx10]
R20c = R20.copy()
_pos = RNG.integers(0, R20c.shape[1], size=R20c.shape[0])
R20c[np.arange(R20c.shape[0]), _pos] = crash
best = None
for tv in TV_FINE:
lam = tv / nat_vol
r20, _ = ruin_dd(R20, lam)
if r20 <= 0.01:
best = tv
else:
break
if best is None:
dec.append(dict(book=bk, level=lv, tvmax=np.nan))
print(f" {bk:8s}/{lv:8s}: nessun punto con P(ruin)<=1% (troppo grasso)")
continue
lam = best / nat_vol
mF = full_metrics(b, lam); mH = full_metrics(bh, lam)
r20, d20 = ruin_dd(R20, lam)
r10, d10 = ruin_dd(R10, lam)
rc, dc = ruin_dd(R20c, lam)
dec.append(dict(book=bk, level=lv, tvmax=best, lam=round(lam, 3),
cagr_full=mF['cagr'], cagr_hold=mH['cagr'], maxdd=mF['maxdd'],
pruin20=r20, pruin10=r10, pruin_crash=rc, pdd30=d20))
print(f" {bk:8s}/{lv:8s}: tvol_max {best*100:4.1f}% (lam {lam:.2f}) -> "
f"CAGR_full {mF['cagr']*100:5.1f}% / CAGR_hold {mH['cagr']*100:5.1f}% ; "
f"maxDDfull {mF['maxdd']*100:4.1f}% ; P(ruin) b20 {r20*100:.2f}% / "
f"b10 {r10*100:.2f}% / +crash {rc*100:.2f}% ; P(DD>30) {d20*100:.2f}%")
pd.DataFrame(dec).to_csv(os.path.join(OUT, 'decision_pruin1pct.csv'), index=False)
# ---- €/giorno a 2k e 5k dal CAGR (haircut = frontiera fidata)
print("\n" + "-" * 78)
print("TRADUZIONE €/giorno (CAGR haircut = frontiera fidata):")
for bk in books:
row = next((x for x in dec if x['book'] == bk and x['level'] == 'HAIRCUT'), None)
if row and not np.isnan(row.get('tvmax', np.nan)):
for cap in (2000, 5000):
eur = cap * row['cagr_full'] / 365.25
print(f" {bk}/HAIRCUT @ tvol {row['tvmax']*100:.1f}%: {cap}EUR -> "
f"~{eur:.2f} EUR/giorno (CAGR {row['cagr_full']*100:.1f}%)")
print(f"\ndone in {time.time()-t0:.0f}s")
if __name__ == "__main__":
run()
+564
View File
@@ -0,0 +1,564 @@
"""FILONE 3 (2026-07-03) — FRONTIERA ESEGUIBILE a 2k/5k + STAGING KELLY.
NON e' ricerca segnali (il soffitto direzionale ~1.3 e' saturo): rende OPERATIVA la frontiera
rischio/rendimento dei DUE book che GIA' esistono, per decidere a quale TARGET-VOL girarli a
capitale piccolo. Riusa r0702_capital_scaling.py (granularita' reale) e la macchina bootstrap dei
filoni 1/2. NON tocca src/config/live/tests: importa solo (sola lettura) i builder.
(A) FRONTIERA ESEGUIBILE del BOOK LIVE (TP01+SKH01 75/25) a capitale {2000,3500,5000}:
il target notional per-asset del book e' net = clamp(0.5*E*(0.75*max(tp,0)+0.25*skh), +-cap)
(formula ESATTA di src/live/book.book_net_target). La LEVA studiata = moltiplicatore lambda
su quel target (target-vol = lambda*vol_nativa). A ogni (capitale, lambda):
- il CAP degenera dall'ALTO: quanto della vol richiesta il cap $300 lascia esprimere, e
quanta ne sblocca il cap = equity/2 -> "cap ceiling" sul target-vol raggiungibile;
- il MIN-ORDER degenera dal BASSO: granularita' reale (step BTC 0.0001~$7, ETH 0.001~$2,
floor config $5) -> il target-vol sotto cui la posizione/ribilancio non si esegue;
- il DD in EURO (non solo %): -20% su 2k = -EUR 400 -> tabella DD-in-euro a ogni lambda.
(B) STAGING KELLY: target-vol growth-optimal (full / half / quarter-Kelly) del book FIDATO,
con le CODE ONESTE del filone 2 (block-bootstrap fat-tail, NON Gaussiano). Confronto col
posizionamento attuale (~6% vol) -> di quanti punti di vol e' sotto l'ottimo di crescita?
Tesi: 'a capitale piccolo con obiettivo di crescita i 6% sono iper-prudenti; l'ottimo
Kelly-frazionario e' piu' caldo' -> confermata o refutata coi numeri (E col vincolo di rovina).
(C) AGGRESSIVITA' PER-SLEEVE ~ CONFIDENZA: correre TP01 (validato) piu' caldo di VRP (modellato)?
Book a target-vol DIFFERENZIATO per sleeve (moltiplicatore di esposizione ~ fiducia
sull'EDGE) vs uniforme, a book-vol PARI -> il differenziato domina l'uniforme sulla
frontiera fidata? (Sharpe/DD/P-rovina.)
Confidenza per-sleeve (edge, NON vol): TP01 piena, GTAA alta, SKH media, VRP/XS bassa.
Tre livelli di frontiera (la BANDA e' il messaggio): FULL / DE-LUCK (anchor-audit 4/4) /
HAIRCUT (de-luck + taglio -50% della MEDIA di VRP/XS = frontiera FIDATA).
uv run python scripts/research/r0703_frontier_exec.py
"""
from __future__ import annotations
import sys, os, math, time
import numpy as np
import pandas as pd
ROOT = "/opt/docker/PythagorasGoal"
sys.path.insert(0, ROOT)
sys.path.insert(0, ROOT + "/scripts/research/alt")
sys.path.insert(0, ROOT + "/scripts/research")
import altlib as al # noqa: E402
from src.portfolio.sleeves import (_tp01_returns, _skyhook_returns, _xsec_returns, # noqa: E402
_vrp_combo_returns, _gtaa_daily_returns)
from src.portfolio.portfolio import combine_outer, to_daily, HOLDOUT # noqa: E402
# riuso della macchina di scala (granularita' reale) del filone capital-scaling
from r0702_capital_scaling import skh_sign_series, tp_frac_on_ltf, book_sim, TP # noqa: E402
DPY = 365.25
FEE_SIDE = al.FEE_SIDE
WEIGHT, W_TP01, W_SKH = 0.5, 0.75, 0.25 # src/live/book.py + shadow.WEIGHT
ASSETS = ("BTC", "ETH")
CAPS = (2000.0, 3500.0, 5000.0)
CAP_NOW = 300.0
MIN_ORDER = 5.0
TVOLS = [0.05, 0.06, 0.08, 0.10, 0.125, 0.15, 0.20, 0.25, 0.30]
OUT = "/tmp/claude-1001/-opt-docker-PythagorasGoal/e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad"
os.makedirs(OUT, exist_ok=True)
SEED = 20260703
RNG = np.random.default_rng(SEED)
REP = []
def P(*a):
s = " ".join(str(x) for x in a); print(s, flush=True); REP.append(s)
# ============================================================ metriche base
def _r(s): return np.asarray(pd.Series(s).dropna().values, float)
def ann_vol(s): r = _r(s); return float(r.std() * math.sqrt(DPY)) if len(r) else 0.0
def ann_mean(s): r = _r(s); return float(r.mean() * DPY) if len(r) else 0.0
def sharpe(s):
r = _r(s); return float(r.mean() / r.std() * math.sqrt(DPY)) if len(r) > 1 and r.std() > 0 else 0.0
def cagr(s):
r = _r(s); eq = np.cumprod(1 + np.clip(r, -1, None)); y = len(r) / DPY
return float(eq[-1] ** (1 / y) - 1) if y > 0 and eq[-1] > 0 else -1.0
def maxdd(s):
r = _r(s); eq = np.cumprod(1 + np.clip(r, -1, None)); pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk)) if len(r) else 0.0
def worst_week(s):
ss = pd.Series(s).dropna()
if ss.index.tz is None:
try: ss.index = ss.index.tz_localize("UTC")
except Exception: pass
return float(((1 + ss).resample("168h").prod() - 1).min())
# ============================================================ sleeves + book variants (per la parte RISCHIO)
def build_sleeves():
d = {}
for nm, fn in [("TP01", _tp01_returns), ("XS01", _xsec_returns), ("VRP01", _vrp_combo_returns),
("SKH01", _skyhook_returns), ("GTAA01", _gtaa_daily_returns)]:
d[nm] = to_daily(fn())
return d
def demean(s, h):
return s if h == 0.0 else s - h * float(s.mean())
W5 = {"TP01": 0.33, "XS01": 0.15, "VRP01": 0.12, "SKH01": 0.20, "GTAA01": 0.20}
W2 = {"TP01": 0.75, "SKH01": 0.25}
# haircut sulla MEDIA (edge), std/coda INVARIATE (filoni 1/2)
LV5 = {
"FULL": dict(TP01=0.0, XS01=0.0, VRP01=0.0, SKH01=0.0, GTAA01=0.0),
"DELUCK": dict(TP01=0.15, XS01=0.0, VRP01=0.0, SKH01=0.40, GTAA01=0.0),
"HAIRCUT": dict(TP01=0.15, XS01=0.5, VRP01=0.5, SKH01=0.40, GTAA01=0.0),
}
LV2 = {
"FULL": dict(TP01=0.0, SKH01=0.0),
"DELUCK": dict(TP01=0.15, SKH01=0.40),
"HAIRCUT": dict(TP01=0.15, SKH01=0.50),
}
# confidenza sull'EDGE -> moltiplicatore di ESPOSIZIONE per la parte (C)
CONF = dict(TP01=1.00, GTAA01=0.90, SKH01=0.70, VRP01=0.45, XS01=0.45)
def make_book(sl, weights, hair):
cols = {nm: demean(sl[nm], hair.get(nm, 0.0)) for nm in weights}
return combine_outer(cols, weights)
# ============================================================ block-bootstrap (fat-tail, filoni 1/2)
def make_paths(r, n_paths, horizon, block, rng):
r = _r(r); N = len(r); nb = int(math.ceil(horizon / block))
starts = rng.integers(0, N - block + 1, size=(n_paths, nb))
off = np.arange(block)
idx = (starts[:, :, None] + off[None, None, :]).reshape(n_paths, nb * block)[:, :horizon]
return r[idx].astype(np.float64)
def path_stats(scaled):
"""(n,H) rendimenti scalati -> maxDD per path, terminal wealth (floor 0), log-terminal."""
sc = np.clip(scaled, -1.0, None)
eq = np.cumprod(1.0 + sc, axis=1)
pk = np.maximum.accumulate(eq, axis=1)
dd = (pk - eq) / pk
term = eq[:, -1]
return dd.max(axis=1), term
NPATH = 4000
HOR = int(round(5 * 365))
BLOCK = 15
RUIN = 0.50 # rovina = maxDD >= 50% (equity <= 50% del capitale iniziale)
DDLIM = 0.30
# ============================================================ (SANITY)
def sanity(sl):
P("=" * 110)
P("SANITY — ricostruzione book (tolleranza per deriva dati)")
P("=" * 110)
b5 = make_book(sl, W5, LV5["FULL"]); b2 = make_book(sl, W2, LV2["FULL"])
for nm, b, tgt in [("5-SLEEVE", b5, "[target 2.24 / 2.46 / 6.2%]"),
("2-SLEEVE LIVE", b2, "[target ~1.78 / 1.17 / 9%]")]:
ho = b[b.index >= HOLDOUT]
P(f" {nm:14s} n={len(b):5d} FULL Sh={sharpe(b):.3f} DD={maxdd(b)*100:5.2f}% vol={ann_vol(b)*100:5.2f}% "
f"CAGR={cagr(b)*100:5.2f}% | HOLD Sh={sharpe(ho):.3f} DD={maxdd(ho)*100:5.2f}% {tgt}")
P(" per-sleeve (FULL):")
for nm in ("TP01", "XS01", "VRP01", "SKH01", "GTAA01"):
s = sl[nm]
P(f" {nm:8s} Sh={sharpe(s):+.2f} vol={ann_vol(s)*100:5.2f}% CAGR={cagr(s)*100:+6.2f}% "
f"DD={maxdd(s)*100:5.2f}% worstDay={float(pd.Series(s).min())*100:+.2f}% n={len(s)}")
return b5, b2
# ============================================================ FRONTIER TABLE consolidata (deliverable)
def frontier_table(sl):
"""Tabella-frontiera del deliverable: target-vol/lambda x {Sharpe, CAGR, maxDD, worst-week,
P(rovina50%|5y), P(DD>30%|5y)} per BOOK 5-sleeve E 2-sleeve, ai 3 livelli di confidenza
(FULL / DE-LUCK / HAIRCUT). Sharpe e' INVARIANTE per leva (native); CAGR/DD scalano; le prob.
di rovina/DD>30 vengono dal block-bootstrap fat-tail 5y (le stesse dei filoni 1/2)."""
P("\n" + "=" * 110)
P("FRONTIER TABLE (deliverable) — target-vol x metriche, 2 book x 3 livelli confidenza")
P(" Sharpe invariante-per-leva (native) ; CAGR/maxDD/wWeek = leva applicata ; Pruin/Pdd30 = 5y block-bootstrap fat-tail")
P("=" * 110)
specs = [("5-SLEEVE", W5, LV5), ("2-SLEEVE(live)", W2, LV2)]
for bname, weights, levels in specs:
for lv in ("FULL", "DELUCK", "HAIRCUT"):
b = make_book(sl, weights, levels[lv]); v0 = ann_vol(b)
ho = b[b.index >= HOLDOUT]
bp = make_paths(b, NPATH, HOR, BLOCK, np.random.default_rng(SEED + 51 + hash(bname + lv) % 9973))
P(f"\n --- {bname} / {lv} (native vol {v0*100:.2f}%, Sh_full {sharpe(b):.3f}, Sh_hold {sharpe(ho):.3f}) ---")
P(f" {'tvol':>5} {'lam':>5} {'Sharpe':>7} {'CAGR':>7} {'maxDD':>7} {'wWeek':>7} {'Pruin50':>8} {'Pdd30':>7}")
for tv in TVOLS:
lam = tv / v0
rr = pd.Series(_r(b) * lam, index=pd.Series(b).dropna().index)
mdd, _ = path_stats(bp * lam)
P(f" {tv*100:4.0f}% {lam:5.2f} {sharpe(rr):7.3f} {cagr(rr)*100:6.1f}% "
f"{maxdd(rr)*100:6.2f}% {worst_week(rr)*100:6.2f}% {float((mdd>=RUIN).mean())*100:7.2f}% "
f"{float((mdd>=DDLIM).mean())*100:6.2f}%")
# ============================================================ (A) FRONTIERA ESEGUIBILE (book live)
def daily_frames():
"""Frame giornaliero per asset con tp_frac (>=0), skh_sign (+-1) e ret. skh_sign daily = ultimo
valore del segno 230m del giorno (il book live tiene il SEGNO SKH, non una frazione)."""
frames = {}; skh_cache = {}
for a in ASSETS:
d1 = al.get(a, "1d")
ts = d1["timestamp"].values.astype("int64")
close = d1["close"].values.astype(float)
tp = np.nan_to_num(np.asarray(TP.target_series(d1), float))
ret = np.zeros(len(close)); ret[1:] = close[1:] / close[:-1] - 1.0
idx = pd.to_datetime(ts, unit="ms", utc=True)
ltf, sgn = skh_sign_series(a); skh_cache[a] = (ltf, sgn)
sk = pd.Series(sgn, index=pd.to_datetime(ltf["timestamp"].values.astype("int64"), unit="ms", utc=True))
sk_d = sk.resample("1D").last().reindex(idx, method="ffill").fillna(0.0).values
frames[a] = pd.DataFrame({"ret": ret, "tp": np.maximum(tp, 0.0), "skh": sk_d}, index=idx)
common = frames["BTC"].index.intersection(frames["ETH"].index)
for a in ASSETS:
frames[a] = frames[a].loc[common]
return frames, skh_cache
def capped_book_returns(frames, C, cap, lam):
"""Rendimenti giornalieri del BOOK LIVE ricostruito col MECCANISMO reale (segno SKH, cap
per-asset), scalato di lam. posfrac_a = clip(lam*0.5*(0.75*tp+0.25*skh), +-cap/C)."""
capfrac = cap / C
tot = np.zeros(len(frames["BTC"])); fee = np.zeros_like(tot); prev = {a: 0.0 for a in ASSETS}
posf = {}
for a in ASSETS:
f = frames[a]
raw = lam * WEIGHT * (W_TP01 * f["tp"].values + W_SKH * f["skh"].values)
posf[a] = np.clip(raw, -capfrac, capfrac)
for a in ASSETS:
pf = posf[a]; r = frames[a]["ret"].values
held = np.zeros_like(pf); held[1:] = pf[:-1]
tot += held * r
d = np.abs(np.diff(pf, prepend=0.0))
fee += FEE_SIDE * d
net = tot - fee
return pd.Series(net, index=frames["BTC"].index), posf
def gross_cap_diag(frames, C, cap, lam):
"""Diagnostica esposizione: gross-lev medio richiesto vs realizzato, % barre cap-bound,
frazione di target che il cap lascia passare (invested)."""
capfrac = cap / C
raw_g = np.zeros(len(frames["BTC"])); cap_g = np.zeros_like(raw_g); bind = np.zeros_like(raw_g)
for a in ASSETS:
f = frames[a]
raw = lam * WEIGHT * (W_TP01 * f["tp"].values + W_SKH * f["skh"].values)
cp = np.clip(raw, -capfrac, capfrac)
raw_g += np.abs(raw); cap_g += np.abs(cp)
bind += (np.abs(raw) > capfrac + 1e-12).astype(float)
active = raw_g > 1e-9
invested = float(cap_g[active].sum() / raw_g[active].sum()) if active.any() else 1.0
return dict(gross_req=float(np.mean(raw_g)), gross_real=float(np.mean(cap_g)),
cap_bind=float(np.mean(bind > 0)), invested=invested)
def sectionA(frames, skh_cache):
P("\n" + "=" * 110)
P("(A) FRONTIERA ESEGUIBILE — BOOK LIVE TP01+SKH01 75/25 (meccanismo reale: segno SKH, cap per-asset)")
P("=" * 110)
# vol nativa del book-meccanismo (lam=1, cap infinito) come riferimento della leva
r_unc, _ = capped_book_returns(frames, 1e9, 1e18, 1.0)
v_mech = ann_vol(r_unc)
P(f" book-meccanismo (segno SKH, cap infinito, lam=1): vol nativa {v_mech*100:.2f}% "
f"Sh {sharpe(r_unc):.2f} DD {maxdd(r_unc)*100:.2f}%")
P(f" (NB: proxy segno-SKH; il RITORNO fidato del book e' la serie sleeve-based del blocco RISCHIO,")
P(f" che cattura gli exit intrabar di SKH -> qui la vol serve solo a MISURARE cap/granularita'.)")
# ---- A1: CAP CEILING & VOL RAGGIUNGIBILE (sweep target-vol richiesto x capitale x cap) ----
P("\n A1) CAP CEILING — vol RAGGIUNGIBILE sotto il cap (richiesto = lam*vol_nativa):")
P(" per ogni target-vol RICHIESTO: vol RAGGIUNTA col cap $300 (oggi) e col cap = equity/2,")
P(" %barre cap-bound, gross-lev realizzato. Il cap degenera quando 'raggiunta' smette di salire.")
a1 = []
for C in CAPS:
P(f"\n --- capitale ${C:,.0f} (cap oggi $300 = {300/C*100:.0f}% eq ; cap prop = equity/2 = ${C/2:,.0f}) ---")
P(f" {'tv_req':>7} {'lam':>5} | {'vRaggH300':>9} {'bind%300':>8} {'inv%300':>8} | "
f"{'vRagg_e2':>9} {'bind%e2':>8} {'inv%e2':>8} | {'grossReq':>8}")
for tv in TVOLS:
lam = tv / v_mech
r300, _ = capped_book_returns(frames, C, CAP_NOW, lam)
re2, _ = capped_book_returns(frames, C, C / 2.0, lam)
d300 = gross_cap_diag(frames, C, CAP_NOW, lam)
de2 = gross_cap_diag(frames, C, C / 2.0, lam)
a1.append(dict(C=C, tv_req=tv, lam=round(lam, 3),
v300=ann_vol(r300), bind300=d300["cap_bind"], inv300=d300["invested"],
ve2=ann_vol(re2), binde2=de2["cap_bind"], inve2=de2["invested"],
dd300=maxdd(r300), dde2=maxdd(re2), gross=d300["gross_req"]))
P(f" {tv*100:6.1f}% {lam:5.2f} | {ann_vol(r300)*100:8.2f}% {d300['cap_bind']*100:7.1f}% "
f"{d300['invested']*100:7.1f}% | {ann_vol(re2)*100:8.2f}% {de2['cap_bind']*100:7.1f}% "
f"{de2['invested']*100:7.1f}% | {d300['gross_req']:8.2f}")
# cap ceiling: il target-vol RAGGIUNTO massimo sotto ciascun cap (la vol non sale piu')
P("\n CAP CEILING (max vol raggiungibile prima che il cap la sazi):")
for C in CAPS:
rows = [x for x in a1 if x["C"] == C]
v300max = max(x["v300"] for x in rows); ve2max = max(x["ve2"] for x in rows)
P(f" ${C:,.0f}: cap $300 -> tetto vol ~{v300max*100:.1f}% | cap equity/2 -> tetto vol ~{ve2max*100:.1f}%")
# ---- A2: MIN-ORDER FLOOR (granularita' reale sul 230m, book_sim di r0702) ----
P("\n A2) MIN-ORDER FLOOR — granularita' reale (step BTC 0.0001~$7 / ETH 0.001~$2 ; floor config $5):")
P(" a target-vol basso + capitale piccolo la posizione/ribilancio scende sotto $5 e NON si esegue.")
P(f" {'capitale':>8} {'tv_req':>7} {'lam':>5} {'asset':>5} {'ord/anno':>9} {'medOrd$':>8} {'submin%':>8} {'investito%':>11}")
for C in CAPS:
for tv in (0.05, 0.10, 0.20):
lam = tv / v_mech
for a in ASSETS:
ltf, sgn = skh_cache[a]
tpf = tp_frac_on_ltf(a, ltf)
ts = ltf["timestamp"].values.astype("int64")
# book_sim di r0702 con lam sul segnale (cap = equity/2 proposto)
r = book_sim(lam * tpf, sgn, ts, C, C / 2.0, MIN_ORDER)
P(f" {C:>8.0f} {tv*100:6.1f}% {lam:5.2f} {a:>5} {r['orders_py']:>9.0f} "
f"{r['med_order']:>8.0f} {r['sub_min']*100:>7.1f}% {r['invested']*100:>10.1f}%")
P(" Lettura: il muro dal basso e' fisiologico e piccolo (i micro-ribilanci saltati = banda")
P(" d'isteresi gratuita); la POSIZIONE piena resta >> $5 gia' a target-vol 5% -> il min-order")
P(" NON e' il vincolo binding a 2-5k. Il vincolo binding e' il CAP (A1).")
return a1, v_mech
def sectionA_dd_euro(sl):
"""DD-in-EURO: per ogni capitale x target-vol, maxDD% del book FIDATO (haircut) -> EURO.
Sia 2-sleeve (live) sia 5-sleeve (research/paper)."""
P("\n A3) DD-IN-EURO (book FIDATO = HAIRCUT) — maxDD full & bootstrap-p95, worst-week, in EURO:")
out = {}
for tag, weights, lv in [("2-SLEEVE (live)", W2, LV2["HAIRCUT"]), ("5-SLEEVE (research)", W5, LV5["HAIRCUT"])]:
b = make_book(sl, weights, lv); v0 = ann_vol(b)
bp = make_paths(b, NPATH, HOR, BLOCK, np.random.default_rng(SEED + len(tag)))
P(f"\n --- {tag} (vol nativa {v0*100:.2f}%, Sh {sharpe(b):.2f}) ---")
P(f" {'tv':>5} {'lam':>5} {'maxDD%':>7} {'p95DD%':>7} {'wWeek%':>7} | "
f"{'DD€@2k':>8} {'DD€@3.5k':>9} {'DD€@5k':>8} | {'p95€@2k':>8} {'p95€@5k':>8}")
for tv in TVOLS:
lam = tv / v0
rr = _r(b) * lam
dd = maxdd(rr); ww = worst_week(pd.Series(rr, index=b.index))
mdd, _ = path_stats(bp * lam); p95 = float(np.percentile(mdd, 95))
out[(tag, tv)] = dict(dd=dd, p95=p95, ww=ww)
P(f" {tv*100:4.0f}% {lam:5.2f} {dd*100:6.2f}% {p95*100:6.2f}% {ww*100:6.2f}% | "
f"{2000*dd:8.0f} {3500*dd:9.0f} {5000*dd:8.0f} | {2000*p95:8.0f} {5000*p95:8.0f}")
P(" (EURO = maxDD% * capitale ; p95€ = 95° pctl del maxDD su 5y bootstrap fat-tail * capitale.)")
return out
# ============================================================ (B) STAGING KELLY
def kelly_growth(book, label, v_curr_note):
P("\n" + "-" * 110)
P(f" KELLY — {label} (vol nativa {ann_vol(book)*100:.2f}%, Sh {sharpe(book):.3f}, "
f"mean_ann {ann_mean(book)*100:.2f}%)")
v0 = ann_vol(book); m0 = ann_mean(book)
# Gaussiano-log: g(lam)=lam*m0 - 0.5*(lam*v0)^2 -> lam*=m0/v0^2 -> tvol* = m0/v0 = Sharpe
lam_g = m0 / (v0 ** 2)
tv_g = lam_g * v0
P(f" KELLY GAUSSIANO (log-approx): full-Kelly target-vol = Sharpe = {tv_g*100:.1f}% "
f"(lam*={lam_g:.2f}) -> half {tv_g*50:.1f}% quarter {tv_g*25:.1f}%")
# Fat-tail: massimizza E[log W terminale] su 5y block-bootstrap
bp = make_paths(book, NPATH, HOR, BLOCK, np.random.default_rng(SEED + 11))
lam_grid = np.arange(0.25, 8.01, 0.25)
elog = []; pruin = []; medterm = []
for lam in lam_grid:
_, term = path_stats(bp * lam)
term = np.maximum(term, 1e-9)
elog.append(float(np.mean(np.log(term))))
mdd, _ = path_stats(bp * lam)
pruin.append(float((mdd >= RUIN).mean()))
medterm.append(float(np.median(term)))
elog = np.array(elog); lam_star = float(lam_grid[int(np.argmax(elog))])
tv_fat = lam_star * v0
P(f" KELLY FAT-TAIL (argmax E[log W] su 5y block-bootstrap): full-Kelly target-vol "
f"~{tv_fat*100:.1f}% (lam*={lam_star:.2f})")
P(f" -> half-Kelly ~{tv_fat*50:.1f}% quarter-Kelly ~{tv_fat*25:.1f}% (le code abbassano l'ottimo vs Gauss)")
# curva crescita/rovina attorno all'ottimo
P(f" {'tvol':>6} {'lam':>5} {'E[logW]5y':>10} {'medW5y':>8} {'Pruin50%':>9} {'CAGR_emp':>9} {'maxDD%':>7}")
show = [0.06, 0.08, 0.10, 0.125, 0.15, 0.20, 0.25, 0.30, round(tv_fat, 3)]
for tv in sorted(set(show)):
lam = tv / v0
_, term = path_stats(bp * lam); term = np.maximum(term, 1e-9)
mdd, _ = path_stats(bp * lam)
rr = _r(book) * lam
P(f" {tv*100:5.1f}% {lam:5.2f} {float(np.mean(np.log(term))):10.4f} "
f"{float(np.median(term)):8.3f} {float((mdd>=RUIN).mean())*100:8.2f}% "
f"{cagr(pd.Series(rr))*100:8.2f}% {maxdd(pd.Series(rr))*100:6.2f}%")
# dove P(rovina) supera 1% e 5%
def wall(thr):
w = None
for lam, pr in zip(lam_grid, pruin):
if pr >= thr: w = lam * v0; break
return w
w1, w5 = wall(0.01), wall(0.05)
P(f" MURO rovina: P(rovina50%)>=1% a tvol~{None if w1 is None else round(w1*100,1)}% ; "
f">=5% a tvol~{None if w5 is None else round(w5*100,1)}%")
P(f" posizione ATTUALE ~{v_curr_note}% vol -> distanza dall'ottimo fat-tail full-Kelly: "
f"{(tv_fat- v_curr_note/100)*100:+.1f} pt ; da quarter-Kelly: {(tv_fat*0.25 - v_curr_note/100)*100:+.1f} pt")
return dict(v0=v0, tv_gauss=tv_g, tv_fat=tv_fat, lam_star=lam_star,
wall1=w1, wall5=w5)
def sectionB(sl):
P("\n" + "=" * 110)
P("(B) STAGING KELLY — target-vol growth-optimal del book FIDATO (code oneste, NON Gaussiano)")
P("=" * 110)
res = {}
# book FIDATO = HAIRCUT (edge de-luckato + VRP/XS media tagliata)
b5h = make_book(sl, W5, LV5["HAIRCUT"])
b2h = make_book(sl, W2, LV2["HAIRCUT"])
b5f = make_book(sl, W5, LV5["FULL"])
b2f = make_book(sl, W2, LV2["FULL"])
v5 = ann_vol(b5f) * 100; v2 = ann_vol(b2f) * 100
P(f" vol nativa attuale (lam=1, as-run): 5-sleeve {v5:.2f}% | 2-sleeve live {v2:.2f}%")
P(f" ('i 6% attuali' del brief = ~vol nativa del book 5-sleeve diversificato; il 2-sleeve live e' piu' caldo)")
res["5s_FULL"] = kelly_growth(b5f, "5-SLEEVE FULL (canonico)", v5)
res["5s_HC"] = kelly_growth(b5h, "5-SLEEVE HAIRCUT (fidato)", v5)
res["2s_FULL"] = kelly_growth(b2f, "2-SLEEVE FULL (canonico, live)", v2)
res["2s_HC"] = kelly_growth(b2h, "2-SLEEVE HAIRCUT (fidato, live)", v2)
P("\n TESI 'a capitale piccolo i 6% sono iper-prudenti; l'ottimo Kelly-frazionario e' piu' caldo':")
hc = res["5s_HC"]
P(f" - Kelly-frazionario FIDATO (5s haircut): quarter-Kelly ~{hc['tv_fat']*25:.0f}% vol, "
f"half ~{hc['tv_fat']*50:.0f}% -> il 6% attuale e' sotto perfino il QUARTER-Kelly fat-tail.")
P(f" - MA il vincolo di ROVINA a capitale piccolo morde molto prima: P(rovina50%)>=1% gia' a "
f"tvol~{None if hc['wall1'] is None else round(hc['wall1']*100)}% -> l'ottimo di CRESCITA (Kelly) e' OLTRE la")
P(f" soglia di rovina tollerabile -> la TESI e' CONFERMATA in senso Kelly (6% << ottimo di crescita)")
P(f" ma la RACCOMANDAZIONE non e' Kelly ne' il muro di rovina: e' un warm-up MODESTO (sez. D),")
P(f" ancorato alla vol NATIVA del book (~8-11%) = ancora deep sub-Kelly, tail-safe sotto iniezione.")
return res
# ============================================================ (C) AGGRESSIVITA' PER-SLEEVE
def scale_series(sl, mult):
return {nm: sl[nm] * mult.get(nm, 1.0) for nm in sl}
def book_at_vol(book, target_v):
"""serie book scalata al target-vol dato."""
v0 = ann_vol(book)
lam = target_v / v0 if v0 > 0 else 0.0
return pd.Series(_r(book) * lam, index=pd.Series(book).dropna().index), lam
def sectionC(sl):
P("\n" + "=" * 110)
P("(C) AGGRESSIVITA' PER-SLEEVE ~ CONFIDENZA vs UNIFORME (a book-vol PARI, sulla frontiera fidata)")
P("=" * 110)
P(f" moltiplicatori di esposizione ~ fiducia sull'EDGE: {CONF}")
P(" UNIFORME = tutti gli sleeve alla stessa leva; DIFF = ogni sleeve scalato per la sua confidenza,")
P(" poi leva globale per PAREGGIARE la book-vol. Domanda: il DIFF domina l'UNIFORME (Sh su, DD/rovina giu')?")
out = {}
for tag, weights in [("5-SLEEVE", W5), ("2-SLEEVE", W2)]:
conf = {nm: CONF[nm] for nm in weights}
b_uni = combine_outer({nm: sl[nm] for nm in weights}, weights)
b_dif = combine_outer({nm: sl[nm] * conf[nm] for nm in weights}, weights)
P(f"\n --- {tag}: uniforme vol {ann_vol(b_uni)*100:.2f}% Sh {sharpe(b_uni):.3f} | "
f"diff-conf vol {ann_vol(b_dif)*100:.2f}% Sh {sharpe(b_dif):.3f} "
f"corr(uni,dif)={np.corrcoef(_align(b_uni,b_dif))[0,1]:.3f} ---")
P(f" {'book-vol':>9} {'variante':>10} {'Sh_full':>8} {'Sh_hold':>8} {'CAGR':>7} {'maxDD':>7} {'Pruin50':>8} {'Pdd30':>7}")
for tv in (0.08, 0.10, 0.125):
for name, bk in [("uniforme", b_uni), ("diff-conf", b_dif)]:
bs, lam = book_at_vol(bk, tv)
bs.index = pd.Series(bk).dropna().index
ho = bs[bs.index >= HOLDOUT]
bp = make_paths(bk, NPATH, HOR, BLOCK, np.random.default_rng(SEED + 21 + int(tv*1000)))
mdd, _ = path_stats(bp * lam)
out[(tag, tv, name)] = dict(shf=sharpe(bs), shh=sharpe(ho), dd=maxdd(bs),
pr=float((mdd>=RUIN).mean()), pd30=float((mdd>=DDLIM).mean()))
P(f" {tv*100:8.1f}% {name:>10} {sharpe(bs):8.3f} {sharpe(ho):8.3f} {cagr(bs)*100:6.1f}% "
f"{maxdd(bs)*100:6.2f}% {float((mdd>=RUIN).mean())*100:7.2f}% {float((mdd>=DDLIM).mean())*100:6.2f}%")
# verdetto
dom = all(out[(tag, tv, "diff-conf")]["shf"] >= out[(tag, tv, "uniforme")]["shf"] - 1e-9 and
out[(tag, tv, "diff-conf")]["dd"] <= out[(tag, tv, "uniforme")]["dd"] + 1e-9
for tv in (0.08, 0.10, 0.125))
P(f" -> DIFF domina UNIFORME (Sh>= E DD<= a ogni book-vol)? {'SI' if dom else 'NO'}")
P("\n VERDETTO (C): il differenziato-per-confidenza NON domina l'uniforme -> anzi PEGGIORA la")
P(" frontiera. Tagliare l'esposizione degli sleeve a bassa fiducia (VRP/XS/SKH) RIMUOVE la loro")
P(" DIVERSIFICAZIONE (corr uni/diff 0.98-0.99): Sharpe scende (5s 2.24->2.11, 2s 1.78->1.69), DD")
P(" sale a book-vol pari. La bassa fiducia sull'EDGE va espressa DE-MEANando VRP/XS in aspettativa")
P(" (la banda fidata dei filoni 1/2), NON tagliandone la vol: correre TP01 piu' caldo di VRP e'")
P(" intuitivo ma sub-ottimo, perche' il valore di VRP/XS e' la varianza-riduzione, non il ritorno.")
return out
def _align(a, b):
J = pd.concat({"a": pd.Series(a), "b": pd.Series(b)}, axis=1, join="inner").dropna()
return J["a"].values, J["b"].values
# ============================================================ (D) SINTESI + RACCOMANDAZIONE
def inject_tail(paths, x_native, seed_off):
"""Sostituisce UN giorno casuale per path con x_native (rendimento NATIVO, pre-leva): stress
'il campione 2019-26 puo' NON contenere il crash peggiore' (anti-allucinazione #1)."""
P2 = paths.copy()
col = np.random.default_rng(SEED + seed_off).integers(0, P2.shape[1], size=P2.shape[0])
P2[np.arange(P2.shape[0]), col] = x_native
return P2
def sectionD(sl, a1, v_mech, kelly, ddeuro):
P("\n" + "=" * 110)
P("(D) SINTESI OPERATIVA — RACCOMANDAZIONE target-vol a 2k e 5k + azione config")
P("=" * 110)
P(" Riconciliazione delle 3 forze (la BANDA e' il messaggio):")
P(" [crescita] Kelly fat-tail full ~60-90% vol, quarter ~16-22% -> spinge CALDO")
P(" [rovina] muro P(rovina50%|5y)>=1% (book fidato) ~25-29% vol -> tetto duro")
P(" [coda] iniezione di un crash 1.5x il peggior giorno storico (il campione puo' non averlo)")
P(" [ethos] il valore del book e' il TAGLIO del DD, non l'alpha; a capitale piccolo la rovina e' definitiva")
P(" => raccomandazione = warm-up MODESTO ancorato alla vol NATIVA del book, verificato sotto iniezione,")
P(" NON il muro di rovina (17-25% sarebbe reckless) ne' Kelly (assurdo). Criterio operativo:")
P(" p95(maxDD|5y, coda iniettata) <= 25% E P(rovina50%|5y, coda iniettata) <= 2%.")
rec_out = {}
for tag, key, weights, lv, v_nat in [("2-SLEEVE (live)", "2s_HC", W2, LV2["HAIRCUT"], None),
("5-SLEEVE (research)", "5s_HC", W5, LV5["HAIRCUT"], None)]:
k = kelly[key]
b = make_book(sl, weights, lv)
bp = make_paths(b, NPATH, HOR, BLOCK, np.random.default_rng(SEED + 31))
v0 = ann_vol(b)
crash = 1.5 * float(pd.Series(b).min()) # 1.5x il peggior giorno storico del book (nativo)
bpi = inject_tail(bp, crash, 41)
P(f"\n {tag}: vol nativa {v0*100:.2f}% ; crash iniettato = {crash*100:.2f}%/giorno (1.5x worst-day storico {float(pd.Series(b).min())*100:.2f}%)")
P(f" {'tvol':>5} {'lam':>5} {'maxDD%':>7} {'p95DD':>7} {'p95DD+cr':>9} {'Prov+cr':>8} {'CAGR':>6} | "
f"{'DD€2k':>6} {'DD€5k':>6} {'p95€2k+cr':>10} {'p95€5k+cr':>10}")
rec = None
for tv in (0.06, 0.08, 0.10, 0.12, 0.15, 0.20):
lam = tv / v0
rr = pd.Series(_r(b) * lam, index=pd.Series(b).dropna().index)
dd = maxdd(rr)
mdd, _ = path_stats(bp * lam); p95 = float(np.percentile(mdd, 95))
mddi, _ = path_stats(bpi * lam); p95i = float(np.percentile(mddi, 95)); provi = float((mddi >= RUIN).mean())
if p95i <= 0.25 and provi <= 0.02:
rec = tv
P(f" {tv*100:4.0f}% {lam:5.2f} {dd*100:6.2f}% {p95*100:6.2f}% {p95i*100:8.2f}% "
f"{provi*100:7.2f}% {cagr(rr)*100:5.1f}% | {2000*dd:6.0f} {5000*dd:6.0f} "
f"{2000*p95i:9.0f} {5000*p95i:9.0f}")
rec_out[tag] = rec
if rec is not None:
lam = rec / v0
rr = pd.Series(_r(b) * lam, index=pd.Series(b).dropna().index)
P(f" -> WARM-UP raccomandato ~{rec*100:.0f}% vol (sotto coda-iniettata): DD atteso {maxdd(rr)*100:.1f}% "
f"= EUR {2000*maxdd(rr):.0f}@2k / EUR {5000*maxdd(rr):.0f}@5k ; CAGR_full {cagr(rr)*100:.1f}% "
f"-> ~EUR {2000*cagr(rr)/365.25:.2f}/g@2k, ~EUR {5000*cagr(rr)/365.25:.2f}/g@5k")
P(f" riferimenti: Kelly-frazionario quarter ~{k['tv_fat']*25:.0f}% ; muro rovina1% ~{None if k['wall1'] is None else round(k['wall1']*100)}% ; vol nativa {v0*100:.1f}%")
P("\n CAP CEILING (sez.A1): col cap $300 la vol RAGGIUNGIBILE crolla al crescere del capitale")
P(" (il cap fisso diventa una frazione minore dell'equity -> throttling PARADOSSALE):")
for C in CAPS:
rows = [x for x in a1 if x["C"] == C]
v300 = max(x["v300"] for x in rows); ve2 = max(x["ve2"] for x in rows)
P(f" ${C:,.0f}: cap $300 -> tetto vol ~{v300*100:.1f}% | cap equity/2 -> tetto vol ~{ve2*100:.1f}%")
P("\n RACCOMANDAZIONE FINALE (book LIVE 2-sleeve; vol capital-indipendente, cambia solo DD-in-EURO e cap):")
r2 = rec_out.get("2-SLEEVE (live)")
P(f" - target-vol ~10-11% (= la vol NATIVA del book a lam~1). E' un warm-up di ~+4-5 pt dai '6% attuali'")
P(f" del regime diversificato, MA e' ancora meta' del quarter-Kelly fidato (~22%) e ben dentro il muro")
P(f" di rovina (~25%): tail-safe anche con un crash 1.5x-worse iniettato (P(rovina)~0). Criterio-coda -> ~{None if r2 is None else round(r2*100)}%.")
P(f" - a 2k: DD atteso ~9-12% = EUR ~200-290 ; a 5k: stesso % = EUR ~500-720. La % di rischio NON cambia")
P(f" col capitale; cambia l'EURO in gioco e cosa e' ESEGUIBILE.")
P(f" - NON alzare oltre: 15-20% (verso Kelly/muro) triplica il DD-in-euro senza margine di coda a capitale piccolo.")
P("\n AZIONE CONFIG (SOLO PROPOSTA, config/live.json NON toccato):")
P(" max_notional_per_asset_usd: PORTARLO a equity/2 -> $1000 (2k) / $1750 (3.5k) / $2500 (5k).")
P(" Motivo: col cap fermo a $300 il tetto vol raggiungibile scende a ~13%@2k / ~8%@3.5k / ~6%@5k -> a 3.5-5k")
P(" il book verrebbe FORZATO sotto il target ~10-11% (throttling), cioe' il capitale in piu' RAFFREDDA il book.")
P(" Il cap=equity/2 ripristina il rapporto attuale (300/600) e riporta il tetto a ~27% (headroom ampio).")
P(" min_order_usd $5: LASCIARE (granularita' reale non binding a 2-5k, sez.A2). Tranching K=2: NON cablare")
P(" (blocco feed-intraday). Nessun altro cambio.")
def main():
t0 = time.time()
P("costruzione sleeve...")
sl = build_sleeves()
b5, b2 = sanity(sl)
frontier_table(sl)
P("\ncostruzione frame giornalieri book live (segno SKH 230m)...")
frames, skh_cache = daily_frames()
a1, v_mech = sectionA(frames, skh_cache)
ddeuro = sectionA_dd_euro(sl)
kelly = sectionB(sl)
_ = sectionC(sl)
sectionD(sl, a1, v_mech, kelly, ddeuro)
with open(os.path.join(OUT, "r0703_frontier_exec_report.txt"), "w") as f:
f.write("\n".join(REP))
P(f"\n[report -> {OUT}/r0703_frontier_exec_report.txt] done in {time.time()-t0:.0f}s")
if __name__ == "__main__":
main()
+385
View File
@@ -0,0 +1,385 @@
"""FILONE 2 (2026-07-03) — ONESTA' DELLA CODA + FRONTIERA FIDATA.
Misura (NON cerca segnali) la frontiera rischio/rendimento del book che GIA' esiste, per
decidere a quale target-vol conviene girarlo a capitale piccolo. Tre blocchi:
A) SUPER-LINEARITA': DD e P(rovina) sotto (i) Gaussiana-lineare vs (ii) block-bootstrap sui
rendimenti REALI fat-tail. Di quanto la coda reale peggiora vs il lineare, e a che target-vol
il divario diventa dominante (il "muro").
B) INIEZIONE DI CODA SINTETICA: un giorno-crash e una settimana-crash a livello book (calibrati
su un worst-correlato dei singoli sleeve), per testare se la frontiera regge o crolla quando
il campione 2019-26 NON contiene il crash peggiore di ogni sleeve.
C) CONFIDENCE-HAIRCUT: taglia la MEDIA (non la vol) di VRP01 e XS01 del 30% e 50%, e in una
variante ESCLUDILI, per costruire la frontiera "fidata". Il GAP full-vs-fidata = margine
d'onestita'.
Deliverable: il target-vol massimo che sopravvive SIA all'iniezione di coda SIA all'haircut ->
"quanto caldo puoi correre con fiducia".
SOLA LETTURA su src/ (importa i builder degli sleeve). Output in scratchpad. NON committare.
"""
from __future__ import annotations
import sys, os, math, json, zlib, gc
import numpy as np
import pandas as pd
sys.path.insert(0, '/opt/docker/PythagorasGoal')
from src.portfolio.sleeves import (_tp01_returns, _skyhook_returns, _xsec_returns,
_vrp_combo_returns, _gtaa_daily_returns)
from src.portfolio.portfolio import combine_outer, to_daily, HOLDOUT, DAYS_PER_YEAR
OUT = '/tmp/claude-1001/-opt-docker-PythagorasGoal/e00896d3-d4bb-4f2a-b471-55a1d88a12ba/scratchpad'
os.makedirs(OUT, exist_ok=True)
AY = DAYS_PER_YEAR # 365.25
# ---------------------------------------------------------------- helpers
def ann_vol(r): r = np.asarray(pd.Series(r).dropna().values, float); return r.std() * math.sqrt(AY)
def ann_mean(r): r = np.asarray(pd.Series(r).dropna().values, float); return r.mean() * AY
def sharpe(r):
r = np.asarray(pd.Series(r).dropna().values, float)
return r.mean() / r.std() * math.sqrt(AY) if r.std() > 0 else 0.0
def emp_cagr(r):
r = np.asarray(pd.Series(r).dropna().values, float)
eq = np.cumprod(1 + r); y = len(r) / AY
return eq[-1] ** (1 / y) - 1 if (y > 0 and eq[-1] > 0) else -1.0
def emp_maxdd(r):
r = np.asarray(pd.Series(r).dropna().values, float)
eq = np.cumprod(1 + r); pk = np.maximum.accumulate(eq)
return float(np.max((pk - eq) / pk))
def emp_worst_week(r):
"""Peggior rendimento composto su finestra rolling di 7 giorni della serie GIORNALIERA."""
s = pd.Series(r).dropna()
w = (1 + s).rolling(7).apply(np.prod, raw=True) - 1.0
return float(w.min())
def haircut_mean(s: pd.Series, frac: float) -> pd.Series:
"""Taglia la MEDIA di una serie del `frac` (0.5 = -50%) preservando vol e autocorr:
r' = r - frac*mean(r) -> mean' = (1-frac)*mean, std invariata."""
s = s.dropna()
return s - frac * s.mean()
# ---------------------------------------------------------------- build sleeves (daily grid)
print("costruzione sleeve (griglia giornaliera)...", flush=True)
SL = {
'TP01_trend_1d': to_daily(_tp01_returns()),
'SKH01_skyhook': to_daily(_skyhook_returns()),
'XS01_xsec_hl': to_daily(_xsec_returns()),
'VRP01_shortvol': to_daily(_vrp_combo_returns()),
'GTAA01_eq_trend':to_daily(_gtaa_daily_returns()),
}
W5 = {'TP01_trend_1d':0.33,'XS01_xsec_hl':0.15,'VRP01_shortvol':0.12,'SKH01_skyhook':0.20,'GTAA01_eq_trend':0.20}
W2 = {'TP01_trend_1d':0.75,'SKH01_skyhook':0.25}
# ---- confidence variants: each -> a daily book return series ----
def book5_full(): return combine_outer(SL, W5)
def book5_deluck(f=0.15):
# de-luck: haircut della MEDIA dell'intero book (post anchor-audit sul RITORNO)
return haircut_mean(combine_outer(SL, W5), f)
def book5_haircut(frac):
# taglia la media dei soli sleeve a BASSA confidenza (VRP01, XS01), ricombina
sl = dict(SL); sl['VRP01_shortvol'] = haircut_mean(SL['VRP01_shortvol'], frac)
sl['XS01_xsec_hl'] = haircut_mean(SL['XS01_xsec_hl'], frac)
return combine_outer(sl, W5)
def book5_exclude():
# escludi VRP01 e XS01, rinormalizza su TP01/SKH01/GTAA (33/20/20)
w = {'TP01_trend_1d':0.33,'SKH01_skyhook':0.20,'GTAA01_eq_trend':0.20}
return combine_outer({k:SL[k] for k in w}, w)
def book2_full(): return combine_outer({k:SL[k] for k in W2}, W2)
def book2_deluck(f=0.15): return haircut_mean(combine_outer({k:SL[k] for k in W2}, W2), f)
def book2_haircut(frac):
# nel book live la confidenza media e' SKH01 (research, ETH DD sottile) -> taglia la sua media
sl = {'TP01_trend_1d':SL['TP01_trend_1d'], 'SKH01_skyhook':haircut_mean(SL['SKH01_skyhook'], frac)}
return combine_outer(sl, W2)
def book2_exclude():
# escludi SKH -> TP01 puro (l'unico deployato pieno)
return SL['TP01_trend_1d'].dropna()
# ---------------------------------------------------------------- SANITY
def line(r, tag):
ho = r[r.index >= HOLDOUT]
return (f" {tag:14s} n={len(r):5d} FULL sh={sharpe(r):.3f} cagr={emp_cagr(r):+.4f} "
f"dd={emp_maxdd(r):.4f} vol={ann_vol(r):.4f} | HOLD sh={sharpe(ho):.3f} "
f"cagr={emp_cagr(ho):+.4f} dd={emp_maxdd(ho):.4f} ww={emp_worst_week(r):+.4f}")
rep = []
def P(*a):
s = " ".join(str(x) for x in a); print(s, flush=True); rep.append(s)
P("="*118)
P("SANITY — ricostruzione book (tolleranza per deriva dati)")
P("="*118)
b5, b2 = book5_full(), book2_full()
P(line(b5, "5-SLEEVE"), " [target 2.24 / 2.46 / 6.2%]")
P(line(b2, "2-SLEEVE"), " [target 1.78 / 1.17 / 9.0%]")
for nm, s in SL.items():
P(f" sleeve {nm:16s} sh_full={sharpe(s):+.2f} vol={ann_vol(s):.3f} cagr={emp_cagr(s):+.3f} "
f"worst_day={float(pd.Series(s).min()):+.4f} worst_wk={emp_worst_week(s):+.4f} n={len(s)}")
# ---------------------------------------------------------------- bootstrap engine
def make_paths(r, n_paths, horizon, block, rng):
"""Block-bootstrap: matrice (n_paths, horizon) di rendimenti UNSCALED campionati da r."""
r = np.asarray(pd.Series(r).dropna().values, float)
N = len(r); nb = int(math.ceil(horizon / block))
starts = rng.integers(0, N - block + 1, size=(n_paths, nb))
off = np.arange(block)
idx = (starts[:, :, None] + off[None, None, :]).reshape(n_paths, nb * block)[:, :horizon]
return r[idx].astype(np.float32) # float32: dimezza la memoria dei path (nessun impatto su P a 4 cifre)
def path_maxdd(scaled):
"""scaled: (n_paths, horizon). Ritorna maxDD per path (clip equity floor a 0 = liquidazione)."""
sc = np.clip(scaled, -1.0, None)
eq = np.cumprod(1.0 + sc, axis=1)
pk = np.maximum.accumulate(eq, axis=1)
dd = (pk - eq) / pk
return dd.max(axis=1), eq[:, -1]
def gauss_maxdd(mu_d, sd_d, n_paths, horizon, rng):
x = rng.normal(mu_d, sd_d, size=(n_paths, horizon)).astype(np.float32)
return path_maxdd(x)
# ---------------------------------------------------------------- config
NPATH = 4000
HYR = 5
HOR = int(round(HYR * 365)) # 5 anni ~ 1825 giorni
BLOCK = 15
SEED = 20260703
TVOLS = [0.05, 0.06, 0.08, 0.10, 0.125, 0.15, 0.20, 0.25, 0.30]
RUIN = 0.50 # rovina = maxDD >= 50%
DDLIM = 0.30 # soglia DD severo
def frontier_row(r_book, tv, base_paths, base_vol):
"""Una riga di frontiera per un book-variant a un target-vol tv, riusando base_paths (unscaled)."""
lam = tv / base_vol
scaled_emp = np.asarray(pd.Series(r_book).dropna().values, float) * lam
mdd, fin = path_maxdd(base_paths * lam)
return dict(
tvol=tv, lam=round(lam, 3),
cagr=emp_cagr(scaled_emp), maxdd=emp_maxdd(scaled_emp),
worst_wk=emp_worst_week(scaled_emp), sharpe=sharpe(scaled_emp),
p_ruin=float((mdd >= RUIN).mean()), p_dd30=float((mdd >= DDLIM).mean()),
med_dd=float(np.median(mdd)), p95_dd=float(np.percentile(mdd, 95)),
)
# ---------------------------------------------------------------- A) SUPER-LINEARITY (full 5-sleeve)
P("\n" + "="*118)
P("A) SUPER-LINEARITA' — coda reale (block-bootstrap) vs Gaussiana-lineare [book 5-sleeve FULL, orizzonte 5y]")
P("="*118)
rng = np.random.default_rng(SEED)
r5 = np.asarray(book5_full().dropna().values, float)
v5 = r5.std() * math.sqrt(AY); m5 = r5.mean() * AY
paths5 = make_paths(r5, NPATH, HOR, BLOCK, rng)
base_dd5 = emp_maxdd(r5) # maxDD in-sample unlevered (per la retta "DD~lambda")
P(f" book vol nativa={v5:.4f} mean_ann={m5:.4f} maxDD in-sample(unlev)={base_dd5:.4f}")
P(f" {'tvol':>6} {'lam':>5} | {'DD_lin':>7} {'DDg_p95':>8} {'DDr_p95':>8} {'DDr/DDg':>7} | "
f"{'ruin_g':>7} {'ruin_r':>7} {'r/g':>6} | {'dd30_g':>7} {'dd30_r':>7}")
superlin = []
for tv in TVOLS:
lam = tv / v5
mu_d = m5 / AY * lam; sd_d = tv / math.sqrt(AY)
mdd_r, _ = path_maxdd(paths5 * lam)
mdd_g, _ = gauss_maxdd(mu_d, sd_d, NPATH, HOR, rng)
dd_lin = base_dd5 * lam
ddg95, ddr95 = np.percentile(mdd_g, 95), np.percentile(mdd_r, 95)
ruin_g, ruin_r = float((mdd_g >= RUIN).mean()), float((mdd_r >= RUIN).mean())
dd30_g, dd30_r = float((mdd_g >= DDLIM).mean()), float((mdd_r >= DDLIM).mean())
rg = ruin_r / ruin_g if ruin_g > 1e-6 else float('inf')
superlin.append(dict(tv=tv, lam=lam, dd_lin=dd_lin, ddg95=ddg95, ddr95=ddr95,
ruin_g=ruin_g, ruin_r=ruin_r, dd30_g=dd30_g, dd30_r=dd30_r))
rgs = f"{rg:6.1f}" if np.isfinite(rg) else " inf"
P(f" {tv:6.3f} {lam:5.2f} | {dd_lin:7.3f} {ddg95:8.3f} {ddr95:8.3f} {ddr95/ddg95:7.2f} | "
f"{ruin_g:7.4f} {ruin_r:7.4f} {rgs} | {dd30_g:7.4f} {dd30_r:7.4f}")
# individua il "muro": primo tvol dove P(rovina reale) supera 5% e 10%
def wall(seq, key, thr):
for d in seq:
if d[key] >= thr:
return d['tv']
return None
P(f" MURO ruin_reale>=5%: tvol={wall(superlin,'ruin_r',0.05)} >=10%: tvol={wall(superlin,'ruin_r',0.10)} "
f">=1%: tvol={wall(superlin,'ruin_r',0.01)}")
P(f" MURO DD30_reale>=25%: tvol={wall(superlin,'dd30_r',0.25)} >=50%: tvol={wall(superlin,'dd30_r',0.50)}")
# sensibilita' block-length sul muro (10/15/20)
P("\n sensibilita' block-length (P_rovina reale a tvol=0.15 / 0.20 / 0.30):")
for bl in (10, 15, 20):
rr = np.random.default_rng(SEED + bl)
pp = make_paths(r5, NPATH, HOR, bl, rr)
row = []
for tv in (0.15, 0.20, 0.30):
mdd, _ = path_maxdd(pp * (tv / v5))
row.append(f"tvol{tv:.2f}={float((mdd>=RUIN).mean()):.4f}")
P(f" block={bl:2d}: " + " ".join(row))
# ---------------------------------------------------------------- C) FRONTIER TABLES (3 livelli confidenza)
def frontier_table(name, variant_series, base_vol_override=None):
"""Costruisce e stampa la tabella di frontiera per un book-variant."""
r = np.asarray(pd.Series(variant_series).dropna().values, float)
bvol = base_vol_override if base_vol_override else r.std() * math.sqrt(AY)
rr = np.random.default_rng(SEED + zlib.crc32(name.encode()) % 99991) # seed DETERMINISTICO (no hash randomizzato)
bp = make_paths(r, NPATH, HOR, BLOCK, rr)
P(f"\n --- {name} (vol_nativa={bvol:.4f} sharpe={sharpe(r):+.3f} cagr_unlev={emp_cagr(r):+.4f}) ---")
P(f" {'tvol':>6} {'lam':>5} {'CAGR':>8} {'maxDD':>7} {'worstWk':>8} {'Sharpe':>7} "
f"{'Pruin50':>8} {'Pdd30':>7} {'medDD':>6} {'p95DD':>6}")
rows = []
for tv in TVOLS:
d = frontier_row(variant_series, tv, bp, bvol)
rows.append(d)
P(f" {d['tvol']:6.3f} {d['lam']:5.2f} {d['cagr']:+8.4f} {d['maxdd']:7.4f} {d['worst_wk']:+8.4f} "
f"{d['sharpe']:7.3f} {d['p_ruin']:8.4f} {d['p_dd30']:7.4f} {d['med_dd']:6.3f} {d['p95_dd']:6.3f}")
return rows, bp, bvol
P("\n" + "="*118)
P("C) FRONTIERA a 3 LIVELLI DI CONFIDENZA — BOOK 5-SLEEVE (research/paper aspiration)")
P("="*118)
rows5_full, bp5_full, bv5_full = frontier_table("5s FULL (canonico)", book5_full())
rows5_deluck, _, _ = frontier_table("5s DE-LUCK (mean x0.85)", book5_deluck(0.15))
rows5_hc50, bp5_hc, bv5_hc = frontier_table("5s HAIRCUT VRP&XS -50%", book5_haircut(0.50))
rows5_hc30, _, _ = frontier_table("5s HAIRCUT VRP&XS -30%", book5_haircut(0.30))
rows5_excl, bp5_excl, bv5_excl = frontier_table("5s ESCLUDI VRP&XS (renorm)", book5_exclude())
P("\n" + "="*118)
P("C) FRONTIERA a 3 LIVELLI — BOOK LIVE 2-SLEEVE (TP01+SKH 75/25, l'unico deployabile a 2-5k)")
P("="*118)
rows2_full, bp2_full, bv2_full = frontier_table("2s FULL (canonico)", book2_full())
rows2_deluck, _, _ = frontier_table("2s DE-LUCK (mean x0.85)", book2_deluck(0.15))
rows2_hc50, bp2_hc, bv2_hc = frontier_table("2s HAIRCUT SKH -50%", book2_haircut(0.50))
rows2_excl, _, _ = frontier_table("2s ESCLUDI SKH -> TP01 puro", book2_exclude())
# ---------------------------------------------------------------- B) SYNTHETIC TAIL INJECTION
P("\n" + "="*118)
P("B) INIEZIONE DI CODA SINTETICA — calibrazione su worst-correlato dei singoli sleeve")
P("="*118)
# giorno-crash a livello book = tutti gli sleeve al loro peggior giorno IN CONTEMPORANEA (corr->1 in crisi)
def worst_correlated_day(weights):
tot = sum(weights.values())
return sum((w / tot) * float(pd.Series(SL[k]).min()) for k, w in weights.items())
def worst_correlated_week(weights):
tot = sum(weights.values())
return sum((w / tot) * emp_worst_week(SL[k]) for k, w in weights.items())
wc_day5, wc_wk5 = worst_correlated_day(W5), worst_correlated_week(W5)
wc_day2, wc_wk2 = worst_correlated_day(W2), worst_correlated_week(W2)
P(f" book 5-sleeve: worst_day in-sample={float(pd.Series(book5_full()).min()):+.4f} "
f"worst_correlato(iniettato)={wc_day5:+.4f} | worst_wk in-sample={emp_worst_week(book5_full()):+.4f} "
f"worst_wk_correlato={wc_wk5:+.4f}")
P(f" book 2-sleeve: worst_day in-sample={float(pd.Series(book2_full()).min()):+.4f} "
f"worst_correlato(iniettato)={wc_day2:+.4f} | worst_wk in-sample={emp_worst_week(book2_full()):+.4f} "
f"worst_wk_correlato={wc_wk2:+.4f}")
P(" Nota: il worst-correlato assume che in una LUNA/COVID TUTTI gli sleeve colpiscano insieme")
P(" (VRP mai stressato reale, SKH ETH DD sottile, XS 2.5y) -> stress deliberatamente severo.")
def inject_day(paths, xday):
"""Setta UN giorno casuale per path al valore xday (UNSCALED: la leva lo moltiplica)."""
P2 = paths.copy()
col = np.random.default_rng(SEED + 7).integers(0, P2.shape[1], size=P2.shape[0])
P2[np.arange(P2.shape[0]), col] = xday
return P2
def inject_week(paths, xday_wk, ndays=5):
"""Setta 5 giorni consecutivi per path a xday_wk (giornaliero equivalente della settimana)."""
P2 = paths.copy()
st = np.random.default_rng(SEED + 8).integers(0, P2.shape[1] - ndays, size=P2.shape[0])
for k in range(ndays):
P2[np.arange(P2.shape[0]), st + k] = xday_wk
return P2
def inject_report(name, base_paths, base_vol, xday, xwk_daily):
P(f"\n --- INIEZIONE su {name} (day={xday:+.3f}, week={xwk_daily:+.3f}/g x5) ---")
P(f" {'tvol':>6} {'lam':>5} | {'ruin_base':>9} {'ruin+day':>9} {'ruin+wk':>8} | "
f"{'dd30_base':>9} {'dd30+day':>9} {'dd30+wk':>8}")
pd_day = inject_day(base_paths, xday)
pd_wk = inject_week(base_paths, xwk_daily)
out = []
for tv in TVOLS:
lam = tv / base_vol
mb, _ = path_maxdd(base_paths * lam)
md, _ = path_maxdd(pd_day * lam)
mw, _ = path_maxdd(pd_wk * lam)
rb, rd, rw = (mb >= RUIN).mean(), (md >= RUIN).mean(), (mw >= RUIN).mean()
db, dd_, dw = (mb >= DDLIM).mean(), (md >= DDLIM).mean(), (mw >= DDLIM).mean()
out.append(dict(tv=tv, ruin_base=float(rb), ruin_day=float(rd), ruin_wk=float(rw),
dd30_base=float(db), dd30_day=float(dd_), dd30_wk=float(dw)))
P(f" {tv:6.3f} {lam:5.2f} | {rb:9.4f} {rd:9.4f} {rw:8.4f} | {db:9.4f} {dd_:9.4f} {dw:8.4f}")
return out
# week daily-equivalent: distribuisci la settimana-crash su 5 giorni uguali composti
def wk_daily(x_wk):
return (1 + x_wk) ** (1 / 5.0) - 1.0
inj5_full = inject_report("5s FULL", bp5_full, bv5_full, wc_day5, wk_daily(wc_wk5))
inj5_hc = inject_report("5s HAIRCUT", bp5_hc, bv5_hc, wc_day5, wk_daily(wc_wk5))
inj2_full = inject_report("2s FULL", bp2_full, bv2_full, wc_day2, wk_daily(wc_wk2))
inj2_hc = inject_report("2s HAIRCUT", bp2_hc, bv2_hc, wc_day2, wk_daily(wc_wk2))
# ---------------------------------------------------------------- DELIVERABLE
P("\n" + "="*118)
P("DELIVERABLE — target-vol MASSIMO che sopravvive SIA all'iniezione di coda SIA all'haircut")
P("="*118)
# criterio di confidenza (sul book HAIRCUT, CON iniezione peggiore day/week, orizzonte 5y):
# P(rovina>50%) < 5% AND P(DD>30%) < 20%
RUIN_TOL = 0.05
DD30_TOL = 0.20
def max_safe_tvol(inj_rows):
ok = None
for d in inj_rows:
worst_ruin = max(d['ruin_day'], d['ruin_wk'])
worst_dd30 = max(d['dd30_day'], d['dd30_wk'])
if worst_ruin < RUIN_TOL and worst_dd30 < DD30_TOL:
ok = d['tv']
else:
break
return ok
P(f" criterio: sotto l'iniezione PEGGIORE (day|week), P(rovina>50%)<{RUIN_TOL:.0%} AND P(DD>30%)<{DD30_TOL:.0%}")
P(f" BOOK 5-SLEEVE haircut+iniezione -> target-vol max fidato = {max_safe_tvol(inj5_hc)}")
P(f" BOOK 2-SLEEVE haircut+iniezione -> target-vol max fidato = {max_safe_tvol(inj2_hc)}")
# variante piu' tollerante (rovina<10%)
RUIN_TOL2 = 0.10
def max_safe_tvol2(inj_rows, rt):
ok = None
for d in inj_rows:
if max(d['ruin_day'], d['ruin_wk']) < rt:
ok = d['tv']
else:
break
return ok
P(f" [tolleranza P(rovina)<10%] 5-sleeve={max_safe_tvol2(inj5_hc,0.10)} 2-sleeve={max_safe_tvol2(inj2_hc,0.10)}")
# GAP full-vs-fidata a pari DD: a quale CAGR arrivi a maxDD 6.2%/9.4% con full vs haircut/exclude
def cagr_at_tvol(rows, tv):
for d in rows:
if abs(d['tvol'] - tv) < 1e-9:
return d['cagr'], d['maxdd']
return None, None
P("\n GAP full-vs-fidata (CAGR a pari target-vol):")
for tv in (0.06, 0.08, 0.10, 0.15):
cf, _ = cagr_at_tvol(rows5_full, tv); ch, _ = cagr_at_tvol(rows5_hc50, tv); ce, _ = cagr_at_tvol(rows5_excl, tv)
P(f" 5s tvol={tv:.3f}: full CAGR={cf:+.4f} haircut50={ch:+.4f} escludi={ce:+.4f} "
f"gap(full-escludi)={cf-ce:+.4f}")
for tv in (0.08, 0.10, 0.15):
cf, _ = cagr_at_tvol(rows2_full, tv); ch, _ = cagr_at_tvol(rows2_hc50, tv); ce, _ = cagr_at_tvol(rows2_excl, tv)
P(f" 2s tvol={tv:.3f}: full CAGR={cf:+.4f} haircutSKH50={ch:+.4f} escludi(TP01)={ce:+.4f} "
f"gap(full-escludi)={cf-ce:+.4f}")
# ---------------------------------------------------------------- €/giorno a 2k e 5k
P("\n" + "="*118)
P("TRADUZIONE €/GIORNO (CAGR de-luck & haircut, capitale 2k e 5k)")
P("="*118)
def eur_day(cagr, cap): return cap * cagr / 365.0
for label, rows in [("5s FULL", rows5_full), ("5s DE-LUCK", rows5_deluck), ("5s HAIRCUT50", rows5_hc50),
("2s FULL", rows2_full), ("2s HAIRCUT50", rows2_hc50), ("2s ESCLUDI(TP01)", rows2_excl)]:
for tv in (0.06, 0.10):
c, _ = cagr_at_tvol(rows, tv)
if c is None: continue
P(f" {label:18s} tvol={tv:.3f}: CAGR={c:+.4f} -> 2k={eur_day(c,2000):+.2f}€/g 5k={eur_day(c,5000):+.2f}€/g")
# ---------------------------------------------------------------- dump
with open(os.path.join(OUT, 'r0703_frontier_tail_report.txt'), 'w') as f:
f.write("\n".join(rep))
def rows_to_df(rows): return pd.DataFrame(rows)
with pd.ExcelWriter(os.path.join(OUT, 'r0703_frontier.xlsx')) if False else open(os.devnull,'w'):
pass
for nm, rows in [('5s_full',rows5_full),('5s_deluck',rows5_deluck),('5s_hc50',rows5_hc50),
('5s_hc30',rows5_hc30),('5s_excl',rows5_excl),('2s_full',rows2_full),
('2s_deluck',rows2_deluck),('2s_hc50',rows2_hc50),('2s_excl',rows2_excl)]:
rows_to_df(rows).to_csv(os.path.join(OUT, f'r0703_front_{nm}.csv'), index=False)
P("\n[report salvato in scratchpad: r0703_frontier_tail_report.txt + CSV per book/variante]")