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) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-21 14:44:20 +00:00
parent 24565974c0
commit 4ae3b42442
4 changed files with 171 additions and 10 deletions
+13
View File
@@ -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 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 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. 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 - **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.
@@ -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 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. 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 1. **`altlib.day_boundary_robust(target_fn, tf)`** — shifta il confine del giorno UTC e ri-misura
giorno UTC di poche ore è un artefatto di etichettatura (ha ucciso open_drive). Da aggiungere ai gate l'uplift marginale: INVARIANT (segnale di prezzo, spread 0) / ROBUST (effetto calendario vero,
per ogni segnale calendar/session-based. resta positivo) / **ARTIFACT-RISK** (l'uplift si inverte = etichettatura). Verificato: riproduce
2. **Realismo fee a piccolo capitale**: `eval_weights` con vol-target genera migliaia di ribilanciamenti da solo il verdetto degli scettici — open_drive → ARTIFACT-RISK (+0.23→−0.33), prevday_breakout
sub-dollaro; a $600 la fee proporzionale su trade infinitesimi è ottimistica. Serve un costo che → ROBUST. Test `tests/test_harness_realism.py`.
**discretizzi i ribilanciamenti** (min-order + fee fissa) per lo Sharpe netto reale. Vale per TUTTI 2.**`altlib.eval_weights_smallcap(df, target, capital=600, min_order=5)`** — salta i
gli sleeve a questo capitale, TP01 incluso. ribilanciamenti sub-min_order (la finzione del micro-trading a $600), riporta lo Sharpe haircut
3. **Causality guard anche nel lab intraday**: l'online-consistency check (max_tail_diff) va integrato reale vs modellato. Vale per ogni sleeve a questo capitale, TP01 incluso. Test idem.
in `intra_score` come in blind/ortho (qui fatto a mano). 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`, File: `scripts/research/intraday/{intra_score,meta_intra,verify_intra}.py`,
`agents/agent_00..15_*.py`, `intra_leaderboard.json`. `agents/agent_00..15_*.py`, `intra_leaderboard.json`.
+85
View File
@@ -555,6 +555,91 @@ def fmt_marginal(rep: dict) -> str:
return "\n".join(lines) 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. # DRIVERS — run a hypothesis across both assets, several TFs, with a fee sweep.
# =========================================================================== # ===========================================================================
+62
View File
@@ -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