Files
Adriano Dal Pastro cff5fa2bf5 research(sweep): 5 thread paralleli — 0 nuovi sleeve, STATARB-RESID LEAD ortogonale+eseguibile
Ricerca onesta su aree inesplorate (harness altlib+xsec_v2_nonmom, tutti i gate incl.
study_family_honest anti-selection-on-holdout). Branch main, nessun impatto live, test 143/143.

1 XSEC low-risk cousins (MAX/idio-vol/Amihud) -> 1 LEAD (IVOL), STAT-MODE, DSR 0.37<0.95
2 XSEC momentum-structure vs XS01            -> tutto REDUNDANT (sostituire XS01 distrugge hold)
3 Meta-allocazione dinamica (4 sleeve)       -> pesi fissi vincono (gia quasi risk-parity)
4 Segnali ortogonali ETH/BTC (2 gambe)       -> STATARB-RESID + DVOLSPREAD LEAD
5 1-gamba a segnale (MACD/RSI/Supertrend/...) -> 0/12 earns_slot (trend=TP01, MR morta, hedge)

LEAD principale STATARB-RESID (mean-rev residuo ETH-b*BTC, OLS rolling, 2 gambe): primo stream
INSIEME ortogonale (corr->book 0.027, beta-mkt 0.013) ED eseguibile a $600 (haircut ~0, NON
STAT-MODE) -> cadono i 2 muri di XS01/opzioni. Resta solo il muro dell'edge (Sharpe 0.84,
DSR 0.929 same-sign <0.95). Causalita+fee verificate dal coordinatore. Forward-monitor, non sleeve.

Soffitto direzionale ~1.3 riconfermato. Diario 2026-06-29-strategy-search-5threads.md, CLAUDE.md agg.

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

411 lines
20 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""META-ALLOCATION — allocazione DINAMICA CAUSALE tra i 4 sleeve esistenti vs PESI FISSI.
TESI (angolo nuovo, NON un 5o sleeve): il portafoglio attivo combina TP01/XS01/VRP01/SKH01 a
PESO FISSO (41.25/18.75/15/25, rinormalizzati per-riga sugli sleeve attivi — vedi
src/portfolio/portfolio.combined_daily). Domanda: una regola di allocazione DINAMICA e CAUSALE
fra gli stessi 4 sleeve batte i pesi fissi OUT-OF-SAMPLE? Cioe' c'e' meta-alpha di timing di
portafoglio, oltre ai pesi fissi?
MECCANISMI testati (tutti CAUSALI: decisione con dati <= t-1, peso applicato in t; ribilancio
SETTIMANALE con costo sul turnover dei pesi |Δw|*cost_rate, cosi' una regola che ribilancia di
continuo PAGA il suo attrito — non si bara):
1. VOL-PARITY — peso inverso alla vol realizzata rolling (risk-parity causale). Pure + tilt.
2. MOMENTUM-OF-SLEEVES — sovrappesa gli sleeve con Sharpe rolling recente migliore (tilt capato).
3. DISPERSION-REGIME — tilt verso XS01 quando la dispersione cross-section degli alt e' alta
(percentile ESPANDENTE causale), verso il resto altrimenti.
4. DRAWDOWN-CONTROL — riduce l'esposizione aggregata (-> cash) o ribilancia verso VRP/SKH
quando il portafoglio e' in drawdown rolling (causale sull'equity propria).
GATE / ONESTA':
- FULL e HOLD-OUT (2025-01-01+) Sharpe + maxDD, per-anno, turnover dei pesi/anno.
- Confronto vs BASE pesi-fissi sulla STESSA finestra e con lo STESSO motore (entrambi pagano il
costo di ribilancio): il miglioramento deve esserci su HOLD-OUT, non solo FULL.
- MULTI-CUT: uplift dello Sharpe a piu' date di taglio (2022/23/24/25). Robusto solo se positivo
su piu' finestre, non su una sola fortunata.
- DE-LEVERING: lo Sharpe e' scale-invariant. Se uno schema ABBASSA DD/vol ma NON alza lo Sharpe,
il taglio di DD e' solo de-levering (replicabile abbassando la leva di BASE) -> NON e' alpha di
timing. Lo riportiamo esplicitamente confrontando BASE de-levered a pari vol.
VERDETTO per schema: BATTE-FISSO / solo-de-levering / RIDONDANTE / SCARTATO.
uv run python scripts/research/meta_allocation.py
"""
from __future__ import annotations
import sys
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 src.portfolio.sleeves import active_sleeves, XS_UNIVERSE, _HL_DIR
from src.portfolio.portfolio import metrics, yearly, HOLDOUT, DAYS_PER_YEAR
REBAL_DAYS = 7 # ribilancio settimanale
COST_RATE = 0.0005 # 5 bps per-lato sul turnover dei pesi (Deribit taker ~ questo ordine)
VOL_WIN = 60 # finestra vol realizzata (risk-parity)
MOM_WIN = 63 # finestra Sharpe rolling (momentum-of-sleeves, ~1 trimestre)
WARMUP = 90 # giorni di warm-up: prima -> fallback ai pesi fissi
# ----------------------------------------------------------------------------- data
def sleeve_matrix() -> tuple[pd.DatetimeIndex, np.ndarray, np.ndarray, list[str], np.ndarray]:
"""Matrice daily allineata dei 4 sleeve (outer-join). Ritorna (index, R, active, names, fixed_w).
R = rendimenti (0 dove inattivo), active = maschera bool di disponibilita'."""
base = active_sleeves()
names = [s.name for s in base]
fixed_w = np.array([s.weight for s in base], float)
cols = {s.name: s.daily() for s in base}
J = pd.concat(cols, axis=1, join="outer").sort_index()
J = J[J.notna().any(axis=1)]
active = J.notna().values
R = np.nan_to_num(J.values, nan=0.0)
return J.index, R, active, names, fixed_w
def dispersion_series(index: pd.DatetimeIndex) -> np.ndarray:
"""Dispersione cross-section dei rendimenti degli alt Hyperliquid (std cross-section dei ritorni
daily sull'universo XS01), allineata all'index del portafoglio. NaN dove non c'e' dato HL."""
cols = {}
for sym in XS_UNIVERSE:
p = _HL_DIR / f"hl_{sym.lower()}_1d.parquet"
if not p.exists():
continue
d = pd.read_parquet(p)
cols[sym] = pd.Series(d["close"].values.astype(float),
index=pd.to_datetime(d["timestamp"], unit="ms", utc=True))
C = pd.concat(cols, axis=1, join="inner").sort_index().dropna()
ret = C.pct_change()
disp = ret.std(axis=1) # dispersione cross-section per giorno
disp.index = disp.index.normalize()
return disp.reindex(index.normalize()).values
# ----------------------------------------------------------------------------- weight helpers
def _renorm_rows(W: np.ndarray, active: np.ndarray, expo: np.ndarray | None = None) -> np.ndarray:
"""Maschera inattivi -> 0, rinormalizza ogni riga alla esposizione `expo` (default 1)."""
Wm = W * active
rs = Wm.sum(axis=1, keepdims=True)
out = np.divide(Wm, rs, out=np.zeros_like(Wm), where=rs > 0)
if expo is not None:
out = out * expo[:, None]
return out
def base_weights(R, active, fixed_w) -> np.ndarray:
"""Pesi FISSI rinormalizzati per-riga sugli sleeve attivi (replica combined_daily)."""
n, A = R.shape
return _renorm_rows(np.tile(fixed_w, (n, 1)), active)
def add_cash(W: np.ndarray) -> tuple[np.ndarray, np.ndarray]:
"""Appende una colonna CASH (rendimento 0) che assorbe 1 - somma-pesi (per schemi che de-levano).
Ritorna (W_aug, is_cash_active=True)."""
cash = np.clip(1.0 - W.sum(axis=1, keepdims=True), 0.0, 1.0)
return np.hstack([W, cash])
# ----------------------------------------------------------------------------- engine
def simulate(R: np.ndarray, active: np.ndarray, Wtgt: np.ndarray,
period_days: int = REBAL_DAYS, cost_rate: float = COST_RATE) -> dict:
"""Motore di ribilancio PERIODICO realistico, CAUSALE.
Wtgt[t] = pesi-bersaglio decisi con dati <= t-1 (vedi costruttori schemi), una colonna CASH in
coda (rend. 0). Fra un ribilancio e l'altro i pesi DERIVANO col rendimento; ogni `period_days`
si torna al target pagando cost_rate*|v-target|. Il costo grava sul rendimento del giorno.
period_days=1, cost=0 -> rebalance-continuo (= combined_daily)."""
n = R.shape[0]
Raug = np.hstack([R, np.zeros((n, 1))]) # colonna cash
v = Wtgt[0].copy() # equity iniziale = 1.0, allocata al target
out = np.zeros(n)
turn_tot = 0.0
n_rebal = 0
for t in range(n):
E_start = float(v.sum())
if t > 0 and (t % period_days == 0) and E_start > 0:
target = Wtgt[t] * E_start
turn = float(np.abs(v - target).sum())
v = Wtgt[t] * (E_start - cost_rate * turn)
turn_tot += turn / E_start
n_rebal += 1
v = v * (1.0 + Raug[t])
E_end = float(v.sum())
out[t] = E_end / E_start - 1.0 if E_start > 0 else 0.0
years = n / DAYS_PER_YEAR
return dict(daily=pd.Series(out),
turnover_per_year=turn_tot / years if years > 0 else 0.0,
n_rebalances=n_rebal)
# ----------------------------------------------------------------------------- schemes (causal Wtgt builders, with cash col)
def scheme_base(index, R, active, fixed_w, **_):
return add_cash(base_weights(R, active, fixed_w))
def _rolling_vol(R, active, win):
"""Vol realizzata rolling per-sleeve, SHIFTATA di 1 (causale: usa <= t-1)."""
df = pd.DataFrame(np.where(active, R, np.nan))
vol = df.rolling(win, min_periods=max(10, win // 2)).std().shift(1).values
return vol
def scheme_volpar_pure(index, R, active, fixed_w, win=VOL_WIN, **_):
"""Risk-parity puro: w_i ∝ 1/vol_i sugli sleeve attivi (causale). Warm-up -> BASE."""
vol = _rolling_vol(R, active, win)
inv = np.divide(1.0, vol, out=np.zeros_like(vol), where=(vol > 0) & np.isfinite(vol))
W = _renorm_rows(inv, active & np.isfinite(vol) & (vol > 0))
bw = base_weights(R, active, fixed_w)
bad = W.sum(axis=1) <= 0
W[bad] = bw[bad]
W[:WARMUP] = bw[:WARMUP]
return add_cash(W)
def scheme_volpar_tilt(index, R, active, fixed_w, win=VOL_WIN, **_):
"""Tilt dei pesi FISSI per inverso-vol: w_i ∝ fixed_i / vol_i (ancorato ai pesi fissi)."""
vol = _rolling_vol(R, active, win)
inv = np.divide(1.0, vol, out=np.zeros_like(vol), where=(vol > 0) & np.isfinite(vol))
W = _renorm_rows(fixed_w[None, :] * inv, active & np.isfinite(vol) & (vol > 0))
bw = base_weights(R, active, fixed_w)
bad = W.sum(axis=1) <= 0
W[bad] = bw[bad]
W[:WARMUP] = bw[:WARMUP]
return add_cash(W)
def scheme_momentum(index, R, active, fixed_w, win=MOM_WIN, tilt=0.5, cap=0.55, **_):
"""Momentum-of-sleeves: tilt dei pesi fissi per lo Sharpe rolling z-scored (causale), capato.
w_i ∝ fixed_i * (1 + tilt*z_i)+, z = standardizzazione cross-sleeve dello Sharpe rolling.
Cap per non concentrare. Warm-up / regime piatto -> BASE."""
df = pd.DataFrame(np.where(active, R, np.nan))
mu = df.rolling(win, min_periods=win // 2).mean().shift(1).values
sd = df.rolling(win, min_periods=win // 2).std().shift(1).values
sh = np.divide(mu, sd, out=np.full_like(mu, np.nan), where=(sd > 0)) * np.sqrt(DAYS_PER_YEAR)
n, A = R.shape
W = np.zeros((n, A))
bw = base_weights(R, active, fixed_w)
for t in range(n):
m = active[t] & np.isfinite(sh[t])
if m.sum() < 2 or t < WARMUP:
W[t] = bw[t]; continue
z = np.zeros(A); s = sh[t][m]
zsd = s.std()
if zsd > 0:
z[m] = (sh[t][m] - s.mean()) / zsd
raw = fixed_w * np.clip(1.0 + tilt * z, 0.0, None) * m
if raw.sum() <= 0:
W[t] = bw[t]; continue
w = raw / raw.sum()
for _ in range(3): # impone il cap iterando
over = w > cap
if not over.any():
break
excess = (w[over] - cap).sum()
w[over] = cap
room = m & ~over
if room.sum() == 0 or w[room].sum() == 0:
break
w[room] += excess * w[room] / w[room].sum()
W[t] = w / w.sum()
return add_cash(W)
def scheme_dispersion(index, R, active, fixed_w, pct=60, minhist=120, boost=2.0, **_):
"""Dispersion-regime: quando la dispersione cross-section degli alt supera il percentile
ESPANDENTE causale (pct), boost del peso XS01; sotto, XS01 -> 0 e redistribuito. Pesi fissi
altrove. XS01 attivo solo dal 2024 (prima: BASE)."""
disp = dispersion_series(index)
n, A = R.shape
names_idx = 1 # XS01 e' la colonna 1 (vedi active_sleeves)
bw = base_weights(R, active, fixed_w)
W = bw.copy()
hist = []
high = np.zeros(n, bool)
for t in range(n):
d = disp[t - 1] if t > 0 else np.nan # causale: dispersione <= t-1
if np.isfinite(d):
thr = np.percentile(hist, pct) if len(hist) >= minhist else np.inf
high[t] = d >= thr
hist.append(d)
for t in range(n):
if t < WARMUP or not active[t, names_idx]:
continue
raw = fixed_w.copy()
raw[names_idx] *= boost if high[t] else 0.05 # boost XS in regime disperso, quasi-spento altrove
W[t] = _renorm_rows(raw[None, :], active[t][None, :])[0]
return add_cash(W)
def scheme_dd_cash(index, R, active, fixed_w, dd_thr=0.05, floor=0.5, win=0, **_):
"""Drawdown-control (DE-LEVERING esplicito): traccia l'equity di BASE (causale, shiftata),
se il drawdown corrente > dd_thr riduce l'esposizione aggregata a `floor` (resto in CASH).
E' il caso-test del de-levering: ci aspettiamo DD piu' basso ma Sharpe NON piu' alto."""
bw = base_weights(R, active, fixed_w)
base_daily = simulate(R, active, add_cash(bw))["daily"].values
eq = np.cumprod(1.0 + base_daily)
pk = np.maximum.accumulate(eq)
dd = (pk - eq) / pk # drawdown realizzato
expo = np.ones(R.shape[0])
for t in range(R.shape[0]):
d = dd[t - 1] if t > 0 else 0.0 # causale
expo[t] = floor if d > dd_thr else 1.0
expo[:WARMUP] = 1.0
W = bw * expo[:, None]
return add_cash(W)
def scheme_dd_defensive(index, R, active, fixed_w, dd_thr=0.05, **_):
"""Drawdown-control DIFENSIVO: in drawdown ribilancia verso VRP01(2)/SKH01(3) (scorrelati),
via TP01(0)/XS01(1). Pienamente investito (no cash) -> isola il timing dal de-levering."""
bw = base_weights(R, active, fixed_w)
base_daily = simulate(R, active, add_cash(bw))["daily"].values
eq = np.cumprod(1.0 + base_daily)
pk = np.maximum.accumulate(eq)
dd = (pk - eq) / pk
n, A = R.shape
defensive = np.array([0.10, 0.10, 0.35, 0.45]) # VRP/SKH pesati in DD
W = bw.copy()
for t in range(n):
d = dd[t - 1] if t > 0 else 0.0
if t >= WARMUP and d > dd_thr:
W[t] = _renorm_rows(defensive[None, :], active[t][None, :])[0]
return add_cash(W)
SCHEMES = [
("BASE (pesi fissi)", scheme_base),
("VOLPAR pure (1/vol)", scheme_volpar_pure),
("VOLPAR tilt (fix/vol)", scheme_volpar_tilt),
("MOMENTUM-of-sleeves", scheme_momentum),
("DISPERSION-regime->XS", scheme_dispersion),
("DRAWDOWN-ctrl (cash)", scheme_dd_cash),
("DRAWDOWN-ctrl (defens.)", scheme_dd_defensive),
]
CUTS = ["2022-01-01", "2023-01-01", "2024-01-01", "2025-01-01"]
# ----------------------------------------------------------------------------- run
def run():
index, R, active, names, fixed_w = sleeve_matrix()
print("=" * 100)
print(" META-ALLOCATION — allocazione dinamica causale tra i 4 sleeve vs PESI FISSI")
print(f" sleeve: {names}")
print(f" pesi fissi: {dict(zip(names, np.round(fixed_w, 4)))}")
print(f" finestra {index.min().date()} -> {index.max().date()} | n={len(index)} giorni | "
f"hold-out {HOLDOUT.date()}+ | ribilancio {REBAL_DAYS}g | costo {COST_RATE*1e4:.0f}bps/lato")
print("=" * 100)
results = {}
for label, fn in SCHEMES:
Wtgt = fn(index, R, active, fixed_w)
sim = simulate(R, active, Wtgt)
d = pd.Series(sim["daily"].values, index=index)
results[label] = dict(daily=d, turnover=sim["turnover_per_year"], W=Wtgt)
base_d = results["BASE (pesi fissi)"]["daily"]
mb_full = metrics(base_d)
mb_hold = metrics(base_d[base_d.index >= HOLDOUT])
print(f"\n {'SCHEMA':<26s} | {'FULL Sh':>7s} {'CAGR':>7s} {'DD':>6s} | {'HOLD Sh':>7s} {'HOLD ret':>8s} {'DD':>6s} | turn/y")
print(" " + "-" * 96)
for label, _ in SCHEMES:
d = results[label]["daily"]
mf = metrics(d); mh = metrics(d[d.index >= HOLDOUT])
print(f" {label:<26s} | {mf['sharpe']:>7.2f} {mf['cagr']*100:>6.1f}% {mf['maxdd']*100:>5.1f}% | "
f"{mh['sharpe']:>7.2f} {mh['ret']*100:>+7.1f}% {mh['maxdd']*100:>5.1f}% | {results[label]['turnover']:>5.2f}")
print(f"\n delta vs BASE (FULL Sh {mb_full['sharpe']:.2f} / HOLD Sh {mb_hold['sharpe']:.2f}):")
print(f" {'SCHEMA':<26s} | {'ΔFULL Sh':>9s} {'ΔHOLD Sh':>9s} {'ΔFULL DD':>9s} {'ΔHOLD DD':>9s} | corr(BASE)")
print(" " + "-" * 96)
for label, _ in SCHEMES:
if label.startswith("BASE"):
continue
d = results[label]["daily"]
mf = metrics(d); mh = metrics(d[d.index >= HOLDOUT])
corr = float(np.corrcoef(d.values, base_d.values)[0, 1])
print(f" {label:<26s} | {mf['sharpe']-mb_full['sharpe']:>+9.2f} {mh['sharpe']-mb_hold['sharpe']:>+9.2f} "
f"{(mf['maxdd']-mb_full['maxdd'])*100:>+8.1f}% {(mh['maxdd']-mb_hold['maxdd'])*100:>+8.1f}% | {corr:>6.3f}")
# ---- MULTI-CUT: uplift Sharpe a piu' date di taglio (anti-overfit hold-out singolo) ----
print("\n MULTI-CUT — ΔSharpe (schema BASE) su finestre [cut, fine]:")
header = " " + f"{'SCHEMA':<26s} | " + " ".join(f"{c[:4]:>7s}" for c in CUTS)
print(header); print(" " + "-" * (len(header) - 2))
for label, _ in SCHEMES:
if label.startswith("BASE"):
continue
d = results[label]["daily"]
row = []
for c in CUTS:
lo = pd.Timestamp(c, tz="UTC")
sd = metrics(d[d.index >= lo])["sharpe"]
sb = metrics(base_d[base_d.index >= lo])["sharpe"]
row.append(f"{sd-sb:>+7.2f}")
print(f" {label:<26s} | " + " ".join(row))
# ---- DE-LEVERING check: BASE de-levered alla vol dello schema -> stesso DD? ----
print("\n DE-LEVERING check (Sharpe e' scale-invariant: DD-piu'-basso a pari-Sharpe = solo de-lever):")
print(f" {'SCHEMA':<26s} | {'vol/volBASE':>11s} | {'DD schema':>9s} {'DD BASE@volSchema':>18s}")
print(" " + "-" * 70)
vol_base = base_d.std()
dd_base = mb_full["maxdd"]
for label, _ in SCHEMES:
if label.startswith("BASE"):
continue
d = results[label]["daily"]
ratio = d.std() / vol_base if vol_base > 0 else 1.0
# BASE riscalato alla stessa vol dello schema -> il suo DD a quella leva
dd_base_scaled = metrics(base_d * ratio)["maxdd"]
print(f" {label:<26s} | {ratio:>11.3f} | {metrics(d)['maxdd']*100:>8.1f}% {dd_base_scaled*100:>17.1f}%")
# ---- PER-ANNO dei due piu' interessanti vs BASE ----
print("\n PER-ANNO ret% (BASE vs schemi):")
yb = yearly(base_d)
yrs = sorted(yb.keys())
print(" " + f"{'SCHEMA':<26s} | " + " ".join(f"{y:>7d}" for y in yrs))
print(" " + "-" * (28 + 8 * len(yrs)))
for label, _ in SCHEMES:
d = results[label]["daily"]; yd = yearly(d)
print(f" {label:<26s} | " + " ".join(f"{yd.get(y,{'ret':0})['ret']*100:>+6.1f}%" for y in yrs))
# ---- VERDETTI ----
print("\n VERDETTI (BATTE-FISSO richiede ΔHOLD Sh > +0.10 E multi-cut maggioritario positivo E"
" non solo de-levering):")
vol_base = base_d.std()
for label, _ in SCHEMES:
if label.startswith("BASE"):
continue
d = results[label]["daily"]
mf = metrics(d); mh = metrics(d[d.index >= HOLDOUT])
dfull = mf["sharpe"] - mb_full["sharpe"]
dhold = mh["sharpe"] - mb_hold["sharpe"]
cut_ups = []
for c in CUTS:
lo = pd.Timestamp(c, tz="UTC")
cut_ups.append(metrics(d[d.index >= lo])["sharpe"] - metrics(base_d[base_d.index >= lo])["sharpe"])
n_pos = sum(1 for x in cut_ups if x > 0.02)
vr = d.std() / vol_base if vol_base > 0 else 1.0
dd_lower = mf["maxdd"] < mb_full["maxdd"] - 0.005
is_delever = (vr < 0.97) and dd_lower and (dfull <= 0.03) # vol giu', DD giu', Sharpe non meglio
if dhold > 0.10 and dfull > -0.05 and n_pos >= 3:
verdict, why = "BATTE-FISSO", f"ΔHOLD {dhold:+.2f}, multi-cut {n_pos}/4 positivi, FULL non peggiore"
elif (dhold <= -0.10) or (n_pos == 0 and dfull < -0.07):
verdict, why = "SCARTATO", f"peggio OOS (ΔFULL {dfull:+.2f}, ΔHOLD {dhold:+.2f}, multi-cut {n_pos}/4 con turn/y {results[label]['turnover']:.1f})"
elif is_delever:
verdict, why = "solo-de-levering", f"vol {vr:.2f}×BASE, DD {mf['maxdd']*100:.1f}%<{mb_full['maxdd']*100:.1f}% ma Sharpe non meglio (ΔFULL {dfull:+.2f}) -> replicabile abbassando la leva"
else:
why = (f"≈BASE OOS (ΔHOLD {dhold:+.2f}); FULL ΔSh {dfull:+.2f}, ΔDD {(mf['maxdd']-mb_full['maxdd'])*100:+.1f}%"
+ (" [marginale in-sample, nullo su hold-out]" if abs(dfull) >= 0.03 else ""))
verdict = "RIDONDANTE"
print(f" {label:<26s} -> {verdict:<16s} {why}")
print("\n" + "=" * 100)
print(" CONCLUSIONE: vedi i verdetti sopra. Soglia BATTE-FISSO deliberatamente alta (anti-overfit):")
print(" l'allocazione dinamica deve battere i pesi fissi su HOLD-OUT *e* multi-cut, non su una")
print(" finestra fortunata, e non per solo de-levering (replicabile abbassando target_vol/leva).")
print("=" * 100)
if __name__ == "__main__":
run()