harness(causality): guardia look-ahead + calendar-artifact self-policing nel lab intraday
- 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) <noreply@anthropic.com>
This commit is contained in:
@@ -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.
|
||||
# ===========================================================================
|
||||
|
||||
Reference in New Issue
Block a user