research: ricerca dispersion/correlation index (165 agenti) — 2 edge su 60, nessun nuovo motore
Workflow 60 celle (15 famiglie x 4 finestre), verifica avversariale 2-skeptic. Esito: 2 edge confermati causali (index_comp_disp W168, rel_idio_fade W24), ma entrambi fade-BTC del residuo idiosincratico -> sovrapposti alle MR esistenti (P migliora-PORT06 ~20%, lezione FR01). 13/15 famiglie rumore (overfit/regime, non look-ahead). Edge preservati in scripts/analysis/dispersion_edges/; harness riusabile dispersion_lab.py (gia' committato). Diario + TODO follow-up. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -45,6 +45,16 @@
|
||||
- [ ] **epoche hardcoded in `hourly_report.lossguard_section`** (LOSSGUARD_SINCE, TRENDSWAP_SINCE):
|
||||
ogni nuova epoca-filtro richiede di editare la funzione. Derivarle da deploy history/config.
|
||||
|
||||
## 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
|
||||
dispersion che merita un test formale (`combine_v2`): misurare corr col MASTER e
|
||||
ΔSharpe/ΔDD. Config in `scripts/analysis/dispersion_edges/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%:
|
||||
è 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)
|
||||
|
||||
- [ ] **PnL live post-swap trend** (epoca TREND in `hourly_report`): verdetto stop-rate fade a n>=30.
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
# 2026-06-08 — Ricerca multi-agente: dispersion / correlation index (165 agenti)
|
||||
|
||||
Workflow `dispersion-correlation-research` (run wf_72edef49-6d9): 60 celle esplorate
|
||||
(15 famiglie × 4 finestre di correlazione W=24/72/168/336), 165 agenti totali, ~10M
|
||||
token, verifica avversariale a 2 skeptic su ogni candidato robusto + sintesi.
|
||||
|
||||
Infrastruttura riusabile: `scripts/analysis/dispersion_lab.py` — feature REALIZED
|
||||
causali (avg pairwise correlation, dispersione cross-sectional, beta vs indice EW,
|
||||
componente idiosincratica `rel_A = ret_A − ret_idx`), universo 8-asset, finestra
|
||||
comune dal 2022-07-22, cache su disco, no-look-ahead verificato. NB: **niente
|
||||
implied** (opzioni non backtestabili — stesso muro ARGO/GEX).
|
||||
|
||||
## Esito: 2 edge confermati su 60, 13 famiglie su 15 sono RUMORE
|
||||
|
||||
Nessun look-ahead "hard" stile-squeeze (le feature hanno retto la perturbazione del
|
||||
futuro ovunque). Il fallimento dominante è **overfit/concentrazione di regime**, non
|
||||
leakage. Tre pattern ricorrenti di falso positivo:
|
||||
1. **OOS circolare**: le feature esistono solo dal 2022-07 → lo split OOS (ultimo 30%)
|
||||
cade nel bull/calm 2024-25 (il regime che il progetto documenta come ottimistico
|
||||
~50%); FULL≈OOS non era robustezza ma "quasi tutti i trade in OOS".
|
||||
2. **Il gate corr/disp non è quasi mai load-bearing**: in ablazione, togliere il gate
|
||||
spesso MIGLIORA i numeri → è un throttle di esposizione, non alpha (e Hurst-guard +
|
||||
EXIT-16 fanno già quel lavoro).
|
||||
3. **Riconferma del prior**: l'edge è sempre mean-reversion del residuo, mai
|
||||
momentum/continuation/breakout cross-sectional.
|
||||
|
||||
## I 2 edge reali (entrambi fade del residuo idiosincratico)
|
||||
|
||||
- **`index_comp_disp` W=168 (BTC)** — il vero "dispersion-trading realizzato": fade
|
||||
della componente idiosincratica estrema verso l'indice, **gated da alta dispersione**
|
||||
(`disp_168 ≥ quantile 0.7`). rel_len=12, z_thr=1.5, TP=1.0·ATR, SL=1.5·ATR, mb=24.
|
||||
FULL +63% / OOS +36% (fee0.2% +23%), win 66%, Sharpe 2.7-3.0, DD 7-9%, 5/5 anni.
|
||||
È l'UNICA famiglia dove il gate dispersione fa davvero qualcosa.
|
||||
- **`rel_idio_fade` W=24** — fade dello z-score di `rel_A` su 24h, exit a tempo (no
|
||||
TP/SL). Cella più pulita BTC z3.0 mb12: win 58.5%, Sharpe 4.40, DD 7%, OOS +98%.
|
||||
Più forte ma è quasi MR07 ri-etichettata in chiave cross-sectional.
|
||||
|
||||
Script preservati: `scripts/analysis/dispersion_edges/{index_comp_disp,rel_idio_fade}.py`.
|
||||
|
||||
## Raccomandazione: bassa priorità
|
||||
|
||||
**Entrambi sono fade-BTC mean-reversion → sovrapposti alle fade MR già deployate.**
|
||||
P(migliora PORT06): ~20-25% per index_comp_disp, ~15-20% per rel_idio_fade. Vale la
|
||||
**lezione FR01** (robusto-individualmente ≠ migliora-il-portafoglio: FR01 era robusto
|
||||
e causale ma diluiva PORT06 OOS Sharpe 8.89→8.72). index_comp_disp ha il profilo trade
|
||||
più distinto (win 66%, hold corto, TP vicino) → è l'unico che merita eventualmente un
|
||||
gate `combine_v2`/PORT06 formale, misurando corr col MASTER e ΔSharpe/ΔDD. Se non
|
||||
decorrela, scartare.
|
||||
|
||||
**13 famiglie rumore** (record in `_disp_scratch/`, gitignored): corr_gate_fade/pairs,
|
||||
disp_xs_reversal/momentum, corr_spike_btc, corr_meanrev, beta_disp, disp_regime_rot,
|
||||
corr_vol_interact, leadlag_corr, corr_trend, disp_compression_breakout, corr_disp_combo
|
||||
— tutte MR/fade ri-etichettate, concentrazione 2024-25, o artefatti di feed morto
|
||||
(ADA/LTC 97% candele flat). disp_compression_breakout riconferma la lezione squeeze
|
||||
(i breakout rientrano).
|
||||
|
||||
**Conclusione onesta: nessun nuovo motore di ritorno.** Il dispersion-trading realizzato
|
||||
funziona solo come l'ennesima faccia della mean-reversion già sfruttata.
|
||||
@@ -0,0 +1,191 @@
|
||||
"""FAMIGLIA index_comp_disp (W=24) — dispersion trading REALIZZATO.
|
||||
|
||||
Idea: l'indice EW vs le componenti. Quando la dispersione cross-sectional rolling
|
||||
(disp_24) e' ALTA, le componenti idiosincratiche estreme (rel_A = ret_A - ret_idx)
|
||||
tendono a RIENTRARE verso l'indice -> fade della componente idiosincratica estrema:
|
||||
- se rel_A e' molto positivo (A ha sovraperformato l'indice oltre soglia) -> SHORT A
|
||||
- se rel_A e' molto negativo (A ha sottoperformato l'indice oltre soglia) -> LONG A
|
||||
condizionato a disp_24 sopra una soglia (regime di alta dispersione).
|
||||
|
||||
CAUSALE: la decisione a close[i] usa SOLO feature note a i:
|
||||
- rel_A[i] = log-ret di A meno log-ret indice nella barra [i-1->i] (nota a close[i])
|
||||
- disp_24[i] = media rolling 24 della disp cross-sectional fino a i (nota a close[i])
|
||||
Ingresso eseguibile a close[i]. Niente uso di i+1 nella decisione.
|
||||
|
||||
Per rendere le soglie comparabili fra asset/tempo, rel_A si normalizza con la sua
|
||||
deviazione standard rolling CAUSALE (rolling 168h, shiftata di 1 per non includere i).
|
||||
disp_24 si normalizza col suo quantile rolling causale (percentile rolling).
|
||||
|
||||
Exit: time-stop max_bars (con TP/SL ATR opzionali). Il fade verso l'indice e' un
|
||||
ritorno alla media -> orizzonte breve.
|
||||
"""
|
||||
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, UNIVERSE # noqa: E402
|
||||
from scripts.analysis.explore_lab import get_df, evaluate, robust, atr # noqa: E402
|
||||
|
||||
W = 24 # finestra famiglia
|
||||
|
||||
|
||||
def _last_rank(x: np.ndarray) -> float:
|
||||
"""Frazione dei valori (esclusi l'ultimo) strettamente < dell'ultimo. Causale:
|
||||
l'ultimo elemento e' la barra i, confrontata coi 719 valori precedenti."""
|
||||
if x.shape[0] < 2:
|
||||
return np.nan
|
||||
return float((x[:-1] < x[-1]).mean())
|
||||
|
||||
|
||||
def _causal_signals(asset: str, df: pd.DataFrame, fa: pd.DataFrame):
|
||||
"""Precalcola (una volta per asset) rel_z, disp_pctl, atr — feature CAUSALI."""
|
||||
a14 = atr(df, 14)
|
||||
rel = fa[f"rel_{asset}"].values.astype(float)
|
||||
disp = fa["disp_24"].values.astype(float)
|
||||
|
||||
# z-score CAUSALE di rel: media/std rolling 168h SHIFTATA di 1 (solo barre <= i-1)
|
||||
rel_s = pd.Series(rel)
|
||||
rmean = rel_s.rolling(168).mean().shift(1)
|
||||
rstd = rel_s.rolling(168).std().shift(1)
|
||||
rel_z = ((rel_s - rmean) / rstd.replace(0, np.nan)).values
|
||||
|
||||
# percentile rolling CAUSALE di disp_24 (rank di disp[i] vs i 720 valori fino a i).
|
||||
# vettoriale via rank rolling: pos dell'ultimo elemento / (win-1).
|
||||
win = 720
|
||||
dr = pd.Series(disp).rolling(win).apply(_last_rank, raw=True)
|
||||
disp_pctl = dr.values
|
||||
return rel_z, disp_pctl, a14
|
||||
|
||||
|
||||
def build_entries(asset: str, df: pd.DataFrame, fa: pd.DataFrame,
|
||||
rel_z_thr: float, disp_pctl_thr: float,
|
||||
max_bars: int, tp_atr: float | None, sl_atr: float | None,
|
||||
precomp=None) -> list[dict]:
|
||||
"""Costruisce entries CAUSALI per il fade della componente idiosincratica."""
|
||||
n = len(df)
|
||||
c = df["close"].values
|
||||
if precomp is None:
|
||||
rel_z, disp_pctl, a14 = _causal_signals(asset, df, fa)
|
||||
else:
|
||||
rel_z, disp_pctl, a14 = precomp
|
||||
|
||||
entries: list[dict] = []
|
||||
for i in range(n - 1):
|
||||
z = rel_z[i]
|
||||
dp = disp_pctl[i]
|
||||
if not np.isfinite(z) or not np.isfinite(dp):
|
||||
continue
|
||||
if dp < disp_pctl_thr: # solo regime di alta dispersione
|
||||
continue
|
||||
if abs(z) < rel_z_thr: # solo componente idio estrema
|
||||
continue
|
||||
d = -1 if z > 0 else +1 # FADE: rel alto -> short A; rel basso -> long A
|
||||
a = a14[i]
|
||||
if not np.isfinite(a) or a <= 0:
|
||||
tp = sl = None
|
||||
else:
|
||||
tp = c[i] + d * tp_atr * a if tp_atr else None
|
||||
sl = c[i] - d * sl_atr * a if sl_atr else None
|
||||
entries.append({"i": i, "d": d, "max_bars": max_bars, "tp": tp, "sl": sl})
|
||||
return entries
|
||||
|
||||
|
||||
def check_no_lookahead(asset: str, df: pd.DataFrame, fa: pd.DataFrame) -> bool:
|
||||
"""Perturba i PREZZI dopo un indice T e verifica che le entries con i<=T non cambino.
|
||||
Qui ricostruiamo la entry-rule su una copia di df/fa col futuro alterato e confrontiamo
|
||||
le entries (i, d) con i<=T."""
|
||||
n = len(df)
|
||||
T = int(n * 0.6)
|
||||
|
||||
base = build_entries(asset, df, fa, rel_z_thr=2.0, disp_pctl_thr=0.6,
|
||||
max_bars=12, tp_atr=None, sl_atr=None)
|
||||
|
||||
# perturba i prezzi dopo T: alza del 50% close/high/low/open
|
||||
df2 = df.copy()
|
||||
for col in ("open", "high", "low", "close"):
|
||||
df2.loc[df2.index > T, col] = df2.loc[df2.index > T, col] * 1.5
|
||||
# perturba anche le feature riferite a barre > T (rel_<asset>, disp_24)
|
||||
fa2 = fa.copy()
|
||||
for col in (f"rel_{asset}", "disp_24"):
|
||||
fa2.loc[fa2.index > T, col] = fa2.loc[fa2.index > T, col] * 1.5
|
||||
|
||||
pert = build_entries(asset, df2, fa2, rel_z_thr=2.0, disp_pctl_thr=0.6,
|
||||
max_bars=12, tp_atr=None, sl_atr=None)
|
||||
|
||||
base_le = {(e["i"], e["d"]) for e in base if e["i"] <= T}
|
||||
pert_le = {(e["i"], e["d"]) for e in pert if e["i"] <= T}
|
||||
ok = base_le == pert_le
|
||||
print(f"[no-look-ahead] entries con i<=T={T} invarianti al futuro: "
|
||||
f"{'OK' if ok else 'VIOLATO'} (base={len(base_le)} pert={len(pert_le)})")
|
||||
if not ok:
|
||||
diff = (base_le ^ pert_le)
|
||||
print(f" differenze: {sorted(diff)[:10]}")
|
||||
return ok
|
||||
|
||||
|
||||
def main():
|
||||
F = features()
|
||||
print(f"feature caricate: {F.shape[0]} barre")
|
||||
|
||||
# asset single-asset da esplorare (i piu' liquidi + qualche alt)
|
||||
assets = ["BTC", "ETH", "SOL", "ADA", "BNB", "DOGE", "LTC", "XRP"]
|
||||
|
||||
# griglia piccola di soglie
|
||||
rel_z_grid = [1.5, 2.0, 2.5]
|
||||
disp_pctl_grid = [0.5, 0.7]
|
||||
mb_grid = [6, 12, 24]
|
||||
# exit: prima senza tp/sl (puro time-stop), poi con un TP/SL ATR moderato
|
||||
exit_grid = [
|
||||
(None, None),
|
||||
(1.5, 2.0),
|
||||
]
|
||||
|
||||
best = None
|
||||
# no-look-ahead check una volta (su ETH)
|
||||
df_eth = get_df("ETH", "1h")
|
||||
fa_eth = align_to(F, df_eth)
|
||||
la_ok = check_no_lookahead("ETH", df_eth, fa_eth)
|
||||
print()
|
||||
|
||||
for asset in assets:
|
||||
df = get_df(asset, "1h")
|
||||
fa = align_to(F, df)
|
||||
precomp = _causal_signals(asset, df, fa) # una volta per asset (costoso)
|
||||
for rz in rel_z_grid:
|
||||
for dp in disp_pctl_grid:
|
||||
for mb in mb_grid:
|
||||
for (tp_a, sl_a) in exit_grid:
|
||||
ents = build_entries(asset, df, fa, rz, dp, mb, tp_a, sl_a,
|
||||
precomp=precomp)
|
||||
if len(ents) < 30:
|
||||
continue
|
||||
tag = f"{asset} rz{rz} dp{dp} mb{mb} tp{tp_a} sl{sl_a}"
|
||||
res = evaluate(tag, ents, df)
|
||||
rb = robust(res)
|
||||
# criterio "migliore": OOS ret, poi sharpe; preferisci robuste
|
||||
score = (rb, res["oos"]["ret"], res["full"]["sharpe"])
|
||||
if best is None or score > best[0]:
|
||||
best = (score, tag, res, rb,
|
||||
dict(asset=asset, rz=rz, dp=dp, mb=mb, tp=tp_a, sl=sl_a))
|
||||
|
||||
print("\n=== MIGLIORE ===")
|
||||
if best is None:
|
||||
print("nessuna cella con abbastanza trade")
|
||||
return
|
||||
score, tag, res, rb, cfg = best
|
||||
print(f"config: {tag}")
|
||||
print(f"robust={rb} lookahead_ok={la_ok}")
|
||||
print(f"FULL ret={res['full']['ret']:+.0f}% OOS ret={res['oos']['ret']:+.0f}% "
|
||||
f"DD={res['full']['dd']:.0f}% Sharpe={res['full']['sharpe']:.2f}")
|
||||
print(f"fee0.2% OOS={res['sweep_oos'][0.002]:+.0f}% anniPos={res['pos_yrs']}/{res['n_yrs']}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,152 @@
|
||||
"""rel_idio_fade (W=24): fade della componente idiosincratica rel_A vs indice.
|
||||
|
||||
Idea: rel_A = ret(A) - ret(indice EW) e' il rendimento idiosincratico (residuo di
|
||||
mercato). Quando l'asset diverge troppo dall'indice (z-score di rel_A su finestra
|
||||
W=24 elevato), si fada il residuo verso l'indice: se A ha sovraperformato troppo
|
||||
(z alto) -> SHORT A; se ha sottoperformato (z basso) -> LONG A. Mean-reversion del
|
||||
residuo.
|
||||
|
||||
ENTRY CAUSALE: la decisione a close[i] usa SOLO rel_A fino a i incluso. Lo z-score
|
||||
e' costruito con media/std rolling su [i-W+1 .. i] (causale). Ingresso eseguibile a
|
||||
close[i]; exit a tempo (max_bars), opzionale TP/SL ad ATR.
|
||||
|
||||
Esegui: cd /opt/docker/PythagorasGoal && PYTHONPATH=. uv run python \
|
||||
scripts/analysis/_disp_scratch/rel_idio_fade_24.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, UNIVERSE # noqa: E402
|
||||
from scripts.analysis.explore_lab import get_df, evaluate, robust, atr # noqa: E402
|
||||
|
||||
W = 24 # finestra correlazione/dispersione (richiesta dalla famiglia)
|
||||
ASSETS = ["BTC", "ETH", "ADA", "BNB", "DOGE", "LTC", "SOL", "XRP"]
|
||||
Z_GRID = [1.5, 2.0, 2.5, 3.0]
|
||||
MB_GRID = [12, 24, 48]
|
||||
TP_ATR = None # exit a tempo puro per il primo screening
|
||||
SL_ATR = None
|
||||
|
||||
|
||||
def build_entries(asset: str, df: pd.DataFrame, fa: pd.DataFrame,
|
||||
z_thr: float, max_bars: int,
|
||||
tp_atr=None, sl_atr=None) -> list[dict]:
|
||||
"""Entries CAUSALI per il fade del residuo idiosincratico.
|
||||
|
||||
z[i] = (rel[i] - mean(rel[i-W+1..i])) / std(rel[i-W+1..i]) -> usa solo dati <= i.
|
||||
rel[i] e' gia' causale (deriva da log-ret fino a close[i]). Quando |z[i]|>=thr:
|
||||
z>0 (A ha sovraperformato l'indice) -> SHORT (d=-1), fade verso l'indice
|
||||
z<0 (A ha sottoperformato) -> LONG (d=+1)
|
||||
"""
|
||||
rel = fa[f"rel_{asset}"].values.astype(float)
|
||||
s = pd.Series(rel)
|
||||
mu = s.rolling(W).mean().values
|
||||
sd = s.rolling(W).std(ddof=0).values
|
||||
z = (rel - mu) / np.where(sd > 0, sd, np.nan)
|
||||
|
||||
a = atr(df, 14)
|
||||
c = df["close"].values
|
||||
n = len(c)
|
||||
entries: list[dict] = []
|
||||
for i in range(W, n - 1):
|
||||
zi = z[i]
|
||||
if not np.isfinite(zi) or abs(zi) < z_thr:
|
||||
continue
|
||||
d = -1 if zi > 0 else 1 # fade del residuo
|
||||
e = {"i": i, "d": d, "max_bars": max_bars}
|
||||
if tp_atr is not None and np.isfinite(a[i]):
|
||||
e["tp"] = c[i] + d * tp_atr * a[i] # TP nella direzione del fade
|
||||
if sl_atr is not None and np.isfinite(a[i]):
|
||||
e["sl"] = c[i] - d * sl_atr * a[i]
|
||||
entries.append(e)
|
||||
return entries
|
||||
|
||||
|
||||
def check_no_lookahead(asset: str, df: pd.DataFrame, fa: pd.DataFrame,
|
||||
z_thr: float, max_bars: int) -> bool:
|
||||
"""Perturba i prezzi DOPO un indice T e verifica che le entries con i<=T non
|
||||
cambino (la entry-rule usa solo dati <= close[i])."""
|
||||
rel = fa[f"rel_{asset}"].values.astype(float)
|
||||
n = len(rel)
|
||||
T = int(n * 0.6)
|
||||
|
||||
def z_of(relv):
|
||||
s = pd.Series(relv)
|
||||
mu = s.rolling(W).mean().values
|
||||
sd = s.rolling(W).std(ddof=0).values
|
||||
return (relv - mu) / np.where(sd > 0, sd, np.nan)
|
||||
|
||||
z0 = z_of(rel)
|
||||
rel2 = rel.copy()
|
||||
rel2[T + 1:] = rel2[T + 1:] + 0.05 # shock del futuro
|
||||
z2 = z_of(rel2)
|
||||
|
||||
def ents_from(z):
|
||||
out = []
|
||||
for i in range(W, n - 1):
|
||||
if i > T:
|
||||
break
|
||||
zi = z[i]
|
||||
if np.isfinite(zi) and abs(zi) >= z_thr:
|
||||
out.append((i, -1 if zi > 0 else 1))
|
||||
return out
|
||||
|
||||
ok = ents_from(z0) == ents_from(z2)
|
||||
print(f" [no-look-ahead {asset}] entries i<=T={T} invarianti al futuro: "
|
||||
f"{'OK' if ok else 'VIOLATO'}")
|
||||
return ok
|
||||
|
||||
|
||||
def main():
|
||||
F = features()
|
||||
print(f"feature caricate: {F.shape[0]} barre, {F.shape[1]} colonne")
|
||||
|
||||
best = None
|
||||
look_ok_all = True
|
||||
for asset in ASSETS:
|
||||
df = get_df(asset, "1h")
|
||||
fa = align_to(F, df)
|
||||
# un check no-look-ahead per asset (config centrale)
|
||||
look_ok_all &= check_no_lookahead(asset, df, fa, z_thr=2.0, max_bars=24)
|
||||
print(f"--- {asset} ---")
|
||||
for z_thr in Z_GRID:
|
||||
for mb in MB_GRID:
|
||||
ents = build_entries(asset, df, fa, z_thr, mb, TP_ATR, SL_ATR)
|
||||
if len(ents) < 30:
|
||||
continue
|
||||
name = f"{asset} z{z_thr} mb{mb}"
|
||||
res = evaluate(name, ents, df)
|
||||
rb = robust(res)
|
||||
score = res["oos"]["ret"] + res["full"]["ret"]
|
||||
cand = {
|
||||
"asset": asset, "z": z_thr, "mb": mb,
|
||||
"full": res["full"]["ret"], "oos": res["oos"]["ret"],
|
||||
"fee02_oos": res["sweep_oos"][0.002],
|
||||
"dd": res["full"]["dd"], "sharpe": res["full"]["sharpe"],
|
||||
"pos_yrs": res["pos_yrs"], "n_yrs": res["n_yrs"],
|
||||
"robust": rb, "score": score, "trades": res["full"]["trades"],
|
||||
}
|
||||
# preferisci robuste; a parita' di robustezza, score piu' alto
|
||||
if best is None or (cand["robust"], cand["score"]) > (best["robust"], best["score"]):
|
||||
best = cand
|
||||
|
||||
print("\n=== CELLA MIGLIORE ===")
|
||||
if best:
|
||||
print(f" asset={best['asset']} z={best['z']} mb={best['mb']} trades={best['trades']}")
|
||||
print(f" FULL={best['full']:+.0f}% OOS={best['oos']:+.0f}% "
|
||||
f"fee0.2%OOS={best['fee02_oos']:+.0f}% DD={best['dd']:.0f}% "
|
||||
f"Sharpe={best['sharpe']:.2f} anniPos={best['pos_yrs']}/{best['n_yrs']} "
|
||||
f"robust={best['robust']}")
|
||||
print(f" no-look-ahead tutti gli asset: {'OK' if look_ok_all else 'VIOLATO'}")
|
||||
return best
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user