Files
PythagorasGoal/scripts/research/xsec_v2_nonmom.py
T
Adriano Dal Pastro aad69f9790 research(crypto): 4 filoni 2026-06-29 — ERM lead sub-daily (forward), 3 scartati/deboli
Ricerca onesta su BTC/ETH + universo HL, branch separato (nessun impatto live).
Harness condiviso altlib (causale, fee 0.10% RT, marginal vs TP01, day-boundary,
haircut $600). Test 19/19 verdi.

- A DVOL direzionale  -> LEAD hedge/DD-dampener, NON sleeve (buy-the-fear; is_hedge).
- B Intraday ERM 8h   -> LEAD forte / forward-monitor: earns_slot=True, ADDS oltre
                         SKH01 (TP01+SKH+ERM 60/25/15 FULL 1.88/HOLD 1.46/DD 8.9%).
                         Caveat: plateau hold-out single-row, multiple-testing non
                         deflazionato, exec 8h. Controllo TOD = FAIL atteso.
- C Cross-sectional non-mom (low-vol HL) -> DEBOLE/forward-monitor (deflated-Sh 0.13,
                         storia 2.5a, non eseguibile $600) STAT-MODE.
- D Macro regime-gate -> RIDONDANTE col trend (corr->TP01 0.989), SCARTATO.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-29 19:48:36 +00:00

434 lines
21 KiB
Python

"""XSEC v2 — segnali cross-sectional NON-MOMENTUM su 51 asset Hyperliquid (STAT-MODE).
TESI (filone C). XS01 (sleeve attivo) e' momentum cross-sectional sui 19 major. Lezione del
progetto (diari 2026-06-19/20): ESPANDERE IL NUMERO di asset NON aiuta il momentum (gli small-cap
diluiscono/invertono il segnale). Quindi qui NON ri-proviamo l'espansione-universo: cerchiamo un
MECCANISMO DIVERSO dal momentum che, market-neutral e scorrelato, possa diversificare il portafoglio.
Meccanismi provati (tutti L/S dollar-neutral, vol-target ~20%, ribilancio periodico, CAUSALI):
REV - short-term REVERSAL cross-sectional grezzo (long i loser di breve, short i winner).
IREV - REVERSAL IDIOSINCRATICO: reversal sul RESIDUO dopo aver tolto il mercato (beta-adjusted).
LOWVOL - factor LOW-VOL: long bassa vol realizzata / short alta vol (betting-against-vol).
IMOM - MOMENTUM IDIOSINCRATICO: momentum sul residuo (toglie il fattore mercato, != raw mom).
BAB - betting-against-beta: long basso beta / short alto beta.
MOM - (riferimento) momentum grezzo, per confronto.
GATE (CLAUDE.md, metodologia obbligatoria):
1. CAUSALE: score a close[i], peso tenuto in i+1 (l'engine shifta: W[i-1]*dret[i]); vol=0 gata.
2. NETTO fee 0.10% RT su OGNI gamba a OGNI ribilancio + sweep fee.
3. OOS hold-out 2025-01-01 + plateau su (lookback, H, k) + 2 universi (51 vs 19 major).
4. Storia ~2.5 anni + molte config -> DEFLATED Sharpe (multiple-testing) e onesta' brutale.
5. Confronto: Sharpe standalone FULL/HOLD/DD, corr vs XS01 e TP01, uplift del portafoglio a 4->5
sleeve (portfolio.py, riusa active_sleeves senza modificarli).
6. CAVEAT: book a molte gambe NON eseguibile a $600 -> STAT-MODE / forward-monitor, non deploy.
uv run python scripts/research/xsec_v2_nonmom.py
"""
from __future__ import annotations
import sys, glob, math
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[2]
sys.path.insert(0, str(PROJECT_ROOT))
import numpy as np
import pandas as pd
from scipy.stats import norm
from src.portfolio.portfolio import to_daily, metrics, HOLDOUT, Sleeve, StrategyPortfolio
from src.portfolio.sleeves import tp01_sleeve, xsec_sleeve, active_sleeves, XS_UNIVERSE
RAW = PROJECT_ROOT / "data" / "raw"
FEE = 0.001 # 0.10% RT (Deribit taker): fee per gamba per lato = FEE/2 = 0.0005
TV = 0.20 # vol-target annuo
DPY = 365.25
# ===========================================================================
# DATI — matrice prezzi/volumi (outer-join: ragged start, NaN prima del listing)
# ===========================================================================
def load_matrix(universe=None):
px, vol = {}, {}
files = sorted(glob.glob(str(RAW / "hl_*_1d.parquet")))
for f in files:
sym = Path(f).stem.replace("hl_", "").replace("_1d", "").upper()
if universe is not None and sym not in universe:
continue
d = pd.read_parquet(f)
idx = pd.to_datetime(d["timestamp"], unit="ms", utc=True)
px[sym] = pd.Series(d["close"].values.astype(float), index=idx)
vol[sym] = pd.Series(d["volume"].values.astype(float), index=idx)
PX = pd.concat(px, axis=1).sort_index()
VOL = pd.concat(vol, axis=1).sort_index().reindex_like(PX)
return PX, VOL
# ===========================================================================
# ENGINE cross-sectional NaN-aware (causale). score_at(i)->(score[A], valid[A]).
# Convenzione UNICA: long alto score / short basso score. Ogni meccanismo passa
# lo score giusto (es. reversal = -ritorno; low-vol = -vol; bab = -beta).
# ===========================================================================
def xs_engine(PX, VOL, score_at, H, k, target_vol=TV, fee=FEE, min_assets=10, warmup=0):
px = PX.values
vol = VOL.values
n, A = px.shape
dret = np.full((n, A), np.nan)
dret[1:] = px[1:] / px[:-1] - 1.0
W = np.zeros((n, A))
w = np.zeros(A)
for i in range(n):
if i >= warmup and i % H == 0:
score, valid = score_at(i)
valid = valid & np.isfinite(score) & (vol[i] > 0)
idxv = np.where(valid)[0]
if len(idxv) >= min_assets:
kk = min(k, len(idxv) // 2)
order = idxv[np.argsort(score[idxv])] # ascendente
lo, hi = order[:kk], order[-kk:] # basso score / alto score
w = np.zeros(A)
w[hi] = 0.5 / kk # long alto score
w[lo] = -0.5 / kk # short basso score
else:
w = np.zeros(A)
W[i] = w
# rendimento book: W[i-1] guadagna dret[i]; NaN (asset non listato) -> 0
gross = np.zeros(n)
gross[1:] = np.nansum(W[:-1] * np.nan_to_num(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)
net = gross - turn * (fee / 2.0)
s = pd.Series(net, index=PX.index)
rv = s.rolling(30, min_periods=15).std().shift(1) * np.sqrt(DPY)
scale = np.clip(np.nan_to_num(target_vol / rv.replace(0, np.nan).values, nan=0.0), 0, 3.0)
turn_py = float(turn.sum() / (n / DPY)) if n else 0.0
return pd.Series(s.values * scale, index=PX.index), turn_py
# ===========================================================================
# SCORE BUILDERS — ognuno ritorna una closure score_at(i) + warmup richiesto.
# Tutti CAUSALI: usano dati <= i (close[i] noto al momento della decisione).
# ===========================================================================
def _precompute(PX):
px = PX.values
n, A = px.shape
DR = PX.pct_change() # ritorni giornalieri (NaN ragged)
m = DR.mean(axis=1) # mercato equal-weight (skipna)
return px, n, A, DR, m
def make_mom(PX, L, sign=+1):
px, n, A, *_ = _precompute(PX)
def score_at(i):
if i - L < 0:
return np.full(A, np.nan), np.zeros(A, bool)
r = px[i] / px[i - L] - 1.0
valid = np.isfinite(px[i]) & np.isfinite(px[i - L])
return sign * r, valid
return score_at, L + 1
def make_lowvol(PX, B):
px, n, A, DR, m = _precompute(PX)
RV = DR.rolling(B, min_periods=int(0.6 * B)).std().values
def score_at(i):
rv = RV[i]
valid = np.isfinite(rv) & np.isfinite(px[i])
return -rv, valid # long bassa vol / short alta vol
return score_at, B + 1
def _rolling_beta(DR, m, B):
mp = int(0.6 * B)
Em = m.rolling(B, min_periods=mp).mean()
Em2 = (m * m).rolling(B, min_periods=mp).mean()
varm = Em2 - Em ** 2
Ex = DR.rolling(B, min_periods=mp).mean()
Exm = DR.mul(m, axis=0).rolling(B, min_periods=mp).mean()
beta = Exm.sub(Ex.mul(Em, axis=0)).div(varm.replace(0, np.nan), axis=0)
return beta.values, varm.values
def make_bab(PX, B):
px, n, A, DR, m = _precompute(PX)
beta, _ = _rolling_beta(DR, m, B)
def score_at(i):
b = beta[i]
valid = np.isfinite(b) & np.isfinite(px[i])
return -b, valid # long basso beta / short alto beta
return score_at, B + 1
def make_resid(PX, L, B, sign):
"""Momentum/reversal IDIOSINCRATICO: residuo = ritorno - beta*mercato (beta su finestra B),
cumulato sugli ultimi L giorni. sign=+1 -> momentum residuo; sign=-1 -> reversal residuo."""
px, n, A, DR, m = _precompute(PX)
beta, _ = _rolling_beta(DR, m, B)
SDR = DR.rolling(L, min_periods=int(0.8 * L)).sum().values # somma ritorni asset su L
SM = m.rolling(L, min_periods=int(0.8 * L)).sum().values # somma mercato su L
cnt = DR.rolling(L, min_periods=1).count().values
def score_at(i):
b = beta[i]
resid_cum = SDR[i] - b * SM[i]
valid = np.isfinite(resid_cum) & (cnt[i] >= 0.8 * L) & np.isfinite(px[i])
return sign * resid_cum, valid
return score_at, max(L, B) + 1
# Catalogo meccanismi: nome -> (builder, lista di config (param dict)).
def mechanisms():
return {
"MOM": (lambda PX, p: make_mom(PX, p["L"], +1),
[dict(L=L, H=H, k=k) for L in (30, 60, 90) for H in (5, 10) for k in (5, 8)]),
"REV": (lambda PX, p: make_mom(PX, p["L"], -1),
[dict(L=L, H=H, k=k) for L in (2, 3, 5, 7, 10) for H in (1, 2, 3, 5) for k in (5, 8)]),
"IREV": (lambda PX, p: make_resid(PX, p["L"], p.get("B", 60), -1),
[dict(L=L, H=H, k=k, B=60) for L in (3, 5, 7, 10) for H in (2, 3, 5) for k in (5, 8)]),
"IMOM": (lambda PX, p: make_resid(PX, p["L"], p.get("B", 60), +1),
[dict(L=L, H=H, k=k, B=60) for L in (30, 60, 90) for H in (5, 10) for k in (5, 8)]),
"LOWVOL": (lambda PX, p: make_lowvol(PX, p["B"]),
[dict(B=B, H=H, k=k) for B in (20, 30, 60) for H in (5, 10) for k in (5, 8)]),
"BAB": (lambda PX, p: make_bab(PX, p["B"]),
[dict(B=B, H=H, k=k) for B in (30, 60) for H in (5, 10) for k in (5, 8)]),
}
# ===========================================================================
# METRICHE / STATISTICA
# ===========================================================================
def yr_breadth(daily):
yr = [float((1 + g).prod() - 1) for _, g in daily.groupby(daily.index.year)]
return (sum(v > 0 for v in yr) / len(yr) if yr else 0.0), yr
def deflated_sharpe(sr_ann, all_sr_ann, daily_ret):
"""Deflated Sharpe Ratio (Bailey & Lopez de Prado): probabilita' che lo Sharpe vero superi
lo Sharpe-massimo atteso sotto il null di N trial indipendenti. Penalizza il multiple-testing.
sr_ann: Sharpe annualizzato della config scelta; all_sr_ann: tutti gli Sharpe testati;
daily_ret: serie ritorni giornalieri (per skew/kurt/T). Ritorna (DSR, sr0_ann)."""
r = np.asarray(pd.Series(daily_ret).dropna().values, float)
T = len(r)
if T < 30 or np.std(r) == 0:
return float("nan"), float("nan")
sr = sr_ann / math.sqrt(DPY) # per-osservazione
trials = np.asarray([s / math.sqrt(DPY) for s in all_sr_ann if np.isfinite(s)], float)
N = max(len(trials), 2)
var_tr = float(np.var(trials, ddof=1)) if N > 1 else 0.0
emc = 0.5772156649
z1 = norm.ppf(1 - 1.0 / N)
z2 = norm.ppf(1 - 1.0 / (N * math.e))
sr0 = math.sqrt(var_tr) * ((1 - emc) * z1 + emc * z2)
sk = float(pd.Series(r).skew())
ku = float(pd.Series(r).kurt()) + 3.0 # pandas kurt = excess
den = math.sqrt(max(1e-9, 1 - sk * sr + (ku - 1) / 4.0 * sr ** 2))
dsr = float(norm.cdf((sr - sr0) * math.sqrt(T - 1) / den))
return dsr, sr0 * math.sqrt(DPY)
def evalcfg(daily):
f = metrics(daily)
h = metrics(daily[daily.index >= HOLDOUT])
pct, _ = yr_breadth(daily)
return f, h, pct
# ===========================================================================
# RUN griglia per meccanismo / universo
# ===========================================================================
def run_grid(PX, VOL, mech_name, builder, cfgs, xs_daily, tp_daily, label):
rows = []
for p in cfgs:
score_at, warm = builder(PX, p)
daily, turn = xs_engine(PX, VOL, score_at, p["H"], p["k"], warmup=warm)
daily = to_daily(daily)
if daily.std() == 0 or len(daily) < 60:
continue
f, h, pct = evalcfg(daily)
cx = _corr(daily, xs_daily)
ct = _corr(daily, tp_daily)
rows.append(dict(cfg=p, daily=daily, full=f["sharpe"], hold=h["sharpe"], dd=f["maxdd"],
ret=f["ret"], pct=pct, corrXS=cx, corrTP=ct, turn=turn))
return rows
def _corr(a, b):
J = pd.concat({"a": a, "b": b}, axis=1, join="inner").dropna()
return float(J["a"].corr(J["b"])) if len(J) > 10 else float("nan")
def tag(p):
return " ".join(f"{k}{v}" for k, v in p.items())
MOM_FAMILY = ("MOM", "IMOM") # momentum (anche residuo) -> NON e' "non-momentum"
def causality_prefix_check(PX, VOL, builder, cfg, frac=0.85, tail=60, tol=1e-9):
"""Guard look-ahead per l'engine cross-sectional: ricostruisce la serie su un PREFISSO della
matrice (primi `frac`) e verifica che la coda combaci con la run completa sugli stessi indici.
Un feature non-causale (finestra centrata, statistica full-sample, shift(-k)) divergerebbe."""
score_full, warm = builder(PX, cfg)
full, _ = xs_engine(PX, VOL, score_full, cfg["H"], cfg["k"], warmup=warm)
cut = int(len(PX) * frac)
PXc, VOLc = PX.iloc[:cut], VOL.iloc[:cut]
score_pre, warm2 = builder(PXc, cfg)
pre, _ = xs_engine(PXc, VOLc, score_pre, cfg["H"], cfg["k"], warmup=warm2)
lo = max(0, cut - tail)
a = full.values[lo:cut]
b = pre.values[lo:cut]
worst = float(np.max(np.abs(a - b))) if len(a) else float("nan")
return dict(ok=bool(worst <= tol), max_tail_diff=worst, cut=cut, tail=len(a))
# ===========================================================================
# PORTAFOGLIO — uplift 4 -> 5 sleeve (riusa active_sleeves SENZA modificarli)
# ===========================================================================
def portfolio_uplift(cand_fn, fractions=(0.10, 0.15)):
base = active_sleeves() # 4 sleeve validati
pf0 = StrategyPortfolio(base)
bt0 = pf0.backtest() # popola le cache degli sleeve
base_full = metrics(pf0.combined_daily())
base_hold = metrics(pf0.combined_daily(lo=HOLDOUT))
out = {"base": (base_full, base_hold), "variants": {}}
for fr in fractions:
wraw = fr / (1.0 - fr) # cand_frac = wraw/(sum_base + wraw), sum_base=1
cand = Sleeve("XSV2_cand", wraw, cand_fn)
pf1 = StrategyPortfolio(base + [cand]) # riusa le cache di base
cf = metrics(pf1.combined_daily())
ch = metrics(pf1.combined_daily(lo=HOLDOUT))
out["variants"][fr] = (cf, ch, pf1.weights().get("XSV2_cand", 0.0))
return out
def main():
print("=" * 100)
print(" XSEC v2 — CROSS-SECTIONAL NON-MOMENTUM su Hyperliquid (STAT-MODE, storia ~2.5 anni)")
print("=" * 100)
tp_daily = tp01_sleeve().daily()
xs_daily = xsec_sleeve().daily()
print(f" riferimenti: TP01 (corr target) e XS01 (momentum, sleeve attivo).")
universes = {
"51-all": None,
"19-major": XS_UNIVERSE,
}
mats = {}
for uname, u in universes.items():
PX, VOL = load_matrix(u)
mats[uname] = (PX, VOL)
print(f" universo {uname:<9}: {PX.shape[1]} asset, {PX.shape[0]} giorni "
f"[{PX.index[0].date()} -> {PX.index[-1].date()}]")
mechs = mechanisms()
all_sr = [] # per deflated-Sharpe (tutti i trial)
best_per_mech = {} # (uname, mech) -> best row by hold
for uname, (PX, VOL) in mats.items():
print("\n" + "#" * 100)
print(f"# UNIVERSO {uname}")
print("#" * 100)
for mech_name, (builder, cfgs) in mechs.items():
rows = run_grid(PX, VOL, mech_name, builder, cfgs, xs_daily, tp_daily, uname)
if not rows:
continue
all_sr.extend([r["full"] for r in rows])
pos_full = sum(r["full"] > 0 for r in rows)
# migliore per HOLD-OUT (diversificatore: vogliamo OOS robusto)
best = max(rows, key=lambda r: r["hold"])
best_per_mech[(uname, mech_name)] = best
print(f"\n [{mech_name}] {len(rows)} config | plateau full>0: {pos_full}/{len(rows)}"
f" | best-hold: {tag(best['cfg'])}")
print(f" {'cfg':<22}{'FULL':>7}{'HOLD':>7}{'DD%':>6}{'ret%':>7}{'anni+':>7}"
f"{'corrXS':>8}{'corrTP':>8}{'turn/y':>8}")
# mostra le top-3 per HOLD per leggere il plateau
for r in sorted(rows, key=lambda r: -r["hold"])[:3]:
print(f" {tag(r['cfg']):<22}{r['full']:>7.2f}{r['hold']:>7.2f}{r['dd']*100:>6.0f}"
f"{r['ret']*100:>+7.0f}{r['pct']*100:>6.0f}%{r['corrXS']:>+8.2f}{r['corrTP']:>+8.2f}"
f"{r['turn']:>8.0f}")
# -------------------------------------------------------------------
# SELEZIONE: miglior candidato NON-MOMENTUM (escluse le famiglie momentum MOM/IMOM).
# gate standalone: FULL>0.5, HOLD>0, |corrXS|<0.6 -> ranking per (FULL+HOLD)/2.
# IMOM/MOM restano in tabella come RIFERIMENTO (sono momentum, non il target del filone).
# -------------------------------------------------------------------
print("\n" + "=" * 100)
print(" SELEZIONE CANDIDATO non-momentum — gate: FULL>0.5, HOLD>0, |corrXS|<0.6 (escluse MOM/IMOM)")
print("=" * 100)
nm = [s for s in all_sr if np.isfinite(s)]
pool = [(u, mn, r) for (u, mn), r in best_per_mech.items()]
nonmom = [(u, mn, r) for (u, mn, r) in pool if mn not in MOM_FAMILY]
elig = [(u, mn, r) for (u, mn, r) in nonmom
if r["full"] > 0.5 and r["hold"] > 0 and abs(r["corrXS"]) < 0.6]
elig.sort(key=lambda x: -0.5 * (x[2]["full"] + x[2]["hold"]))
for u, mn, r in sorted(pool, key=lambda x: -0.5 * (x[2]["full"] + x[2]["hold"])):
fam = "(momentum-ref)" if mn in MOM_FAMILY else ""
flag = "OK" if (u, mn, r) in elig else "--"
print(f" [{flag}] {mn:<7} {u:<9} {tag(r['cfg']):<20} FULL {r['full']:+.2f} HOLD {r['hold']:+.2f}"
f" DD {r['dd']*100:.0f}% corrXS {r['corrXS']:+.2f} corrTP {r['corrTP']:+.2f} {fam}")
if not elig:
print("\n >>> NESSUN candidato NON-momentum supera il gate standalone. SCARTATO.")
_final_note()
return
print(f"\n candidati idonei (non-momentum): {len(elig)}")
# valuta UPLIFT PORTAFOGLIO per i top-3 idonei (LOWVOL/BAB/...): cache base riusata
base = active_sleeves()
pf0 = StrategyPortfolio(base); pf0.backtest()
bf = metrics(pf0.combined_daily()); bh = metrics(pf0.combined_daily(lo=HOLDOUT))
print("\n UPLIFT PORTAFOGLIO (active_sleeves 4 -> 5 sleeve; candidato come 5o sleeve):")
print(f" BASE (4 sleeve) FULL Sh {bf['sharpe']:.2f} DD {bf['maxdd']*100:.0f}%"
f" | HOLD Sh {bh['sharpe']:.2f} DD {bh['maxdd']*100:.0f}%")
uplifts = {}
for u, mn, r in elig[:3]:
cand_fn = (lambda d: (lambda: d))(r["daily"])
best_var = None
for fr in (0.10, 0.15):
wraw = fr / (1.0 - fr)
cand = Sleeve("XSV2_cand", wraw, cand_fn)
pf1 = StrategyPortfolio(base + [cand])
cf = metrics(pf1.combined_daily()); ch = metrics(pf1.combined_daily(lo=HOLDOUT))
wgt = pf1.weights().get("XSV2_cand", 0.0)
print(f" +{mn:<6} [{u}] {tag(r['cfg']):<16} @{wgt*100:>4.1f}% "
f"FULL {cf['sharpe']:.2f} ({cf['sharpe']-bf['sharpe']:+.2f}) DD {cf['maxdd']*100:.0f}%"
f" | HOLD {ch['sharpe']:.2f} ({ch['sharpe']-bh['sharpe']:+.2f})")
d_full, d_hold = cf['sharpe'] - bf['sharpe'], ch['sharpe'] - bh['sharpe']
if best_var is None or (d_full + d_hold) > best_var:
best_var = d_full + d_hold
uplifts[(u, mn)] = best_var
# TOP candidato = miglior non-momentum idoneo
u, mn, best = elig[0]
daily = best["daily"]
f, h, pct = evalcfg(daily)
dsr, sr0 = deflated_sharpe(f["sharpe"], all_sr, daily)
caus = causality_prefix_check(*mats[u], mechs[mn][0], best["cfg"])
print("\n" + "=" * 100)
print(f" TOP CANDIDATO non-momentum: {mn} [{u}] {tag(best['cfg'])}")
print("=" * 100)
print(f" FULL Sharpe {f['sharpe']:.2f} | HOLD {h['sharpe']:.2f} | DD {f['maxdd']*100:.0f}%"
f" | ret {f['ret']*100:+.0f}% | anni+ {pct*100:.0f}% | turnover/y {best['turn']:.0f}")
print(f" corr vs XS01 {best['corrXS']:+.2f} | corr vs TP01 {best['corrTP']:+.2f}")
print(f" CAUSALITA' (prefix-check): ok={caus['ok']} max_tail_diff={caus['max_tail_diff']:.2e}")
print(f" DEFLATED Sharpe (N={len(nm)} trial GLOBALI): {dsr:.3f}"
f" | soglia Sharpe-max-null annualizz. {sr0:.2f} (serve DSR>0.95)")
_, yrs = yr_breadth(daily)
per = [(int(y), round(v, 3)) for y, v in zip([yy for yy, _ in daily.groupby(daily.index.year)], yrs)]
print(f" per-anno: {per}")
helps = (uplifts.get((u, mn), -9) or -9) > 0.10 # uplift combinato full+hold meaningful
robust = dsr > 0.95 and best["hold"] > 0.3 and best["full"] > 0.7 and caus["ok"]
print("\n VERDETTO INDICATIVO:",
"PASS-LEAD (forward-monitor)" if (helps and robust) else
("DEBOLE/forward-monitor" if (helps or (best['full'] > 0.7 and best['hold'] > 0.3)) else "SCARTATO"))
_final_note()
def _final_note():
print("\n CAVEAT (immutabili): storia ~2.5 anni (deflated-Sharpe + multiple-testing), book a molte")
print(" gambe NON eseguibile a $600 -> STAT-MODE / forward-monitor, MAI deploy. Nessuno sleeve")
print(" registrato: questo e' solo lavoro statistico (vincoli del filone C).")
if __name__ == "__main__":
main()