harness(marginal): indurisci marginal_vs_tp01 con la lezione dell'onda ortho (17/18 -> 1)
Lo scorer fisso-HOLDOUT + jackknife-mese era ingannabile: 17/18 book relative-value "ADDS"
su una sola finestra 2025 (ETH-bleed dove TP01 è debole). Tre gate nuovi in
altlib.marginal_vs_tp01:
1. persistenza multi-cut (uplift a più date di taglio, non solo 2025) -> robust_oos
2. has_insample_edge: Sharpe standalone PRE-holdout >= 0.5 (la basket faceva 0.29).
null_pctl_* (vs asset-rumore corr-zero) restano come CONTESTO (diversification math).
3. is_hedge: low-corr che paga solo quando TP01 è debole = hedge, non alpha.
Verdetti nuovi HEDGE/NOISE; earns_slot = ADDS + robust_oos + has_insample_edge + not hedge.
Effetto: sull'onda ortho 17/18 "ADDS" -> 1 (dvol_spread, unico con edge in-sample reale 0.57);
gli altri 16 -> NOISE/HEDGE. Un sleeve sintetico Sharpe~1.3 scorrelato resta ADDS (non rigetta
i diversificatori veri). +5 test (noise/hedge/single-regime/high-Sharpe-uncorr/in-sample-edge);
suite 37 passed. CLAUDE.md aggiornato.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+124
-11
@@ -334,14 +334,56 @@ def candidate_daily(target_fn, tf: str = "1d", fee_side: float = FEE_SIDE) -> pd
|
||||
return _to_daily(0.5 * J[CERTIFIED[0]] + 0.5 * J[CERTIFIED[1]])
|
||||
|
||||
|
||||
def _uplift_series(B: pd.Series, C: pd.Series, w: float = 0.25) -> float:
|
||||
"""Sharpe of the (1-w)*TP01 + w*candidate blend minus Sharpe of TP01 alone."""
|
||||
return _sh((1 - w) * B + w * C) - _sh(B)
|
||||
|
||||
|
||||
def _null_uplift_pctl(B: pd.Series, C: pd.Series, w: float = 0.25,
|
||||
n: int = 300, seed: int = 20260621):
|
||||
"""Where does the candidate's blend-uplift sit vs the NULL of a zero-correlation
|
||||
noise asset with the SAME mean & vol? Lesson of 2026-06-21: a low-corr asset with a
|
||||
little positive drift 'adds' ~+0.03 Sharpe by pure diversification MATH — that is not
|
||||
a signal. We draw `n` iid-normal assets (same mean/std as C, independent of B => corr 0
|
||||
by construction), measure each one's uplift, and return (real_uplift, percentile of
|
||||
real vs the null). pctl >= ~0.8 => the uplift is meaningfully above diversification
|
||||
math; pctl ~0.5 => it IS diversification math. Seeded -> deterministic."""
|
||||
Bx, Cx = B.align(C, join="inner")
|
||||
bs, cs = Bx.values.astype(float), Cx.values.astype(float)
|
||||
if len(cs) < 30:
|
||||
return None, None
|
||||
base = _sh(Bx)
|
||||
real = _sh((1 - w) * Bx + w * Cx) - base
|
||||
mu, sd = float(np.nanmean(cs)), float(np.nanstd(cs))
|
||||
if sd == 0:
|
||||
return round(real, 3), None
|
||||
rng = np.random.default_rng(seed)
|
||||
draws = rng.normal(mu, sd, size=(n, len(cs)))
|
||||
blends = (1 - w) * bs[None, :] + w * draws
|
||||
m, s = blends.mean(axis=1), blends.std(axis=1)
|
||||
null = np.where(s > 0, m / s * np.sqrt(365.25), 0.0) - base
|
||||
return round(float(real), 3), round(float(np.mean(null <= real)), 3)
|
||||
|
||||
|
||||
def marginal_vs_tp01(cand_daily: pd.Series, weights=(0.25, 0.5)) -> dict:
|
||||
"""Does this candidate IMPROVE the TP01 portfolio? Returns correlation, blend uplift
|
||||
(full & hold-out, per weight), TP01-beta + residual alpha, and a verdict:
|
||||
ADDS -> meaningfully lifts the OOS blend and is not just leverage-of-trend
|
||||
ADDS -> lifts the blend, PERSISTENTLY (multi-cut), beats the zero-corr noise
|
||||
null, in BOTH TP01-up and TP01-down regimes
|
||||
HEDGE -> low corr but only pays when TP01 is WEAK (a drawdown dampener, not a
|
||||
standing premium): real, but price it as a hedge, not as alpha
|
||||
NOISE -> uplift indistinguishable from a random zero-corr asset (diversification
|
||||
math, not a signal)
|
||||
REDUNDANT -> ~identical to TP01 (corr high, ~zero uplift): a re-skin, no slot
|
||||
DILUTES -> drags the blend down
|
||||
NEUTRAL -> changes little either way (a weak, optional satellite at best)
|
||||
Score a NEW sleeve on THIS, not on absolute Sharpe."""
|
||||
Score a NEW sleeve on THIS, not on absolute Sharpe.
|
||||
|
||||
Hardened 2026-06-21 (ortho wave): the fixed-HOLDOUT uplift + drop-month jackknife was
|
||||
fooled (17/18 relative-value books 'ADDS' on a single 2025 ETH-bleed window). Three
|
||||
gates added: (1) MULTI-CUT persistence (positive uplift at several hold-out starts, not
|
||||
only 2025); (2) NOISE-NULL (uplift must beat a zero-corr random asset); (3) HEDGE vs
|
||||
alpha (a low-corr sleeve that only helps when TP01 is down is a hedge)."""
|
||||
B = tp01_baseline_daily()
|
||||
J = pd.concat({"B": B, "C": cand_daily}, axis=1, join="inner").dropna()
|
||||
if len(J) < 30:
|
||||
@@ -378,12 +420,10 @@ def marginal_vs_tp01(cand_daily: pd.Series, weights=(0.25, 0.5)) -> dict:
|
||||
# the blend uplift to be positive in the earliest CLEAN hold-out year AND to survive a
|
||||
# drop-one-month jackknife. This is lesson #2 of the 2026-06-20 sweep, in code.
|
||||
out["clean_year_uplift"] = out["jackknife_min_uplift"] = None
|
||||
out["robust_oos"] = False
|
||||
robust_h = False
|
||||
if has_h:
|
||||
ww = 0.25
|
||||
|
||||
def _u(sub):
|
||||
return _sh((1 - ww) * sub["B"] + ww * sub["C"]) - _sh(sub["B"])
|
||||
return _uplift_series(sub["B"], sub["C"])
|
||||
yrs = sorted(set(JH.index.year))
|
||||
clean = JH[JH.index.year == yrs[0]]
|
||||
cu = _u(clean) if len(clean) > 20 else None
|
||||
@@ -392,17 +432,79 @@ def marginal_vs_tp01(cand_daily: pd.Series, weights=(0.25, 0.5)) -> dict:
|
||||
if len(months) > 1 else _u(JH))
|
||||
out["clean_year_uplift"] = round(cu, 3) if cu is not None else None
|
||||
out["jackknife_min_uplift"] = round(jk, 3) if jk is not None else None
|
||||
out["robust_oos"] = bool(cu is not None and cu > 0.02 and jk is not None and jk > 0.0)
|
||||
# verdict (weight 0.25 = a satellite slot; hold-out is what the defensive stack cares about)
|
||||
robust_h = bool(cu is not None and cu > 0.02 and jk is not None and jk > 0.0)
|
||||
|
||||
# --- GATE 1: MULTI-CUT PERSISTENCE -------------------------------------------------
|
||||
# Uplift at the start of each year (not only the fixed HOLDOUT). A real edge adds at
|
||||
# SEVERAL cuts incl. an early one; a regime artifact only adds at the latest window.
|
||||
mc = {}
|
||||
for y in sorted(set(J.index.year))[1:]:
|
||||
sub = J[J.index >= pd.Timestamp(f"{y}-01-01", tz="UTC")]
|
||||
if len(sub) >= 120:
|
||||
mc[y] = round(_uplift_series(sub["B"], sub["C"]), 3)
|
||||
out["multicut_uplift"] = mc
|
||||
pos = [u for u in mc.values() if u > 0]
|
||||
earliest = mc[min(mc)] if mc else None
|
||||
multicut_persistent = bool(len(mc) >= 2 and len(pos) / len(mc) >= 0.6
|
||||
and earliest is not None and earliest > 0.0)
|
||||
out["multicut_persistent"] = multicut_persistent
|
||||
|
||||
# --- GATE 2: NOISE-NULL (uplift must beat a random zero-corr asset) -----------------
|
||||
JI = J[J.index < HOLDOUT] # in-sample part (not the lucky recent window)
|
||||
real_is, pctl_is = _null_uplift_pctl(JI["B"], JI["C"]) if len(JI) >= 60 else (None, None)
|
||||
real_f, pctl_f = _null_uplift_pctl(J["B"], J["C"])
|
||||
cand_is_sharpe = round(_sh(JI["C"]), 3) if len(JI) >= 60 else None
|
||||
out["null_pctl_insample"] = pctl_is
|
||||
out["null_pctl_full"] = pctl_f
|
||||
out["cand_insample_sharpe"] = cand_is_sharpe
|
||||
# A candidate must STAND ON ITS OWN before the hold-out: a real in-sample standalone
|
||||
# Sharpe. The ortho basket's in-sample Sharpe was 0.29 -> its only "value" was the
|
||||
# diversification math of a near-zero-Sharpe stream, dressed up by the lucky 2025 window.
|
||||
# (null_pctl_* are reported as the diversification-math context: a low-corr asset adds
|
||||
# ~+0.03 Sharpe by math, so pctl~0.5 just means "no TP01-specific timing" — true of GOOD
|
||||
# and BAD uncorrelated sleeves alike, so it can't be the gate. The in-sample edge is.)
|
||||
has_insample_edge = (cand_is_sharpe is None) or (cand_is_sharpe >= 0.5)
|
||||
out["has_insample_edge"] = bool(has_insample_edge)
|
||||
out["beats_noise_null"] = bool(has_insample_edge) # back-compat alias for the gate
|
||||
|
||||
# --- GATE 3: HEDGE vs ALPHA (does it only pay when TP01 is weak?) -------------------
|
||||
yr_sh, yr_up = [], []
|
||||
for y in sorted(set(J.index.year)):
|
||||
sub = J[J.index.year == y]
|
||||
if len(sub) >= 40:
|
||||
yr_sh.append(_sh(sub["B"])); yr_up.append(_uplift_series(sub["B"], sub["C"]))
|
||||
hedge_corr = (round(float(np.corrcoef(yr_sh, yr_up)[0, 1]), 3)
|
||||
if len(yr_sh) >= 3 and np.std(yr_sh) > 0 and np.std(yr_up) > 0 else None)
|
||||
trail = J["B"].rolling(60, min_periods=20).sum().shift(1)
|
||||
up_seg, dn_seg = J[trail > 0], J[trail <= 0]
|
||||
u_up = _uplift_series(up_seg["B"], up_seg["C"]) if len(up_seg) > 30 else None
|
||||
u_dn = _uplift_series(dn_seg["B"], dn_seg["C"]) if len(dn_seg) > 30 else None
|
||||
out["hedge_yearly_corr"] = hedge_corr
|
||||
out["uplift_tp01_up"] = round(u_up, 3) if u_up is not None else None
|
||||
out["uplift_tp01_down"] = round(u_dn, 3) if u_dn is not None else None
|
||||
is_hedge = bool(hedge_corr is not None and hedge_corr < -0.5
|
||||
and u_up is not None and u_up <= 0.0
|
||||
and u_dn is not None and u_dn > 0.05)
|
||||
out["is_hedge"] = is_hedge
|
||||
|
||||
# robust_oos now REQUIRES multi-cut persistence (kills the single-window winners)
|
||||
out["robust_oos"] = bool(robust_h and multicut_persistent)
|
||||
|
||||
# --- VERDICT ----------------------------------------------------------------------
|
||||
up_h = blends["w25"]["uplift_hold"]
|
||||
up_f = blends["w25"]["uplift_full"]
|
||||
ch = out["corr_hold"] if out["corr_hold"] is not None else out["corr_full"]
|
||||
if out["corr_full"] > 0.9 and (up_h is None or abs(up_h) < 0.05):
|
||||
v = "REDUNDANT"
|
||||
elif up_h is not None and up_h >= 0.05 and up_f > -0.15 and ch < 0.85:
|
||||
v = "ADDS"
|
||||
elif up_f <= -0.10 and (up_h is None or up_h <= 0.0):
|
||||
v = "DILUTES"
|
||||
elif is_hedge:
|
||||
v = "HEDGE"
|
||||
elif not has_insample_edge:
|
||||
v = "NOISE"
|
||||
elif (up_h is not None and up_h >= 0.05 and up_f > -0.15 and ch < 0.85
|
||||
and multicut_persistent):
|
||||
v = "ADDS"
|
||||
else:
|
||||
v = "NEUTRAL"
|
||||
out["marginal_verdict"] = v
|
||||
@@ -416,8 +518,12 @@ def study_marginal(name: str, target_fn, tf: str = "1d", fee_side: float = FEE_S
|
||||
absolute = study_weights(name, target_fn, tfs=(tf,))
|
||||
marg = marginal_vs_tp01(candidate_daily(target_fn, tf=tf, fee_side=fee_side))
|
||||
abs_grade = absolute["verdict"]["grade"]
|
||||
# ADDS already embeds multi-cut + beats-null + not-hedge; we also require robust_oos
|
||||
# (multi-cut robustness) explicitly. A HEDGE/NOISE/NEUTRAL never earns a live slot.
|
||||
earns_slot = (abs_grade != "FAIL" and marg.get("marginal_verdict") == "ADDS"
|
||||
and marg.get("robust_oos", False))
|
||||
and marg.get("robust_oos", False)
|
||||
and marg.get("beats_noise_null", False)
|
||||
and not marg.get("is_hedge", False))
|
||||
return dict(name=name, tf=tf, absolute=absolute, marginal=marg,
|
||||
abs_grade=abs_grade, marginal_verdict=marg.get("marginal_verdict"),
|
||||
earns_slot=earns_slot)
|
||||
@@ -432,6 +538,13 @@ def fmt_marginal(rep: dict) -> str:
|
||||
f"beta {m.get('beta_to_tp01')} resid Sharpe {m.get('resid_sharpe_full')} alpha/yr {m.get('alpha_ann')}")
|
||||
lines.append(f" OOS robustness: clean-year uplift {m.get('clean_year_uplift')} "
|
||||
f"drop-best-month {m.get('jackknife_min_uplift')} robust_oos={m.get('robust_oos')}")
|
||||
lines.append(f" multi-cut persistence: {m.get('multicut_uplift')} persistent={m.get('multicut_persistent')}")
|
||||
lines.append(f" in-sample edge: standalone Sharpe {m.get('cand_insample_sharpe')} "
|
||||
f"has_insample_edge={m.get('has_insample_edge')} "
|
||||
f"(diversification-math null pctl in-sample {m.get('null_pctl_insample')} full {m.get('null_pctl_full')})")
|
||||
lines.append(f" hedge check: yearly corr(TP01-Sh, uplift) {m.get('hedge_yearly_corr')} "
|
||||
f"uplift TP01-up {m.get('uplift_tp01_up')} / TP01-down {m.get('uplift_tp01_down')} "
|
||||
f"is_hedge={m.get('is_hedge')}")
|
||||
lines.append(f" standalone: TP01 full {m.get('tp01_full_sharpe')}/hold {m.get('tp01_hold_sharpe')} | "
|
||||
f"cand full {m.get('cand_full_sharpe')}/hold {m.get('cand_hold_sharpe')}")
|
||||
for w, d in bl.items():
|
||||
|
||||
Reference in New Issue
Block a user