research(wave): ondata 3 filoni — esecuzione SKH01, DVOLSPREAD, XSR01 fuori dal crypto

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>
This commit is contained in:
Adriano Dal Pastro
2026-07-25 19:51:52 +00:00
parent 8b4c698ad9
commit 2c3882b60c
6 changed files with 1190 additions and 0 deletions
+202
View File
@@ -0,0 +1,202 @@
#!/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()
+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env python
"""r0726_skh_onbook — T1 dell'ondata 2026-07-26: il -0.35 Sharpe del path live di SKH01
e' recuperabile con ORDINI RESTING ON-BOOK invece dell'exit software del cron?
CONTESTO. Il book live Deribit (TP01 0.75 + SKH01 0.25) gira SKH01 col cron ORARIO e uscite
SOFTWARE: il codice guarda il prezzo una volta all'ora e, se il livello SL/TP e' stato superato,
chiude a mercato. Costo misurato (audit 2026-07-02, riconfermato il 24/07): alla lente 50/50 il
book passa FULL 1.46 -> 1.19, HOLD 1.64 -> 1.15, DD 18% -> 25%. E' il **buco singolo piu' grande
del progetto** (~ -0.35 di Sharpe), e il 24/07 ha gia' stabilito che **un cron piu' veloce non lo
recupera** ("nessun cron riporta l'exit software sul livello SL dentro una barra che gappa";
allineare la cadenza a 230m vale ~ +0.01/+0.02 Sharpe = rumore).
L'ANGOLO MAI TESTATO: l'unico meccanismo che non e' un cron — gli **ordini resting on-book**.
E li' c'e' un'ASIMMETRIA che nessuno ha sfruttato:
* il **TP e' un LIMIT order** appoggiato sul book al livello. Se il prezzo ci arriva, il fill
e' AL LIVELLO **per costruzione** (sei tu il maker: il prezzo non puo' attraversare il tuo
limite senza eseguirlo). -> il lato TP e' **integralmente recuperabile**, e per giunta a
fee MAKER invece che taker.
* lo **SL e' uno STOP-MARKET**: al trigger diventa un ordine a mercato. In condizioni normali
riempie a ~pochi bps dal livello; **in un gap riempie molto peggio**. -> il lato SL e'
recuperabile solo IN PARTE, e il gap resta (com'e' giusto: e' un rischio vero, non un
artefatto di modello).
Da cui l'ipotesi da falsificare: **il grosso del -0.35 e' sul lato TP, quindi e' recuperabile
senza prendersi nessun rischio nuovo.** Se invece il grosso e' sul lato SL, la conclusione del
24/07 ("il degrado e' strutturale") si estende anche agli ordini on-book e non c'e' niente da fare.
MODALITA' SIMULATE (stesse trade boundaries, cambia SOLO il prezzo d'esecuzione)
canonical : entry a close[i], exit AL LIVELLO -> il BACKTEST (riferimento ottimista)
hourly : entry/exit al close 5m del prossimo multiplo orario -> il PATH LIVE DI OGGI
onbook : entry al prossimo multiplo orario (l'ingresso resta una decisione del cron:
e' un segnale a chiusura di barra, non si puo' appoggiare in anticipo), ma:
- TP -> fill AL LIVELLO, fee MAKER (limit resting)
- SL -> stop-market: fill al livello se la barra 5m del trigger lo ATTRAVERSA;
fill all'OPEN della barra 5m se la barra ci GAPPA dentro (open gia'
oltre il livello), in entrambi i casi meno SLIPPAGE dichiarato
- time-> resta al prossimo multiplo orario (e' il cron che decide)
onbook_tp : contro-fattuale che ISOLA il lato TP (TP on-book, SL e time come oggi) -> dice
quanta parte del recupero viene dal lato che NON aggiunge rischio.
ONESTA'
* la rilevazione del gap e' a risoluzione **5m** (il gap vero e' piu' fine) -> il fill SL
modellato e' un OTTIMISMO residuo su quel lato; il lato TP invece non ha questo problema
(un limit resting non ha bisogno di risoluzione: o il prezzo tocca il livello o no).
* si ignora la POSIZIONE IN CODA sul limit TP: a $300 di nozionale su BTC perp e' una
semplificazione benigna (size irrilevante rispetto al book), ma e' dichiarata.
* risultati riportati sull'off 0 E sulla **banda dei 23 offset** a priori: il progetto ha gia'
stabilito che l'off 0 e' al 93-98 pctl (best-of-23) -> la mediana di banda e' la lente onesta.
* niente file di produzione toccato. Se il risultato regge, la modifica al live e' una
RACCOMANDAZIONE, non un fatto compiuto.
Uso: `uv run python scripts/research/r0726_skh_onbook.py` (~5-10 min: 23 offset x 2 asset)
"""
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"))
sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt"))
import r0702_anchor_skh01 as r02 # noqa: E402
from src.portfolio.portfolio import metrics # noqa: E402
MSH = 3_600_000
MS_LTF = r02.MS_LTF
FEE_TAKER_SIDE = 0.0005 # Deribit taker ~0.05%/lato (il RT 0.10% del progetto)
FEE_MAKER_SIDE = 0.0000 # limit resting: maker (Deribit futures ~0, a volte rebate)
SL_SLIP = 0.0005 # slippage dichiarata sullo stop-market (5 bps)
HOLDOUT = "2025-01-01"
# --------------------------------------------------------------------------- simulazione
def sim_equity_exec(ltf: pd.DataFrame, ent: list, mode: str,
ts5_close: np.ndarray, o5: np.ndarray, h5: np.ndarray,
l5: np.ndarray, c5: np.ndarray) -> np.ndarray:
"""Come r0702.sim_equity ma con le modalita' d'esecuzione ON-BOOK.
Le trade boundaries (barra d'ingresso, barra di trigger, non-overlap) sono IDENTICHE al
canonico in ogni modalita': cambia solo il prezzo a cui si esegue e la fee del lato.
"""
c = ltf["close"].values.astype(float)
h = ltf["high"].values.astype(float)
lo = 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
j = np.searchsorted(ts5_close, hb, side="left")
return c5[min(j, len(c5) - 1)]
def stop_fill(t_start: int, t_end: int, level: float, direction: int) -> float:
"""Prezzo di fill di uno STOP-MARKET on-book fra due timestamp, a risoluzione 5m.
Trova la prima barra 5m che tocca il livello: se ci GAPPA dentro (open gia' oltre)
il fill e' l'open (il gap e' un rischio vero, non un artefatto); altrimenti e' il
livello. In entrambi i casi meno la slippage dichiarata.
"""
a = int(np.searchsorted(ts5_close, t_start, side="left"))
b = int(np.searchsorted(ts5_close, t_end, side="right"))
for j in range(a, min(b + 1, len(c5))):
touched = (l5[j] <= level) if direction == 1 else (h5[j] >= level)
if touched:
gapped = (o5[j] < level) if direction == 1 else (o5[j] > level)
px = o5[j] if gapped else level
return px * (1.0 - SL_SLIP * direction)
return level * (1.0 - SL_SLIP * direction) # non trovato a 5m: cap al livello
initial = 1000.0
capital = initial
equity = np.full(n, capital, dtype=float)
busy_until = -1
stats = dict(sl=0, tp=0, time=0, gap_sl=0)
for i in range(n):
e = ent[i] if i < len(ent) else None
if e is None or e.get("dir", 0) == 0 or 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 = c[i] if mode == "canonical" else px_hour(close_ts[i])
fee_entry = FEE_TAKER_SIDE
# --- detection identica al canonico (SL prioritario)
exit_idx = min(i + max_bars, n - 1)
exit_lvl = c[exit_idx]
kind = "time"
for j in range(i + 1, min(i + max_bars + 1, n)):
hit_sl = sl is not None and (
(direction == 1 and lo[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 lo[j] <= tp))
if hit_sl:
exit_lvl, exit_idx, kind = sl, j, "sl"
break
if hit_tp:
exit_lvl, exit_idx, kind = tp, j, "tp"
break
exit_lvl, exit_idx = c[j], j
stats[kind] += 1
# --- prezzo d'uscita per modalita'
if mode == "canonical":
exit_price, fee_exit = exit_lvl, FEE_TAKER_SIDE
elif mode == "hourly":
exit_price, fee_exit = px_hour(close_ts[exit_idx]), FEE_TAKER_SIDE
elif mode in ("onbook", "onbook_tp"):
if kind == "tp":
exit_price, fee_exit = tp, FEE_MAKER_SIDE # limit resting: al livello
elif kind == "sl" and mode == "onbook":
t0 = int(ltf["timestamp"].values[exit_idx])
exit_price = stop_fill(t0, t0 + MS_LTF, sl, direction)
fee_exit = FEE_TAKER_SIDE
if (direction == 1 and exit_price < sl * (1 - SL_SLIP) - 1e-9) or \
(direction == -1 and exit_price > sl * (1 + SL_SLIP) + 1e-9):
stats["gap_sl"] += 1
else: # sl in onbook_tp, o time
exit_price, fee_exit = px_hour(close_ts[exit_idx]), FEE_TAKER_SIDE
else:
raise ValueError(mode)
gross = (exit_price - entry) / entry * direction
net = gross - (fee_entry + fee_exit)
capital = max(capital + capital * net, 1.0)
equity[i:exit_idx + 1] = capital
busy_until = exit_idx
return equity, stats
def skh_series(off: int, mode: str) -> tuple[pd.Series, dict]:
"""Rendimenti daily dello sleeve SKH01 (50/50 BTC+ETH) a un offset e una modalita'."""
out = {}
agg = dict(sl=0, tp=0, time=0, gap_sl=0)
for asset in ("BTC", "ETH"):
_, _, ltf, ent = r02.run_asset(asset, off)
df5 = r02.get5m(asset)
ts5 = df5["timestamp"].values.astype(np.int64) + 300_000
eq, st = sim_equity_exec(ltf, ent, mode, ts5,
df5["open"].values.astype(float),
df5["high"].values.astype(float),
df5["low"].values.astype(float),
df5["close"].values.astype(float))
for k in agg:
agg[k] += st[k]
idx = pd.to_datetime(ltf["timestamp"].values + MS_LTF, unit="ms", utc=True)
s = pd.Series(eq, index=idx).resample("1D").last().ffill()
out[asset] = s.pct_change().fillna(0.0)
J = pd.concat(out, axis=1, join="inner").fillna(0.0)
return pd.Series(0.5 * J["BTC"].values + 0.5 * J["ETH"].values, index=J.index), agg
def book(skh: pd.Series, w: float) -> pd.Series:
tp = r02.al.tp01_baseline_daily()
tp = tp.dropna()
if tp.index.tz is None:
tp.index = tp.index.tz_localize("UTC")
tp.index = tp.index.floor("D")
s = skh.copy()
s.index = s.index.floor("D")
J = pd.concat({"tp": tp, "skh": s}, axis=1, join="inner").fillna(0.0)
return pd.Series((1 - w) * J["tp"].values + w * J["skh"].values, index=J.index)
def sh3(s: pd.Series) -> tuple[float, float, float, float]:
m_all = metrics(s)
m_is = metrics(s[s.index < HOLDOUT])
m_ho = metrics(s[s.index >= HOLDOUT])
return m_all["sharpe"], m_is["sharpe"], m_ho["sharpe"], m_all["maxdd"]
MODES = ("canonical", "hourly", "onbook_tp", "onbook")
LABEL = {"canonical": "canonical (backtest, fill-al-livello)",
"hourly": "hourly (PATH LIVE DI OGGI)",
"onbook_tp": "onbook_tp (solo TP a limite)",
"onbook": "onbook (TP limite + SL stop-market)"}
def main() -> None:
global SL_SLIP
print("=" * 100)
print(" T1 — SKH01: il degrado del path live e' recuperabile con ordini RESTING ON-BOOK?")
print("=" * 100)
print(f"\n fee: taker {FEE_TAKER_SIDE:.4%}/lato, maker {FEE_MAKER_SIDE:.4%}/lato | "
f"slippage stop-market {SL_SLIP:.2%} | hold-out {HOLDOUT}")
# ------------------------------------------------ offset 0 (la griglia che gira davvero)
print("\n" + "-" * 100)
print(" A. OFFSET 0 — la griglia che gira davvero nel live")
print("-" * 100)
ser0 = {}
print(f" {'modalita':>40} {'SKH standalone':>28} {'book 75/25':>26}")
print(f" {'':>40} {'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6} "
f"{'FULL':>6} {'IS':>6} {'HOLD':>6} {'DD':>6}")
stats0 = {}
for m in MODES:
s, st = skh_series(0, m)
ser0[m] = s
stats0[m] = st
a = sh3(s)
b = sh3(book(s, 0.25))
print(f" {LABEL[m]:>40} {a[0]:>6.2f} {a[1]:>6.2f} {a[2]:>6.2f} {a[3]:>5.1%} "
f"{b[0]:>6.2f} {b[1]:>6.2f} {b[2]:>6.2f} {b[3]:>5.1%}")
st = stats0["onbook"]
tot = st["sl"] + st["tp"] + st["time"]
print(f"\n composizione delle uscite (off 0, BTC+ETH): {tot} trade — "
f"SL {st['sl']} ({st['sl']/tot:.0%}) | TP {st['tp']} ({st['tp']/tot:.0%}) | "
f"time {st['time']} ({st['time']/tot:.0%})")
print(f" di cui SL con GAP dentro la barra 5m (fill peggiore del livello): "
f"{st['gap_sl']} = {st['gap_sl']/max(st['sl'],1):.0%} degli SL")
# ------------------------------------------------ scomposizione del recupero
print("\n" + "-" * 100)
print(" B. DOVE VIVE IL DEGRADO — scomposizione del gap canonical -> live")
print("-" * 100)
b_can, b_hr = sh3(book(ser0["canonical"], 0.25)), sh3(book(ser0["hourly"], 0.25))
b_tp, b_ob = sh3(book(ser0["onbook_tp"], 0.25)), sh3(book(ser0["onbook"], 0.25))
gap_tot = b_can[0] - b_hr[0]
print(f" book 75/25, Sharpe FULL: canonical {b_can[0]:.3f} -> live {b_hr[0]:.3f} "
f"(degrado {gap_tot:+.3f})")
if abs(gap_tot) > 1e-9:
print(f" recupero col solo TP a limite : {b_tp[0]-b_hr[0]:+.3f} "
f"= {(b_tp[0]-b_hr[0])/gap_tot:>5.0%} del degrado [NESSUN rischio nuovo]")
print(f" recupero TP + SL on-book : {b_ob[0]-b_hr[0]:+.3f} "
f"= {(b_ob[0]-b_hr[0])/gap_tot:>5.0%} del degrado")
print(f" residuo NON recuperabile : {b_can[0]-b_ob[0]:+.3f} "
f"= {(b_can[0]-b_ob[0])/gap_tot:>5.0%} del degrado [gap + entry del cron]")
# ------------------------------------------------ banda dei 23 offset (la lente onesta)
print("\n" + "-" * 100)
print(" C. BANDA DEI 23 OFFSET — la lente de-luckata (off 0 e' al 93-98 pctl, audit 02/07)")
print("-" * 100)
rows = {m: [] for m in MODES}
for off in r02.OFFSETS:
for m in MODES:
s, _ = skh_series(off, m)
rows[m].append(sh3(book(s, 0.25)))
print(f" {'modalita':>40} {'FULL med':>9} {'IS med':>8} {'HOLD med':>9} "
f"{'DD med':>7} {'HOLD min':>9} {'HOLD max':>9}")
med = {}
A = {m: np.array(rows[m]) for m in MODES}
for m in MODES:
med[m] = np.median(A[m], axis=0)
print(f" {LABEL[m]:>40} {med[m][0]:>9.2f} {med[m][1]:>8.2f} {med[m][2]:>9.2f} "
f"{med[m][3]:>6.1%} {A[m][:, 2].min():>9.2f} {A[m][:, 2].max():>9.2f}")
# ⚠️ gli offset sono coppie APPAIATE: la statistica giusta e' la mediana delle DIFFERENZE
# per-offset, non la differenza delle mediane (che confronta offset diversi fra loro).
print("\n differenze APPAIATE per-offset (mediana [min, max] sui 23 offset):")
print(f" {'':>40} {'dFULL':>22} {'dHOLD':>22} {'dDD':>20}")
for m in ("canonical", "onbook_tp", "onbook"):
d0 = A[m][:, 0] - A["hourly"][:, 0]
d2 = A[m][:, 2] - A["hourly"][:, 2]
d3 = A[m][:, 3] - A["hourly"][:, 3]
print(f" {LABEL[m]:>40} "
f"{np.median(d0):>+7.3f} [{d0.min():+.2f},{d0.max():+.2f}] "
f"{np.median(d2):>+7.3f} [{d2.min():+.2f},{d2.max():+.2f}] "
f"{np.median(d3):>+6.2%} [{d3.min():+.1%},{d3.max():+.1%}]")
dmax = np.median(A["canonical"][:, 0] - A["hourly"][:, 0])
dob = np.median(A["onbook"][:, 0] - A["hourly"][:, 0])
dtp = np.median(A["onbook_tp"][:, 0] - A["hourly"][:, 0])
print(f"\n degrado recuperabile al massimo (canonical-hourly, mediana appaiata): {dmax:+.3f} FULL")
if abs(dmax) > 1e-9:
print(f" recuperato dal solo TP a limite: {dtp:+.3f} = {dtp/dmax:>5.0%}")
print(f" recuperato da TP + SL on-book : {dob:+.3f} = {dob/dmax:>5.0%}")
print("\n in quanti dei 23 offset la modalita' MIGLIORA il path live di oggi?")
for m in ("onbook_tp", "onbook"):
nf = int((A[m][:, 0] - A["hourly"][:, 0] > 0).sum())
nh = int((A[m][:, 2] - A["hourly"][:, 2] > 0).sum())
nd = int((A[m][:, 3] - A["hourly"][:, 3] < 0).sum())
print(f" {LABEL[m]:>40}: FULL {nf}/23 HOLD {nh}/23 DD {nd}/23")
# il lato SL da solo = differenza fra onbook (TP+SL) e onbook_tp (solo TP)
dsl = A["onbook"][:, 0] - A["onbook_tp"][:, 0]
print(f"\n contributo del SOLO lato SL on-book (onbook - onbook_tp), FULL: "
f"mediana {np.median(dsl):+.3f} [{dsl.min():+.2f},{dsl.max():+.2f}], "
f"positivo in {int((dsl > 0).sum())}/23 offset")
# ------------------------------------------------ robustezza alla slippage assunta
print("\n" + "-" * 100)
print(" D. SENSIBILITA' ALLA SLIPPAGE ASSUNTA sullo stop-market (off 0, book 75/25)")
print("-" * 100)
keep = SL_SLIP
print(f" {'slippage':>10} {'FULL':>7} {'HOLD':>7} {'DD':>7} (live di oggi: "
f"FULL {b_hr[0]:.2f} / HOLD {b_hr[2]:.2f})")
for slip in (0.0000, 0.0005, 0.0020, 0.0050, 0.0100):
SL_SLIP = slip
s, _ = skh_series(0, "onbook")
r = sh3(book(s, 0.25))
flag = "" if r[0] > b_hr[0] else " <- peggio del live"
print(f" {slip:>9.2%} {r[0]:>7.2f} {r[2]:>7.2f} {r[3]:>6.1%}{flag}")
SL_SLIP = keep
print("\n" + "=" * 100)
if __name__ == "__main__":
main()
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env python
"""r0726_xsr_equity — T3 dell'ondata 2026-07-26: XSR01 regge fuori dal crypto e fuori dai
2.6 anni monoregime? Test di FALSIFICAZIONE su 26+ anni di ETF settoriali.
PERCHE'. XSR01 (candidato in forward-monitor dal 25/07, gate pre-registrato al 2026-10-23) ha
superato tutto quello che gli e' stato chiesto — marginale ADDS, deflated-Sharpe 0.985, corr ~0
a tutti e 5 gli sleeve, tre null a fee-neutrale — MA la sua debolezza numero uno e' dichiarata:
"storia 2.6 anni MONOREGIME e CRESCENTE (Sharpe 2024 1.03 / 2025 1.98 / 2026 3.11):
l'edge e' recente."
Con 2.6 anni non si distingue un meccanismo reale da un regime fortunato. L'unico modo di
guadagnare potenza statistica senza aspettare anni e' portare il meccanismo CONGELATO su un
pannello **diverso per asset class e lungo decenni**, e vedere se sopravvive. Se regge su 26 anni
di settoriali US, l'ipotesi "e' un artefatto del bear alt 2024-2026" e' molto piu' difficile da
sostenere. Se non regge, il gate del 23/10 deve saperlo.
NON E' IL TEST DEL 25/07. `r0725_statarb_eq.py` ha gia' portato il meccanismo sulle azioni ed e'
stato SCARTATO (paniere Sharpe -1.00 netta, -0.17 lorda) — ma quello era il meccanismo a **COPPIE**
(12 coppie a priori, ampiezza effettiva 9.9). XSR01 non e' un paniere di coppie: e' la versione
**DEMEANATA cross-sezionalmente**, ed e' proprio il demeaning che ha cambiato tutto (ampiezza
effettiva 4.5 -> 37.4) annullando algebricamente la gamba comune. Testare il demean su equity e'
quindi una domanda DIVERSA, mai posta. Il risultato lordo del 25/07 (-0.17, cioe' il residuo
REVERTE debolmente sulle azioni mentre CONTINUA sul crypto) e' l'ipotesi nulla di questo test.
DISEGNO
* meccanismo CONGELATO importato da r0725_statarb_multi (W=45, sgn=+1, residuo OLS causale su
un fattore comune, z-score, tanh, vol-target 20%, cap 2x) — nessuna reimplementazione;
* demeaning cross-sezionale giornaliero, identico a r0725_statarb_basket_gate;
* fattore comune = **SPY** (analogo di BTC: il fattore che il demeaning deve annullare);
* due universi, entrambi a priori:
SECT9 = i 9 settoriali SPDR classici (1998+, ~26 anni) — cross-section OMOGENEA, il
parallelo piu' stretto degli alt (un solo fattore comune forte);
ALL28 = tutti gli ETF su disco tranne SPY — parallelo del "ALL50" di XSR01;
* split NON aggiustati riparati in lettura (eqlib.load_eq -> src/data/eq_splits) — senza questo
IWM/EFA avvelenerebbero il pannello (difetto trovato il 25/07);
* LORDO **e** netto: la lettura del 25/07 e' che sulle azioni il segno vive nel lordo e il
turnover se lo mangia. Un venue a PAVIMENTO FISSO (IB) e' anche peggio di una fee
proporzionale a questa taglia (lezione 25/07) -> il netto proporzionale e' gia' generoso;
* null di permutazione cross-sezionale **A FEE ZERO** (lezione XSR01: permutare un segnale ne
fa esplodere il turnover, quindi un null a fee piena perderebbe per COSTO invece che per
assenza d'informazione, e regalerebbe un p-value trionfale e falso);
* decomposizione per DECENNIO: 26 anni servono a questo, non a fare una media unica.
Uso: `uv run python scripts/research/r0726_xsr_equity.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"))
from eqlib import SECTORS_CLASSIC, load_eq # noqa: E402
from r0725_statarb_multi import (FEE_LEG, SGN, W, _dd, _sh, signal) # noqa: E402
RAW = ROOT / "data" / "raw"
BASE = "SPY"
MIN_BARS = 400
N_PERM = 300
BLOCK = 20
SEED = 20260726
ANN = np.sqrt(252.0) # ⚠️ giorni di BORSA, non 365 (lezione 25/07 sull'annualizzazione)
def all_symbols() -> list[str]:
out = []
for p in sorted(RAW.glob("eq_*_1d.parquet")):
s = p.stem.replace("eq_", "").replace("_1d", "").upper()
if s != BASE:
out.append(s)
return out
def _px(sym: str) -> pd.Series:
s = load_eq(sym)["close"].astype(float)
return s[~s.index.duplicated(keep="last")].sort_index()
def build(cols: list[str]) -> tuple[pd.DataFrame, pd.DataFrame]:
"""Posizioni e spread per ogni gamba, col meccanismo congelato vs SPY."""
base = _px(BASE)
P, S = {}, {}
for sym in cols:
try:
t = _px(sym)
except FileNotFoundError:
continue
ix = base.index.intersection(t.index)
if len(ix) < MIN_BARS:
continue
p, s = signal(base[ix], t[ix])
P[sym] = pd.Series(p, index=ix)
S[sym] = pd.Series(s, index=ix)
return (pd.concat(P, axis=1, sort=True).sort_index(),
pd.concat(S, axis=1, sort=True).sort_index())
def pnl_fee(pos: np.ndarray, spread: np.ndarray, fee_leg: float) -> np.ndarray:
held = np.concatenate([[0.0], pos[:-1]])
turn = np.abs(np.diff(held, prepend=0.0))
return held * spread - 2.0 * fee_leg * turn
def basket(P: pd.DataFrame, S: pd.DataFrame, demean: bool, fee_leg: float) -> pd.Series:
Q = P.sub(P.mean(axis=1), axis=0) if demean else P
out = {c: pd.Series(pnl_fee(Q[c].to_numpy(float), S[c].to_numpy(float), fee_leg), index=Q.index)
for c in Q.columns}
return pd.concat(out, axis=1, sort=True).mean(axis=1, skipna=True).dropna()
def eff_breadth(P: pd.DataFrame, S: pd.DataFrame, demean: bool) -> float:
Q = P.sub(P.mean(axis=1), axis=0) if demean else P
M = pd.concat({c: pd.Series(pnl_fee(Q[c].to_numpy(float), S[c].to_numpy(float), 0.0),
index=Q.index) for c in Q.columns}, axis=1)
C = M.corr().values
n = C.shape[0]
off = (C.sum() - np.trace(C)) / (n * (n - 1))
return float(n / (1.0 + (n - 1) * off))
def sh(r) -> float:
r = np.asarray(r, float)
r = r[np.isfinite(r)]
return float(r.mean() / r.std() * ANN) if len(r) > 30 and r.std() > 0 else 0.0
def main() -> None:
print("=" * 100)
print(" T3 — XSR01 fuori dal crypto: il meccanismo congelato su 26 anni di ETF settoriali")
print("=" * 100)
print(f"\n meccanismo: W={W}, sgn={SGN:+d}, residuo OLS causale vs {BASE}, z-score, tanh,")
print(" vol-target 20%, cap 2x, demean cross-sezionale giornaliero [CONGELATO, importato]")
print(f" annualizzazione: sqrt(252) = giorni di BORSA (NON 365)")
universes = {
"SECT9 9 settoriali SPDR (1998+)": SECTORS_CLASSIC,
"ALL28 tutti gli ETF su disco": all_symbols(),
}
store = {}
for name, cols in universes.items():
P, S = build(cols)
store[name] = (P, S)
print("\n" + "-" * 100)
print(f" {name}{P.shape[1]} gambe, {len(P)} barre, "
f"{P.index[0].date()} -> {P.index[-1].date()}")
print("-" * 100)
print(f" {'variante':>28} {'ampiezza eff.':>14} {'Sharpe LORDA':>13} "
f"{'Sharpe NETTA':>13} {'maxDD':>8} {'ret/anno':>9}")
for lab, dm in (("coppie (no demean)", False), ("DEMEAN (= XSR01)", True)):
g = basket(P, S, dm, 0.0)
n = basket(P, S, dm, FEE_LEG)
eb = eff_breadth(P, S, dm)
cagr = float(np.nanmean(n.values)) * 252.0
print(f" {lab:>28} {eb:>14.1f} {sh(g):>13.2f} {sh(n):>13.2f} "
f"{_dd(n.values):>7.1%} {cagr:>8.1%}")
# ------------------------------------------------------------------ per decennio
print("\n" + "-" * 100)
print(" A. PER DECENNIO — 26 anni servono a questo (variante DEMEAN, la stessa di XSR01)")
print("-" * 100)
print(f" {'universo':>34} {'periodo':>14} {'anni':>5} {'Sh LORDA':>9} {'Sh NETTA':>9} {'giorni':>7}")
for name, (P, S) in store.items():
g = basket(P, S, True, 0.0)
n = basket(P, S, True, FEE_LEG)
for lo, hi in (("1998", "2005"), ("2005", "2012"), ("2012", "2019"), ("2019", "2027")):
kg = (g.index >= lo) & (g.index < hi)
kn = (n.index >= lo) & (n.index < hi)
if kg.sum() < 250:
continue
print(f" {name.split()[0]:>34} {lo + '-' + hi:>14} {kg.sum() / 252:>5.1f} "
f"{sh(g[kg]):>9.2f} {sh(n[kn]):>9.2f} {int(kg.sum()):>7d}")
# ------------------------------------------------------------------ null a fee ZERO
print("\n" + "-" * 100)
print(" B. NULL DI PERMUTAZIONE CROSS-SEZIONALE — a FEE ZERO (lezione XSR01)")
print("-" * 100)
print(" Permutare un segnale ne fa esplodere il turnover: a fee piena il null perderebbe")
print(" per COSTO invece che per assenza d'informazione -> p-value trionfale e falso.")
rng = np.random.default_rng(SEED)
print(f"\n {'universo':>34} {'candidato LORDO':>16} {'null medio':>11} {'null p95':>9} "
f"{'null max':>9} {'p-value':>8}")
for name, (P, S) in store.items():
cand = sh(basket(P, S, True, 0.0))
Pv = P.to_numpy(float)
draws = []
for _ in range(N_PERM):
idx = rng.permutation(Pv.shape[1])
Pp = pd.DataFrame(Pv[:, idx], index=P.index, columns=P.columns)
draws.append(sh(basket(Pp, S, True, 0.0)))
d = np.array(draws)
pval = float((d >= cand).mean())
print(f" {name.split()[0]:>34} {cand:>16.2f} {d.mean():>11.2f} "
f"{np.percentile(d, 95):>9.2f} {d.max():>9.2f} {pval:>8.3f}")
# ------------------------------------------------------------------ confronto col crypto
print("\n" + "-" * 100)
print(" C. CONFRONTO DIRETTO col candidato crypto (numeri del 25/07, non ricalcolati)")
print("-" * 100)
print(" XSR01 (50 alt HL, 2.6 anni): ampiezza eff. 37.4 | LORDA 2.70 | NETTA 1.82 | DD -2.6%")
print(" STATARB-EQ a COPPIE (25/07): ampiezza eff. 9.9 | LORDA -0.17 | NETTA -1.00")
print("\n La domanda del test: il DEMEAN — che sul crypto ha fatto 4.5 -> 37.4 di ampiezza e")
print(" ha creato l'edge — fa la stessa cosa sulle azioni, o li' il residuo resta reverting?")
print("\n" + "=" * 100)
if __name__ == "__main__":
main()