"""Locks the two harness-realism gates codified from the 2026-06-21 intraday wave: * day_boundary_robust — a calendar/session/hour signal whose marginal uplift INVERTS when the UTC day boundary is shifted is a labeling ARTIFACT (this killed open_drive). A price signal that ignores the calendar is INVARIANT; a genuine calendar effect is ROBUST. * eval_weights_smallcap — at ~$600 a sub-min_order rebalance can't execute; the modeled proportional fee on thousands of sub-dollar moves is a fiction. The realistic evaluator skips them. """ import sys from pathlib import Path import numpy as np import pandas as pd ROOT = Path(__file__).resolve().parents[1] sys.path.insert(0, str(ROOT / "scripts" / "research" / "alt")) import altlib as al # noqa: E402 # --- LESSON 1: day-boundary robustness ------------------------------------------------- def test_day_boundary_invariant_for_price_signal(): """A signal that never reads the calendar is INVARIANT to the day-boundary shift.""" def mom(df): c = df["close"].values return np.tanh(3 * (c / al.sma(c, 200) - 1)) r = al.day_boundary_robust(mom, offsets=(0, 6, 12, 18)) assert r["verdict"] == "INVARIANT" assert r["spread"] == 0.0 assert r["calendar_sensitive"] is False def test_day_boundary_flags_calendar_artifact(): """A pure hour-of-day bias is calendar-sensitive: its uplift swings with the boundary (the open_drive failure mode). It must be flagged, never INVARIANT.""" def hourbias(df): h = pd.to_datetime(df["datetime"], utc=True).dt.hour.values return np.where(h < 8, 1.0, 0.0) r = al.day_boundary_robust(hourbias, offsets=(0, 6, 12, 18)) assert r["calendar_sensitive"] is True assert r["spread"] > 0.05 assert r["verdict"] != "INVARIANT" # --- LESSON 2: small-capital fill realism ---------------------------------------------- def test_smallcap_skips_subdollar_rebalances(): """Thousands of sub-min_order wiggles (a vol-target overlay's fiction) do NOT execute.""" df = al.get("BTC", "1h") rng = np.random.default_rng(0) micro = 0.5 + 0.001 * rng.standard_normal(len(df)) # tiny drift around 0.5 r = al.eval_weights_smallcap(df, micro, capital=600.0, min_order=5.0) modeled_turn = al.eval_weights(df, micro)["turnover_per_year"] assert r["executed_turnover_per_year"] < modeled_turn * 0.2 assert r["n_executed_trades"] < len(df) * 0.05 def test_smallcap_keeps_real_trades(): """A low-turnover signal with genuine large moves executes them, ~no Sharpe haircut.""" df = al.get("BTC", "1h") step = np.zeros(len(df)); step[: len(df) // 2] = 0.5; step[len(df) // 2:] = -0.5 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 # --- LESSON 4: selection-on-holdout gate (codified 2026-06-29, filone B) ---------------- def _mom_factory(): """Tiny continuous momentum factory parametrized by SMA lookback (for grid tests).""" def factory(tf, win): def fn(df): c = df["close"].values.astype(float) return al.vol_target(np.tanh(3 * (c / al.sma(c, win) - 1)), df, 0.20, 30, 2.0) return fn return factory def test_deflated_sharpe_penalizes_multiple_testing(): """The SAME Sharpe deflates toward 0 when it was the best of MANY wide-dispersion trials, but survives when it stood alone among a few tight ones (Bailey & Lopez de Prado).""" rng = np.random.default_rng(0) T = 1500 idx = pd.date_range("2020-01-01", periods=T, freq="D", tz="UTC") sr_target = 1.0 ret = pd.Series(0.01 * (sr_target / np.sqrt(365.25) + rng.standard_normal(T)), index=idx) sr_ann = al._sh(ret) many = list(np.linspace(-3.0, 2.5, 120)) # 120 wide-spread trials few = [0.1, 0.0, -0.1, 0.05] # 4 tight trials dsr_many, sr0_many = al.deflated_sharpe(sr_ann, many, ret) dsr_few, sr0_few = al.deflated_sharpe(sr_ann, few, ret) assert sr0_many > sr0_few # more/wider search -> higher null max assert dsr_many < dsr_few # multiple-testing is penalized assert dsr_many < 0.5 and dsr_few > 0.8 assert 0.0 <= dsr_many <= 1.0 and 0.0 <= dsr_few <= 1.0 def test_select_cell_insample_ranks_by_insample_only(): """The cell chosen must be the IN-SAMPLE (