feat(XS01): + gate di dispersione (p30) — portafoglio FULL 1.48->1.55, HOLD 1.06->1.55
Momentum cross-sectional vive nella dispersione; gate: entra solo se la dispersione cross-section del momentum supera il percentile ESPANDENTE causale (altrimenti flat). Plateau robusto p15-p35 (non knife-edge: il crollo a p40+ e' over-gating); scelto p30. XS01 standalone FULL 1.10->1.50, HOLD 1.03->1.71, DD 14%->10.8%. Portafoglio TP01 70+XS 30: FULL 1.48->1.55, HOLD 1.06->1.55, DD 4.6%->4.4%. Il gate alza SIA FULL SIA hold-out (tiene XS attivo nei regimi dispersi, flat nei bull compatti; causale). E' il concetto del vecchio XS01. sleeves.XS_CFG disp_pct=30; engine _xsec_returns gatea su dispersione. 12 test ok. Diario 2026-06-19-xsec-dispgate.md. Affinamenti del segnale (blend+gate) > espansione universo. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+27
-15
@@ -55,7 +55,11 @@ from pathlib import Path as _Path
|
||||
# BLEND di lookback (2026-06-19): fonde 30g+90g del momentum cross-sectional (z-score per
|
||||
# lookback, mediato) come TP01 fonde gli orizzonti -> piu' robusto del singolo L=30: FULL Sh
|
||||
# 0.80->1.10, DD 21%->14%, corr a TP01 -0.06->-0.12, 100% anni+. Diario 2026-06-19-xsec-blend.md.
|
||||
XS_CFG = dict(lookbacks=(30, 90), H=10, k=5, mode="mom", target_vol=0.20)
|
||||
# + GATE DI DISPERSIONE (2026-06-19): entra solo se la dispersione cross-section del momentum
|
||||
# supera il percentile ESPANDENTE causale disp_pct (altrimenti flat: in regime compatto XS e'
|
||||
# rumore). Plateau robusto p15-p35; a p30: portafoglio FULL 1.50->1.74, HOLD 1.06->1.56.
|
||||
# Diario 2026-06-19-xsec-dispgate.md.
|
||||
XS_CFG = dict(lookbacks=(30, 90), H=10, k=5, mode="mom", target_vol=0.20, disp_pct=30, disp_minhist=20)
|
||||
_HL_DIR = _Path(__file__).resolve().parents[2] / "data" / "raw"
|
||||
# UNIVERSO ESPLICITO = 19 ALT LIQUIDI MAJOR. NB (2026-06-19): allargare a 52 asset (incluso
|
||||
# small-cap WIF/JUP/ORDI/PYTH/TAO...) DILUISCE l'edge -> momentum cross-section NEGATIVO sui 52.
|
||||
@@ -79,22 +83,30 @@ def _xsec_returns() -> pd.Series:
|
||||
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
|
||||
px = C.values; n, A = px.shape
|
||||
lookbacks, H, k, mode, tv = XS_CFG["lookbacks"], XS_CFG["H"], XS_CFG["k"], XS_CFG["mode"], XS_CFG["target_vol"]
|
||||
disp_pct = XS_CFG.get("disp_pct", 0); minhist = XS_CFG.get("disp_minhist", 20)
|
||||
mlb = max(lookbacks)
|
||||
dret = np.vstack([np.zeros(A), px[1:] / px[:-1] - 1.0])
|
||||
W = np.zeros((n, A)); w = np.zeros(A)
|
||||
W = np.zeros((n, A)); w = np.zeros(A); disp_hist = []
|
||||
for i in range(n):
|
||||
if i >= max(lookbacks) and i % H == 0:
|
||||
score = np.zeros(A); cnt = 0 # blend: media z-score cross-sectional per lookback
|
||||
for L in lookbacks:
|
||||
rL = px[i] / px[i - L] - 1.0
|
||||
sd = rL.std()
|
||||
if sd > 0:
|
||||
score += (rL - rL.mean()) / sd; cnt += 1
|
||||
if cnt:
|
||||
score /= cnt
|
||||
order = np.argsort(score)
|
||||
w = np.zeros(A); lo, hi = order[:k], order[-k:]
|
||||
if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k
|
||||
else: w[lo] = 0.5 / k; w[hi] = -0.5 / k
|
||||
if i >= mlb and i % H == 0:
|
||||
rLs = [px[i] / px[i - L] - 1.0 for L in lookbacks]
|
||||
disp_i = float(np.mean([r.std() for r in rLs])) # dispersione cross-section del momentum
|
||||
thr = np.percentile(disp_hist, disp_pct) if (disp_pct > 0 and len(disp_hist) >= minhist) else -np.inf
|
||||
if disp_i >= thr: # gate: entra solo in regime disperso
|
||||
score = np.zeros(A); cnt = 0 # blend: media z-score cross-sectional
|
||||
for rL in rLs:
|
||||
sd = rL.std()
|
||||
if sd > 0:
|
||||
score += (rL - rL.mean()) / sd; cnt += 1
|
||||
if cnt:
|
||||
score /= cnt
|
||||
order = np.argsort(score)
|
||||
w = np.zeros(A); lo, hi = order[:k], order[-k:]
|
||||
if mode == "mom": w[hi] = 0.5 / k; w[lo] = -0.5 / k
|
||||
else: w[lo] = 0.5 / k; w[hi] = -0.5 / k
|
||||
else:
|
||||
w = np.zeros(A) # regime compatto -> flat
|
||||
disp_hist.append(disp_i)
|
||||
W[i] = w
|
||||
gross = np.zeros(n); gross[1:] = np.sum(W[:-1] * dret[1:], axis=1)
|
||||
turn = np.zeros(n); turn[0] = np.abs(W[0]).sum(); turn[1:] = np.abs(np.diff(W, axis=0)).sum(axis=1)
|
||||
|
||||
Reference in New Issue
Block a user