feat: whole-market premarket squeeze scan via Alpaca snapshots
Discovery finds premarket squeezes far too late. Across 53 trades by a
professional small-cap momentum trader, our scanner had found only 4 of his
names before his entry, and 27 of his 28 entries were premarket (07:00-09:30
ET). KITT on 2026-09-25 ran $3.50 -> $8.39 at 09:00-09:01 ET (prior close
$0.49); the loop first "Discovered KITT via manual" at 09:35. The existing
sources (Alpaca movers / most-actives screeners) only reflect the regular
session, so a premarket squeeze is invisible until the open.
research/ingestion/snapshot_scan.py sweeps every active, tradable, listed US
equity (~11.9k names; OTC, warrants/units/rights and DISCOVER_EXCLUDE dropped;
asset list cached per ET day) through GET /v2/stocks/snapshots in 2,000-symbol
batches. The endpoint is bound by URL length rather than symbol count (8,000
worked; ~12.7k returned 414). A name qualifies when:
- the LATEST TRADE price is inside WATCHER_PRICE_MIN/MAX (never the prior
close, which is routinely < $1 on these names),
- % change vs the prior close >= DISCOVER_SNAPSHOT_MIN_PCT (20),
- it is trading now: last trade <= 180s old, latest minute bar <= 300s old
with >= 5,000 shares, and >= 100,000 shares since 04:00 ET (summed from
minute bars for the short list, one extra request).
Qualifiers are ranked by % change, capped per sweep, and announced once per
day. dailyBar OHLC ignores extended-hours prints (KITT's dailyBar high was
$3.24 against a premarket high of $8.39), so in premarket it can still be
yesterday's bar. The prior close is prevDailyBar.c only when dailyBar is dated
today, otherwise dailyBar.c.
run_live_paper.py runs it on its own daemon thread (04:00-16:00 ET weekdays,
every 45s), using the plan-019 pattern: its own DB connection, the thread-safe
Watchlist and the generic interval driver. Each new name emits
symbol_discovered with source="snapshot_scan" and price/prev_close/pct/minute
and cumulative volume in symbol_data, so first-sighting latency can be
measured. It gets a daily-history backfill and joins the watchlist, so the
normal new-symbol ingest path gives it its first full-session pull. This is
discovery only; no trading gate changes.
Gated by DISCOVER_SNAPSHOT_SCAN_ENABLED (default 1); knobs in .env.example.
Live read-only sweep at 13:10 ET: 11,871 names in 8 requests / 6.3s cold
(7 requests / 3.5s warm); top hits KITT +376%, MSGY +262%, INLF +60%.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7f1ebe9cab
commit
1cead3f29b
4 files changed
+647
No files matched your search
@@ -84,6 +84,22 @@ LIVE_BARS_INTERVAL_SECONDS=20
|
||||
LIVE_BARS_REFRESH_MINUTES=20
|
||||
WATCHER_INTERVAL_SECONDS=15
|
||||
DISCOVER_INTERVAL_SECONDS=120
|
||||
# --- whole-market premarket squeeze scan (research/ingestion/snapshot_scan.py) ---
|
||||
# The movers/most-actives screeners only see the regular session, so premarket
|
||||
# squeezes were invisible until 09:30 (KITT 2026-09-25 ran at 09:00, found 09:35).
|
||||
# This sweeps every listed, tradable US equity through Alpaca snapshots
|
||||
# 04:00-16:00 ET on its own thread and adds qualifying names to the watchlist
|
||||
# (symbol_discovered source=snapshot_scan). Discovery only; all trading gates apply.
|
||||
# Price band = WATCHER_PRICE_MIN/MAX on the LATEST trade (not the prior close).
|
||||
DISCOVER_SNAPSHOT_SCAN_ENABLED=1
|
||||
DISCOVER_SNAPSHOT_INTERVAL_SECONDS=45 # one full sweep (~7 requests)
|
||||
DISCOVER_SNAPSHOT_MIN_PCT=20 # % change vs prior close to qualify
|
||||
DISCOVER_SNAPSHOT_MAX_PER_SWEEP=5 # new names injected per sweep (top by %)
|
||||
DISCOVER_SNAPSHOT_MIN_BAR_VOLUME=5000 # shares in the latest minute bar
|
||||
DISCOVER_SNAPSHOT_MIN_CUM_VOLUME=100000 # shares traded today since 04:00 ET
|
||||
DISCOVER_SNAPSHOT_MAX_TRADE_AGE_SECONDS=180
|
||||
DISCOVER_SNAPSHOT_MAX_BAR_AGE_SECONDS=300
|
||||
DISCOVER_SNAPSHOT_BATCH=2000 # symbols per request (URL-length bound; 414 at ~12.7k)
|
||||
# Active setups. The backtest sweep found the opening-range breakout is the only
|
||||
# profitable one (others lose and fire later); empty = all setups. See learn_params.py.
|
||||
STRATEGY_SETUPS=opening_range_break
|
||||
|
||||
Reference in new issue
Block a user