From d5dd6f4b7222868b5f07628bef69293e5337f4fa Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 21 Jun 2026 15:22:58 +0000 Subject: [PATCH] harness(causality): guardia look-ahead + calendar-artifact self-policing nel lab intraday MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - altlib.causality_ok(target_fn, tf): online-consistency guard (ricalcola il target su un prefisso, la coda deve combaciare col full). eval_weights shifta la posizione ma non vede una feature non-causale (finestra centrata/shift(-k)/stat full-sample) -> questa sì. - intra_score integra DUE gate prima/dopo lo scoring: causality (leak -> LEAK, squalificato) e day_boundary_robust (ARTIFACT-RISK -> fuori dagli slot). Effetto sul leaderboard intraday: open_drive + weekly_seasonality + overnight -> CAL-ARTIFACT (da soli, niente skeptic); prevday_range_breakout resta (ROBUST). earns_slot 10 -> 8. - +2 test (causal-ok / leak), suite intera verde. Il lab intraday ora auto-becca leak e artefatti-calendario che ieri richiedevano 3 scettici. Chiude la 3a lezione harness dell'onda intraday. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-06-21-intraday-microstructure.md | 9 +- scripts/research/alt/altlib.py | 30 ++++ .../research/intraday/intra_leaderboard.json | 160 ++++++++++++------ scripts/research/intraday/intra_score.py | 26 ++- tests/test_harness_realism.py | 21 +++ 5 files changed, 187 insertions(+), 59 deletions(-) diff --git a/docs/diary/2026-06-21-intraday-microstructure.md b/docs/diary/2026-06-21-intraday-microstructure.md index 6ad90a5..526b5f8 100644 --- a/docs/diary/2026-06-21-intraday-microstructure.md +++ b/docs/diary/2026-06-21-intraday-microstructure.md @@ -76,8 +76,13 @@ short-leg/regime-2025. Trattamento = come `dvol_spread` / XS01 / STA05. 2. ✅ **`altlib.eval_weights_smallcap(df, target, capital=600, min_order=5)`** — salta i ribilanciamenti sub-min_order (la finzione del micro-trading a $600), riporta lo Sharpe haircut reale vs modellato. Vale per ogni sleeve a questo capitale, TP01 incluso. Test idem. -3. ⏳ **Causality guard nel lab intraday**: qui fatta a mano (max_tail_diff = 0 su tutti e 5). Da - integrare in `intra_score` come in blind/ortho (non ancora codificata). +3. ✅ **`altlib.causality_ok(target_fn, tf)`** — guardia look-ahead/online-consistency (ricalcola + il target su un prefisso e pretende che la coda combaci con il full): eval_weights shifta la + posizione ma NON vede una feature non-causale (finestra centrata / shift(-k) / stat full-sample). + Integrata in `intra_score` (un leak è squalificato prima dello scoring). + il calendar-artifact + gate (`day_boundary_robust`) ora gira dentro `intra_score`: **open_drive/weekly_seasonality/ + overnight → CAL-ARTIFACT, fuori dagli slot da soli**; prevday_breakout resta (ROBUST). Il lab + intraday ora auto-becca leak e artefatti-calendario che ieri richiedevano gli scettici. Test idem. File: `scripts/research/intraday/{intra_score,meta_intra,verify_intra}.py`, `agents/agent_00..15_*.py`, `intra_leaderboard.json`. diff --git a/scripts/research/alt/altlib.py b/scripts/research/alt/altlib.py index 4ad8a52..a7ce9e4 100644 --- a/scripts/research/alt/altlib.py +++ b/scripts/research/alt/altlib.py @@ -640,6 +640,36 @@ def eval_weights_smallcap(df: pd.DataFrame, target, capital: float = 600.0, executed_turnover_per_year=round(float(turn.sum() / (len(turn) / bpy_d)), 1)) +def causality_ok(target_fn, tf: str = "1h", assets=CERTIFIED, + tail: int = 80, tol: float = 1e-3) -> dict: + """Online-consistency / LOOK-AHEAD guard for a continuous target_fn(df) [or (df, asset)]. + eval_weights SHIFTS the position so you cannot leak by multiplying a weight by the SAME + bar's return — but it does NOT verify the FEATURE construction is causal: a centered + window, a .shift(-k), or a full-sample statistic would pass eval_weights yet peek at the + future. Here we recompute the target on a TRUNCATED prefix and require its tail to MATCH + target(full)[:cut] (the bars a deployable signal would have emitted in real time). Any + future-peeking diverges. Run this in every altlib-based lab (blind/ortho already do).""" + worst = 0.0; bad = False; checked = 0 + for a in assets: + df = get(a, tf) + full = np.nan_to_num(np.asarray(_call_target(target_fn, df, a), float)) + n = len(df) + for cut in (int(n * 0.80), int(n * 0.92)): + if cut <= tail + 5 or cut >= n: + continue + sub = df.iloc[:cut].reset_index(drop=True) + s = np.nan_to_num(np.asarray(_call_target(target_fn, sub, a), float)) + if len(s) != cut: + bad = True + continue + d = np.abs(s[cut - tail:cut] - full[cut - tail:cut]) + worst = max(worst, float(np.max(d)) if len(d) else 0.0) + checked += 1 + return dict(ok=bool((not bad) and worst <= tol), + max_tail_diff=round(worst, 8), checked=checked, + reason=("length-mismatch on prefix" if bad else None)) + + # =========================================================================== # DRIVERS — run a hypothesis across both assets, several TFs, with a fee sweep. # =========================================================================== diff --git a/scripts/research/intraday/intra_leaderboard.json b/scripts/research/intraday/intra_leaderboard.json index 884521f..829559b 100644 --- a/scripts/research/intraday/intra_leaderboard.json +++ b/scripts/research/intraday/intra_leaderboard.json @@ -2,6 +2,7 @@ { "name": "agent_13_range_compression_intra", "tf": "1h", + "causal": true, "abs_grade": "PASS", "marginal_verdict": "ADDS", "earns_slot": true, @@ -18,32 +19,14 @@ "abs_hold_sharpe": 0.928, "turnover_per_year": 13.4, "fee020_full_sharpe": 0.488, - "fee_survives": true - }, - { - "name": "agent_05_open_drive", - "tf": "1h", - "abs_grade": "PASS", - "marginal_verdict": "ADDS", - "earns_slot": true, - "corr_full": 0.133, - "corr_hold": -0.055, - "uplift_hold": 0.716, - "uplift_full": 0.232, - "cand_insample_sharpe": 0.924, - "has_insample_edge": true, - "is_hedge": false, - "robust_oos": true, - "multicut_persistent": true, - "abs_full_sharpe": 0.678, - "abs_hold_sharpe": 1.052, - "turnover_per_year": 21.4, - "fee020_full_sharpe": 0.641, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 }, { "name": "agent_09_prevday_range_breakout", "tf": "1h", + "causal": true, "abs_grade": "PASS", "marginal_verdict": "ADDS", "earns_slot": true, @@ -60,32 +43,14 @@ "abs_hold_sharpe": 0.916, "turnover_per_year": 56.2, "fee020_full_sharpe": 0.985, - "fee_survives": true - }, - { - "name": "agent_11_weekly_seasonality", - "tf": "1h", - "abs_grade": "PASS", - "marginal_verdict": "ADDS", - "earns_slot": true, - "corr_full": 0.44, - "corr_hold": 0.32, - "uplift_hold": 0.402, - "uplift_full": 0.328, - "cand_insample_sharpe": 1.728, - "has_insample_edge": true, - "is_hedge": false, - "robust_oos": true, - "multicut_persistent": true, - "abs_full_sharpe": 1.418, - "abs_hold_sharpe": 0.861, - "turnover_per_year": 86.0, - "fee020_full_sharpe": 1.262, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "ROBUST", + "boundary_spread": 0.196 }, { "name": "agent_06_vol_event_revert_15m", "tf": "15m", + "causal": true, "abs_grade": "PASS", "marginal_verdict": "ADDS", "earns_slot": true, @@ -102,11 +67,14 @@ "abs_hold_sharpe": 0.949, "turnover_per_year": 13.8, "fee020_full_sharpe": 0.593, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 }, { "name": "agent_07_volume_spike_revert", "tf": "1h", + "causal": true, "abs_grade": "WEAK", "marginal_verdict": "ADDS", "earns_slot": true, @@ -123,11 +91,14 @@ "abs_hold_sharpe": 0.096, "turnover_per_year": 25.3, "fee020_full_sharpe": 0.349, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 }, { "name": "agent_14_multi_session_momentum", "tf": "1h", + "causal": true, "abs_grade": "PASS", "marginal_verdict": "ADDS", "earns_slot": true, @@ -144,11 +115,14 @@ "abs_hold_sharpe": 0.436, "turnover_per_year": 28.8, "fee020_full_sharpe": 1.144, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "ROBUST", + "boundary_spread": 0.022 }, { "name": "agent_08_gap_fill", "tf": "1h", + "causal": true, "abs_grade": "PASS", "marginal_verdict": "ADDS", "earns_slot": true, @@ -165,11 +139,14 @@ "abs_hold_sharpe": 0.432, "turnover_per_year": 11.6, "fee020_full_sharpe": 0.501, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 }, { "name": "agent_12_close_location", "tf": "1h", + "causal": true, "abs_grade": "PASS", "marginal_verdict": "ADDS", "earns_slot": true, @@ -186,11 +163,14 @@ "abs_hold_sharpe": 0.501, "turnover_per_year": 14.4, "fee020_full_sharpe": 1.166, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 }, { "name": "agent_10_trend_quality_intra", "tf": "1h", + "causal": true, "abs_grade": "PASS", "marginal_verdict": "ADDS", "earns_slot": true, @@ -207,11 +187,62 @@ "abs_hold_sharpe": 0.535, "turnover_per_year": 13.5, "fee020_full_sharpe": 0.807, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 + }, + { + "name": "agent_05_open_drive", + "tf": "1h", + "causal": true, + "abs_grade": "PASS", + "marginal_verdict": "ADDS", + "earns_slot": false, + "corr_full": 0.133, + "corr_hold": -0.055, + "uplift_hold": 0.716, + "uplift_full": 0.232, + "cand_insample_sharpe": 0.924, + "has_insample_edge": true, + "is_hedge": false, + "robust_oos": true, + "multicut_persistent": true, + "abs_full_sharpe": 0.678, + "abs_hold_sharpe": 1.052, + "turnover_per_year": 21.4, + "fee020_full_sharpe": 0.641, + "fee_survives": true, + "boundary_verdict": "ARTIFACT-RISK", + "boundary_spread": 0.484 + }, + { + "name": "agent_11_weekly_seasonality", + "tf": "1h", + "causal": true, + "abs_grade": "PASS", + "marginal_verdict": "ADDS", + "earns_slot": false, + "corr_full": 0.44, + "corr_hold": 0.32, + "uplift_hold": 0.402, + "uplift_full": 0.328, + "cand_insample_sharpe": 1.728, + "has_insample_edge": true, + "is_hedge": false, + "robust_oos": true, + "multicut_persistent": true, + "abs_full_sharpe": 1.418, + "abs_hold_sharpe": 0.861, + "turnover_per_year": 86.0, + "fee020_full_sharpe": 1.262, + "fee_survives": true, + "boundary_verdict": "ARTIFACT-RISK", + "boundary_spread": 0.456 }, { "name": "agent_04_intraday_range_size", "tf": "1h", + "causal": true, "abs_grade": "PASS", "marginal_verdict": "REDUNDANT", "earns_slot": false, @@ -228,11 +259,14 @@ "abs_hold_sharpe": 0.353, "turnover_per_year": 40.6, "fee020_full_sharpe": 0.942, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 }, { "name": "agent_01_session_overlay", "tf": "1h", + "causal": true, "abs_grade": "WEAK", "marginal_verdict": "REDUNDANT", "earns_slot": false, @@ -249,11 +283,14 @@ "abs_hold_sharpe": 0.03, "turnover_per_year": 46.9, "fee020_full_sharpe": 0.904, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.013 }, { "name": "agent_03_funding_clock_15m", "tf": "15m", + "causal": true, "abs_grade": "FAIL", "marginal_verdict": "NEUTRAL", "earns_slot": false, @@ -270,11 +307,14 @@ "abs_hold_sharpe": -0.323, "turnover_per_year": 56.2, "fee020_full_sharpe": 0.742, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 }, { "name": "agent_02_overnight_vs_intraday", "tf": "1h", + "causal": true, "abs_grade": "FAIL", "marginal_verdict": "NEUTRAL", "earns_slot": false, @@ -291,11 +331,14 @@ "abs_hold_sharpe": -0.41, "turnover_per_year": 98.2, "fee020_full_sharpe": 0.497, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "ARTIFACT-RISK", + "boundary_spread": 0.081 }, { "name": "agent_15_intraday_meanrev_gated", "tf": "1h", + "causal": true, "abs_grade": "FAIL", "marginal_verdict": "NOISE", "earns_slot": false, @@ -312,11 +355,14 @@ "abs_hold_sharpe": -1.194, "turnover_per_year": 19.1, "fee020_full_sharpe": -0.013, - "fee_survives": false + "fee_survives": false, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 }, { "name": "agent_00_hour_of_day_bias", "tf": "1h", + "causal": true, "abs_grade": "FAIL", "marginal_verdict": "NEUTRAL", "earns_slot": false, @@ -333,6 +379,8 @@ "abs_hold_sharpe": -0.582, "turnover_per_year": 10.3, "fee020_full_sharpe": 0.593, - "fee_survives": true + "fee_survives": true, + "boundary_verdict": "INVARIANT", + "boundary_spread": 0.0 } ] \ No newline at end of file diff --git a/scripts/research/intraday/intra_score.py b/scripts/research/intraday/intra_score.py index 4b13cce..9158215 100644 --- a/scripts/research/intraday/intra_score.py +++ b/scripts/research/intraday/intra_score.py @@ -40,6 +40,15 @@ def score(path: Path, tf: str) -> dict: target = _target(path) except Exception as e: return {**rec, "error": f"import: {e}", "earns_slot": False} + # LOOK-AHEAD guard first: eval_weights' shift can't catch a non-causal FEATURE (centered + # window / shift(-k) / full-sample stat). A leak is disqualified no matter its Sharpe. + try: + caus = al.causality_ok(target, tf=tf) + rec["causal"] = bool(caus["ok"]) + if not caus["ok"]: + return {**rec, "causality": caus, "marginal_verdict": "LEAK", "earns_slot": False} + except Exception as e: + return {**rec, "error": f"causality: {e}", "causal": False, "earns_slot": False} try: rep = al.study_marginal(path.stem, target, tf=tf) except Exception as e: @@ -64,6 +73,17 @@ def score(path: Path, tf: str) -> dict: turnover_per_year=round(turn, 1), fee020_full_sharpe=round(fee020, 3), fee_survives=cell.get("fee_survives"), ) + # calendar-artifact guard: a signal whose marginal uplift INVERTS under a UTC day-boundary + # shift is a labeling artifact (open_drive), not an intraday effect. INVARIANT (price + # signal) and ROBUST (genuine calendar effect, e.g. prevday breakout) pass. + try: + db = al.day_boundary_robust(target, tf=tf) + rec["boundary_verdict"] = db["verdict"] + rec["boundary_spread"] = db["spread"] + if db["verdict"] == "ARTIFACT-RISK": + rec["earns_slot"] = False + except Exception as e: + rec["boundary_verdict"] = f"err:{e}" return rec @@ -85,10 +105,14 @@ def main(): for r in rows: if "error" in r: print(f" {r['name'][:26]:<26}{r['tf']:>4} ERROR {r['error'][:40]}"); continue + if r.get("causal") is False: + print(f" {r['name'][:26]:<26}{r['tf']:>4} LEAK (look-ahead, disqualified) " + f"max_tail_diff={r.get('causality', {}).get('max_tail_diff')}"); continue + bflag = " CAL-ARTIFACT" if r.get("boundary_verdict") == "ARTIFACT-RISK" else "" print(f" {r['name'][:26]:<26}{r['tf']:>4} {str(r['marginal_verdict']):<9}" f"{str(r['abs_grade']):>5}{str(r.get('corr_hold')):>6}{str(r.get('uplift_hold')):>6}" f"{str(r.get('cand_insample_sharpe')):>6}{str(r.get('turnover_per_year')):>6}" - f"{str(r.get('fee020_full_sharpe')):>7} {'<<<' if r.get('earns_slot') else ''}") + f"{str(r.get('fee020_full_sharpe')):>7} {'<<<' if r.get('earns_slot') else bflag}") slots = [r["name"] for r in rows if r.get("earns_slot")] print(f"\n EARNS SLOT: {slots or 'NONE'}") (HERE / "intra_leaderboard.json").write_text(json.dumps(rows, indent=2, default=str)) diff --git a/tests/test_harness_realism.py b/tests/test_harness_realism.py index d272dc8..64c67a9 100644 --- a/tests/test_harness_realism.py +++ b/tests/test_harness_realism.py @@ -60,3 +60,24 @@ def test_smallcap_keeps_real_trades(): r = al.eval_weights_smallcap(df, step, capital=600.0, min_order=5.0) assert r["n_executed_trades"] >= 2 assert abs(r["sharpe_haircut"]) < 0.2 + + +# --- LESSON 3: look-ahead (online-consistency) guard ----------------------------------- +def test_causality_passes_causal_signal(): + """A causal signal recomputed on a prefix matches the full run on its tail.""" + def mom(df): + c = df["close"].values + return np.tanh(3 * (c / al.sma(c, 200) - 1)) + r = al.causality_ok(mom, tf="1h") + assert r["ok"] is True + assert r["max_tail_diff"] == 0.0 + + +def test_causality_flags_lookahead(): + """A signal that reads tomorrow's move (future peek) is disqualified.""" + def leaky(df): + c = df["close"].values + f = np.zeros(len(c)); f[:-1] = np.sign(c[1:] - c[:-1]) # uses close[i+1] + return f + r = al.causality_ok(leaky, tf="1h") + assert r["ok"] is False