24565974c0
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>
98 lines
4.1 KiB
Python
98 lines
4.1 KiB
Python
"""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
|