#!/usr/bin/env python """r0702_anchor_skh01.py — AUDIT timing-luck della FASE della griglia dual-TF di SKH01-V2-DD. CONTESTO (diario 2026-07-02-timing-crt-wave.md): l'hold-out di TP01 (Sharpe 0.31) si e' rivelato la MIGLIORE delle 24 ancore orarie possibili (mediana 0.04) — timing-luck dell'ancora, dimensione di multiple-testing non contata dal deflated-Sharpe. SKH01 e' nel BOOK LIVE Deribit (TP01+SKH01 75/25) -> audit analogo, con rilevanza operativa diretta. SPAZIO DI LUCK: l'origine della griglia. SKH01 resampla il 5m certificato a 230m (exec) e 690m (segnale) con origin='epoch'. Shiftando l'origin di k x 5m si spostano COERENTEMENTE entrambe le griglie (690 = 3 x 230 -> i confini HTF restano sottoinsieme dei LTF); la struttura congiunta si ripete con periodo 690m = 138 step da 5m. Campioniamo 23 OFFSET UNIFORMI (ogni 30m) su [0, 690), dichiarati A PRIORI — offset 0 = canonico. Parametri IDENTICI (SKH01_V2_DD) su tutti gli offset; nessuna selezione. NB strutturale: 230m e 690m NON dividono 24h -> la griglia MIGRA attraverso la giornata (nessun offset "possiede" un'ora del giorno) — l'ancora e' meno "speciale" a priori di quella daily di TP01, ma va misurato. COSA FA: 1. SANITY: a offset 0 riproduce ESATTAMENTE la serie daily di _skyhook_returns() (max|dif| ~0) e i confini HTF c LTF per ogni offset. 2. Per 23 offset x {BTC,ETH}: Sharpe FULL/IS/HOLD (equity daily-step, convenzione canonica), maxDD (equity harness), n trade -> tabella + min/med/max + pctl di off 0. 3. GATE ammissione ri-valutati alla mediana/peggiore: (a) maxDD<30% entrambi gli asset, (b) minFull ~0.99 / minHold ~1.26, (c) blend 0.75*TP01+0.25*SKH hold-out (baseline al.tp01_baseline_daily; claim 0.31->1.17), (d) corr a TP01 (~0.09). 4. BOOK 5-sleeve 33/15/12/20/20 (combine_outer, outer-join rinormalizzato, era crypto): HOLD/FULL con SKH a ogni offset + ensemble degli offset (lens de-luckato, NON eseguibile). 5. BOOTSTRAP (block ~20g, maniera scettico r0702_skeptic_offset): P(un offset qualsiasi mostri uno spike >= quello del canonico sull'hold-out del blend). 6. RILEVANZA LIVE: il cron del book e' ORARIO (crontab 0 * * * * -> scripts/cron_book.sh) ma i confini 230m NON sono allineati all'ora (230 mod 60 = 50 -> ciclo di 6 barre, ritardo confine->prossima-ora in {0,10,20,30,40,50} min). Quantifica la distribuzione e l'impatto: re-sim con fill al close 5m del PROSSIMO multiplo orario dopo la chiusura della barra di segnale/exit (entry E exit software ritardati; detection dei trade INVARIATA -> isola il puro effetto prezzo-esecuzione). Sanity: la modalita' canonica della re-sim riproduce backtest_signals bit-exact. CAVEAT DICHIARATI: - equity daily-step (lens Sharpe) come il canonico — non e' mark-to-market intrabar; - costi di ribilanciamento del book 230m gia' flaggati a deploy (diario skyhook); - l'ensemble di offset NON e' eseguibile live (una sola griglia gira): serve SOLO come stima de-luckata; - la sim 'hourly' modella fill a close 5m del multiplo orario (cron a minuto 0, runtime del job trascurato) e NON modella slippage/parziali. TECNICA: mai .view('int64') su tz-aware (epoca esplicita in ms); htf_features/merge_htf_to_ltf RIUSATE via skyhook_entries (importate, non riscritte). Vincoli: nessun file toccato fuori da questo script; niente commit. Runtime ~3-6 min (46 run skyhook + book + bootstrap). """ from __future__ import annotations import sys import time 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)) import altlib as al # noqa: E402 from src.backtest.harness import backtest_signals # noqa: E402 from src.data.downloader import load_data # noqa: E402 from src.portfolio.portfolio import combine_outer, to_daily # noqa: E402 from src.strategies.skyhook import ( # noqa: E402 HTF_MIN, LTF_MIN, SKH01_V2_DD, skyhook_entries) HOLDOUT = al.HOLDOUT ASSETS = ("BTC", "ETH") OFFSETS = tuple(range(0, 690, 30)) # 23 offset a priori (ogni 30m su [0,690)), 0 = canonico MS5 = 300_000 MSH = 3_600_000 MS_LTF = LTF_MIN * 60_000 BLEND_W = {"TP": 0.75, "SKH": 0.25} BOOK_W = {"TP": 0.33, "XS": 0.15, "VRP": 0.12, "SKH": 0.20, "GTAA": 0.20} B_BOOT = 4000 # --------------------------------------------------------------------------- # Dati + resample con fase spostata (identico a skyhook.resample_5m + offset) # --------------------------------------------------------------------------- @lru_cache(maxsize=4) def get5m(asset: str) -> pd.DataFrame: df = load_data(asset, "5m").reset_index(drop=True) if "datetime" not in df.columns: df["datetime"] = pd.to_datetime(df["timestamp"], unit="ms", utc=True) return df def resample_off(df5: pd.DataFrame, minutes: int, off: int) -> pd.DataFrame: """Identico a skyhook.resample_5m ma con griglia spostata di `off` minuti: origin='epoch' + offset -> confini a epoch + off + n*minutes. Con `off` comune a 230m e 690m (690=3x230) i confini HTF restano sottoinsieme dei confini LTF.""" g = df5[["timestamp", "open", "high", "low", "close", "volume"]].copy() g.index = pd.to_datetime(g["timestamp"], unit="ms", utc=True) out = (g.resample(f"{minutes}min", label="left", closed="left", origin="epoch", offset=pd.Timedelta(minutes=off)) .agg({"open": "first", "high": "max", "low": "min", "close": "last", "volume": "sum"}) .dropna(subset=["open"])) out["datetime"] = out.index epoch = pd.Timestamp("1970-01-01", tz="UTC") out["timestamp"] = ((out.index - epoch) // pd.Timedelta(milliseconds=1)).astype("int64") return out.reset_index(drop=True)[ ["timestamp", "open", "high", "low", "close", "volume", "datetime"]] # --------------------------------------------------------------------------- # Run per (asset, offset) — pipeline IDENTICA a sleeves._skyhook_returns # --------------------------------------------------------------------------- _CACHE: dict = {} def run_asset(asset: str, off: int): """(daily equity-step series, Metrics, ltf, entries) per un asset a fase `off`.""" key = (asset, off) if key in _CACHE: return _CACHE[key] df5 = get5m(asset) ltf = resample_off(df5, LTF_MIN, off) htf = resample_off(df5, HTF_MIN, off) # confini HTF c confini LTF: vale per TUTTE le barre tranne al piu' la PRIMA (parziale: # se il feed parte a meta' di un bin 690m il sub-bin 230m con la stessa label puo' essere # vuoto). Identico al comportamento del canonico build_frames (origin='epoch'). assert np.isin(htf["timestamp"].values[1:], ltf["timestamp"].values).all(), \ f"confini HTF NON sottoinsieme dei LTF (asset={asset}, off={off})" ent = skyhook_entries(ltf, htf, SKH01_V2_DD) # riusa htf_features/merge_htf_to_ltf m = backtest_signals(ltf, ent, fee_rt=0.001, leverage=1.0, asset=asset, tf="230m") s = pd.Series(m.equity, index=pd.DatetimeIndex(pd.to_datetime(m.eq_index, utc=True))) daily = s.resample("1D").last().ffill().pct_change().dropna() _CACHE[key] = (daily, m, ltf, ent) return _CACHE[key] @lru_cache(maxsize=32) def skh_port(off: int) -> pd.Series: """Book 50/50 BTC+ETH daily-step alla fase `off` (convenzione di _skyhook_returns).""" series = {a: run_asset(a, off)[0] for a in ASSETS} J = pd.concat(series, axis=1, join="inner").fillna(0.0) return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) 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 pctl_of_first(v: np.ndarray) -> float: return float((v < v[0]).mean() + 0.5 * (v == v[0]).mean()) * 100 # --------------------------------------------------------------------------- # (1) SANITY # --------------------------------------------------------------------------- def sanity() -> None: from src.portfolio.sleeves import _skyhook_returns mine = skh_port(0) ref = _skyhook_returns() assert len(mine) == len(ref), f"sanity len: {len(mine)} vs {len(ref)}" dmax = float(np.max(np.abs(mine.values - ref.values))) assert dmax < 1e-12, f"sanity offset 0: max|dif| = {dmax:.2e}" f, i, h = sh3(mine) print(f"[SANITY] offset 0 == _skyhook_returns(): max|dif| = {dmax:.2e} " f"su {len(mine)} giorni (FULL {f:.3f} / IS {i:.3f} / HOLD {h:.3f})") for a in ASSETS: _, m, _, _ = run_asset(a, 0) print(f"[SANITY] {a} off=0: maxDD harness {m.max_dd:.1%}, trade {m.n_trades} " f"(diario: BTC 21.4% / ETH 27.4%)") # --------------------------------------------------------------------------- # (6) LIVE — re-sim con fill al prossimo multiplo orario (path del cron) # --------------------------------------------------------------------------- def sim_equity(ltf: pd.DataFrame, ent: list, mode: str, ts5_close: np.ndarray | None = None, c5: np.ndarray | None = None) -> np.ndarray: """Replica del loop di backtest_signals con prezzi d'esecuzione iniettabili. mode='canonical': entry a close[i], exit AL LIVELLO sl/tp (SL prioritario) o close -> DEVE riprodurre backtest_signals bit-exact (sanity della re-sim). mode='barclose': stessi trade (stessa detection), fill entry/exit al CLOSE della barra 230m di segnale/trigger -> quota dell'ottimismo 'fill al livello' senza cron. mode='hourly': fill entry/exit al close 5m del PROSSIMO multiplo orario dopo la chiusura della barra di segnale/trigger (path reale del cron 0 * * * *). In tutti i modi le TRADE BOUNDARIES (barra entry, barra trigger, non-overlap) sono identiche al canonico: cambia solo il prezzo d'esecuzione.""" c = ltf["close"].values.astype(float) h = ltf["high"].values.astype(float) l = ltf["low"].values.astype(float) n = len(c) close_ts = ltf["timestamp"].values.astype(np.int64) + MS_LTF def px_hour(t: int) -> float: hb = ((t + MSH - 1) // MSH) * MSH # prossimo multiplo orario >= t j = np.searchsorted(ts5_close, hb, side="left") return c5[min(j, len(c5) - 1)] initial = 1000.0 capital = initial equity = np.full(n, capital, dtype=float) busy_until = -1 for i in range(n): e = ent[i] if i < len(ent) else None if e is None or e.get("dir", 0) == 0: equity[i] = capital continue if i <= busy_until: equity[i] = capital continue direction = int(e["dir"]) tp = e.get("tp"); sl = e.get("sl") max_bars = int(e.get("max_bars") or 24) # entry price per modalita' if mode == "canonical": entry = c[i] elif mode == "barclose": entry = c[i] else: # hourly entry = px_hour(close_ts[i]) exit_idx = min(i + max_bars, n - 1) exit_lvl = c[exit_idx] # default: time exit a close hit_kind = "time" for j in range(i + 1, min(i + max_bars + 1, n)): hit_sl = sl is not None and ( (direction == 1 and l[j] <= sl) or (direction == -1 and h[j] >= sl)) hit_tp = tp is not None and ( (direction == 1 and h[j] >= tp) or (direction == -1 and l[j] <= tp)) if hit_sl: exit_lvl, exit_idx, hit_kind = sl, j, "sl" break if hit_tp: exit_lvl, exit_idx, hit_kind = tp, j, "tp" break exit_lvl, exit_idx = c[j], j if mode == "canonical": exit_price = exit_lvl # fill al livello (come harness) elif mode == "barclose": exit_price = c[exit_idx] # fill al close della barra trigger else: exit_price = px_hour(close_ts[exit_idx]) # fill al prossimo multiplo orario gross = (exit_price - entry) / entry * direction net = gross - 0.001 # fee_rt 0.10%, leverage 1 capital += capital * net capital = max(capital, 1.0) equity[i:exit_idx + 1] = capital busy_until = exit_idx _ = hit_kind # stessa forward-fill robusta del harness last = initial for k in range(n): if equity[k] != last and equity[k] != initial: last = equity[k] else: equity[k] = last return equity def live_delay_section() -> None: print("\n" + "=" * 100) print("(6) RILEVANZA LIVE — cron ORARIO (0 * * * *) vs confini 230m non allineati all'ora") print("=" * 100) # distribuzione del ritardo confine-230m -> prossima ora (struttura: 230 mod 60 = 50) _, _, ltf0, _ = run_asset("BTC", 0) close_ts = ltf0["timestamp"].values.astype(np.int64) + MS_LTF delay_min = ((MSH - (close_ts % MSH)) % MSH) // 60_000 vals, cnts = np.unique(delay_min, return_counts=True) tot = cnts.sum() print("ritardo chiusura-barra-230m -> prossimo run orario del cron:") for v, cn in zip(vals, cnts): print(f" {int(v):>3} min : {cn / tot:>6.1%}") print(f" media {float(delay_min.mean()):.1f} min, max {int(delay_min.max())} min " f"(ciclo di 6 barre = 23h: la griglia migra attraverso la giornata)") # impatto: re-sim canonico (sanity bit-exact) / barclose / hourly print(f"\nimpatto sull'equity daily-step (offset 0, canonico; fill hourly = close 5m del " f"prossimo multiplo orario dopo la chiusura della barra di segnale/exit):") print(f"{'asset':<5} {'modo':<10} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7}") dailies: dict[str, dict[str, pd.Series]] = {m: {} for m in ("canonical", "barclose", "hourly")} for a in ASSETS: _, m0, ltf, ent = run_asset(a, 0) df5 = get5m(a) ts5_close = df5["timestamp"].values.astype(np.int64) + MS5 c5 = df5["close"].values.astype(float) for mode in ("canonical", "barclose", "hourly"): eq = sim_equity(ltf, ent, mode, ts5_close=ts5_close, c5=c5) if mode == "canonical": dmax = float(np.max(np.abs(eq - m0.equity))) assert dmax < 1e-6, f"re-sim canonica != harness ({a}): max|dif|={dmax:.2e}" print(f" [sanity] {a}: re-sim canonica == backtest_signals " f"(max|dif equity| = {dmax:.2e})") s = pd.Series(eq, index=pd.DatetimeIndex(pd.to_datetime(ltf["datetime"], utc=True))) d = s.resample("1D").last().ffill().pct_change().dropna() dailies[mode][a] = d f, i_, h = sh3(d) dd = al._dd_ret(d) print(f"{a:<5} {mode:<10} {f:>7.3f} {i_:>7.3f} {h:>7.3f} {dd:>6.1%}") print("\nbook 50/50 BTC+ETH per modo:") print(f"{'modo':<10} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7}") port_mode = {} for mode in ("canonical", "barclose", "hourly"): J = pd.concat(dailies[mode], axis=1, join="inner").fillna(0.0) p = pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index) port_mode[mode] = p f, i_, h = sh3(p) print(f"{mode:<10} {f:>7.3f} {i_:>7.3f} {h:>7.3f} {al._dd_ret(p):>6.1%}") dfull = al._sh(port_mode['hourly']) - al._sh(port_mode['canonical']) dhold = (al._sh(port_mode['hourly'][port_mode['hourly'].index >= HOLDOUT]) - al._sh(port_mode['canonical'][port_mode['canonical'].index >= HOLDOUT])) print(f"\n-> delta hourly vs canonico: FULL {dfull:+.3f}, HOLD {dhold:+.3f} " f"(barclose isola il fill-al-livello; hourly aggiunge il ritardo 0-50 min)") # --------------------------------------------------------------------------- # Main # --------------------------------------------------------------------------- def main() -> None: t0 = time.time() print("=" * 100) print("r0702 — SKH01-V2-DD: timing-luck della FASE della griglia dual-TF 230m/690m") print(f"23 offset a priori (ogni 30m su [0,690)), parametri IDENTICI, fee 0.10% RT, " f"HOLD-OUT >= {HOLDOUT.date()}") print("=" * 100) sanity() # ---- (2) per-offset x asset ------------------------------------------- rows = [] for off in OFFSETS: rec = {"off": off} for a in ASSETS: d, m, _, _ = run_asset(a, off) f, i_, h = sh3(d) rec[f"{a}_full"] = f; rec[f"{a}_is"] = i_; rec[f"{a}_hold"] = h rec[f"{a}_dd"] = m.max_dd; rec[f"{a}_ntr"] = m.n_trades p = skh_port(off) f, i_, h = sh3(p) rec["P_full"] = f; rec["P_is"] = i_; rec["P_hold"] = h rec["P_dd"] = al._dd_ret(p) rec["minFull"] = min(rec["BTC_full"], rec["ETH_full"]) rec["minHold"] = min(rec["BTC_hold"], rec["ETH_hold"]) rows.append(rec) print(f" [{time.time()-t0:5.0f}s] offset {off:>3}m fatto " f"(minFull {rec['minFull']:+.2f}, minHold {rec['minHold']:+.2f}, " f"DD {rec['BTC_dd']:.0%}/{rec['ETH_dd']:.0%})") T = pd.DataFrame(rows).set_index("off") print("\n--- (2) PER-OFFSET (equity daily-step, come il canonico) ---") for a in ASSETS: print(f"\n{a}:") print(f"{'off':>4} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7} {'trade':>6}") for off, r in T.iterrows(): tag = " <- canonico" if off == 0 else "" print(f"{off:>4} {r[f'{a}_full']:>7.3f} {r[f'{a}_is']:>7.3f} " f"{r[f'{a}_hold']:>7.3f} {r[f'{a}_dd']:>6.1%} {int(r[f'{a}_ntr']):>6}{tag}") print(" min/med/max [pctl canonico]:") for col, lbl in ((f"{a}_full", "ShFULL"), (f"{a}_is", "ShIS"), (f"{a}_hold", "ShHOLD"), (f"{a}_dd", "maxDD")): v = T[col].values print(f" {lbl:<7} {v.min():>7.3f} / {np.median(v):>7.3f} / {v.max():>7.3f} " f" [off0 = {v[0]:.3f}, {pctl_of_first(v):.0f} pctl]") print(f"\nBOOK 50/50 (portafoglio SKH01 standalone):") print(f"{'off':>4} {'ShFULL':>7} {'ShIS':>7} {'ShHOLD':>7} {'maxDD':>7}") for off, r in T.iterrows(): tag = " <- canonico" if off == 0 else "" print(f"{off:>4} {r.P_full:>7.3f} {r.P_is:>7.3f} {r.P_hold:>7.3f} {r.P_dd:>6.1%}{tag}") for col, lbl in (("P_full", "ShFULL"), ("P_is", "ShIS"), ("P_hold", "ShHOLD"), ("P_dd", "maxDD"), ("minFull", "minFull"), ("minHold", "minHold")): v = T[col].values print(f" {lbl:<8} min {v.min():>7.3f} / med {np.median(v):>7.3f} / max {v.max():>7.3f} " f" [off0 = {v[0]:.3f}, {pctl_of_first(v):.0f} pctl]") # ---- (3) gate di ammissione ------------------------------------------ print("\n--- (3) GATE DI AMMISSIONE ri-valutati sui 23 offset ---") viol_b = T["BTC_dd"] >= 0.30 viol_e = T["ETH_dd"] >= 0.30 viol = viol_b | viol_e print(f"(a) maxDD<30%: violazioni su 23 offset: BTC {int(viol_b.sum())}, " f"ETH {int(viol_e.sum())}, almeno-un-asset {int(viol.sum())} " f"({[int(o) for o in T.index[viol]]})") print(f" mediana DD: BTC {T['BTC_dd'].median():.1%}, ETH {T['ETH_dd'].median():.1%}; " f"peggiore: BTC {T['BTC_dd'].max():.1%} (off {int(T['BTC_dd'].idxmax())}), " f"ETH {T['ETH_dd'].max():.1%} (off {int(T['ETH_dd'].idxmax())})") print(f"(b) minFull (canonico +0.99): mediana {T['minFull'].median():+.2f}, " f"peggiore {T['minFull'].min():+.2f} (off {int(T['minFull'].idxmin())})") print(f" minHold (canonico +1.26): mediana {T['minHold'].median():+.2f}, " f"peggiore {T['minHold'].min():+.2f} (off {int(T['minHold'].idxmin())})") # (c) blend 0.75 TP01 + 0.25 SKH — TP01 baseline canonico FISSO B = al.tp01_baseline_daily() b_hold = al._sh(B[B.index >= HOLDOUT]) blends_hold, blends_full, corrs = [], [], [] blend_series_hold = {} for off in OFFSETS: s = skh_port(off) bl = combine_outer({"TP": B, "SKH": s}, BLEND_W) bl = bl[bl.index >= B.index.min()] blends_hold.append(al._sh(bl[bl.index >= HOLDOUT])) blends_full.append(al._sh(bl)) blend_series_hold[off] = bl[bl.index >= HOLDOUT] J = pd.concat({"TP": B, "SKH": s}, axis=1, join="inner").dropna() corrs.append(float(J["TP"].corr(J["SKH"]))) bh = np.array(blends_hold); bf = np.array(blends_full); co = np.array(corrs) print(f"(c) blend 0.75*TP01+0.25*SKH (TP01 h=0 canonico; suo HOLD da solo = {b_hold:.2f}):") print(f" Sharpe HOLD blend: off0 {bh[0]:.2f} | min {bh.min():.2f} / med " f"{np.median(bh):.2f} / max {bh.max():.2f} [off0 al {pctl_of_first(bh):.0f} pctl]" f" (claim: 0.31 -> 1.17)") print(f" uplift HOLD vs TP01 solo: off0 {bh[0]-b_hold:+.2f} | min {bh.min()-b_hold:+.2f} " f"/ med {np.median(bh)-b_hold:+.2f} / max {bh.max()-b_hold:+.2f}") print(f" Sharpe FULL blend: off0 {bf[0]:.2f} | min {bf.min():.2f} / med " f"{np.median(bf):.2f} / max {bf.max():.2f}") print(f"(d) corr(SKH, TP01) full: off0 {co[0]:+.3f} | min {co.min():+.3f} / med " f"{np.median(co):+.3f} / max {co.max():+.3f} (claim ~0.09)") med_minfull = T["minFull"].median(); med_minhold = T["minHold"].median() med_uplift = float(np.median(bh) - b_hold) med_dd_ok = T["BTC_dd"].median() < 0.30 and T["ETH_dd"].median() < 0.30 worst_ok = (not viol.any()) and T["minFull"].min() > 0 and (bh.min() - b_hold) > 0 print("\nVERDETTO ammissione alla fase MEDIANA: " f"DD<30% {'PASS' if med_dd_ok else 'FAIL'} alla mediana" f" (violazioni puntuali {int(viol.sum())}/23), minFull {med_minfull:+.2f}, " f"minHold {med_minhold:+.2f}, uplift blend HOLD {med_uplift:+.2f}, " f"corr {np.median(co):+.2f}") print(f" al PEGGIORE dei 23: DD {'PASS tutti' if not viol.any() else 'FAIL su ' + str(int(viol.sum())) + ' offset'}, " f"minFull {T['minFull'].min():+.2f}, minHold {T['minHold'].min():+.2f}, " f"uplift blend HOLD {bh.min()-b_hold:+.2f} " f"-> {'regge anche al peggiore' if worst_ok else 'NON regge al peggiore'}") # ---- (4) book 5-sleeve ------------------------------------------------- print("\n--- (4) BOOK 5-SLEEVE (TP 33 / XS 15 / VRP 12 / SKH 20 / GTAA 20) ---") from src.portfolio.sleeves import (_gtaa_daily_returns, _tp01_returns, _vrp_combo_returns, _xsec_returns) tp_d = to_daily(_tp01_returns()) cols_fixed = dict(TP=tp_d, XS=to_daily(_xsec_returns()), VRP=to_daily(_vrp_combo_returns()), GTAA=to_daily(_gtaa_daily_returns())) lo = tp_d.index.min() bkh, bkf = [], [] for off in OFFSETS: s = combine_outer(dict(SKH=skh_port(off), **cols_fixed), BOOK_W, lo=lo) bkh.append(al._sh(s[s.index >= HOLDOUT])); bkf.append(al._sh(s)) bkh = np.array(bkh); bkf = np.array(bkf) ens = pd.concat({o: skh_port(o) for o in OFFSETS}, axis=1).mean(axis=1) s_ens = combine_outer(dict(SKH=ens, **cols_fixed), BOOK_W, lo=lo) i_med_h = int(np.argsort(bkh)[len(bkh) // 2]); i_worst_h = int(np.argmin(bkh)) print(f"HOLD: off0 {bkh[0]:.2f} | min {bkh.min():.2f} (off {OFFSETS[i_worst_h]}) / med " f"{np.median(bkh):.2f} (off {OFFSETS[i_med_h]}) / max {bkh.max():.2f} " f"[off0 al {pctl_of_first(bkh):.0f} pctl] | ENSEMBLE " f"{al._sh(s_ens[s_ens.index >= HOLDOUT]):.2f}") print(f"FULL: off0 {bkf[0]:.2f} | min {bkf.min():.2f} / med {np.median(bkf):.2f} / max " f"{bkf.max():.2f} [off0 al {pctl_of_first(bkf):.0f} pctl] | ENSEMBLE {al._sh(s_ens):.2f}") print("(l'ensemble di offset NON e' eseguibile live — una sola griglia gira: e' solo la " "stima de-luckata)") # ---- (5) bootstrap alla maniera dello scettico ------------------------- print("\n--- (5) BOOTSTRAP (block) — lo spike del canonico sull'hold-out del blend ---") Mdf = pd.concat(blend_series_hold, axis=1, join="inner").dropna() M = Mdf.values def _sh_cols(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) sh_obs = np.array([al._sh(Mdf[c]) for c in Mdf.columns]) g0_obs = float(sh_obs[0] - np.median(sh_obs[1:])) corrM = np.corrcoef(M.T); iu = np.triu_indices(M.shape[1], 1) print(f"hold-out: {M.shape[0]} giorni x {M.shape[1]} offset; Sh blend off0 {sh_obs[0]:.3f}, " f"mediana altri {np.median(sh_obs[1:]):.3f}, spike osservato g0 = {g0_obs:+.3f}") print(f"corr daily fra i 23 blend (hold-out): mediana {np.median(corrM[iu]):.3f}, " f"min {corrM[iu].min():.3f}") n, K = M.shape for blk in (10, 20, 40): rng = np.random.default_rng(42 + blk) nblocks = int(np.ceil(n / blk)) gmaxs, g0s = [], [] done = 0 while done < B_BOOT: b = min(500, B_BOOT - done) starts = rng.integers(0, n, size=(b, nblocks)) idx = (starts[:, :, None] + np.arange(blk)[None, None, :]) % n idx = idx.reshape(b, -1)[:, :n] R = M[idx] # (b, n, K) Sh = np.stack([_sh_cols(R[:, :, k]) for k in range(K)], axis=1) med_others = np.empty_like(Sh) for k in range(K): med_others[:, k] = np.median(np.delete(Sh, k, axis=1), axis=1) g = Sh - med_others gmaxs.append(g.max(axis=1)); g0s.append(g[:, 0]) done += b gmax = np.concatenate(gmaxs); g0 = np.concatenate(g0s) print(f" block={blk:>2}: P(spike di UN offset qualsiasi >= {g0_obs:+.2f}) = " f"{float(np.mean(gmax >= g0_obs)):.3f} | P(g0<=0) = {float(np.mean(g0 <= 0)):.3f} " f"| CI95 g0 [{np.percentile(g0, 2.5):+.2f},{np.percentile(g0, 97.5):+.2f}]") # ---- (6) live ---------------------------------------------------------- live_delay_section() # ---- (7) caveat -------------------------------------------------------- print("\n--- (7) CAVEAT ---") print("- equity daily-step (lens Sharpe), stessa convenzione del canonico;") print("- costi di ribilanciamento del book 230m gia' flaggati a deploy (diario skyhook);") print("- ensemble di offset NON eseguibile live (una sola griglia gira) -> solo stima de-luckata;") print("- sim 'hourly': fill a close 5m del multiplo orario (cron minuto 0), niente slippage/parziali;") print("- nessuna selezione: 23 offset uniformi dichiarati a priori, parametri identici ovunque.") print(f"\nFatto in {time.time()-t0:.0f}s.") if __name__ == "__main__": main()