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