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:
1 parent
3fdf268227
commit
9bfd458956
2 files changed
+219
No files matched your search
@@ -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":
|
||||
|
||||
Reference in new issue
Block a user