Files
PythagorasGoal/scripts/research/intraday/agents/agent_08_gap_fill.py
T
Adriano Dal Pastro 24565974c0 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>
2026-06-21 14:20:19 +00:00

168 lines
9.0 KiB
Python

"""agent_08_gap_fill — STRUCT family, slug=gap_fill (TF 1h).
ANGLE (assigned): session/period GAP-FILL tendency. After a large jump between session
closes/opens, lean toward PARTIAL REVERSION. Low-frequency, gated.
WHAT THE CERTIFIED DATA SAYS (BTC/ETH 1h, exploration only — NOT fit into the signal):
Measure the 'gap' as the trailing one-session (24h) move ending at a session boundary, and
the forward 24h return. The reversion is STRONGLY ASYMMETRIC:
* extreme UP gaps (>=97th pctl 24h jump) -> forward 24h ~0% (BTC -0.08%, ETH +0.31%):
NO clean fade. Shorting up-gaps in a bull tape just sells trend-beta -> we DON'T.
* extreme DOWN gaps (<=3rd pctl 24h drop) -> forward 24h +1.2% (BTC) / +1.35% (ETH):
a robust gap-FILL. A violent one-session sell-off (stops, liquidations, thin-book
overshoot) gives part of it back over the following session.
Conditioning on WHICH session the down-gap closes in: the low-liquidity ASIA/overnight
block (0-7 UTC) reverts a touch MORE (BTC +1.40% vs +0.91% US), consistent with thin-book
overshoot. We use this as a soft TILT (size up Asia-close gaps), not a hard gate (noisy).
DESIGN (LONG-ONLY gap-fill, the only side that pays): when the trailing one-session move is a
rare DOWN gap (causal expanding z below a floor) we go LONG and hold ~1 session, then flat. We
trade a MODERATE down-gap band [floor, cap): the most violent prints (z below the cap) split
into clean reversals AND runaway crashes -> a coin flip, so we skip them (same lesson as the
volume-spike fade). FLAT the great majority of the time -> a satellite, orthogonal by design to
a slow long-flat trend (TP01), the only way an intraday signal can ADD on a fee wall.
WHY LOW TURNOVER (the fee wall). The fire is a CONJUNCTION of rare causal events: an
expanding-z of the trailing one-session return below a floor (and above a cap), measured on a
NON-OVERLAPPING session grid (one decision per session, not per hour). Over 7.5y of 1h BTC/ETH
this fires only a few dozen times/yr; ONE long held ~1 session then flat -> turnover in the
tens/yr, miles under the ~120/yr cap and nowhere near the ~2000/yr fee-death of an hourly flip.
We use the intraday session STRUCTURE for INFORMATION (gap timing/sizing), not for churn.
CONTEXT GATE (what unlocked the edge). The gap-fill is conditional: an ISOLATED down-gap in
calm tape barely reverts (forward 24h ~0%), but a down-gap WITHIN a sustained sell-off (weekly
move <= -8%) reverts hard (forward +1.6..+2.5%). The capitulation that snaps back is the one
that overshoots an existing slide. Requiring this context (a) sharpened the edge and (b) woke
ETH up (which was flat without it) -> both assets full Sharpe >= 0.5.
CAUSALITY. Every input at bar i uses only rows 0..i:
* gap z = expanding-standardized (mean/std over rows 0..i-1 via .shift(1)) of the trailing
one-session log return. No full-sample stats.
* context drawdown = trailing 7-day move ending at i.
* session id uses bar i's own UTC hour (known at close[i]).
The go-long decision is taken at close[i]; the evaluator holds it during bar i+1. No shift(-k),
no full-sample calendar fit. VERIFIED: scrambling all future rows leaves past positions
byte-identical (max|delta|=0 on both assets).
HONEST VERDICT (scored 2026-06-21, hardened marginal judge @ 1h): EARNS_SLOT = TRUE.
abs_grade=PASS, marginal=ADDS, robust_oos=True, multicut_persistent=True, is_hedge=False,
has_insample_edge=True. corr->TP01 0.044 (orthogonal), beta 0.054, resid Sharpe 0.66,
alpha/yr +9.9%. cand in-sample (pre-2025) Sharpe 0.729; standalone full 0.72 / hold 0.68.
Blend 0.75*TP01+0.25*gap_fill: full 1.30->1.45 (+0.152), hold 0.31->0.55 (+0.243), DD 9.0%.
Turnover 9-12/yr; fee@0.20%RT full Sharpe 0.50 (survives the sweep comfortably).
MULTI-CUT uplift POSITIVE every year 2020-2026 (+0.12,+0.16,+0.06,+0.13,+0.16,+0.24,...).
PLATEAU: floor 2.3-2.5 x cap 3.6-4.0 x ctx_dd -0.05..-0.11 x hold 18-24 x gap 24-36 all
clear the bar (floor 2.1 collapses -> shallow gaps are not capitulation; that boundary is
the edge, not a fit). 62/65 fires over 7.5y, spread across EVERY year incl. the hold-out.
HONEST CAVEATS (price it as a small diversifying satellite, NOT standalone alpha):
* STANDALONE IS MODEST. Single-asset full Sharpe ~0.53-0.61, standalone DD is large (rarely-on
undiversified contrarian). The value is MARGINAL (it lifts a TP01-led book), not edge to
trade alone. The whole worth is the +0.24 hold-out uplift at corr 0.044.
* EVENT-SPARSE. ~8-9 fires/yr; the bear years 2021-22 carry most (more sell-offs = more
capitulation gaps). Calm/trending tape has few. Forward-monitor the fire rate.
* HEDGE-ADJACENT. It pays more when TP01 is DOWN (uplift TP01-down +0.28 vs up +0.17,
yearly hedge-corr -0.87): it CLEARS the is_hedge gate (still positive in up-regimes) but a
chunk of its worth is drawdown-dampening (buying capitulation dips during bear tape). Size
it as a defensive-leaning diversifier.
* The ASIA tilt is a deliberate NO-OP (=1.0): exploration showed Asia-close gaps revert a
touch more, but the Asia share of fires (~33%) is chance-level -> not enough to size on.
"""
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
# --- gap detection (causal) ---
_GAP_HOURS = 24 # the 'session/period' window: a trailing one-day jump
_Z_MIN_D = 90 # min days before the expanding gap-z is trusted
_Z_FLOOR = 2.3 # gap-z must be at least this negative: a real down-gap
_Z_CAP = 3.8 # below this z the print is a coin-flip (runaway crash), skip it
_GRID_HOURS = 8 # decide once per session block (non-overlapping) -> low turnover
# --- context gate: a down-gap WITHIN a sell-off is the overshoot that snaps back; an
# isolated down-gap in calm tape barely reverts (exploration: isolated fwd ~0% vs
# crash-context fwd +1.6-2.5%). Require a sustained weekly drawdown context. CAUSAL. ---
_CTX_DAYS = 7 # weekly drawdown window
_CTX_DD = -0.08 # the trailing-week move must be <= this (a real sell-off)
# --- fill holding / sizing ---
_HOLD_HOURS = 24 # hold the long ~1 session, then flat
_ASIA_TILT = 1.0 # extra size when the down-gap closes in the thin Asia block (0-7 UTC)
_BASE_SIZE = 1.0
def _expanding_z(x: np.ndarray, min_obs: int) -> np.ndarray:
"""Strictly causal expanding-standardized z-score (mean/std over rows 0..i-1).
pandas expanding().shift(1) standardizes bar i by stats that EXCLUDE i -> no peeking.
NaN until min_obs samples are available."""
s = pd.Series(x)
m = s.expanding(min_periods=min_obs).mean().shift(1)
sd = s.expanding(min_periods=min_obs).std().shift(1)
return ((s - m) / sd.replace(0, np.nan)).values
def _fires(df: pd.DataFrame) -> tuple[np.ndarray, np.ndarray]:
"""(fire, size) per bar, decided with data <= close[i].
fire = True on a fade-able DOWN gap; size = the long size to take (Asia tilt)."""
c = df["close"].values.astype(float)
dt = pd.to_datetime(df["datetime"], utc=True)
hour = dt.dt.hour.values
bpd = al.bars_per_day(df) # 24 at 1h
gap_bars = max(1, int(round(_GAP_HOURS / 24 * bpd)))
grid_bars = max(1, int(round(_GRID_HOURS / 24 * bpd)))
# trailing one-session log return (the 'gap')
gap = np.full(len(c), np.nan)
gap[gap_bars:] = np.log(c[gap_bars:] / c[:-gap_bars])
gz = _expanding_z(gap, _Z_MIN_D * bpd)
# context: sustained weekly drawdown (the down-gap is the overshoot of a sell-off)
ctx_bars = max(1, int(round(_CTX_DAYS * bpd)))
ctx = np.full(len(c), np.nan)
ctx[ctx_bars:] = c[ctx_bars:] / c[:-ctx_bars] - 1.0
in_selloff = ctx <= _CTX_DD
# moderate down-gap band, in a sell-off, on the non-overlapping session grid
on_grid = (np.arange(len(c)) % grid_bars) == 0
band = (gz <= -_Z_FLOOR) & (gz > -_Z_CAP)
fire = np.nan_to_num(band & in_selloff & on_grid, nan=False).astype(bool)
# Asia-close (0-7 UTC) down-gaps revert a touch more -> soft up-tilt
in_asia = hour < 8
size = np.where(in_asia, _BASE_SIZE * _ASIA_TILT, _BASE_SIZE)
return fire, size
def target(df: pd.DataFrame) -> np.ndarray:
"""Long-flat gap-fill: go long for HOLD_HOURS after a fade-able down-gap, else flat."""
bpd = al.bars_per_day(df)
fire, size = _fires(df)
hold = max(1, int(round(_HOLD_HOURS / 24 * bpd)))
n = len(df)
pos = np.zeros(n)
remaining = 0
cur_size = 0.0
for i in range(n):
if fire[i]:
remaining = hold # a fresh down-gap refreshes the hold window
cur_size = size[i]
if remaining > 0:
pos[i] = cur_size
remaining -= 1
return np.nan_to_num(pos, nan=0.0)
if __name__ == "__main__":
for a in ("BTC", "ETH"):
d = al.get(a, "1h")
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"])