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>
This commit is contained in:
Adriano Dal Pastro
2026-06-29 20:50:33 +00:00
parent 02d875b07b
commit cff5fa2bf5
12 changed files with 2692 additions and 0 deletions
+410
View File
@@ -0,0 +1,410 @@
"""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()