#!/usr/bin/env python """r0702_skeptic_offset.py — VERIFICA AVVERSARIALE INDIPENDENTE di r0702_tp01_offset.py. Linee d'attacco (tutte con codice INDIPENDENTE dal finding, cross-check contro le sue funzioni): A. COSTRUZIONE: daily-offset ricostruito via floor-division su epoca ms (niente pandas.resample); h=0 deve == al.get('1d') e == tp01_baseline_daily; mapping daily->1h via searchsorted (niente merge_asof); guardia troncamento del feed 1h (nessun look-ahead a h!=0). B. STATISTICA: block-bootstrap congiunto delle 24 ancore sull'hold-out — lo spike di h=0 (Sh(h0) - mediana(altri)) e' speciale o e' il massimo atteso di 24 stime correlate? + hold-out finti (2020..2024): l'ancora migliore e' stabile o gira a caso? C. TRANCHING: identita' K=4 == EW dei 4 book ancorati (netting non nasconde nulla)? turnover verificato; DD del K=4 vs DD della ROTAZIONE TIPICA (non vs h=0 sfortunato); bootstrap appaiato della differenza IS. D. IMPATTO: blend TP+SKH 75/25 e book 5-sleeve ricalcolati con TP01 alle 24 ancore. Nessun file toccato fuori da questo script. Runtime ~3-6 min (SKH/XS/VRP/GTAA inclusi). """ from __future__ import annotations import sys from functools import lru_cache from pathlib import Path import numpy as np import pandas as pd ROOT = Path("/opt/docker/PythagorasGoal") sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) sys.path.insert(0, str(ROOT / "scripts" / "research")) sys.path.insert(0, str(ROOT)) import altlib as al # noqa: E402 import r0702_tp01_offset as RF # il finding, SOLO per cross-check # noqa: E402 from src.strategies.trend_portfolio import CANONICAL, TrendPortfolio # noqa: E402 TP = TrendPortfolio(**CANONICAL) HOLDOUT = al.HOLDOUT ASSETS = ("BTC", "ETH") MS_H = 3_600_000 MS_D = 86_400_000 FEE = al.FEE_SIDE RNG = np.random.default_rng(42) B_BOOT = 4000 BLOCK = 20 # =========================================================================== # A. COSTRUZIONE INDIPENDENTE # =========================================================================== @lru_cache(maxsize=8) def get1h(asset: str) -> pd.DataFrame: return al.get(asset, "1h") @lru_cache(maxsize=64) def sk_daily(asset: str, h: int) -> pd.DataFrame: """Daily-offset costruito a mano: day_id = (ts - h*1h) // 24h su epoca ms (open-labeled).""" df = get1h(asset) ts = df["timestamp"].values.astype(np.int64) day = (ts - h * MS_H) // MS_D uday, first = np.unique(day, return_index=True) o = df["open"].values.astype(float) hi = df["high"].values.astype(float) lo = df["low"].values.astype(float) c = df["close"].values.astype(float) v = df["volume"].values.astype(float) last = np.r_[first[1:], len(ts)] - 1 out = pd.DataFrame(dict( timestamp=uday * MS_D + h * MS_H, open=o[first], high=np.maximum.reduceat(hi, first), low=np.minimum.reduceat(lo, first), close=c[last], volume=np.add.reduceat(v, first), )) out["datetime"] = pd.to_datetime(out["timestamp"], unit="ms", utc=True) return out def sk_net_daily(asset: str, h: int) -> pd.Series: """Rendimenti netti TP01 sul grid daily-offset (pipeline mia: shift+fee espliciti).""" d = sk_daily(asset, h) c = d["close"].values.astype(float) r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0 tgt = TP.target_series(d) pos = np.zeros(len(tgt)); pos[1:] = tgt[:-1] net = pos * r - FEE * np.abs(np.diff(pos, prepend=0.0)) net[0] = 0.0 return pd.Series(net, index=pd.DatetimeIndex(d["datetime"])) @lru_cache(maxsize=32) def sk_port_daily(h: int) -> pd.Series: J = pd.concat({a: sk_net_daily(a, h) for a in ASSETS}, axis=1, join="inner").fillna(0.0) return al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"]) def sk_pos_hourly(asset: str, hs: tuple, df1h: pd.DataFrame | None = None) -> np.ndarray: """Posizione TENUTA durante ogni barra 1h (ensemble media delle ancore hs), via searchsorted: pos durante barra i = target dell'ultima barra daily-offset con close nominale <= open(barra i).""" df = get1h(asset) if df1h is None else df1h open_ms = df["timestamp"].values.astype(np.int64) pos = np.zeros(len(open_ms)) for h in hs: d = sk_daily(asset, h) if df1h is None else sk_daily_from(df, h) tgt = np.nan_to_num(TP.target_series(d), nan=0.0) close_ms = d["timestamp"].values.astype(np.int64) + MS_D j = np.searchsorted(close_ms, open_ms, side="right") - 1 p = np.where(j >= 0, tgt[np.clip(j, 0, None)], 0.0) pos += p / len(hs) return pos def sk_daily_from(df1h: pd.DataFrame, h: int) -> pd.DataFrame: """sk_daily ma da un frame 1h arbitrario (per il test di troncamento).""" ts = df1h["timestamp"].values.astype(np.int64) day = (ts - h * MS_H) // MS_D uday, first = np.unique(day, return_index=True) c = df1h["close"].values.astype(float) last = np.r_[first[1:], len(ts)] - 1 out = pd.DataFrame(dict(timestamp=uday * MS_D + h * MS_H, close=c[last])) out["datetime"] = pd.to_datetime(out["timestamp"], unit="ms", utc=True) return out def sk_book_hourly(hs: tuple) -> tuple[pd.Series, float, dict]: """Book 0.5/0.5 sul grid 1h con posizioni ensemble; ritorna (daily, turnover/y, per-asset net).""" nets, turns = {}, 0.0 for a in ASSETS: df = get1h(a) c = df["close"].values.astype(float) r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0 pos = sk_pos_hourly(a, hs) turn = np.abs(np.diff(pos, prepend=0.0)) net = pos * r - FEE * turn net[0] = 0.0 idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) nets[a] = pd.Series(net, index=idx) yrs = len(net) / (24 * 365.25) turns += 0.5 * turn.sum() / yrs J = pd.concat(nets, axis=1, join="inner").fillna(0.0) return al._to_daily(0.5 * J["BTC"] + 0.5 * J["ETH"]), turns, nets def sh3(s: pd.Series) -> tuple[float, float, float]: return (al._sh(s), al._sh(s[s.index < HOLDOUT]), al._sh(s[s.index >= HOLDOUT])) def part_A() -> None: print("=" * 100) print("A. COSTRUZIONE — ricostruzione indipendente (floor-division ms / searchsorted)") print("=" * 100) # A1: daily-offset mio vs al.get('1d') (h=0) e vs il loro resample_offset (h campionati) for a in ASSETS: ref = al.get(a, "1d") mine0 = sk_daily(a, 0) assert len(mine0) == len(ref), f"A1 len mismatch {a}" for col in ("timestamp", "open", "high", "low", "close", "volume"): rtol = 1e-9 if col == "volume" else 0.0 # volume: solo ordine di sommatoria float assert np.allclose(mine0[col].values.astype(float), ref[col].values.astype(float), atol=0, rtol=rtol), f"A1 h=0 mismatch {a}:{col}" for h in (1, 5, 11, 13, 21, 23): theirs = RF.daily_off(a, h) m = sk_daily(a, h) assert len(m) == len(theirs), f"A1 len mismatch {a} h={h}" for col in ("timestamp", "open", "high", "low", "close", "volume"): rtol = 1e-9 if col == "volume" else 0.0 assert np.allclose(m[col].values.astype(float), theirs[col].values.astype(float), atol=0, rtol=rtol), \ f"A1 h={h} mismatch {a}:{col}" print("[A1] daily-offset: costruzione mia == al.get('1d') (h=0) == loro resample_offset " "(h=1,5,11,13,21,23, tutte le colonne, bit-exact): OK") # A2: pipeline completa h=0 vs baseline del progetto mine = sk_port_daily(0) base = al.tp01_baseline_daily() assert len(mine) == len(base) and np.allclose(mine.values, base.values, atol=1e-12), "A2 FAIL" f, i, ho = sh3(mine) print(f"[A2] portafoglio h=0 (pipeline mia) == tp01_baseline_daily: OK " f"(FULL {f:.4f} / IS {i:.4f} / HOLD {ho:.4f})") # A3: troncamento del feed 1h -> posizioni orarie IDENTICHE su tutto il range troncato for a in ASSETS: df = get1h(a) for cut in (len(df) - 3000, len(df) - 777): dtr = df.iloc[:cut].reset_index(drop=True) for h in (0, 5, 13, 21): p_full = sk_pos_hourly(a, (h,)) p_tr = sk_pos_hourly(a, (h,), df1h=dtr) assert np.allclose(p_full[:cut], p_tr, atol=1e-12), \ f"A3 look-ahead {a} h={h} cut={cut}" print("[A3] troncamento 1h (2 cut x 4 ancore x 2 asset): posizioni orarie invariate " "sul prefisso -> nessun look-ahead nel mapping daily->1h: OK") # A4: vol-target ricalcolata per-offset? (fatto strutturale + evidenza numerica) for a in ASSETS: for h in (5, 13): assert TP._bpd(sk_daily(a, h)) == 1, "A4 bpd" t0 = TP.target_series(sk_daily(a, 0)) t13 = TP.target_series(sk_daily(a, 13)) m = min(len(t0), len(t13)) d = np.abs(t0[300:m] - t13[300:m]) print(f"[A4] {a}: target h=0 vs h=13 stesso giorno-calendario, |diff| media " f"{np.nanmean(d):.4f} (max {np.nanmax(d):.3f}) -> vol e segnale RICALCOLATI " f"sul grid dell'ancora (target_series riceve il grid offset)") # A5: cross-check book orario mio vs loro (K=1 h0 e K=4) for name, hs in (("K=1 h0", (0,)), ("K=4", (0, 6, 12, 18))): mine_s, mine_t, _ = sk_book_hourly(hs) theirs_s, theirs_t = RF.port_hourly(hs) common = mine_s.index.intersection(theirs_s.index) dmax = float(np.max(np.abs(mine_s.loc[common].values - theirs_s.loc[common].values))) print(f"[A5] {name}: book 1h mio vs loro — max|diff ret giornaliero| {dmax:.2e}, " f"turn/y {mine_t:.2f} vs {theirs_t:.2f}") assert dmax < 1e-10, f"A5 mismatch {name}" # =========================================================================== # B. STATISTICA — lo spike h=0 e' speciale? # =========================================================================== @lru_cache(maxsize=2) def anchor_matrix() -> pd.DataFrame: cols = {f"h{h:02d}": sk_port_daily(h) for h in range(24)} return pd.concat(cols, axis=1, join="inner").dropna() def _sh_mat(R: np.ndarray) -> np.ndarray: mu = R.mean(axis=1) sd = R.std(axis=1) return np.where(sd > 0, mu / sd, 0.0) * np.sqrt(365.25) def block_boot_stats(M: np.ndarray, B: int, block: int, rng) -> dict: n, K = M.shape nblocks = int(np.ceil(n / block)) g0s, gmaxs, med_all, sh0s = [], [], [], [] done = 0 while done < B: b = min(500, B - done) starts = rng.integers(0, n, size=(b, nblocks)) idx = (starts[:, :, None] + np.arange(block)[None, None, :]) % n idx = idx.reshape(b, -1)[:, :n] R = M[idx] # (b, n, K) Sh = np.stack([_sh_mat(R[:, :, k]) for k in range(K)], axis=1) med_others = np.empty_like(Sh) for h in range(K): others = np.delete(Sh, h, axis=1) med_others[:, h] = np.median(others, axis=1) g = Sh - med_others g0s.append(g[:, 0]) gmaxs.append(g.max(axis=1)) med_all.append(np.median(Sh, axis=1)) sh0s.append(Sh[:, 0]) done += b return dict(g0=np.concatenate(g0s), gmax=np.concatenate(gmaxs), med=np.concatenate(med_all), sh0=np.concatenate(sh0s)) def part_B() -> None: print("\n" + "=" * 100) print("B. STATISTICA — spike h=0 sull'hold-out: speciale o massimo atteso di 24 stime correlate?") print("=" * 100) Mdf = anchor_matrix() Mh = Mdf[Mdf.index >= HOLDOUT].values sh_hold = _sh_mat(Mh.T) med_others_obs = np.median(sh_hold[1:]) g0_obs = sh_hold[0] - med_others_obs corr = np.corrcoef(Mh.T) iu = np.triu_indices(24, 1) print(f"hold-out: {Mh.shape[0]} giorni, 24 ancore; Sh h=0 {sh_hold[0]:.3f}, " f"mediana altri {med_others_obs:.3f}, spike osservato g0 = {g0_obs:.3f}") print(f"correlazione daily fra ancore (hold-out): mediana {np.median(corr[iu]):.3f}, " f"min {corr[iu].min():.3f}") for blk in (10, 20, 40): bs = block_boot_stats(Mh, B_BOOT, blk, np.random.default_rng(42 + blk)) p_any = float(np.mean(bs["gmax"] >= g0_obs)) p_g0 = float(np.mean(bs["g0"] <= 0.0)) ci_g0 = np.percentile(bs["g0"], [2.5, 97.5]) ci_med = np.percentile(bs["med"], [2.5, 97.5]) ci_sh0 = np.percentile(bs["sh0"], [2.5, 97.5]) print(f" block={blk:>2}: P(max-spike di UNA QUALSIASI ancora >= {g0_obs:.2f}) = " f"{p_any:.3f} | P(g0<=0) = {p_g0:.3f} | CI95 g0 [{ci_g0[0]:+.2f},{ci_g0[1]:+.2f}] " f"| CI95 Sh mediana-ancore [{ci_med[0]:+.2f},{ci_med[1]:+.2f}] " f"| CI95 Sh h=0 [{ci_sh0[0]:+.2f},{ci_sh0[1]:+.2f}]") # hold-out finti: l'ancora migliore per finestra e' stabile? print("\n finestre annuali (hold-out finti) — best/worst anchor, h=0, spread:") print(f" {'finestra':<9} {'best':>5} {'ShBest':>7} {'worst':>6} {'ShWorst':>8} " f"{'mediana':>8} {'h=0':>6} {'pctl h0':>8} {'max-med':>8}") years = [2020, 2021, 2022, 2023, 2024] windows: list[tuple[str, pd.DataFrame]] = [ (str(y), Mdf[Mdf.index.year == y]) for y in years] + [("2025+", Mdf[Mdf.index >= HOLDOUT])] sh_by_win = {} from scipy.stats import spearmanr for name, W in windows: sh = _sh_mat(W.values.T) sh_by_win[name] = sh pctl0 = float((sh < sh[0]).mean() + 0.5 * (sh == sh[0]).mean()) * 100 print(f" {name:<9} {int(np.argmax(sh)):>5} {sh.max():>7.3f} {int(np.argmin(sh)):>6} " f"{sh.min():>8.3f} {np.median(sh):>8.3f} {sh[0]:>6.3f} {pctl0:>7.0f}° " f"{sh.max() - np.median(sh):>8.3f}") names = [n for n, _ in windows] print("\n stabilita' del ranking ancore (Spearman fra finestre consecutive):") for a, b in zip(names[:-1], names[1:]): rho, p = spearmanr(sh_by_win[a], sh_by_win[b]) print(f" {a} vs {b}: rho={rho:+.2f} (p={p:.2f})") # l'ancora migliore di ogni finestra, quanto rende NELLE ALTRE finestre? (pctl medio) print(" best-anchor di ogni finestra valutata nelle ALTRE finestre (pctl medio su 24):") for name in names: h_star = int(np.argmax(sh_by_win[name])) pct = [float((sh_by_win[o] < sh_by_win[o][h_star]).mean()) * 100 for o in names if o != name] print(f" best({name}) = h={h_star:>2} -> pctl medio altrove {np.mean(pct):.0f}° " f"(per finestra: {', '.join(f'{p:.0f}' for p in pct)})") # ritorno totale hold-out per ancora (per la narrativa '+3.5%') tot = np.prod(1 + Mh, axis=0) - 1 print(f"\n ritorno TOTALE hold-out per ancora: min {tot.min():+.1%} / mediana " f"{np.median(tot):+.1%} / max {tot.max():+.1%} (h=0: {tot[0]:+.1%})") dd = [al._dd_ret(pd.Series(Mh[:, k])) for k in range(24)] print(f" maxDD hold-out per ancora: min {min(dd):.1%} / mediana {np.median(dd):.1%} / " f"max {max(dd):.1%} (h=0: {dd[0]:.1%}) [B&H 50/50 2025-26: DD ~60%]") # =========================================================================== # C. TRANCHING — gratis davvero? # =========================================================================== def part_C() -> None: print("\n" + "=" * 100) print("C. TRANCHING — identita' EW, turnover, DD vs rotazione tipica, significativita' IS") print("=" * 100) # C1: K=4 book == EW dei 4 book ancorati? (identita' esatta, incluse fee) for a in ASSETS: df = get1h(a) c = df["close"].values.astype(float) r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0 hs = (0, 6, 12, 18) pos_e = sk_pos_hourly(a, hs) net_e = pos_e * r - FEE * np.abs(np.diff(pos_e, prepend=0.0)); net_e[0] = 0.0 nets_1 = [] turns_1 = [] for h in hs: p = sk_pos_hourly(a, (h,)) t = np.abs(np.diff(p, prepend=0.0)) n1 = p * r - FEE * t; n1[0] = 0.0 nets_1.append(n1) turns_1.append(t.sum()) ew = np.mean(nets_1, axis=0) turn_e = np.abs(np.diff(pos_e, prepend=0.0)).sum() print(f"[C1] {a}: max|net K4 - EW(4 book singoli)| = {np.max(np.abs(net_e - ew)):.2e} ; " f"turnover K4 {turn_e:.1f} vs media singoli {np.mean(turns_1):.1f} " f"(rapporto {turn_e / np.mean(turns_1):.4f})") # C2: tutte le rotazioni (mie): livelli e dispersione, DD compreso fams = {"singole(24)": [(h,) for h in range(24)], "K=2(12)": [(h, h + 12) for h in range(12)], "K=4(6)": [tuple(h + 6 * j for j in range(4)) for h in range(6)]} stats = {} for fam, rots in fams.items(): rec = [] for hs in rots: s, t, _ = sk_book_hourly(hs) f, i, ho = sh3(s) rec.append(dict(hs=hs, full=f, is_=i, hold=ho, dd=al._dd_ret(s), dd_h=al._dd_ret(s[s.index >= HOLDOUT]), turn=t)) stats[fam] = pd.DataFrame(rec) print("\n[C2] rotazioni complete (book 1h, misura identica per tutte):") print(f" {'famiglia':<12} {'IS med[min,max]':>24} {'HOLD med[min,max]':>26} " f"{'maxDD med[min,max]':>24} {'turn/y med':>10}") for fam, T in stats.items(): print(f" {fam:<12} {T.is_.median():>8.3f} [{T.is_.min():.3f},{T.is_.max():.3f}]" f" {T.hold.median():>9.3f} [{T.hold.min():+.3f},{T.hold.max():+.3f}]" f" {T.dd.median():>8.1%} [{T.dd.min():.1%},{T.dd.max():.1%}]" f" {T.turn.median():>8.2f}") s24, _, _ = sk_book_hourly(tuple(range(24))) f24, i24, h24 = sh3(s24) print(f" K=24 IS {i24:.3f} HOLD {h24:+.3f} maxDD {al._dd_ret(s24):.1%}") T1 = stats["singole(24)"] T4 = stats["K=4(6)"] print(f"\n -> claim 'maxDD 14.7->11.9': h=0 singolo DD {T1.dd.iloc[0]:.1%} ma la MEDIANA " f"delle 24 singole e' {T1.dd.median():.1%}; K=4 mediano {T4.dd.median():.1%} " f"=> beneficio del tranching vs ancora TIPICA = {T1.dd.median() - T4.dd.median():+.1%}pt, " f"vs h=0 = {T1.dd.iloc[0] - T4.dd.median():+.1%}pt (in gran parte 'h=0 era sfortunato sul DD')") print(f" -> claim 'IS 1.49->1.54/1.56': mediana IS delle 24 singole = {T1.is_.median():.3f} " f"(K=4 mediano {T4.is_.median():.3f}) => il 'miglioramento' e' tornare alla MEDIA delle " f"ancore, h=0 era al {(T1.is_ < T1.is_.iloc[0]).mean() * 100:.0f}° pctl IS") # C3: significativita' IS del K=4 vs h=0 (bootstrap appaiato a blocchi) s0 = sk_book_hourly((0,))[0] s4 = sk_book_hourly((0, 6, 12, 18))[0] common = s0.index.intersection(s4.index) A = s4.loc[common]; Bser = s0.loc[common] mask = common < HOLDOUT Ai, Bi = A[mask].values, Bser[mask].values n = len(Ai) nblocks = int(np.ceil(n / BLOCK)) d_obs = al._sh(A[mask]) - al._sh(Bser[mask]) ds = [] rng = np.random.default_rng(7) for _ in range(B_BOOT // 500): starts = rng.integers(0, n, size=(500, nblocks)) idx = (starts[:, :, None] + np.arange(BLOCK)[None, None, :]) % n idx = idx.reshape(500, -1)[:, :n] Ra, Rb = Ai[idx], Bi[idx] sa = Ra.mean(1) / Ra.std(1) * np.sqrt(365.25) sb = Rb.mean(1) / Rb.std(1) * np.sqrt(365.25) ds.append(sa - sb) ds = np.concatenate(ds) print(f"\n[C3] IS: Sh(K4) - Sh(h0) = {d_obs:+.3f}; bootstrap appaiato (block {BLOCK}, " f"B={len(ds)}): CI95 [{np.percentile(ds, 2.5):+.3f}, {np.percentile(ds, 97.5):+.3f}], " f"P(diff<=0) = {np.mean(ds <= 0):.3f}") # e vs l'ancora mediana (piu' onesto): K4 confrontato con OGNI singola dvs = [d for h in range(24) for d in [al._sh(A[mask]) - al._sh(sk_book_hourly((h,))[0].loc[common][mask])]] print(f" Sh_IS(K4) - Sh_IS(singola h) sulle 24 ancore: min {min(dvs):+.3f} / " f"mediana {np.median(dvs):+.3f} / max {max(dvs):+.3f} " f"-> vs ancora tipica il guadagno IS e' ~{np.median(dvs):+.2f}, non +0.05/+0.07") # C4: small-cap $600 (mia implementazione min-order) print("\n[C4] small-cap $600 (min order $5, quota 0.5/asset):") for name, hs in (("K=1 h0", (0,)), ("K=2", (0, 12)), ("K=4", (0, 6, 12, 18)), ("K=24", tuple(range(24)))): nets_r, nets_m, ntr = {}, {}, 0 for a in ASSETS: df = get1h(a) c = df["close"].values.astype(float) r = np.zeros(len(c)); r[1:] = c[1:] / c[:-1] - 1.0 t = 0.5 * sk_pos_hourly(a, hs) held = np.empty(len(t)); cur = 0.0 for i in range(len(t)): if abs(t[i] - cur) * 600.0 >= 5.0: cur = t[i]; ntr += 1 held[i] = cur pos = np.zeros(len(held)); pos[1:] = held[:-1] nr = pos * r - FEE * np.abs(np.diff(pos, prepend=0.0)); nr[0] = 0.0 posm = np.zeros(len(t)); posm[1:] = t[:-1] nm = posm * r - FEE * np.abs(np.diff(posm, prepend=0.0)); nm[0] = 0.0 idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) nets_r[a] = pd.Series(nr, index=idx); nets_m[a] = pd.Series(nm, index=idx) Jr = pd.concat(nets_r, axis=1, join="inner").fillna(0.0) Jm = pd.concat(nets_m, axis=1, join="inner").fillna(0.0) dr = al._to_daily(Jr["BTC"] + Jr["ETH"]); dm = al._to_daily(Jm["BTC"] + Jm["ETH"]) yrs = len(dr) / 365.25 print(f" {name:<8} Sh real {al._sh(dr):.3f} (model {al._sh(dm):.3f}, haircut " f"{al._sh(dm) - al._sh(dr):+.3f}) trade/y {ntr / yrs:.0f}") # =========================================================================== # D. IMPATTO sui numeri del progetto (blend SKH e book 5-sleeve, TP01 per ancora) # =========================================================================== def part_D() -> None: print("\n" + "=" * 100) print("D. IMPATTO — blend TP+SKH 75/25 e book 5-sleeve con TP01 alle 24 ancore") print("=" * 100) from src.portfolio.portfolio import combine_outer, to_daily try: from src.portfolio.sleeves import (_gtaa_daily_returns, _skyhook_returns, _vrp_combo_returns, _xsec_returns) skh = to_daily(_skyhook_returns()) except Exception as e: print(f" [SKIP] sleeve non calcolabili: {type(e).__name__}: {e}") return def hold_sh(s: pd.Series) -> float: return al._sh(s[s.index >= HOLDOUT]) # blend deribit book 75/25 blends = [] for h in range(24): tp = sk_port_daily(h) b = combine_outer({"TP": tp, "SKH": skh}, {"TP": 0.75, "SKH": 0.25}) b = b[b.index >= tp.index.min()] blends.append(hold_sh(b)) b24 = combine_outer({"TP": sk_book_hourly(tuple(range(24)))[0], "SKH": skh}, {"TP": 0.75, "SKH": 0.25}) print(f"[D1] blend 0.75*TP01(h)+0.25*SKH — Sharpe HOLD: h=0 {blends[0]:.2f} | " f"min {min(blends):.2f} / mediana {np.median(blends):.2f} / max {max(blends):.2f} | " f"TP=K24 {hold_sh(b24[b24.index >= sk_port_daily(0).index.min()]):.2f} " f"(claim del progetto: 0.31 -> 1.17)") # book 5-sleeve (pesi CLAUDE.md), attivazione era crypto try: cols_fixed = dict(XS=to_daily(_xsec_returns()), VRP=to_daily(_vrp_combo_returns()), SKH=skh, GTAA=to_daily(_gtaa_daily_returns())) except Exception as e: print(f" [SKIP 5-sleeve] {type(e).__name__}: {e}") return W = dict(TP=0.33, XS=0.15, VRP=0.12, SKH=0.20, GTAA=0.20) lo = sk_port_daily(0).index.min() books = [] for h in range(24): cols = dict(TP=sk_port_daily(h), **cols_fixed) s = combine_outer(cols, W, lo=lo) books.append((hold_sh(s), al._sh(s))) bh = [b[0] for b in books]; bf = [b[1] for b in books] s24b = combine_outer(dict(TP=sk_book_hourly(tuple(range(24)))[0], **cols_fixed), W, lo=lo) print(f"[D2] book 5-sleeve (TP 33/XS 15/VRP 12/SKH 20/GTAA 20) — Sharpe HOLD: " f"h=0 {bh[0]:.2f} | min {min(bh):.2f} / mediana {np.median(bh):.2f} / max {max(bh):.2f} " f"| TP=K24 {hold_sh(s24b):.2f}") print(f" Sharpe FULL: h=0 {bf[0]:.2f} | min {min(bf):.2f} / mediana {np.median(bf):.2f} " f"/ max {max(bf):.2f} | TP=K24 {al._sh(s24b):.2f}") def main() -> None: part_A() part_B() part_C() part_D() print("\nFatto (scettico).") if __name__ == "__main__": main()