research(intraday): asse intraday/microstruttura — lead più vicino al reale ma NON deployabile

16 agenti su segnali low-turnover intraday (sessione/funding, reversione post-evento, breakout
range del giorno prima) su feed certificati 1h/15m, giudice = marginal scorer indurito + fee-sweep.
Lab: intra_score.py (wrappa study_marginal a TF scelto + turnover/fee), meta_intra.py (corr-TP01 +
per-cut), verify_intra.py (walk-forward + in-sample-null + drop-one + fee-stress).

Esito: 10/16 "earns_slot" -> 5 genuinamente ortogonali (corr<0.4). Combo dei 5: Sharpe 1.80, corr
0.17, leak-free, passa walk-forward (+0.30/+0.37 dove l'ortho dava -0.07), pre-2025 uplift +0.28,
drop-one e fee-robusto. Sembrava IL lead.

3 scettici: (1) open_drive = ARTEFATTO etichettatura UTC (shift confine 4h -> uplift negativo);
prevday_range_breakout REGGE (unico onesto, eseguibile). (2) combo fallisce il null a corr-zero
(20-24° pctl: aggiunge meno del rumore), è HEDGE (corr -0.57..-0.80 a Sharpe-TP01) + tail-luck
(80% PnL in top-5 giorni delle gambe revert). (3) robust-plateau ma null-pctl 0.20 = diversificazione
di stream ortogonale, non timing-alpha; + finzione fee micro-ribilanciamento a $600.

Verdetto: niente in live, resta solo TP01. Lead forward-monitor: prevday_range_breakout. Lezioni
harness da codificare: test shift-confine-giorno (artefatti calendar), fee discretizzata a piccolo
capitale, causality guard nel lab intraday. Diario 2026-06-21-intraday-microstructure.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Adriano Dal Pastro
2026-06-21 14:20:19 +00:00
parent 62d3b23cc6
commit 24565974c0
21 changed files with 2700 additions and 0 deletions
@@ -0,0 +1,97 @@
"""agent_01_session_overlay — SESSION OVERLAY on the daily TSMOM trend (TF=1h).
ANGLE [family=session, slug=session_overlay]: be in the daily trend position only during
the strongest session (Asia/EU/US blocks); reduce/flat in the weak session. Causal
session-return estimates. MINIMIZE flips.
STRUCTURAL BASIS (measured, both BTC & ETH): crypto drift is concentrated in EU+US hours;
Asia hours (UTC 0-7) carry ~0 mean return but full variance -> bad reward/risk. So holding
the trend through dead Asia hours adds vol without return. The overlay down-weights the
session that is causally the weakest.
FEE DISCIPLINE: a naive in/out-per-session flip churns ~700x/yr = fee-death. We keep
turnover bounded by (a) a SLOW trend (TP01 horizons 30/90/180d -> monthly flips), and (b)
modulating exposure across only 2 levels with a session weight that itself changes slowly
(a causal EXPANDING ranking of sessions, re-evaluated, not per-bar noise).
CAUSAL: the session strength is an expanding mean of past per-session hourly returns
(data strictly < current bar). No full-sample calendar fit.
"""
from __future__ import annotations
import sys
import numpy as np
import pandas as pd
sys.path.insert(0, "/opt/docker/PythagorasGoal/scripts/research/alt")
import altlib as al # noqa: E402
# Session blocks in UTC (8h each). Asia tends to be the dead block for crypto.
# 0=Asia(0-7), 1=EU(8-15), 2=US(16-23)
def _session_id(hours: np.ndarray) -> np.ndarray:
return np.where(hours < 8, 0, np.where(hours < 16, 1, 2)).astype(int)
def _causal_session_weak(r: np.ndarray, sess: np.ndarray, bpd: int,
warmup_days: int = 180) -> np.ndarray:
"""For each bar i, return the id of the session that is CAUSALLY weakest by expanding
mean hourly return using data strictly before i. Before warmup -> -1 (no opinion).
Computed once per day (at the first bar of each session-0 day) so it changes slowly."""
n = len(r)
weak = np.full(n, -1, dtype=int)
# running sums per session
ssum = np.zeros(3)
scnt = np.zeros(3)
warm = warmup_days * bpd
# We update the running stats with bar i-1 before deciding for bar i (strictly causal).
cur_weak = -1
for i in range(1, n):
s_prev = sess[i - 1]
ssum[s_prev] += r[i - 1]
scnt[s_prev] += 1
if i >= warm and scnt.min() > 0:
means = ssum / scnt
cur_weak = int(np.argmin(means))
weak[i] = cur_weak
return weak
def target(df: pd.DataFrame) -> np.ndarray:
c = df["close"].values.astype(float)
dt = pd.to_datetime(df["datetime"], utc=True)
hours = dt.dt.hour.values
bpd = al.bars_per_day(df) # 24 at 1h
# --- TP01-style slow trend direction (long-flat) -------------------------
horizons = tuple(d * bpd for d in (30, 90, 180))
nbar = len(c)
acc = np.zeros(nbar); cnt = np.zeros(nbar)
for h in horizons:
s = np.full(nbar, np.nan)
s[h:] = np.sign(c[h:] / c[:-h] - 1.0)
v = np.isfinite(s)
acc[v] += s[v]; cnt[v] += 1
direction = np.zeros(nbar)
nz = cnt > 0
direction[nz] = acc[nz] / cnt[nz]
direction = np.clip(direction, 0, None) # long-flat like TP01
# vol-target (TP01 canonical)
base = al.vol_target(direction, df, target_vol=0.20, vol_win_days=30, leverage_cap=2.0)
# --- session overlay -----------------------------------------------------
r = al.simple_returns(c)
sess = _session_id(hours)
weak = _causal_session_weak(r, sess, bpd, warmup_days=180)
# weight: full exposure outside the causally-weak session, reduced during it.
# NOTE (honest, after a full sweep): every step away from 1.0 (i.e. MORE overlay)
# strictly degrades both Sharpe and turnover vs plain TP01 — the dead-Asia effect is
# already captured by TP01's vol-targeting, and gating removes good trend days too.
# 0.9 is the least-harmful overlay. The angle does NOT earn a slot (see report notes).
w_weak = 0.9
sess_w = np.where(sess == weak, w_weak, 1.0)
sess_w[weak < 0] = 1.0 # no opinion -> full (TP01 behavior)
return base * sess_w