Files
PythagorasGoal/Old/scripts/analysis/dispersion_lab.py
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

166 lines
6.7 KiB
Python

"""Harness CONDIVISO per la ricerca dispersion/correlation index (crypto).
Feature CAUSALI (dalle sole close, nessun feed opzioni — la dispersion IMPLICITA
non e' backtestabile, muro ARGO/GEX documentato). Calcolate sull'universo comune
e allineabili a ogni singolo asset. Tutte note a close[i] (nessun look-ahead):
- avg_corr[W] : correlazione media a coppie dei log-rendimenti, rolling W (causale)
- disp[W] : dispersione cross-sectional (std cross-asset del rendimento di barra),
media rolling W
- idx_ret : rendimento dell'"indice" equal-weight (proxy mercato)
- beta_<A>[W] : beta rolling dell'asset A vs indice
- rel_<A> : rendimento di A meno rendimento indice (componente idiosincratica)
Uso dagli agenti di ricerca:
from scripts.analysis.dispersion_lab import features, align_to, UNIVERSE, COMMON_START
from scripts.analysis.explore_lab import get_df, evaluate, robust
F = features() # DataFrame indicizzato per timestamp(ms)
df = get_df("ETH", "1h")
fa = align_to(F, df) # feature riallineate alle barre di df (ffill causale)
# ... costruisci entries causali (entry decisa con dati <= close[i]) ...
res = evaluate("nome", entries, df); robust(res)
Check no-look-ahead: `python -m scripts.analysis.dispersion_lab` (perturba il futuro
e verifica che le feature fino a T non cambino).
"""
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.analysis.explore_lab import get_df # noqa: E402
UNIVERSE = ["ADA", "BNB", "BTC", "DOGE", "ETH", "LTC", "SOL", "XRP"]
COMMON_START = "2022-07-22" # ultimo asset a entrare (LTC) -> universo completo
WINDOWS = [24, 72, 168, 336] # 1g, 3g, 1sett, 2sett in barre 1h
_CACHE: dict | None = None
def _panel():
"""{asset: close-series} allineato sui timestamp comuni dell'universo (1h)."""
frames = {}
for a in UNIVERSE:
d = get_df(a, "1h")
frames[a] = pd.Series(d["close"].values, index=d["timestamp"].values, name=a)
P = pd.concat(frames, axis=1).dropna()
P = P[P.index >= int(pd.Timestamp(COMMON_START, tz="UTC").timestamp() * 1000)]
return P
def _avg_pairwise_corr(R: np.ndarray, win: int) -> np.ndarray:
"""Media delle correlazioni a coppie dei log-rendimenti su finestra rolling.
CAUSALE: la riga i usa R[i-win+1 .. i]. Vettoriale via media/var rolling delle
scommatorie (corr di Pearson per coppia, poi media off-diagonale)."""
n, m = R.shape
out = np.full(n, np.nan)
# somme rolling per asset
df = pd.DataFrame(R)
s = df.rolling(win).sum().values # Σx
ss = (df * df).rolling(win).sum().values # Σx²
for i in range(win - 1, n):
w = R[i - win + 1:i + 1] # (win, m)
mean = s[i] / win
var = ss[i] / win - mean * mean
sd = np.sqrt(np.clip(var, 1e-18, None))
# matrice di covarianza della finestra
cov = (w.T @ w) / win - np.outer(mean, mean)
corr = cov / np.outer(sd, sd)
iu = np.triu_indices(m, k=1)
vals = corr[iu]
vals = vals[np.isfinite(vals)]
if vals.size:
out[i] = float(np.mean(vals))
return out
_CACHE_FILE = PROJECT_ROOT / "data" / "regime" / "dispersion_features.parquet"
def features(use_disk: bool = True) -> pd.DataFrame:
"""DataFrame indicizzato per timestamp(ms) con le feature causali. Cache di
processo + cache su disco (i molti agenti di ricerca la caricano invece di
ricalcolarla; la corr rolling e' costosa)."""
global _CACHE
if _CACHE is not None:
return _CACHE
if use_disk and _CACHE_FILE.exists():
_CACHE = pd.read_parquet(_CACHE_FILE)
return _CACHE
P = _panel()
logp = np.log(P.values)
R = np.vstack([np.zeros((1, P.shape[1])), np.diff(logp, axis=0)]) # log-ret per barra
R[0] = 0.0
idx_ret = R.mean(axis=1) # indice EW
out = pd.DataFrame(index=P.index)
out["idx_ret"] = idx_ret
# dispersione cross-sectional (std cross-asset del rendimento di barra) + medie rolling
xs = R.std(axis=1)
out["disp_bar"] = xs
for w in WINDOWS:
out[f"avg_corr_{w}"] = _avg_pairwise_corr(R, w)
out[f"disp_{w}"] = pd.Series(xs, index=P.index).rolling(w).mean().values
# componente idiosincratica e beta rolling vs indice (per ogni asset)
ir = pd.Series(idx_ret, index=P.index)
for k, a in enumerate(UNIVERSE):
ra = pd.Series(R[:, k], index=P.index)
out[f"rel_{a}"] = (ra - ir).values
for w in (72, 168):
cov = ra.rolling(w).cov(ir)
var = ir.rolling(w).var()
out[f"beta_{a}_{w}"] = (cov / var.replace(0, np.nan)).values
if use_disk:
_CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
out.to_parquet(_CACHE_FILE)
_CACHE = out
return out
def align_to(F: pd.DataFrame, df: pd.DataFrame) -> pd.DataFrame:
"""Riallinea le feature (indicizzate per ts comuni) alle barre di `df` (un asset),
con ffill CAUSALE (riempie in avanti: la feature a i usa l'ultima nota <= ts[i])."""
f = F.reindex(F.index.union(df["timestamp"].values)).sort_index().ffill()
return f.reindex(df["timestamp"].values).reset_index(drop=True)
def _check_no_lookahead() -> bool:
"""Perturba il FUTURO dei prezzi e verifica che le feature fino a T non cambino."""
global _CACHE
_CACHE = None
F0 = features().copy()
P = _panel()
T = int(len(P) * 0.6)
# perturbo le close DOPO T per tutti gli asset
P2 = P.copy()
P2.iloc[T + 1:] = P2.iloc[T + 1:] * 1.5
# ricostruisco le feature da P2 inline (stessa logica)
_CACHE = None
saved = globals()["_panel"]
globals()["_panel"] = lambda: P2
_CACHE = None
F1 = features()
globals()["_panel"] = saved
_CACHE = None
cols = [c for c in F0.columns if c.startswith(("avg_corr", "disp", "beta"))]
a = F0[cols].iloc[:T - max(WINDOWS)].values
b = F1[cols].iloc[:T - max(WINDOWS)].values
ok = np.allclose(np.nan_to_num(a), np.nan_to_num(b), atol=1e-9)
print(f"[no-look-ahead] feature fino a T={T} invarianti al futuro: {'OK' if ok else 'VIOLATO'}")
return ok
if __name__ == "__main__":
F = features()
P = _panel()
print(f"universo {UNIVERSE}")
print(f"finestra comune: {pd.to_datetime(P.index[0], unit='ms', utc=True).date()} "
f"-> {pd.to_datetime(P.index[-1], unit='ms', utc=True).date()} ({len(P)} barre)")
print(f"feature: {list(F.columns)}")
print(F[[f'avg_corr_{w}' for w in WINDOWS]].describe().round(3).to_string())
_check_no_lookahead()