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:
@@ -106,6 +106,17 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis
|
|||||||
(marginal==ADDS)`. **Regola: una nuova strategia direzionale si giudica su `earns_slot`, non sullo
|
(marginal==ADDS)`. **Regola: una nuova strategia direzionale si giudica su `earns_slot`, non sullo
|
||||||
Sharpe assoluto** (gli overlay-su-TSMOM ereditano lo Sharpe di trend e prendono PASS fasulli —
|
Sharpe assoluto** (gli overlay-su-TSMOM ereditano lo Sharpe di trend e prendono PASS fasulli —
|
||||||
es. CMB04 PASS assoluto → NEUTRAL marginale). Demo `marginal_demo.py`, test `tests/test_marginal_scorer.py`.
|
es. CMB04 PASS assoluto → NEUTRAL marginale). Demo `marginal_demo.py`, test `tests/test_marginal_scorer.py`.
|
||||||
|
⚠️ **INDURITO 2026-06-21 (onda ortho):** la versione 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 `marginal_vs_tp01`: **(1) persistenza multi-cut** (uplift positivo a più
|
||||||
|
date di taglio, non solo 2025); **(2) edge in-sample** (`has_insample_edge`: lo Sharpe standalone
|
||||||
|
PRE-holdout dev'essere ≥0.5 — un low-corr a Sharpe ~0.3 "aggiunge" solo matematica di
|
||||||
|
diversificazione, riportata via `null_pctl_*` vs un asset-rumore a corr-zero); **(3) hedge vs
|
||||||
|
alpha** (`is_hedge`: un low-corr che paga SOLO quando TP01 è debole — `corr(Sharpe-TP01, uplift
|
||||||
|
annuo)` molto negativa — è un hedge, non alpha). Verdetti nuovi: HEDGE, NOISE. Sull'onda ortho lo
|
||||||
|
scorer indurito collassa 17/18 → **1** (`dvol_spread`, unico con edge in-sample reale; comunque
|
||||||
|
forward-monitor per multiple-testing/storia DVOL corta). Lezione: un nuovo sleeve si giudica su
|
||||||
|
edge-in-sample + persistenza multi-cut + non-hedge, non sull'uplift di una finestra fortunata.
|
||||||
- **Onestà sul target €50/giorno:** NON raggiungibile su 2000 in 1-2 anni (servono ~130k di
|
- **Onestà sul target €50/giorno:** NON raggiungibile su 2000 in 1-2 anni (servono ~130k di
|
||||||
capitale o un DD da rovina). La leva non è la scorciatoia; la via è target-vol + capitale +
|
capitale o un DD da rovina). La leva non è la scorciatoia; la via è target-vol + capitale +
|
||||||
tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000.
|
tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000.
|
||||||
|
|||||||
@@ -79,3 +79,21 @@ da monitorare, non alpha da eseguire — e la versione a 2 asset è ancora più
|
|||||||
|
|
||||||
File: `scripts/research/ortho/{ortholib,ortho_score,meta_ortho,sleeve_rv}.py`,
|
File: `scripts/research/ortho/{ortholib,ortho_score,meta_ortho,sleeve_rv}.py`,
|
||||||
`agents/agent_00..17_*.py`, `ortho_leaderboard.json`, skeptic `skeptic_{basket,regime,null}.py`.
|
`agents/agent_00..17_*.py`, `ortho_leaderboard.json`, skeptic `skeptic_{basket,regime,null}.py`.
|
||||||
|
|
||||||
|
## AGGIORNAMENTO — lezione codificata in `altlib.marginal_vs_tp01` (stesso giorno)
|
||||||
|
|
||||||
|
I tre gate sono ora **codice**, non solo prosa (test `tests/test_marginal_scorer.py`, +5 test):
|
||||||
|
1. **persistenza multi-cut** (`multicut_uplift`/`multicut_persistent`): uplift a ogni inizio anno,
|
||||||
|
non solo all'HOLDOUT fisso → uccide i 2025-only (es. `kalman_spread`, negativo a ogni cut pre-2025).
|
||||||
|
2. **edge in-sample** (`has_insample_edge`): lo Sharpe standalone PRE-holdout dev'essere ≥0.5. È il
|
||||||
|
discriminante onesto (la basket faceva 0.29). I `null_pctl_*` (vs asset-rumore a corr-zero) restano
|
||||||
|
come CONTESTO — mostrano che un low-corr "aggiunge" ~+0.03 per matematica, vero per sleeve buoni e
|
||||||
|
cattivi, quindi non possono essere IL gate; l'edge in-sample sì.
|
||||||
|
3. **hedge vs alpha** (`is_hedge`): `corr(Sharpe-TP01, uplift annuo)` molto negativa + paga solo
|
||||||
|
quando TP01 è giù → HEDGE, non alpha.
|
||||||
|
|
||||||
|
Verdetti nuovi **HEDGE** e **NOISE**; `earns_slot` ora pretende ADDS + robust_oos + has_insample_edge
|
||||||
|
+ not is_hedge. **Sull'onda ortho lo scorer indurito ribalta 17/18 "ADDS" → 1** (`dvol_spread`, unico
|
||||||
|
con edge in-sample reale 0.57; gli altri 16 → NOISE/HEDGE). Controllo: un sleeve sintetico Sharpe~1.3
|
||||||
|
scorrelato resta **ADDS** (non rigetta i diversificatori veri — XS01-like). La verifica avversariale
|
||||||
|
di 3 giorni è ora una chiamata di funzione.
|
||||||
|
|||||||
+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]])
|
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:
|
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
|
"""Does this candidate IMPROVE the TP01 portfolio? Returns correlation, blend uplift
|
||||||
(full & hold-out, per weight), TP01-beta + residual alpha, and a verdict:
|
(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
|
REDUNDANT -> ~identical to TP01 (corr high, ~zero uplift): a re-skin, no slot
|
||||||
DILUTES -> drags the blend down
|
DILUTES -> drags the blend down
|
||||||
NEUTRAL -> changes little either way (a weak, optional satellite at best)
|
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()
|
B = tp01_baseline_daily()
|
||||||
J = pd.concat({"B": B, "C": cand_daily}, axis=1, join="inner").dropna()
|
J = pd.concat({"B": B, "C": cand_daily}, axis=1, join="inner").dropna()
|
||||||
if len(J) < 30:
|
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
|
# 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.
|
# 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["clean_year_uplift"] = out["jackknife_min_uplift"] = None
|
||||||
out["robust_oos"] = False
|
robust_h = False
|
||||||
if has_h:
|
if has_h:
|
||||||
ww = 0.25
|
|
||||||
|
|
||||||
def _u(sub):
|
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))
|
yrs = sorted(set(JH.index.year))
|
||||||
clean = JH[JH.index.year == yrs[0]]
|
clean = JH[JH.index.year == yrs[0]]
|
||||||
cu = _u(clean) if len(clean) > 20 else None
|
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))
|
if len(months) > 1 else _u(JH))
|
||||||
out["clean_year_uplift"] = round(cu, 3) if cu is not None else None
|
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["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)
|
robust_h = 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)
|
|
||||||
|
# --- 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_h = blends["w25"]["uplift_hold"]
|
||||||
up_f = blends["w25"]["uplift_full"]
|
up_f = blends["w25"]["uplift_full"]
|
||||||
ch = out["corr_hold"] if out["corr_hold"] is not None else out["corr_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):
|
if out["corr_full"] > 0.9 and (up_h is None or abs(up_h) < 0.05):
|
||||||
v = "REDUNDANT"
|
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):
|
elif up_f <= -0.10 and (up_h is None or up_h <= 0.0):
|
||||||
v = "DILUTES"
|
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:
|
else:
|
||||||
v = "NEUTRAL"
|
v = "NEUTRAL"
|
||||||
out["marginal_verdict"] = v
|
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,))
|
absolute = study_weights(name, target_fn, tfs=(tf,))
|
||||||
marg = marginal_vs_tp01(candidate_daily(target_fn, tf=tf, fee_side=fee_side))
|
marg = marginal_vs_tp01(candidate_daily(target_fn, tf=tf, fee_side=fee_side))
|
||||||
abs_grade = absolute["verdict"]["grade"]
|
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"
|
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,
|
return dict(name=name, tf=tf, absolute=absolute, marginal=marg,
|
||||||
abs_grade=abs_grade, marginal_verdict=marg.get("marginal_verdict"),
|
abs_grade=abs_grade, marginal_verdict=marg.get("marginal_verdict"),
|
||||||
earns_slot=earns_slot)
|
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')}")
|
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')} "
|
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')}")
|
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')} | "
|
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')}")
|
f"cand full {m.get('cand_full_sharpe')}/hold {m.get('cand_hold_sharpe')}")
|
||||||
for w, d in bl.items():
|
for w, d in bl.items():
|
||||||
|
|||||||
@@ -47,3 +47,76 @@ def test_call_target_passes_asset_when_accepted():
|
|||||||
|
|
||||||
al.candidate_daily(two_arg)
|
al.candidate_daily(two_arg)
|
||||||
assert seen == {"BTC": True, "ETH": True}
|
assert seen == {"BTC": True, "ETH": True}
|
||||||
|
|
||||||
|
|
||||||
|
# --- hardening gates (2026-06-21 ortho wave) -------------------------------------------
|
||||||
|
# A low-corr asset adds ~+0.03 Sharpe by pure diversification MATH; a sleeve that only
|
||||||
|
# pays when TP01 is weak is a HEDGE; an edge living in one late window is NOT robust.
|
||||||
|
# These pin that the scorer rejects all three, but still PASSES a real high-Sharpe
|
||||||
|
# uncorrelated sleeve (the diversification of a genuine return stream IS value).
|
||||||
|
import pandas as pd # noqa: E402
|
||||||
|
|
||||||
|
|
||||||
|
def _tp01_index():
|
||||||
|
return al.tp01_baseline_daily().index
|
||||||
|
|
||||||
|
|
||||||
|
def test_noise_sleeve_is_not_adds():
|
||||||
|
"""Near-zero-Sharpe, zero-corr asset = diversification math, not signal -> not ADDS."""
|
||||||
|
idx = _tp01_index()
|
||||||
|
rng = np.random.default_rng(1)
|
||||||
|
c = pd.Series(rng.normal(0.0001, 0.02, len(idx)), index=idx)
|
||||||
|
m = al.marginal_vs_tp01(c)
|
||||||
|
assert m["marginal_verdict"] != "ADDS"
|
||||||
|
assert m["beats_noise_null"] is False
|
||||||
|
|
||||||
|
|
||||||
|
def test_high_sharpe_uncorrelated_is_not_rejected_as_noise():
|
||||||
|
"""A genuine Sharpe~1.3 uncorrelated sleeve (XS01-like) must NOT be called NOISE."""
|
||||||
|
idx = _tp01_index()
|
||||||
|
rng = np.random.default_rng(2)
|
||||||
|
c = pd.Series(rng.normal(0.0011, 0.013, len(idx)), index=idx)
|
||||||
|
m = al.marginal_vs_tp01(c)
|
||||||
|
assert m["beats_noise_null"] is True
|
||||||
|
assert m["marginal_verdict"] == "ADDS"
|
||||||
|
|
||||||
|
|
||||||
|
def test_hedge_only_when_tp01_weak_is_flagged():
|
||||||
|
"""A sleeve that pays only when TP01 trails down is a HEDGE, not alpha -> no slot."""
|
||||||
|
B = al.tp01_baseline_daily()
|
||||||
|
trail = B.rolling(60, min_periods=20).sum().shift(1)
|
||||||
|
base = np.where(trail.values <= 0, 0.004, -0.001)
|
||||||
|
rng = np.random.default_rng(4)
|
||||||
|
c = pd.Series(base + rng.normal(0, 0.004, len(B)), index=B.index)
|
||||||
|
m = al.marginal_vs_tp01(c)
|
||||||
|
assert m["is_hedge"] is True
|
||||||
|
assert m["marginal_verdict"] == "HEDGE"
|
||||||
|
|
||||||
|
|
||||||
|
def test_flat_in_sample_late_edge_earns_no_slot():
|
||||||
|
"""An edge FLAT in-sample that only appears in the recent window has NO standalone
|
||||||
|
in-sample merit -> NOISE (not ADDS), no slot. This is the ortho 2025-window pattern."""
|
||||||
|
B = al.tp01_baseline_daily()
|
||||||
|
rng = np.random.default_rng(3)
|
||||||
|
c = pd.Series(0.0, index=B.index) # exactly flat in-sample
|
||||||
|
hold = B.index >= al.HOLDOUT
|
||||||
|
c[hold] = 0.004 + rng.normal(0, 0.002, int(hold.sum())) # edge only in the hold-out
|
||||||
|
m = al.marginal_vs_tp01(c)
|
||||||
|
assert m["has_insample_edge"] is False
|
||||||
|
assert m["marginal_verdict"] == "NOISE"
|
||||||
|
assert al.marginal_vs_tp01(c)["marginal_verdict"] != "ADDS"
|
||||||
|
|
||||||
|
|
||||||
|
def test_pre_holdout_underperformer_earns_no_slot():
|
||||||
|
"""A sleeve with no real in-sample standalone edge (it bleeds before the hold-out and
|
||||||
|
only wins in the recent window) never earns a slot — the 2025-only ortho pattern."""
|
||||||
|
B = al.tp01_baseline_daily()
|
||||||
|
early = B.index < al.HOLDOUT
|
||||||
|
rng = np.random.default_rng(5)
|
||||||
|
c = pd.Series(0.0, index=B.index)
|
||||||
|
c[early] = -0.0008 + rng.normal(0, 0.012, int(early.sum())) # weak/negative in-sample
|
||||||
|
c[~early] = 0.010 + rng.normal(0, 0.012, int((~early).sum())) # strong only post-hold-out
|
||||||
|
m = al.marginal_vs_tp01(c)
|
||||||
|
assert m["cand_insample_sharpe"] < 0.5
|
||||||
|
assert m["has_insample_edge"] is False
|
||||||
|
assert m["marginal_verdict"] != "ADDS"
|
||||||
|
|||||||
Reference in New Issue
Block a user