research: rivalutazione della strategia — 0 cambi, 75/25 confermato per la terza volta

Delle sei misure prodotte oggi solo UNA apriva una decisione: il peso 75/25 fu
confermato il 24/07 con la lente HOURLY, e il 26/07 quella lente e' risultata
pessimistica su ENTRAMBI i lati di SKH01 (uscite +0.081, ingressi +0.048 di Sharpe di
book). Se SKH01 vale piu' di come e' stato pesato, 0.25 poteva non essere piu' l'ottimo.
Contro tirava il LOO de-luckato dello stesso giorno (SKH01 = il meno affidabile dei
cinque). Due correzioni in versi opposti: si misura, non si deduce.

MISURA (path live intra_entry=True, 8 offset, mediana delle differenze APPAIATE):
argmax a w=0.35, e 0.30-0.40 batte 0.25 nell'88% degli offset -> il segnale c'e' ed e'
nel verso previsto dalla correzione di lente. Ma il plateau entro 0.05 di Sharpe e'
[0.25 ... 0.50] e il peso live e' DENTRO, e `weights_tilt_null` FALLISCE
(delta_insample -0.0026, gate_pass False).

IL MOTIVO VERO sta in cio' che la mediana nasconde: il guadagno e' tutto nella coda ALTA.
p10 per peso 1.463 / 1.465 / 1.455 / 1.435 / 1.374 mentre il p90 sale monotono
1.915 -> 2.064. Alzare SKH01 non compra Sharpe, compra dipendenza da quale ancora ti e'
capitata (banda da 0.45 a 0.69). Coerente con altre due misure indipendenti dello stesso
giorno: SKH01 ha la frazione d'ancora piu' grande da restituire (LOO) ed e' 4x piu'
fee-sensibile. Tre misure indipendenti dicono che SKH01 e' la gamba fragile del book e
che 0.25 sta all'estremo prudente della regione robusta.

LA RIVALUTAZIONE CHE CONTA NON E' SULLA STRATEGIA. Ordini di grandezza a confronto:
ottimizzare il peso = +0.030 Sharpe (gate fallito); versare EUR 250/mese invece di EUR 0
= da MAI a 16.2 anni (P entro 20a = 92%); perdere il conto Deribit a p=1% = -18% di
probabilita' di arrivare, ripartendo da zero. Il book e' dentro il suo plateau su ogni
asse misurato: la ricerca ha smesso di essere il vincolo binding. I vincoli binding oggi
sono capitale che entra e conto che non sparisce.

Book, pesi, cron, config INVARIATI. Gate pre-registrati alle loro date (anticiparli
sarebbe selezione sull'hold-out). 365 test verdi (+6).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-07-26 20:06:23 +00:00
parent 4450321a81
commit 1dd26342ca
4 changed files with 358 additions and 0 deletions
@@ -0,0 +1,137 @@
#!/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()