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:
@@ -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.
|
||||
|
||||
@@ -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`.
|
||||
|
||||
@@ -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.
|
||||
# ===========================================================================
|
||||
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user