2c3882b60c
Tre questioni che il progetto aveva lasciato aperte e decidibili oggi. T1 — ESECUZIONE SKH01. Testato l'unico meccanismo che non e' un cron: ordini resting on-book (TP=limit al livello per costruzione + fee maker, SL=stop-market). A offset 0 recupera il 94% del degrado, ma l'audit 02/07 aveva de-luckato i numeri headline di SKH01 e NON il degrado. Sulla banda appaiata dei 23 offset il degrado recuperabile e' +0.054 Sh di book, non -0.35 di sleeve. Errore di metodo corretto in sessione: mediana(A)-mediana(B) fra offset confronta offset diversi; serve la mediana delle DIFFERENZE appaiate. Col fix il verdetto si ribalta: solo TP a limite = +0.054 FULL / +0.061 HOLD, positivo in 19/23 e 21/23 offset; lo SL on-book PEGGIORA (mediana -0.010, positivo in 11/23) perche' cristallizza la perdita mentre l'exit ritardata incassa il rimbalzo. Raccomandazione NON eseguita: TP a limite si, SL strategico on-book no. T2 — DVOLSPREAD esce dal limbo (fermo dal 21/06). Passato ai due gate che allora non esistevano. La griglia dichiarata "72 celle" ne contiene 729: valutate tutte. Plateau reale (729/729 hold-out positivo). Selection-on-holdout confermata ma mite: cella pubblicata 83a/729 sull'hold-out, 471a/729 in-sample. Cella onesta FULL 0.68 / HOLD 0.69 (non 0.93) e DSR 0.953 PASS; la pubblicata fallisce 0.947. Promosso a forward-monitor coi parametri onesti, NON nel book (hold-out attivo 1.6 anni, DSR sul filo, weights_tilt_null mai affrontato). T3 — XSR01 non generalizza. Meccanismo congelato su 9 settoriali (1998+) e 28 ETF (30 anni), versione DEMEANATA (il test del 25/07 era a coppie). Lordo +0.24 p=0.193 e -0.14 p=0.747 vs null a fee zero. Il risultato che conta e' l'ampiezza: il demean fa 4.5->37.4 (8x) sul crypto ma 5.3->6.2 (1.2x) sulle azioni, perche' sul crypto il residuo-vs-BTC lascia un enorme fattore comune e sulle azioni il residuo-vs-SPY e' gia' indipendente. XSR01 e' crypto-specifico; NON e' falso. Soglie del gate 23/10 non toccate. Book/pesi/cron INVARIATI. Suite 245 verdi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
203 lines
9.6 KiB
Python
203 lines
9.6 KiB
Python
#!/usr/bin/env python
|
|
"""r0726_dvolspread_gate — T2 dell'ondata 2026-07-26: chiudere il lead DVOLSPREAD, in limbo
|
|
dal 2026-06-21.
|
|
|
|
CHI E'. `agent_14_dvol_spread` (onda ortho, 2026-06-21) e' **l'unico sopravvissuto** al marginal
|
|
scorer indurito: dei 18 book relative-value che facevano "ADDS" con lo scorer vecchio, l'indurito
|
|
ne ha lasciato in piedi **1**, questo — "l'unico con edge in-sample reale". Fu lasciato in
|
|
forward-monitor con due riserve dichiarate: *multiple-testing* e *storia DVOL corta*. Da allora
|
|
non e' mai stato ripreso: non e' nel book, non e' in un paper monitor, non e' stato rifiutato.
|
|
E' semplicemente rimasto li'.
|
|
|
|
PERCHE' RIPRENDERLO ORA. Perche' nel frattempo il progetto ha codificato **due gate che il 21/06
|
|
non esistevano**, e sono esattamente quelli che colpiscono la sua riserva:
|
|
|
|
1. **`deflated_sharpe`** (29/06). Il docstring dell'agente dichiara *da se'* uno **sweep di 72
|
|
celle** su ZWIN/LW/ZW/TGT/SVW. 72 trial sono precisamente cio' che il DSR sconta. Nel giugno
|
|
"72/72 celle ADDS" veniva letto come prova di robustezza; il DSR dice che e' anche 72 tentativi.
|
|
2. **`select_cell_insample` / `study_family_honest`** (29/06). Il plateau dell'agente e' descritto
|
|
in termini di **`uplift_hold`** ("ogni vicino resta ADDS con uplift_hold in [0.34,0.39]"),
|
|
cioe' la cella e' stata guardata SULL'HOLD-OUT. E' la firma che il gate SELECTION-ON-HOLDOUT
|
|
e' stato scritto per catturare — lo stesso difetto che uccise il lead ERM intraday.
|
|
|
|
DOMANDA SECCA: scegliendo la cella con il solo in-sample, e scontando i 72 trial, DVOLSPREAD
|
|
resta uno sleeve o era un falso positivo sopravvissuto solo perche' i gate non c'erano ancora?
|
|
|
|
Terza verifica, indipendente dalle prime due: la **storia DVOL** parte 2021-03, quindi il book e'
|
|
FLAT per meta' del campione. Si misura quanto e' corto il campione ATTIVO (non quello nominale),
|
|
perche' e' su quello che vanno letti tutti i numeri.
|
|
|
|
Uso: `uv run python scripts/research/r0726_dvolspread_gate.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" / "alt"))
|
|
sys.path.insert(0, str(ROOT / "scripts" / "research" / "ortho"))
|
|
|
|
import altlib as al # noqa: E402
|
|
import ortholib as ol # noqa: E402
|
|
|
|
# griglia DICHIARATA dall'agente stesso (docstring PLATEAU): 72 celle
|
|
GRID = dict(zwin=[120, 150, 180], tanh_k=[1.5, 1.8, 2.0],
|
|
lw=[0.3, 0.45, 0.6], zw=[0.9, 1.0, 1.1],
|
|
tgt=[0.11, 0.13, 0.17], svw=[40, 50, 60])
|
|
CHOSEN = dict(zwin=150, tanh_k=1.8, lw=0.45, zw=1.0, tgt=0.13, svw=50)
|
|
LEG_CAP = 0.5
|
|
|
|
|
|
def make_book(zwin, tanh_k, lw, zw, tgt, svw):
|
|
"""Fabbrica il book dell'agente a parametri arbitrari (stessa formula, nessuna variante)."""
|
|
def _book(btc, eth):
|
|
import warnings
|
|
bc = btc["close"].values.astype(float)
|
|
ec = eth["close"].values.astype(float)
|
|
rb = ol.simple_returns(bc)
|
|
re = ol.simple_returns(ec)
|
|
db = al.dvol(btc, "BTC")
|
|
de = al.dvol(eth, "ETH")
|
|
with np.errstate(invalid="ignore", divide="ignore"):
|
|
ldvr = np.log(db) - np.log(de)
|
|
ldvr = np.where(np.isfinite(ldvr), ldvr, np.nan)
|
|
level = np.tanh(tanh_k * np.nan_to_num(ldvr, nan=0.0))
|
|
zdev = np.tanh(tanh_k * np.nan_to_num(ol.zscore(ldvr, zwin), nan=0.0))
|
|
g_dir = np.where(np.isfinite(ldvr), lw * level + zw * zdev, 0.0)
|
|
with warnings.catch_warnings():
|
|
warnings.simplefilter("ignore", category=RuntimeWarning)
|
|
spvol = ol.realized_vol(re - rb, svw, 365.25)
|
|
scal = np.where((spvol > 0) & np.isfinite(spvol), tgt / spvol, 0.0)
|
|
g = np.nan_to_num(np.clip(g_dir * scal, -LEG_CAP, LEG_CAP), nan=0.0)
|
|
return g, -g
|
|
return _book
|
|
|
|
|
|
_EVAL_CACHE: dict = {}
|
|
|
|
|
|
def evaluate(**kw) -> dict:
|
|
"""Valutazione col MOTORE ORIGINALE dell'agente (ortholib.eval_book): stesso shift, stesse
|
|
fee su entrambe le gambe, stessa serie daily. Riusarlo — invece di riscriverlo — e' cio' che
|
|
rende questo un RE-TEST dello stesso oggetto e non di una sua approssimazione."""
|
|
key = tuple(sorted(kw.items()))
|
|
if key not in _EVAL_CACHE:
|
|
_EVAL_CACHE[key] = ol.eval_book(make_book(**kw))
|
|
return _EVAL_CACHE[key]
|
|
|
|
|
|
def daily_returns(**kw) -> pd.Series:
|
|
return evaluate(**kw)["daily"].dropna()
|
|
|
|
|
|
def sh(s: pd.Series, lo=None, hi=None) -> float:
|
|
x = s.dropna()
|
|
if lo is not None:
|
|
x = x[x.index >= lo]
|
|
if hi is not None:
|
|
x = x[x.index < hi]
|
|
return float(x.mean() / x.std() * np.sqrt(365.25)) if len(x) > 30 and x.std() > 0 else 0.0
|
|
|
|
|
|
HOLDOUT = "2025-01-01"
|
|
|
|
|
|
def main() -> None:
|
|
print("=" * 100)
|
|
print(" T2 — DVOLSPREAD: il lead del 21/06 passa i gate del 29/06?")
|
|
print("=" * 100)
|
|
|
|
base = daily_returns(**CHOSEN)
|
|
act = base[base != 0.0]
|
|
print(f"\n campione NOMINALE: {len(base)} giorni ({base.index[0].date()} -> {base.index[-1].date()})")
|
|
print(f" campione ATTIVO : {len(act)} giorni ({len(act)/len(base):.0%}) — "
|
|
f"prima del 2021-03 non c'e' DVOL e il book e' FLAT")
|
|
print(f" attivo pre-holdout {len(act[act.index < HOLDOUT])} g | "
|
|
f"attivo hold-out {len(act[act.index >= HOLDOUT])} g "
|
|
f"({len(act[act.index >= HOLDOUT])/365:.1f} anni)")
|
|
|
|
# ------------------------------------------------------- griglia dichiarata dall'agente
|
|
print("\n" + "-" * 100)
|
|
print(" A. LA GRIGLIA CHE L'AGENTE DICHIARA DA SE' (72 celle: ZWIN x K x LW x ZW x TGT x SVW)")
|
|
print("-" * 100)
|
|
import itertools
|
|
keys = list(GRID)
|
|
cells = []
|
|
for combo in itertools.product(*[GRID[k] for k in keys]):
|
|
kw = dict(zip(keys, combo))
|
|
r = daily_returns(**kw)
|
|
cells.append(dict(kw=kw, full=sh(r), is_=sh(r, hi=HOLDOUT), hold=sh(r, lo=HOLDOUT)))
|
|
df = pd.DataFrame([{**c["kw"], "full": c["full"], "is": c["is_"], "hold": c["hold"]}
|
|
for c in cells])
|
|
print(f" celle valutate: {len(df)}")
|
|
print(f" Sharpe FULL : min {df['full'].min():.2f} | mediana {df['full'].median():.2f} | "
|
|
f"max {df['full'].max():.2f}")
|
|
print(f" Sharpe HOLD : min {df['hold'].min():.2f} | mediana {df['hold'].median():.2f} | "
|
|
f"max {df['hold'].max():.2f}")
|
|
print(f" celle con HOLD > 0: {int((df['hold'] > 0).sum())}/{len(df)}")
|
|
|
|
# ------------------------------------------------------- selezione IN-SAMPLE vs hold-out
|
|
print("\n" + "-" * 100)
|
|
print(" B. SELECTION-ON-HOLDOUT — la cella scelta in-sample e' la stessa scelta sull'hold-out?")
|
|
print("-" * 100)
|
|
i_is = int(df["is"].idxmax())
|
|
i_ho = int(df["hold"].idxmax())
|
|
i_ch = int(df[[*CHOSEN]].eq(pd.Series(CHOSEN)).all(axis=1).idxmax())
|
|
for lab, i in (("cella scelta IN-SAMPLE (onesta)", i_is),
|
|
("cella scelta sull'HOLD-OUT (proibita)", i_ho),
|
|
("cella PUBBLICATA dall'agente", i_ch)):
|
|
row = df.loc[i]
|
|
pars = {k: row[k] for k in CHOSEN}
|
|
print(f" {lab:>40}: FULL {row['full']:>5.2f} IS {row['is']:>5.2f} "
|
|
f"HOLD {row['hold']:>5.2f} {pars}")
|
|
rank_ch_hold = int((df["hold"] > df.loc[i_ch, "hold"]).sum()) + 1
|
|
rank_ch_is = int((df["is"] > df.loc[i_ch, "is"]).sum()) + 1
|
|
print(f"\n rango della cella PUBBLICATA: {rank_ch_hold}/{len(df)} sull'HOLD-OUT, "
|
|
f"{rank_ch_is}/{len(df)} sull'IN-SAMPLE")
|
|
print(" (se e' molto meglio classificata sull'hold-out che in-sample -> selezione-sull'hold-out)")
|
|
|
|
# ------------------------------------------------------- deflated Sharpe sui 72 trial
|
|
print("\n" + "-" * 100)
|
|
print(" C. DEFLATED SHARPE sui trial DICHIARATI DALL'AGENTE STESSO")
|
|
print("-" * 100)
|
|
r_is = daily_returns(**{k: df.loc[i_is, k] for k in CHOSEN})
|
|
for lab, i, r in (("cella IN-SAMPLE", i_is, r_is),
|
|
("cella PUBBLICATA", i_ch, base)):
|
|
dsr, sr0 = al.deflated_sharpe(sh(r), list(df["full"].values), r.dropna().values)
|
|
verdict = "PASS" if (np.isfinite(dsr) and dsr >= 0.95) else "FAIL"
|
|
print(f" {lab:>20}: Sharpe FULL {sh(r):>5.2f} | max atteso sotto il null "
|
|
f"{sr0:>5.2f} | DSR {dsr:>6.3f} -> {verdict}")
|
|
|
|
# ------------------------------------------------------- marginale vs TP01
|
|
print("\n" + "-" * 100)
|
|
print(" D. MARGINALE vs TP01 (scorer indurito) — sulla cella scelta IN-SAMPLE")
|
|
print("-" * 100)
|
|
tp = al.tp01_baseline_daily()
|
|
for lab, r, kwc in (("cella IN-SAMPLE", r_is, {k: df.loc[i_is, k] for k in CHOSEN}),
|
|
("cella PUBBLICATA", base, CHOSEN)):
|
|
J = pd.concat({"tp": tp, "c": r}, axis=1, join="inner").fillna(0.0)
|
|
corr = float(J["tp"].corr(J["c"]))
|
|
for w in (0.15, 0.25):
|
|
bl = (1 - w) * J["tp"] + w * J["c"]
|
|
print(f" {lab:>20} w={w:.0%}: corr {corr:>+5.2f} | "
|
|
f"dSharpe FULL {sh(bl) - sh(J['tp']):>+5.2f} | "
|
|
f"dSharpe HOLD {sh(bl, lo=HOLDOUT) - sh(J['tp'], lo=HOLDOUT):>+5.2f}")
|
|
mg = al.marginal_vs_tp01(r)
|
|
print(f" {'':>20} verdetto {mg['marginal_verdict']} | robust_oos={mg['robust_oos']} | "
|
|
f"multicut={mg['multicut_persistent']} | is_hedge={mg['is_hedge']} | "
|
|
f"insample_edge={mg['has_insample_edge']} | beats_noise={mg['beats_noise_null']}")
|
|
print(f" {'':>20} jackknife_min_uplift {mg['jackknife_min_uplift']:+.3f} | "
|
|
f"alpha_ann {mg['alpha_ann']:+.1%} | beta_TP01 {mg['beta_to_tp01']:+.3f} | "
|
|
f"uplift TP01-up {mg['uplift_tp01_up']:+.3f} / TP01-down {mg['uplift_tp01_down']:+.3f}")
|
|
|
|
print("\n" + "=" * 100)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|