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,159 @@
"""agent_12_close_location — STRUCT family, slug=close_location (suggested TF 1h).
ANGLE (assigned): where price closes WITHIN the day range — the close-location-value
CLV = (close - low) / (high - low) in [0,1] — predicts next-day direction. CLV near 1 = bulls
close at the highs (buying pressure / accumulation); near 0 = bears close at the lows
(distribution / weakness). One decision/day -> naturally low turnover.
WHAT THE DATA ACTUALLY SAYS (explored before designing — honesty first):
* RAW single-day CLV is mildly MEAN-REVERTING, not continuation: low CLV (closed near low)
=> HIGHER next-day return (+26bp BTC / +35bp ETH), high CLV => lower (+3bp / +9bp). The
quintile gradient is monotone but the effect is weak (corr ~-0.03).
* A pure CLV mean-reversion book (buy weak closes / fade strong closes) is anti-trend: it has
a NEGATIVE full Sharpe (-0.47), only a lucky-2025 hold-out (candH +0.59), and DILUTES the
full blend (-0.45). It fails the in-sample-edge gate -> NOISE/regime-luck. NOT this.
* A persistent-CLV CONTINUATION book (long when closes have been strong for weeks) is just a
slow trend proxy: good full (~0.9) but NEGATIVE hold-out (broke in 2025 like buy&hold) and
~redundant with TP01. NOT this either.
THE DESIGN THAT EARNS A SLOT (agent_10's lesson, applied to CLV): use CLV as a HARD FLAT-GATE on
the TP01 carrier, NOT a soft sizer. A soft CLV multiplier stays corr ~0.93-0.96 to TP01 =
REDUNDANT. To decorrelate we must do what a c2c trend CANNOT: go fully FLAT in the regime where
closes are persistently WEAK (bearish CLV / distribution at the top), and ride the trend at full
size when closes confirm it. Going to 0 (not trimming) removes specific trend days TP01 holds
through that turn out badly -> corr drops to ~0.82 and the blend lift is real.
carrier = TP01-style long-flat 30/90/180d TSMOM, c2c vol-targeted (this IS the TP01 leg)
CLV gate = multi-window (3/5/10d EMA of CLV) -> causal EXPANDING-z -> averaged -> a hard ramp
that reaches 0 when CLV-z is in its bottom regime (kill_z=0.3), EMA-smoothed.
Multi-window (like TP01's multi-horizon trend) is more robust than any single span
and bites only when weak-closes are confirmed across horizons -> fewer false kills.
WHY IT'S INTRADAY-NATIVE (not derivable from c2c): two days with the SAME close-to-close move can
have wildly different CLV — one closed at the high after dipping (strong), one faded from the high
(weak). c2c vol (what TP01 targets on) is blind to it. The gate withholds risk in
distribution/weak-close tape and presses in clean accumulation.
CAUSAL: CLV[i] uses high/low/close[i] (all <= close[i]); the expanding-z standardizes by mean/std
over rows STRICTLY before i (shift(1)); the gate is a pure function of past bars. No full-sample
calendar/quantile fit. The evaluator holds position[i] during bar i+1 (no leak by construction).
TURNOVER: ~11/yr (the carrier flips ~monthly; the gate is a slow, smoothed, multi-day quantity)
-> far under the 120/yr fee cap; survives the 0.20% RT fee sweep.
HONEST VERDICT (scored 2026-06-21 @ tf=1d): ADDS / abs=PASS / EARNS_SLOT=True.
corr->TP01 0.815 (hold 0.734), beta 0.468, residual Sharpe 0.536 (+2.2%/yr alpha beyond trend).
uplift_hold +0.067 / uplift_full +0.045 ; standalone BTC full 1.10/hold 0.59, ETH 1.16/hold 0.61.
in-sample standalone Sharpe 1.50 (stands on its own, not a lucky window). turnover ~11/yr (BTC) /
~8/yr (ETH); fees survive to 0.20% RT (full 1.04). Multi-cut PERSISTENT: the flat-gate lift is
positive at EVERY yearly cut 2020-2025 (+0.041..+0.079). is_hedge=False (it adds in BOTH TP01-up
+0.029 and TP01-down +0.069). Plateau: kill_z 0.35..0.60 all give clean-year uplift ~+0.08.
CAVEATS (honest — fees usually win, and they nearly did here):
* The HOLD-OUT lift is concentrated: dropping 2025-10 alone takes the hold-out uplift from
+0.062 to ~0 -> the drop-one-month jackknife clears by a HAIR (+0.001). The in-sample edge and
the 6-year multi-cut persistence are the real backbone; the 2025-26 hold-out is short (537d)
and one-month-leaning. Treat as a SATELLITE, forward-monitor the hold-out, do NOT over-weight.
* It is a TP01-LED book (shares the carrier; corr 0.82) -> it IMPROVES TP01 risk-adjusted via a
flat-gate TP01 cannot see (CLV/within-day close location), it is NOT a standalone alpha.
* The mean-reversion reading of CLV (buy weak closes) was REJECTED: negative full Sharpe, lucky-
2025-only -> NOISE. The continuation-as-gate framing is what survives the hardened judge.
"""
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
# --- carrier (TP01-style slow long-flat trend) ---
_HORIZONS_D = (30, 90, 180)
_VOL_TARGET = 0.20
_VOL_WIN_D = 30
_LEV_CAP = 2.0
# --- close-location-value (CLV) flat-gate ---
# Multi-window EMA of CLV -> expanding-z -> averaged -> hard ramp to 0 in the weak-close regime.
_CLV_SPANS_D = (3, 5, 10) # EMA spans (days) for the multi-window CLV blend
_EXP_MIN_D = 180 # min days before the expanding standardization is trusted
_KILL_Z = 0.45 # below this expanding-z of CLV -> closes are weak -> kill exposure.
# 0.45 = the plateau CENTER (kz 0.35..0.60 all give clean-year uplift
# ~+0.08, hold uplift ~+0.06, corr ~0.81); 0.45 is the lowest-corr point
# that also clears the drop-one-month jackknife (see HONEST VERDICT).
_RAMP = 0.5 # ramp width -> gate reaches 0 in confirmed weak-close tape
_SMOOTH_D = 3 # EMA smoothing of the gate (keeps turnover low)
_GATE_LO, _GATE_HI = 0.0, 1.0
def _tsmom_long_flat(c: np.ndarray, bpd: int) -> np.ndarray:
nbar = len(c)
acc = np.zeros(nbar)
cnt = np.zeros(nbar)
for d in _HORIZONS_D:
h = d * bpd
if h >= nbar:
continue
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]
return np.clip(direction, 0, None) # long-flat
def _expanding_z(x: np.ndarray, min_obs: int) -> np.ndarray:
"""Strictly causal expanding-standardized z-score (mean/std over rows 0..i-1)."""
s = pd.Series(x)
m = s.expanding(min_periods=min_obs).mean().shift(1)
sd = s.expanding(min_periods=min_obs).std().shift(1)
z = (s - m) / sd.replace(0, np.nan)
return z.values
def _clv(df: pd.DataFrame) -> np.ndarray:
"""Close-location-value in [0,1]: where close sits within the bar's high-low range.
1 = closed at the high (max buying pressure), 0 = closed at the low. 0.5 if range is 0."""
h, l, c = df["high"].values.astype(float), df["low"].values.astype(float), df["close"].values.astype(float)
rng = h - l
safe = np.where(rng > 0, rng, 1.0) # avoid 0/0 on flat (high==low) bars
return np.where(rng > 0, (c - l) / safe, 0.5) # 0.5 (neutral) when the bar has no range
def target(df: pd.DataFrame) -> np.ndarray:
c = df["close"].values.astype(float)
bpd = al.bars_per_day(df)
# --- carrier: slow long-flat TSMOM, c2c vol-targeted (this IS the TP01 leg) ----
direction = _tsmom_long_flat(c, bpd)
base = al.vol_target(direction, df, _VOL_TARGET, _VOL_WIN_D, _LEV_CAP)
# --- intraday-native signal: multi-window CLV, causal expanding-z, averaged -----
clv = _clv(df)
zacc = np.zeros(len(c))
for sp in _CLV_SPANS_D:
zacc += np.nan_to_num(_expanding_z(al.ema(clv, sp * bpd), _EXP_MIN_D * bpd), nan=0.0)
clv_z = zacc / len(_CLV_SPANS_D)
# HARD FLAT-GATE: full trend when closes confirm (CLV-z high), fully FLAT when closes are
# persistently weak (CLV-z below kill, = distribution). A soft ramp reaching 0 in confirmed
# weak-close tape, EMA-smoothed to keep turnover low. Going to 0 is what decorrelates from TP01.
raw_gate = np.clip((clv_z - _KILL_Z) / _RAMP + 0.5, _GATE_LO, _GATE_HI)
gate = al.ema(raw_gate, _SMOOTH_D * bpd)
gate = np.clip(gate, _GATE_LO, _GATE_HI)
pos = base * gate
return np.nan_to_num(pos, nan=0.0)
if __name__ == "__main__":
for a in ("BTC", "ETH"):
d = al.get(a, "1d")
ev = al.eval_weights(d, target(d))
print(a, "full", ev["full"]["sharpe"], "hold", ev["holdout"]["sharpe"],
"turn/yr", ev["turnover_per_year"], "TiM", ev["time_in_market"])