research: gate PORT06 index_comp_disp — promosso marginale, documentato e rimandato
Decorrela bene (corr 0.06 col MASTER, smentisce il timore ridondanza) ma beneficio OOS nullo (Sharpe 8.58->8.56, DD 1.36->1.40); migliora solo FULL DD 3.96->3.73. Non deployato (wiring + simulato per guadagno OOS nel rumore). Gate riusabile committato. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+4
-7
@@ -47,13 +47,10 @@
|
|||||||
|
|
||||||
## Ricerca dispersion/correlation (2026-06-08, 165 agenti) — follow-up opzionale
|
## Ricerca dispersion/correlation (2026-06-08, 165 agenti) — follow-up opzionale
|
||||||
|
|
||||||
- [ ] **Gate PORT06 di `index_comp_disp` W=168 (BTC)** — l'unico candidato della ricerca
|
- [x] ~~Gate PORT06 di `index_comp_disp` W=168~~ — FATTO (2026-06-08): PROMOSSO MARGINALE.
|
||||||
dispersion che merita un test formale (`combine_v2`): misurare corr col MASTER e
|
Decorrela bene (corr 0.06 col MASTER) ma OOS PIATTO (Sharpe −0.01). **Documentato e
|
||||||
ΔSharpe/ΔDD. Config in `scripts/analysis/dispersion_edges/index_comp_disp.py`
|
rimandato** (non deployato): gate in `dispersion_edges/gate_index_comp_disp.py`,
|
||||||
(rel_len=12, z_thr=1.5, disp_q=0.7, TP=1.0ATR, SL=1.5ATR, mb=24). P(migliora)~20-25%:
|
riprendere solo se si costruisce una famiglia DISP più ampia. Diario aggiornato.
|
||||||
è fade-BTC, rischio sovrapposizione con le MR. Se non decorrela → scartare.
|
|
||||||
Diario `docs/diary/2026-06-08-dispersion-correlation-search.md`. **Bassa priorità**
|
|
||||||
(nessun nuovo motore emerso; vale la lezione FR01).
|
|
||||||
|
|
||||||
## Monitoraggio (osservare, non agire subito)
|
## Monitoraggio (osservare, non agire subito)
|
||||||
|
|
||||||
|
|||||||
@@ -56,3 +56,21 @@ corr_vol_interact, leadlag_corr, corr_trend, disp_compression_breakout, corr_dis
|
|||||||
|
|
||||||
**Conclusione onesta: nessun nuovo motore di ritorno.** Il dispersion-trading realizzato
|
**Conclusione onesta: nessun nuovo motore di ritorno.** Il dispersion-trading realizzato
|
||||||
funziona solo come l'ennesima faccia della mean-reversion già sfruttata.
|
funziona solo come l'ennesima faccia della mean-reversion già sfruttata.
|
||||||
|
|
||||||
|
## Gate PORT06 del candidato n.1 (2026-06-08) — PROMOSSO MARGINALE, NON deployato
|
||||||
|
|
||||||
|
`scripts/analysis/dispersion_edges/gate_index_comp_disp.py` (config W=168: rel_len=12,
|
||||||
|
z_win=336, z_thr=1.5, disp_q=0.7, TP=1.0ATR, SL=1.5ATR, mb=24; equity daily innestata
|
||||||
|
come 18° sleeve, pesi cap):
|
||||||
|
|
||||||
|
- **Sorpresa positiva**: decorrela DAVVERO. corr daily col candidato: MR01_BTC +0.01,
|
||||||
|
MR02_BTC +0.05, MR07_BTC +0.06, DIP01_BTC +0.02, MASTER(EW) +0.06. Il timore di
|
||||||
|
ridondanza con le fade BTC era infondato (gate dispersione + TP vicino = profilo trade
|
||||||
|
distinto). Standalone 311 trade, FULL +67% / OOS +30%.
|
||||||
|
- **Ma beneficio nel rumore**: PORT06 BASE→+DISP: FULL Sharpe 6.43→6.47, FULL DD
|
||||||
|
3.96→3.73 (migliora), **OOS Sharpe 8.58→8.56 (−0.01), OOS DD 1.36→1.40 (+0.04)** —
|
||||||
|
l'OOS è PIATTO. Passa il gate tecnico ma il guadagno è solo nel FULL (regime storico).
|
||||||
|
- **Decisione (utente): documenta e rimanda.** NON deployato — wiring (nuova Strategy+worker,
|
||||||
|
famiglia DISP, peso cap) + resterebbe simulato (no executor), per un beneficio OOS nullo.
|
||||||
|
Gate script committato e pronto: riprendere SOLO se si costruisce una famiglia DISP più
|
||||||
|
ampia (più asset/sleeve) che insieme sposti l'OOS. Esito ~20% previsto, confermato.
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
"""GATE PORT06 del candidato index_comp_disp W=168 (ricerca dispersion 2026-06-08).
|
||||||
|
|
||||||
|
Edge confermato avversarialmente: fade della componente idiosincratica di BTC verso
|
||||||
|
l'indice EW, gated da alta dispersione. Config: rel_len=12, z_win=336, z_thr=1.5,
|
||||||
|
disp_168 >= quantile rolling 0.7 (win 720), TP=1.0*ATR14, SL=1.5*ATR14, max_bars=24.
|
||||||
|
|
||||||
|
Domanda del gate (lezione FR01: robusto != migliora-il-portafoglio):
|
||||||
|
1) correlazione daily col MASTER e con le fade BTC esistenti (e' un diversificatore?)
|
||||||
|
2) PORT06 BASE (17 sleeve) vs +DISP (18 sleeve) con pesi cap: DeltaSharpe/DeltaDD FULL e OOS.
|
||||||
|
PROMOSSO solo se decorrela E migliora (o non degrada) l'OOS.
|
||||||
|
|
||||||
|
uv run python scripts/analysis/dispersion_edges/gate_index_comp_disp.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[3]
|
||||||
|
sys.path.insert(0, str(PROJECT_ROOT))
|
||||||
|
|
||||||
|
from scripts.analysis.dispersion_lab import features, align_to
|
||||||
|
from scripts.analysis.explore_lab import get_df, atr
|
||||||
|
from scripts.analysis.combine_portfolio import _norm, IDX, port_returns, metrics, SPLIT, OOS_DATE
|
||||||
|
from scripts.analysis.honest_improve2 import _daily_equity
|
||||||
|
from scripts.portfolios._defs import PORTFOLIOS
|
||||||
|
from src.portfolio import weighting as W
|
||||||
|
|
||||||
|
FEE_RT, LEV, POS, INIT = 0.001, 3.0, 0.15, 1000.0
|
||||||
|
CFG = dict(rel_len=12, z_win=336, z_thr=1.5, disp_q=0.7, disp_q_win=720,
|
||||||
|
tp_atr=1.0, sl_atr=1.5, max_bars=24)
|
||||||
|
|
||||||
|
|
||||||
|
def _last_rank(x):
|
||||||
|
if x.shape[0] < 2:
|
||||||
|
return np.nan
|
||||||
|
return float((x[:-1] < x[-1]).mean())
|
||||||
|
|
||||||
|
|
||||||
|
def build_trades(asset="BTC"):
|
||||||
|
"""Entries CAUSALI + exit intrabar (TP/SL/max_bars) -> [(i, j, ret_netto)]."""
|
||||||
|
df = get_df(asset, "1h")
|
||||||
|
F = features()
|
||||||
|
fa = align_to(F, df)
|
||||||
|
c, h, l = df["close"].values, df["high"].values, df["low"].values
|
||||||
|
n = len(c)
|
||||||
|
a14 = atr(df, 14)
|
||||||
|
rel = fa[f"rel_{asset}"].values.astype(float)
|
||||||
|
disp = fa["disp_168"].values.astype(float)
|
||||||
|
# somma rolling rel su rel_len, z-score causale (mean/std rolling z_win shift 1)
|
||||||
|
rs = pd.Series(rel).rolling(CFG["rel_len"]).sum()
|
||||||
|
rmean = rs.rolling(CFG["z_win"]).mean().shift(1)
|
||||||
|
rstd = rs.rolling(CFG["z_win"]).std().shift(1)
|
||||||
|
z = ((rs - rmean) / rstd.replace(0, np.nan)).values
|
||||||
|
dpct = pd.Series(disp).rolling(CFG["disp_q_win"]).apply(_last_rank, raw=True).values
|
||||||
|
fee = FEE_RT * LEV
|
||||||
|
out = []
|
||||||
|
last = -1
|
||||||
|
for i in range(n - 1):
|
||||||
|
if i <= last or not np.isfinite(z[i]) or not np.isfinite(dpct[i]):
|
||||||
|
continue
|
||||||
|
if dpct[i] < CFG["disp_q"] or abs(z[i]) < CFG["z_thr"]:
|
||||||
|
continue
|
||||||
|
ai = a14[i]
|
||||||
|
if not np.isfinite(ai) or ai <= 0:
|
||||||
|
continue
|
||||||
|
d = -1 if z[i] > 0 else 1
|
||||||
|
tp = c[i] + d * CFG["tp_atr"] * ai
|
||||||
|
sl = c[i] - d * CFG["sl_atr"] * ai
|
||||||
|
mb = CFG["max_bars"]
|
||||||
|
j = min(i + mb, n - 1)
|
||||||
|
exit_p = c[j]
|
||||||
|
for k in range(1, mb + 1):
|
||||||
|
j = i + k
|
||||||
|
if j >= n:
|
||||||
|
j = n - 1; exit_p = c[j]; break
|
||||||
|
if d == 1:
|
||||||
|
if l[j] <= sl: exit_p = sl; break
|
||||||
|
if h[j] >= tp: exit_p = tp; break
|
||||||
|
else:
|
||||||
|
if h[j] >= sl: exit_p = sl; break
|
||||||
|
if l[j] <= tp: exit_p = tp; break
|
||||||
|
if k == mb: exit_p = c[j]
|
||||||
|
out.append((i, j, (exit_p - c[i]) / c[i] * d * LEV - fee))
|
||||||
|
last = j
|
||||||
|
return df, out
|
||||||
|
|
||||||
|
|
||||||
|
def daily_equity(df, trades):
|
||||||
|
ts = pd.to_datetime(df["timestamp"], unit="ms", utc=True)
|
||||||
|
cap = INIT; eq_ts, eq_v = [], []
|
||||||
|
for i, j, ret in sorted(trades, key=lambda t: t[1]):
|
||||||
|
cap = max(cap + cap * POS * ret, 10.0)
|
||||||
|
eq_ts.append(ts.iloc[j]); eq_v.append(cap)
|
||||||
|
return _norm(_daily_equity(eq_ts, eq_v, IDX))
|
||||||
|
|
||||||
|
|
||||||
|
def pmetrics(members, p, extra=None):
|
||||||
|
ids = list(p.sleeve_ids) + ([extra] if extra else [])
|
||||||
|
dr = pd.DataFrame({i: members[i].pct_change().fillna(0.0) for i in ids})
|
||||||
|
if extra:
|
||||||
|
caps = dict(p.caps); caps["DISP"] = caps.get("DISP", None)
|
||||||
|
w = W.weight_vector(p.weighting, ids, dr, weights=p.weights,
|
||||||
|
caps=p.caps, clusters={**{i:(p.clusters or {}).get(i,i) for i in p.sleeve_ids},
|
||||||
|
**({extra:"disp"} if extra else {})},
|
||||||
|
lookback=p.vol_lookback)
|
||||||
|
drp = port_returns({i: members[i] for i in ids}, w)
|
||||||
|
return metrics(drp), metrics(drp, lo=SPLIT)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
p = PORTFOLIOS["PORT06"]
|
||||||
|
print("=" * 100)
|
||||||
|
print(" GATE PORT06 — candidato index_comp_disp W=168 (BTC) | famiglia DISP nuova")
|
||||||
|
print(f" config {CFG} | OOS da {OOS_DATE}")
|
||||||
|
print("=" * 100)
|
||||||
|
|
||||||
|
from src.portfolio.sleeves import all_sleeve_equities
|
||||||
|
eq_base = dict(all_sleeve_equities())
|
||||||
|
|
||||||
|
df, trades = build_trades("BTC")
|
||||||
|
disp_eq = daily_equity(df, trades)
|
||||||
|
fr = (disp_eq.iloc[-1] / disp_eq.iloc[0] - 1) * 100
|
||||||
|
o = disp_eq.iloc[SPLIT:]; ofr = (o.iloc[-1] / o.iloc[0] - 1) * 100
|
||||||
|
print(f"\n[1] candidato standalone: {len(trades)} trade | FULL {fr:+.0f}% | OOS {ofr:+.0f}%")
|
||||||
|
|
||||||
|
# correlazione daily col MASTER e con le fade BTC
|
||||||
|
dr_cand = disp_eq.pct_change().fillna(0.0)
|
||||||
|
print("\n[2] correlazione daily col candidato (decorrela?):")
|
||||||
|
for sid in ["MR01_BTC", "MR02_BTC", "MR07_BTC", "DIP01_BTC"]:
|
||||||
|
corr = dr_cand.corr(eq_base[sid].pct_change().fillna(0.0))
|
||||||
|
print(f" {sid:<12} corr {corr:+.3f}")
|
||||||
|
master_dr = pd.DataFrame({i: eq_base[i].pct_change().fillna(0.0) for i in p.sleeve_ids}).mean(axis=1)
|
||||||
|
print(f" {'MASTER(EW)':<12} corr {dr_cand.corr(master_dr):+.3f}")
|
||||||
|
|
||||||
|
# PORT06 base vs +DISP
|
||||||
|
f_b, o_b = pmetrics(eq_base, p)
|
||||||
|
members = dict(eq_base); members["DISP_BTC"] = disp_eq
|
||||||
|
f_e, o_e = pmetrics(members, p, extra="DISP_BTC")
|
||||||
|
print("\n[3] PORT06 BASE (17) vs +DISP (18):")
|
||||||
|
print(f" {'':<10}{'FULL Sh':>9}{'FULL DD%':>10}{'OOS Sh':>9}{'OOS DD%':>9}")
|
||||||
|
print(f" {'BASE':<10}{f_b['sharpe']:>9.2f}{f_b['dd']:>10.2f}{o_b['sharpe']:>9.2f}{o_b['dd']:>9.2f}")
|
||||||
|
print(f" {'+DISP':<10}{f_e['sharpe']:>9.2f}{f_e['dd']:>10.2f}{o_e['sharpe']:>9.2f}{o_e['dd']:>9.2f}")
|
||||||
|
print(f" {'DELTA':<10}{f_e['sharpe']-f_b['sharpe']:>+9.2f}{f_e['dd']-f_b['dd']:>+10.2f}"
|
||||||
|
f"{o_e['sharpe']-o_b['sharpe']:>+9.2f}{o_e['dd']-o_b['dd']:>+9.2f}")
|
||||||
|
|
||||||
|
promoted = (o_e['sharpe'] >= o_b['sharpe'] - 0.02 and o_e['dd'] <= o_b['dd'] + 0.20
|
||||||
|
and f_e['sharpe'] >= f_b['sharpe'] - 0.02)
|
||||||
|
print("\n VERDETTO: " + (">>> PROMOSSO <<<" if promoted else ">>> BOCCIATO (diluisce, come FR01) <<<"))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user