fix: verify squeezes on a median volume baseline and ingest from the premarket

KITT on 2026-09-25 (+557%, 9.3M shares, the day's biggest mover) was benched
as "gap 557% squeeze unverified (rvol 1.54)". Two separate faults:

1. The rvol baseline was the 20-day MEAN, and two 11-14M-share days on
   09-01/09-02 put it at 1.66M against a 333k median. Squeeze verification
   (gap >= 150% needs rvol >= 5) now takes the stronger of the mean-baseline
   reading and the median-baseline one. Both are cumulative volume over a
   full day's baseline, not prorated, so they share the units the rvol >= 5
   boundary was calibrated in. A reverse-split fake still reads ~0.01 on
   both. This applies in the watcher pre-filter, the trigger book's squeeze
   exception and the fast-path population gate. The ranking and the tod-rvol
   arm signal are unchanged.

2. A name's first ingest reached back only to ~09:15 (09:30 + 15 min, or
   the 20-minute refresh floor). KITT was found at 09:35, so its stored day
   began at 09:13 and missed the 09:00 squeeze and ~6M premarket shares.
   The first pass now covers the session from 04:00 ET.

With both fixed, KITT's volume at 09:35 is ~20x its median day.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
momentum devandClaude Opus 5.5 committed 2026-09-25 12:08:13 -05:00
1 parent 7f1ebe9cab
commit 81e505b845
8 files changed
+119 -14

No files matched your search

+9
View File
@@ -32,6 +32,12 @@ class GapperCandidate:
# 29.4M). None when no baseline. Legacy relative_volume is kept untouched —
# validated boundaries (gap-squeeze rvol>=5) are calibrated in its units.
relative_volume_tod: float | None = None
# Squeeze-verification rvol: cum volume vs MEDIAN(20d), NOT prorated — the
# legacy units the gap-squeeze rvol>=5 boundary was calibrated in, but on a
# baseline one or two prior spike days can't poison. KITT 2026-09-25 (+557%,
# 9.3M shares) read legacy 1.54 because two 11-14M days put the 20d mean at
# 1.66M vs a 333k median, and was benched as "squeeze unverified".
relative_volume_med: float | None = None
def _session_elapsed_fraction(now: datetime | None = None) -> float:
@@ -111,6 +117,8 @@ def scan_gappers(
rvol = (float(cum_volume) / float(adv)) if adv and adv > 0 else 0.0
rvol_tod = (float(cum_volume) / (float(med_vol) * frac)
if med_vol and float(med_vol) > 0 else None)
rvol_med = (float(cum_volume) / float(med_vol)
if med_vol and float(med_vol) > 0 else None)
# either rvol flavour clears the bar: legacy is ~0.1-0.3 early in the
# session even for a heavy name (cum-so-far vs full-day mean), so the
# tod variant keeps real early movers from being silently dropped
@@ -127,6 +135,7 @@ def scan_gappers(
relative_volume=rvol,
vwap=float(vwap) if vwap is not None else None,
relative_volume_tod=rvol_tod,
relative_volume_med=rvol_med,
)
)
candidates.sort(key=lambda c: (c.gap_pct * max(c.relative_volume, 0.1)), reverse=True)
+11 -1
View File
@@ -71,6 +71,9 @@ class ResearchWatchlistProvider:
avg_daily_volume=research_query.query_avg_daily_volume(
self.con, symbol, session_date
),
median_daily_volume=research_query.query_median_daily_volume(
self.con, symbol, session_date
),
source="manual",
)
)
@@ -89,6 +92,9 @@ class ResearchWatchlistProvider:
avg_daily_volume=research_query.query_avg_daily_volume(
self.con, symbol, session_date
),
median_daily_volume=research_query.query_median_daily_volume(
self.con, symbol, session_date
),
source="research",
)
)
@@ -124,7 +130,7 @@ class LiveWatchlistProvider:
for symbol in self.symbols:
daily = self._daily_bars(symbol)
prev_close = None
adv = None
adv = med = None
if daily:
# last bar strictly before the session date
prior = [
@@ -135,11 +141,15 @@ class LiveWatchlistProvider:
prev_close = float(prior[-1]["c"])
recent = prior[-20:]
adv = sum(float(b["v"]) for b in recent) / len(recent)
vols = sorted(float(b["v"]) for b in recent)
mid = len(vols) // 2
med = vols[mid] if len(vols) % 2 else (vols[mid - 1] + vols[mid]) / 2
candidates.append(
WatchCandidate(
symbol=symbol,
previous_close=prev_close,
avg_daily_volume=adv,
median_daily_volume=med,
source="live",
)
)