feat: tod1 shadow track — zero-gap intraday volume-explosion detector

The 07-21 misses (CPHI +417%, DFNS +94%, GREE/WGRX +40%) were all static-gap
<10% names the validated gapper population excludes by design, but they share
a signature no refuted detector (liq1/run1) measured: rvol_tod (cum volume vs
median-20d prorated by elapsed session) at 50-1000x on a flat open. New
labeler setup tod1 fires on the rising edge of rvol_tod>=50 AND price>=
prev_close*1.20, restricted to static gap <10%, stop = 5-bar swing low,
scored via the shared _forward_labels. Shadow-only; pre-registered promotion
bar locked before forward data (median fwdmax60 >=+10%, median dd60 >-6%,
+1R>50%, top stratum rvol_tod>=200 clears alone).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Fable 5 committed 2026-07-21 11:59:51 -05:00
1 parent 3fdf268227
commit 9bfd458956
2 files changed
+219

No files matched your search

+177
View File
@@ -122,6 +122,27 @@ PREMARKET_PROMO_MEDIAN_ADVERSE = -0.06 # (2) median max-drawdown > -6%
PREMARKET_PROMO_MIN_1R = 0.50 # (3) +1R-before-(-1R) > 50%
# (4) top gap-stratum (>=50% gap) must clear (3) — the trader's real premarket runners.
# tod1: zero-gap intraday volume-explosion shadow track (2026-07-21). The 07-21 misses
# (CPHI +417%, DFNS +94%, GREE/WGRX +40% — all static gap <10%, all excluded by the
# validated gapper population) share ONE signature the refuted liq1/run1 detectors never
# had: rvol_tod = cum volume / (MEDIAN(20d prior daily volume) * elapsed-session fraction)
# at 50-1000x while the static gap is ~0. Tests whether THAT population converts. Shadow-only.
TOD1_SETUP_VERSION = "tod1"
TOD1_NAME = "intraday_volume_explosion"
TOD1_MIN_RVOL_TOD = 50.0 # detection floor (CPHI 1039, GREE 137, WGRX 66 on 07-21)
TOD1_MIN_DYN_GAIN = 0.20 # price >= prev_close * 1.20 (a real intraday runner)
TOD1_MAX_STATIC_GAP = 0.10 # the EXCLUDED class only (static gap under the live 10% floor)
TOD1_MIN_RTH_BARS = 24 # forward room so outcome labels aren't truncated
TOD1_STOP_LOOKBACK = 5 # invalidation = low of the prior N RTH bars (swing low)
# PRE-REGISTERED promotion bar (locked 2026-07-21, BEFORE forward data — same null as the
# 7 refuted levers: separates a population but doesn't convert). Promote off shadow ONLY if
# out-of-sample: (1) median max-upside 60m >= +10%; (2) median max-drawdown 60m > -6%;
# (3) +1R-before-(-1R) > 50%; (4) the top stratum (rvol_tod >= 200) clears (3) on its own.
TOD1_PROMO_MEDIAN_FWDMAX_60M = 0.10
TOD1_PROMO_MEDIAN_ADVERSE_60M = -0.06
TOD1_PROMO_MIN_1R = 0.50
TOD1_PROMO_TOP_STRATUM = 200.0
# No-chase entry shadow-validation (the final mechanic lever). Realistic per-name round-trip
# cost proxy (minute_bars has no NBBO, so a price-tiered spread/slippage stand-in). PRE-
# REGISTERED bar locked BEFORE forward data: promote the no-chase VWAP-pullback entry only if
@@ -1156,6 +1177,154 @@ def compute_premarket_setups(symbol, session_date, df, prior_close, avg_vol,
return out
def _preload_med_vol(con) -> dict:
"""{(symbol, session_date): median of the <=20 prior daily volumes} — the SAME
baseline the live scanner's rvol_tod uses (median kills reverse-split and
prior-squeeze poisoning that inflates the 20d mean; BIYA: mean 29.4M vs median 1.5M)."""
rows = con.execute(
"SELECT symbol, trade_date, volume FROM daily_bars "
"WHERE symbol IN (SELECT DISTINCT symbol FROM minute_bars) "
"ORDER BY symbol, trade_date").fetchall()
out: dict = {}
prior: dict = {}
for sym, d, vol in rows:
pv = prior.get(sym)
if pv:
out[(sym, d)] = _median(pv[-20:])
if vol is not None:
prior.setdefault(sym, []).append(float(vol))
return out
def compute_tod1_setups(symbol, session_date, df, prior_close, med_vol,
cats: dict, *, cooldown=10, spy_session=None) -> list:
"""0..N intraday_volume_explosion shadow fires for one session. Fires on the
rising edge of (rvol_tod >= floor AND price >= prev_close*(1+gain)) restricted
to the population the live gates EXCLUDE (static gap < 10%). Cum volume includes
premarket (matches the live scanner); the elapsed fraction is RTH-based with the
same 0.05 floor. Labels via the shared _forward_labels. PURE."""
if not prior_close or prior_close <= 0 or not med_vol or med_vol <= 0:
return []
df = df.copy()
df["is_regular_hours"] = df["is_regular_hours"].astype(bool)
rth = df[df["is_regular_hours"]].reset_index(drop=True)
if len(rth) < TOD1_MIN_RTH_BARS:
return []
session_open = float(rth.iloc[0]["open"])
if session_open <= 0:
return []
static_gap = (session_open - float(prior_close)) / float(prior_close)
if static_gap >= TOD1_MAX_STATIC_GAP:
return [] # gapper class — the LIVE population owns it
pm_vol = float(df[~df["is_regular_hours"]]
[df["timestamp"] < rth.iloc[0]["timestamp"]]["volume"].sum())
cum = pm_vol
dyn_floor = float(prior_close) * (1.0 + TOD1_MIN_DYN_GAIN)
out, prev_fire, last_emit = [], False, -10 ** 9
for i in range(len(rth)):
bar = rth.iloc[i]
cum += float(bar["volume"])
frac = min(1.0, max(0.05, (i + 1) / 390.0))
rvol_tod = cum / (float(med_vol) * frac)
firing = rvol_tod >= TOD1_MIN_RVOL_TOD and float(bar["close"]) >= dyn_floor
rising = firing and not prev_fire and (i - last_emit) >= cooldown
prev_fire = firing
if not rising or i < TOD1_STOP_LOOKBACK:
continue
entry_ref = float(bar["close"])
invalidation = float(rth.iloc[i - TOD1_STOP_LOOKBACK:i]["low"].min())
if entry_ref <= invalidation:
continue
last_emit = i
setup_ts = bar["timestamp"]
_, cat_fresh = _catalyst_at(cats, symbol, setup_ts)
spy_ret, tape = _spy_context(spy_session, setup_ts)
sid = f"{symbol}:{session_date}:b{i}:{TOD1_SETUP_VERSION}"
setup = dict(setup_id=sid, symbol=symbol, setup_time=setup_ts, session_date=session_date,
setup_name=TOD1_NAME, setup_version=TOD1_SETUP_VERSION,
entry_reference_price=entry_ref, invalidation_price=invalidation,
target_r_multiple=0.0, gap_pct=round(static_gap * 100.0, 3),
relative_volume=round(rvol_tod, 2), relative_volume_source="tod1",
pullback_volume_ratio=cum, # cumulative volume (liquidity strat)
catalyst_freshness_at_trigger=cat_fresh, session_minute_number=i + 1,
tape_regime_at_trigger=tape, spy_intraday_return_pct_at_trigger=spy_ret)
lab = _forward_labels(rth, setup_ts, entry_ref, invalidation, session_open)
out.append((setup, dict(setup_id=sid, label_version=LABEL_VERSION, **lab)))
return out
def build_tod1(con, limit=None, rebuild=False, cooldown=10):
if rebuild:
con.execute("DELETE FROM outcome_labels WHERE setup_id IN "
"(SELECT setup_id FROM setup_events WHERE setup_version=?)", [TOD1_SETUP_VERSION])
con.execute("DELETE FROM setup_events WHERE setup_version=?", [TOD1_SETUP_VERSION])
daily = _preload_daily(con)
med = _preload_med_vol(con)
cats = _preload_catalysts(con)
spy = _preload_spy(con)
sessions = con.execute(
"SELECT DISTINCT symbol, session_date FROM minute_bars ORDER BY session_date, symbol"
+ (f" LIMIT {int(limit)}" if limit else "")).fetchall()
built = sess_with = 0
for sym, d in sessions:
rows = con.execute(
f"SELECT {', '.join(_MB_COLS)} FROM minute_bars WHERE symbol=? AND session_date=? "
"ORDER BY timestamp", [sym, d]).fetchall()
if not rows:
continue
dfb = pd.DataFrame(rows, columns=_MB_COLS)
pc, _av = daily.get((sym, d), (None, None))
try:
setups = compute_tod1_setups(sym, d, dfb, pc, med.get((sym, d)), cats,
cooldown=cooldown, spy_session=spy.get(d))
except Exception as exc: # noqa: BLE001 — one bad session can't stop the build
print(f" ! {sym} {d}: {exc}")
continue
if setups:
sess_with += 1
for setup, label in setups:
_insert(con, "setup_events", setup)
_insert(con, "outcome_labels", label)
built += 1
print(f"built {built} {TOD1_NAME} shadow fires across {sess_with} sessions "
f"(of {len(sessions)} symbol-sessions scanned)")
return built
def report_tod1(con):
n = con.execute("SELECT count(*) FROM setup_events WHERE setup_version=?",
[TOD1_SETUP_VERSION]).fetchone()[0]
if not n:
print("No intraday_volume_explosion setups — run `build-tod1` first.")
return
days, syms = con.execute(
"SELECT count(DISTINCT session_date), count(DISTINCT symbol) FROM setup_events "
"WHERE setup_version=?", [TOD1_SETUP_VERSION]).fetchone()
print(f"=== {TOD1_NAME} shadow report ({n} fires, {syms} symbols, {days} session-days) ===")
print(f"pre-registered bar: median fwdmax60 >= {TOD1_PROMO_MEDIAN_FWDMAX_60M:+.0%}, "
f"median dd60 > {TOD1_PROMO_MEDIAN_ADVERSE_60M:+.0%}, +1R>{TOD1_PROMO_MIN_1R:.0%}, "
f"top stratum rvol_tod >= {TOD1_PROMO_TOP_STRATUM:g} clears on its own")
for name, cond in [("ALL", "1=1"),
(f"rvol_tod >= {TOD1_PROMO_TOP_STRATUM:g}",
f"se.relative_volume >= {TOD1_PROMO_TOP_STRATUM}"),
("rvol_tod < 200", "se.relative_volume < 200")]:
row = con.execute(
"SELECT count(*), "
"avg(CASE WHEN l.reached_1r_before_minus_1r THEN 1.0 ELSE 0 END), "
"percentile_cont(0.5) WITHIN GROUP (ORDER BY l.max_upside_next_60m), "
"percentile_cont(0.5) WITHIN GROUP (ORDER BY l.max_drawdown_next_60m), "
"avg(CASE WHEN l.failed_breakout_flag THEN 1.0 ELSE 0 END) "
"FROM outcome_labels l JOIN setup_events se ON se.setup_id = l.setup_id "
f"WHERE se.setup_version=? AND {cond}", [TOD1_SETUP_VERSION]).fetchone()
if not row or not row[0]:
print(f" [{name}] n=0")
continue
print(f" [{name}] n={row[0]} +1R-first {row[1]*100:.0f}% "
f"median fwdmax60 {row[2]*100 if row[2] is not None else 0:+.1f}% "
f"median dd60 {row[3]*100 if row[3] is not None else 0:+.1f}% "
f"failed-breakout {row[4]*100:.0f}%")
def build_premarket(con, limit=None, rebuild=False, cooldown=10):
if rebuild:
con.execute("DELETE FROM outcome_labels WHERE setup_id IN "
@@ -1531,6 +1700,10 @@ def main():
bp = sub.add_parser("build-premarket", help="build the premarket_setup shadow track (premarket bars)")
bp.add_argument("--limit", type=int); bp.add_argument("--rebuild", action="store_true")
bp.add_argument("--cooldown", type=int, default=10)
bt = sub.add_parser("build-tod1", help="build the intraday_volume_explosion shadow track")
bt.add_argument("--limit", type=int); bt.add_argument("--rebuild", action="store_true")
bt.add_argument("--cooldown", type=int, default=10)
sub.add_parser("report-tod1")
sub.add_parser("report-premarket", help="forward outcomes + gap stratification of the premarket track")
sub.add_parser("validate-premarket", help="score the premarket track vs its locked pre-registered bar")
args = ap.parse_args()
@@ -1563,6 +1736,10 @@ def main():
report_liquid(con)
elif args.cmd == "validate-liquid":
validate_liquid(con)
elif args.cmd == "build-tod1":
build_tod1(con, limit=args.limit, rebuild=args.rebuild, cooldown=args.cooldown)
elif args.cmd == "report-tod1":
report_tod1(con)
elif args.cmd == "build-premarket":
build_premarket(con, limit=args.limit, rebuild=args.rebuild, cooldown=args.cooldown)
elif args.cmd == "report-premarket":