#!/usr/bin/env python """r0726_reeval_live_weight.py — il peso 75/25 regge dopo le correzioni di oggi? PERCHE' RIAPRIRLO. Il peso del book live (TP01 0.75 / SKH01 0.25) fu confermato il **24/07** (`r0724_skh_live_weight.py`) misurando il path live con la lente **hourly**. Il 26/07 quella lente e' risultata **sistematicamente pessimistica su ENTRAMBI i lati** di SKH01: * USCITE: `resample_5m` non scarta il bin in corso e `_skyhook_positions` ci itera dentro -> il live rileva SL/TP intra-barra; la lente hourly sottostima **+0.081 Sharpe FULL di book**; * INGRESSI: il cron valuta a ogni confine orario dentro il bin 230m, non a chiusura di bin -> mediana appaiata **+0.112** a livello di sleeve, **+0.048** di Sharpe FULL di book. Se SKH01 vale piu' di come e' stato pesato, **l'ottimo potrebbe non essere piu' 0.25**. E in senso opposto tira il leave-one-out de-luckato dello stesso giorno, che lo da' come il **meno affidabile** dei cinque sleeve (+0.142 FULL, positivo nel 91% delle estrazioni, contro +0.390/100% di TP01). Due correzioni misurate che puntano in versi OPPOSTI: la domanda non e' retorica, e la risposta non si puo' dedurre. Qui si misura. METODO * path live vero: `intra_entry=True` (ingressi intra-bin) sui segnali gia' in cache; * griglia di peso 0.00-0.50, su 8 offset (sottocampione a priori `OFFSETS_SUB`); * statistica = **mediana delle differenze APPAIATE** per offset (mai differenza di mediane); * qualunque cambio proposto passa poi da `weights_tilt_null`, che e' il gate del progetto. uv run python scripts/research/r0726_reeval_live_weight.py """ from __future__ import annotations import sys from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[2] sys.path.insert(0, str(ROOT)) sys.path.insert(0, str(ROOT / "scripts" / "research")) import r0726_skh_live_book as LB # noqa: E402 import r0726_skh_sigcache as SC # noqa: E402 from src.portfolio.portfolio import (combine_outer, metrics, # noqa: E402 to_daily, weights_tilt_null) from src.portfolio.sleeves import tp01_sleeve # noqa: E402 W_GRID = (0.00, 0.10, 0.15, 0.20, 0.25, 0.30, 0.35, 0.40, 0.50) W_LIVE = 0.25 HOLDOUT = pd.Timestamp("2025-01-01", tz="UTC") def _sh(s: pd.Series) -> float: v = s.dropna() return float(v.mean() / v.std() * np.sqrt(365)) if len(v) > 2 and v.std() > 0 else 0.0 def main() -> None: print("=" * 100) print(" r0726 — RIVALUTAZIONE del peso live TP01/SKH01 dopo le correzioni del 26/07") print("=" * 100) print(" Il 24/07 confermo' 0.25 con la lente HOURLY. Oggi quella lente e' misurata") print(" pessimistica su entrambi i lati di SKH01 (uscite +0.081, ingressi +0.048 di book).") print(" Contro: il LOO de-luckato da' SKH01 come il MENO affidabile dei cinque.") have = [o for o in SC.OFFSETS_SUB if (SC.CACHE_DIR / f"sigtab_btc_off{o:03d}.parquet").exists() and (SC.CACHE_DIR / f"sigtab_eth_off{o:03d}.parquet").exists()] print(f"\n offset disponibili in cache: {have} ({len(have)}/23 della griglia piena)") if len(have) < 4: print(" ✋ troppo pochi offset: rilanciare r0726_skh_sigcache.py") return tp = tp01_sleeve(weight=1.0).daily() # ---- per ogni offset, il path LIVE di SKH01 e le curve di Sharpe al variare del peso rows_full, rows_hold = {w: [] for w in W_GRID}, {w: [] for w in W_GRID} for off in have: dailies = {} for a in ("BTC", "ETH"): dailies[a] = LB.paths_for(a, off)["live"] skh = LB.skh_book(dailies) for w in W_GRID: bk = combine_outer({"TP01": tp, "SKH01": skh}, {"TP01": 1.0 - w, "SKH01": w}) rows_full[w].append(_sh(bk)) rows_hold[w].append(_sh(bk[bk.index >= HOLDOUT])) print(f"\n {'peso SKH':>9}{'Sh FULL med':>13}{'banda p10-p90':>22}" f"{'Sh HOLD med':>13}{'d vs 0.25 (appaiata)':>23}") base_f = np.array(rows_full[W_LIVE]) base_h = np.array(rows_hold[W_LIVE]) best_f, best_w = -9e9, None for w in W_GRID: f = np.array(rows_full[w]) h = np.array(rows_hold[w]) dpair = np.median(f - base_f) # mediana delle DIFFERENZE, non differenza di mediane mf = float(np.median(f)) if mf > best_f: best_f, best_w = mf, w tag = " <- LIVE" if w == W_LIVE else "" print(f" {w:>9.2f}{mf:>13.3f}" f" [{np.percentile(f,10):>6.3f},{np.percentile(f,90):>6.3f}]" f"{float(np.median(h)):>13.3f}{dpair:>+15.3f}" f" ({(f>base_f).mean()*100:>3.0f}%){tag}") print(f"\n argmax della mediana FULL: w = {best_w:.2f} (live = {W_LIVE:.2f})") # ---- plateau: un argmax senza plateau e' rumore near = [w for w in W_GRID if np.median(rows_full[w]) >= best_f - 0.05] print(f" plateau entro 0.05 di Sharpe: {near}") if W_LIVE in near: print(f" -> il peso live E' dentro il plateau: la differenza col massimo non e'") print(f" distinguibile, e cambiare peso su questa base sarebbe rumore.") else: print(f" -> il peso live e' FUORI dal plateau: il cambio va valutato dal gate.") # ---- gate del progetto su un eventuale cambio if best_w != W_LIVE: print(f"\n GATE `weights_tilt_null` sul cambio {W_LIVE:.2f} -> {best_w:.2f}:") off0 = have[0] skh0 = LB.skh_book({a: LB.paths_for(a, off0)["live"] for a in ("BTC", "ETH")}) res = weights_tilt_null({"TP01": to_daily(tp), "SKH01": to_daily(skh0)}, {"TP01": 1 - W_LIVE, "SKH01": W_LIVE}, {"TP01": 1 - best_w, "SKH01": best_w}, k_seen=len(W_GRID)) for k in ("delta_insample", "pctl_hold", "gate_pass"): if k in res: print(f" {k:<16} {res[k]}") print(f"\n ⚠️ Anche a gate passato servirebbero finestre OOS disgiunte e un controllo di") print(f" eseguibilita' al peso aumentato: il gate e' NECESSARIO, non sufficiente.") else: print(f"\n Nessun cambio da valutare: l'argmax coincide col peso live.") # ---- sensibilita' alla fee, che il 1 agosto puo' cambiare print(f"\n PROMEMORIA fee (r0726_fee_sensitivity): SKH01 e' ~4x piu' sensibile di TP01") print(f" (-0.69% vs -0.09% di CAGR per bps/lato). La regola gia' decisa resta:") print(f" taker <=5bps -> non si tocca nulla; >10bps -> rivedere PRIMA il peso di SKH01.") if __name__ == "__main__": main()