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:
@@ -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]")
|
||||
Reference in New Issue
Block a user