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>
This commit is contained in:
@@ -0,0 +1,189 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user