From 4ae3b424427765569fb710bbd22c02c36f154eb5 Mon Sep 17 00:00:00 2001 From: Adriano Dal Pastro Date: Sun, 21 Jun 2026 14:44:20 +0000 Subject: [PATCH] harness(realism): codifica le 2 lezioni dell'onda intraday (day-boundary + small-cap fills) Due gate nuovi in altlib.py (test tests/test_harness_realism.py, suite intera verde): 1. day_boundary_robust(target_fn, tf): shifta il confine del giorno UTC e ri-misura l'uplift marginale. INVARIANT (segnale di prezzo, spread 0) / ROBUST (effetto calendario vero, resta positivo) / ARTIFACT-RISK (uplift si inverte = etichettatura). Riproduce da solo il verdetto degli scettici: open_drive +0.23@00:00 -> -0.33@+8h = ARTIFACT-RISK; prevday_breakout = ROBUST. Decoupling chiave: il segnale vede il clock shiftato, il backtest usa il calendario reale. 2. eval_weights_smallcap(df, target, capital=600, min_order=5): salta i ribilanciamenti di nozionale < min_order (la finzione del micro-trading sub-dollaro che eval_weights costa come fee proporzionale su un overlay vol-target), riporta lo Sharpe haircut reale vs modellato. Vale per ogni sleeve a $600, TP01 incluso. CLAUDE.md aggiornato (sezione HARNESS REALISM). La pipeline di falsificazione ora becca da sola artefatti-calendario e finzioni-fee, oltre a hedge/regime-luck/leakage gia' codificati. Co-Authored-By: Claude Opus 4.8 (1M context) --- CLAUDE.md | 13 +++ .../2026-06-21-intraday-microstructure.md | 21 ++--- scripts/research/alt/altlib.py | 85 +++++++++++++++++++ tests/test_harness_realism.py | 62 ++++++++++++++ 4 files changed, 171 insertions(+), 10 deletions(-) create mode 100644 tests/test_harness_realism.py diff --git a/CLAUDE.md b/CLAUDE.md index 087d36b..712d1cc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -117,6 +117,19 @@ Prima ondata di ricerca onesta su BTC/ETH certificati (5 track, harness condivis 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. +- **HARNESS REALISM (codificato 2026-06-21, onda intraday)** — due gate nuovi in `altlib.py`, + test `tests/test_harness_realism.py`: + - **`day_boundary_robust(target_fn, tf)`** — un effetto ora/sessione/giorno il cui uplift + marginale **si inverte** spostando il confine del giorno UTC di poche ore è un **artefatto di + etichettatura calendario** (ha ucciso `open_drive`: +0.23 a 00:00 → −0.33 a +8h → ARTIFACT-RISK). + Un segnale di prezzo è INVARIANT (spread 0); un effetto calendario vero è ROBUST (resta positivo; + es. `prevday_range_breakout`). **Regola: ogni segnale calendar/session/hour passa questo test + prima di crederci.** + - **`eval_weights_smallcap(df, target, capital=600, min_order=5)`** — a ~$600 un ribilanciamento + di nozionale < min_order **non si esegue**; la fee proporzionale che `eval_weights` applica a + migliaia di micro-trade sub-dollaro (tipici di un overlay vol-target) è **finzione**. Salta i + sub-min_order e riporta lo **Sharpe haircut** reale vs modellato. **Vale per OGNI sleeve a questo + capitale, TP01 incluso** — lo Sharpe netto onesto a $600 è quello small-cap, non quello modellato. - **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 + tempo. La strategia che *guadagna* esiste, ma a ~+€1.5/giorno su 2000. diff --git a/docs/diary/2026-06-21-intraday-microstructure.md b/docs/diary/2026-06-21-intraday-microstructure.md index d73bbd4..6ad90a5 100644 --- a/docs/diary/2026-06-21-intraday-microstructure.md +++ b/docs/diary/2026-06-21-intraday-microstructure.md @@ -66,17 +66,18 @@ gonfiato dal micro-ribilanciamento sub-dollaro a $600. Lo Sharpe standalone 1.80 allo scettico d'esecuzione (breakout del range del giorno prima, eseguibile, leak-free), con caveat short-leg/regime-2025. Trattamento = come `dvol_spread` / XS01 / STA05. -### Lezioni harness da codificare (il vero ritorno) +### Lezioni harness — CODIFICATE (il vero ritorno) -1. **Test di shift del confine-giorno**: un effetto "ora/sessione" che inverte spostando l'inizio del - giorno UTC di poche ore è un artefatto di etichettatura (ha ucciso open_drive). Da aggiungere ai gate - per ogni segnale calendar/session-based. -2. **Realismo fee a piccolo capitale**: `eval_weights` con vol-target genera migliaia di ribilanciamenti - sub-dollaro; a $600 la fee proporzionale su trade infinitesimi è ottimistica. Serve un costo che - **discretizzi i ribilanciamenti** (min-order + fee fissa) per lo Sharpe netto reale. Vale per TUTTI - gli sleeve a questo capitale, TP01 incluso. -3. **Causality guard anche nel lab intraday**: l'online-consistency check (max_tail_diff) va integrato - in `intra_score` come in blind/ortho (qui fatto a mano). +1. ✅ **`altlib.day_boundary_robust(target_fn, tf)`** — shifta il confine del giorno UTC e ri-misura + l'uplift marginale: INVARIANT (segnale di prezzo, spread 0) / ROBUST (effetto calendario vero, + resta positivo) / **ARTIFACT-RISK** (l'uplift si inverte = etichettatura). Verificato: riproduce + da solo il verdetto degli scettici — open_drive → ARTIFACT-RISK (+0.23→−0.33), prevday_breakout + → ROBUST. Test `tests/test_harness_realism.py`. +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). 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 0f5b779..4ad8a52 100644 --- a/scripts/research/alt/altlib.py +++ b/scripts/research/alt/altlib.py @@ -555,6 +555,91 @@ def fmt_marginal(rep: dict) -> str: return "\n".join(lines) +# =========================================================================== +# HARNESS REALISM — two gates codified from the 2026-06-21 intraday wave. +# +# LESSON 1 (day-boundary): open_drive ("first 8h UTC predicts rest-of-day") scored a +# +0.23 uplift but INVERTED to -0.10 when the UTC day start was shifted 4h — a calendar- +# LABELING artifact, not an intraday effect. A real hour/session/day edge degrades +# gracefully under a boundary shift; an artifact flips sign. +# +# LESSON 2 (small-cap fills): eval_weights charges fee on EVERY |Δposition|, incl. the +# thousands of sub-dollar rebalances a vol-target overlay produces. At ~$600 real capital a +# $0.03 trade can't execute — the modeled proportional fee is a continuous-rebalancing +# fiction. eval_weights_smallcap skips changes below min_order and reports the Sharpe haircut. +# =========================================================================== +def _shift_calendar(df: pd.DataFrame, offset_hours: int) -> pd.DataFrame: + """Relabel the clock the SIGNAL sees by +offset_hours (datetime & timestamp), leaving + prices/returns untouched -> the signal's .dt.hour / day-grouping shifts, the backtest + does not. (get() is cached; copy so we never mutate the shared frame.)""" + d = df.copy() + dt = pd.to_datetime(d["datetime"], utc=True) + pd.Timedelta(hours=offset_hours) + d["datetime"] = dt + if "timestamp" in d: + d["timestamp"] = d["timestamp"].astype("int64") + int(offset_hours * 3600 * 1000) + return d + + +def day_boundary_robust(target_fn, tf: str = "1h", + offsets=(0, 3, 6, 9, 12, 15, 18, 21), w: float = 0.25) -> dict: + """Is a candidate's marginal uplift ROBUST to shifting the UTC day boundary? For each + offset we relabel the calendar the signal sees, recompute its 50/50 BTC+ETH daily series + and the blend uplift vs TP01. A datetime-independent signal is INVARIANT (spread ~0); a + calendar signal that stays positive is ROBUST; one whose uplift flips sign is ARTIFACT-RISK + (open_drive). Run this on ANY hour/session/day-of-week signal before believing it.""" + B = tp01_baseline_daily() + per = {} + for off in offsets: + series = {} + for a in CERTIFIED: + df0 = get(a, tf) # ORIGINAL bars/dates + tgt = _call_target(target_fn, _shift_calendar(df0, off), a) # signal sees shifted clock + ev = eval_weights(df0, tgt) # backtest on the real calendar + series[a] = pd.Series(ev["net"], index=ev["idx"]) + J = pd.concat(series, axis=1, join="inner").fillna(0.0) + cand = _to_daily(0.5 * J[CERTIFIED[0]] + 0.5 * J[CERTIFIED[1]]) + JJ = pd.concat({"B": B, "C": cand}, axis=1, join="inner").dropna() + per[int(off)] = round(_sh((1 - w) * JJ["B"] + w * JJ["C"]) - _sh(JJ["B"]), 3) if len(JJ) > 30 else None + ups = [v for v in per.values() if v is not None] + if not ups: + return dict(per_offset=per, verdict="N/A", reason="no evaluable offsets") + spread = round(max(ups) - min(ups), 3) + calendar_sensitive = spread > 0.02 + robust = min(ups) > 0 + verdict = ("INVARIANT" if not calendar_sensitive else ("ROBUST" if robust else "ARTIFACT-RISK")) + return dict(per_offset=per, base=per[offsets[0]], min=min(ups), max=max(ups), + spread=spread, calendar_sensitive=calendar_sensitive, + robust_to_boundary=robust, verdict=verdict) + + +def eval_weights_smallcap(df: pd.DataFrame, target, capital: float = 600.0, + min_order: float = 5.0, fee_side: float = FEE_SIDE) -> dict: + """Honest net at SMALL capital. A desired position change whose notional |Δw|*capital is + below min_order is NOT executed (held -> tracking error, no trade) — removing the + continuous-rebalancing fiction. Returns realistic vs modeled metrics, the Sharpe haircut, + and the number of trades that actually execute. (Applies to ANY sleeve at this capital, + TP01 included.)""" + c = df["close"].values.astype(float) + tgt = np.clip(np.nan_to_num(np.asarray(target, float)), -10, 10) + held = np.empty(len(tgt)); cur = 0.0; n_tr = 0 + for i in range(len(tgt)): + if abs(tgt[i] - cur) * capital >= min_order: + cur = tgt[i]; n_tr += 1 + held[i] = cur + r = simple_returns(c) + pos = np.zeros(len(held)); pos[1:] = held[:-1] + turn = np.abs(np.diff(pos, prepend=0.0)) + net = pos * r - fee_side * turn; net[0] = 0.0 + idx = pd.DatetimeIndex(pd.to_datetime(df["datetime"], utc=True)) + real = _metrics_from_net(net, idx) + modeled = eval_weights(df, tgt, fee_side=fee_side)["full"] + bpy_d = bars_per_day(df) * 365.25 + return dict(realistic=real, modeled=modeled, + sharpe_haircut=round(modeled["sharpe"] - real["sharpe"], 3), + n_executed_trades=int(n_tr), + executed_turnover_per_year=round(float(turn.sum() / (len(turn) / bpy_d)), 1)) + + # =========================================================================== # DRIVERS — run a hypothesis across both assets, several TFs, with a fee sweep. # =========================================================================== diff --git a/tests/test_harness_realism.py b/tests/test_harness_realism.py new file mode 100644 index 0000000..d272dc8 --- /dev/null +++ b/tests/test_harness_realism.py @@ -0,0 +1,62 @@ +"""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