Files
Adriano Dal Pastro 14522262e6 chore(reset): v2.0.0 — storico certificato Deribit mainnet, ripartenza pulita
Reset del progetto su fondamenta verificate dopo la scoperta che l'intera
libreria "validata OOS" era artefatto di feed contaminato (print fantasma del
feed Cerbero TESTNET + storico Binance/USDT).

- Storico ricostruito da Deribit MAINNET (ccxt pubblico, tokenless) e
  CERTIFICATO (certify_feed.py): BTC/ETH puliti su TUTTA la storia
  (mediana 2-6 bps vs Coinbase USD), integrita' OHLC + coerenza resample
  (maxΔ 0.00) + cross-venue OK. Alt esclusi (illiquidi/divergenti: LTC/DOGE
  50-82% barre flat; XRP/BNB non certificabili).
- Verdetto sul feed pulito: FADE / PAIRS / XS01 / TSM01 morti (ogni
  portafoglio Sharpe -2.3..-3.0, DD ~40%); solo SH01 e frammenti HONEST
  con segnale residuo, da ri-validare in isolamento.
- Cleanup "restart pulito": strategie, stack live (src/live, src/portfolio,
  runner/executor, yml, docker), ~100 script ricerca/gate, waste/games/
  portfolios, dati non certificati + cache e 60+ diari -> archiviati in Old/
  (preservati, non cancellati). Diario consolidato in un unico documento.
- Skeleton ricerca tenuto: Strategy ABC + indicatori + src/fractal +
  src/backtest/engine + load_data; tool dati certificati (rebuild_history,
  certify_feed, audit_feed, multi_source_check).
- Universo dati ATTIVO: solo BTC/ETH (5m/15m/1h); guardrail fisico
  (load_data su alt -> FileNotFoundError). Esecuzione DISABILITATA, conto flat.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-19 15:20:59 +00:00

190 lines
7.7 KiB
Python

"""XS01 dispersion-gate — la reversione cross-sectional va accesa solo in certi regimi?
Motivazione: l'edge XS01 e' concentrato (2025 domina, 2023 debole). Ipotesi da
testare: il fattore reversione cross-sezionale paga quando c'e' DISPERSIONE da
far rientrare (spread cross-section largo) e/o correlazione media alta (mosse
idiosincratiche = rumore che rientra), e perde nei regime-break (dispersione da
trend divergente, es. melt-up di un singolo asset).
Metodo (anti-multiple-testing):
[1] DIAGNOSTICA: engine XS01 canonico SENZA gate, registrando per ogni trade
il valore di 3 feature di regime alla barra di ENTRY (tutte causali,
calcolate dallo stesso panel closes <= i):
g_disp = std cross-section del segnale stesso (logC[i]-logC[i-lb])
g_corr = correlazione media pairwise 72h (identita' var dell'indice)
g_vol = vol realizzata BTC 168h
Bucket per quintili (quintili dal TRAIN) -> mean net per bucket,
TRAIN e OOS SEPARATI. Si prosegue solo se la relazione e' monotona
e con lo stesso segno in entrambe le finestre.
[2] GATE: per la feature promossa, sweep soglie (percentili TRAIN
30/40/50/60/70) -> TRAIN/OOS Sharpe/PnL/DD vs base. Serve PLATEAU.
[3] Solo se [2] regge: gate PORT06 (swap equity sleeve XS01).
uv run python scripts/analysis/xs01_dispersion_gate.py
"""
from __future__ import annotations
import sys
from pathlib import Path
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
from scripts.strategies.XS01_cross_sectional import (
aligned_panel, UNIVERSE, FEE_RT, LEV, POS, OOS_FRAC, LB, HOLD)
N_A = len(UNIVERSE)
def build_features(M, lb=LB):
"""Feature di regime causali dal panel closes (nessun feed esterno)."""
logC = np.log(M.values)
r = np.diff(logC, axis=0, prepend=logC[:1]) # ret orari (r[0]=0)
R = pd.DataFrame(r, index=M.index)
# g_disp: std cross-section del momentum lb (il segnale che fadiamo)
D = pd.DataFrame(logC).diff(lb).to_numpy()
g_disp = np.nanstd(D, axis=1)
# g_corr 72h: avg pairwise corr via identita' della varianza dell'indice
w = 72
idx_var = R.mean(axis=1).rolling(w).var().to_numpy()
mean_var = R.rolling(w).var().mean(axis=1).to_numpy()
with np.errstate(divide="ignore", invalid="ignore"):
g_corr = (N_A * idx_var / mean_var - 1) / (N_A - 1)
# g_vol: vol BTC 168h annualizzata
b = UNIVERSE.index("BTC")
g_vol = R[b].rolling(168).std().to_numpy() * np.sqrt(24 * 365)
return dict(g_disp=g_disp, g_corr=g_corr, g_vol=g_vol)
def sim_with_trace(M, feats, gate=None, lb=LB, hold=HOLD, fee_rt=FEE_RT,
lev=LEV, pos=POS):
"""Engine XS01 canonico (stessa logica/ordine di XS01_cross_sectional.xsec_sim)
+ trace per-trade (entry index, net, feature) + gate opzionale bool[i]."""
C = M.values
ts = pd.to_datetime(M.index, unit="ms", utc=True)
n = len(C)
logC = np.log(C)
cap = peak = 1000.0
dd = 0.0
rows = []
eq_ts, eq_v = [], []
last = -1
i = lb
fee = 2 * fee_rt
while i < n - hold:
if i <= last:
i += 1
continue
if gate is not None and not gate[i]:
i += 1
continue
dm = (logC[i] - logC[i - lb])
dm = dm - dm.mean()
w = -dm
gw = np.sum(np.abs(w))
if gw < 1e-9:
i += 1
continue
w = w / gw
book = float(np.sum(w * (logC[i + hold] - logC[i])))
net = book - fee
cap = max(cap + cap * pos * lev * net, 10.0)
peak = max(peak, cap)
dd = max(dd, (peak - cap) / peak)
rows.append((i, int(ts[i].year), net,
feats["g_disp"][i], feats["g_corr"][i], feats["g_vol"][i]))
eq_ts.append(ts[i + hold])
eq_v.append(cap)
last = i + hold
i += 1
tr = pd.DataFrame(rows, columns=["i", "year", "net", "g_disp", "g_corr", "g_vol"])
yrs_span = (ts[-1] - ts[0]).days / 365.25 or 1
out = dict(trades=len(tr), cap=cap, dd=dd * 100, eq_ts=eq_ts, eq_v=eq_v, tr=tr)
if len(tr) > 1 and tr["net"].std() > 0:
out["sharpe"] = float(tr["net"].mean() / tr["net"].std()
* np.sqrt(len(tr) / yrs_span))
else:
out["sharpe"] = 0.0
out["pnl_add"] = float(tr["net"].sum() * 100) if len(tr) else 0.0
out["win"] = float((tr["net"] > 0).mean() * 100) if len(tr) else 0.0
out["tpm"] = len(tr) / (yrs_span * 12)
return out
def metrics_window(tr, lo, hi, yrs_span):
t = tr[(tr["i"] >= lo) & (tr["i"] < hi)]
if len(t) < 2 or t["net"].std() == 0:
return dict(n=len(t), pnl=0.0, sh=0.0, win=0.0)
sh = float(t["net"].mean() / t["net"].std() * np.sqrt(len(t) / yrs_span))
return dict(n=len(t), pnl=float(t["net"].sum() * 100), sh=sh,
win=float((t["net"] > 0).mean() * 100))
def main():
M = aligned_panel()
n = len(M)
cut = int(n * (1 - OOS_FRAC))
ts = pd.to_datetime(M.index, unit="ms", utc=True)
feats = build_features(M)
print("=" * 96)
print(f" XS01 dispersion-gate | panel {ts[0].date()} -> {ts[-1].date()} "
f"({n} ore, 8 asset) | TRAIN 70% (-> {ts[cut].date()}) / OOS 30%")
print("=" * 96)
base = sim_with_trace(M, feats)
tr = base["tr"]
yrs_tr = (ts[cut] - ts[0]).days / 365.25
yrs_oo = (ts[-1] - ts[cut]).days / 365.25
# [1] DIAGNOSTICA per quintili (quintili dal TRAIN)
print("\n[1] DIAGNOSTICA — mean net per trade (bps) per quintile feature @entry")
ttr = tr[tr["i"] < cut]
too = tr[tr["i"] >= cut]
for g in ("g_disp", "g_corr", "g_vol"):
qs = ttr[g].quantile([0.2, 0.4, 0.6, 0.8]).to_numpy()
def bucket(x):
return int(np.searchsorted(qs, x))
print(f" {g:<7s} | " + " | ".join(
f"Q{q+1} TR {ttr[ttr[g].apply(bucket) == q]['net'].mean()*1e4:+6.1f} "
f"OOS {too[too[g].apply(bucket) == q]['net'].mean()*1e4:+6.1f}"
for q in range(5)) +
f" (n TR {len(ttr)}, OOS {len(too)})")
# [2] GATE sweep — per ogni feature, tieni SOPRA o SOTTO il percentile
print("\n[2] GATE — TRAIN/OOS vs base (soglie = percentili del TRAIN; "
"side scelto dal segno della diagnostica TRAIN)")
b_tr = metrics_window(tr, 0, cut, yrs_tr)
b_oo = metrics_window(tr, cut, n, yrs_oo)
print(f" {'BASE':<24s} TRAIN n {b_tr['n']:>4} pnl {b_tr['pnl']:>+7.1f}% "
f"Sh {b_tr['sh']:>5.2f} | OOS n {b_oo['n']:>4} pnl {b_oo['pnl']:>+7.1f}% "
f"Sh {b_oo['sh']:>5.2f}")
for g in ("g_disp", "g_corr", "g_vol"):
# segno dal TRAIN: correlazione quintile->ret
qs5 = ttr[g].quantile([0.2, 0.4, 0.6, 0.8]).to_numpy()
means = [ttr[ttr[g].apply(lambda x: int(np.searchsorted(qs5, x))) == q]["net"].mean()
for q in range(5)]
side = "above" if means[-1] > means[0] else "below"
for pct in (30, 40, 50, 60, 70):
thr = float(np.nanpercentile(feats[g][:cut], pct))
gv = feats[g]
gate = (gv >= thr) if side == "above" else (gv <= thr)
gate = np.nan_to_num(gate, nan=False).astype(bool)
r = sim_with_trace(M, feats, gate=gate)
g_tr = metrics_window(r["tr"], 0, cut, yrs_tr)
g_oo = metrics_window(r["tr"], cut, n, yrs_oo)
print(f" {g} {side} p{pct:<3d}{'':<6s} TRAIN n {g_tr['n']:>4} "
f"pnl {g_tr['pnl']:>+7.1f}% Sh {g_tr['sh']:>5.2f} | "
f"OOS n {g_oo['n']:>4} pnl {g_oo['pnl']:>+7.1f}% Sh {g_oo['sh']:>5.2f}")
# breakdown annuale base (riferimento concentrazione)
print("\n base, net additivo per anno (%):",
{int(y): round(float(v * 100), 1)
for y, v in tr.groupby("year")["net"].sum().items()})
if __name__ == "__main__":
main()